From 9ce6a94a8c23014a692a1cf07284a508aa49fda2 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 06:57:47 -0400 Subject: [PATCH 01/15] Document disjoint employee health premium inputs --- .../expense/health/health_insurance_premiums.py | 10 +++++++--- ...alth_insurance_premiums_without_medicare_part_b.py | 8 ++++++++ .../health/long_term_health_insurance_premiums.py | 8 ++++++++ .../medical_expense_health_insurance_premiums.py | 11 +++++++---- .../health/medicare_part_b_premiums_reported.py | 9 +++++++++ .../expense/health/other_health_insurance_premiums.py | 10 +++++++--- .../health/pre_tax_health_insurance_premiums.py | 8 +++++--- .../health/self_employed_health_insurance_premiums.py | 9 ++++++++- .../input/employer_sponsored_insurance_premiums.py | 2 +- 9 files changed, 60 insertions(+), 15 deletions(-) diff --git a/policyengine_us/variables/household/expense/health/health_insurance_premiums.py b/policyengine_us/variables/household/expense/health/health_insurance_premiums.py index 936031f7fc9..e7ed3b9f651 100644 --- a/policyengine_us/variables/household/expense/health/health_insurance_premiums.py +++ b/policyengine_us/variables/household/expense/health/health_insurance_premiums.py @@ -9,7 +9,11 @@ class health_insurance_premiums(Variable): definition_period = YEAR uprating = "calibration.gov.hhs.cms.moop_per_capita" documentation = ( - "Person-level health insurance premiums supplied directly as an input. " - "SPM MOOP and statutory medical expense definitions use decomposed " - "premium aggregates." + "Annual person-paid health insurance premiums supplied directly as an " + "input, excluding premiums paid through pretax payroll deductions and " + "employer-paid premiums. Pretax payroll payments belong only in " + "pre_tax_health_insurance_premiums; the two sets are disjoint and total " + "person-paid premiums equal their sum. This direct non-pretax amount is " + "an alternative to the applicable decomposed non-pretax inputs, not an " + "additional expense." ) diff --git a/policyengine_us/variables/household/expense/health/health_insurance_premiums_without_medicare_part_b.py b/policyengine_us/variables/household/expense/health/health_insurance_premiums_without_medicare_part_b.py index 92be6e51a96..b9ded858ef9 100644 --- a/policyengine_us/variables/household/expense/health/health_insurance_premiums_without_medicare_part_b.py +++ b/policyengine_us/variables/household/expense/health/health_insurance_premiums_without_medicare_part_b.py @@ -7,4 +7,12 @@ class health_insurance_premiums_without_medicare_part_b(Variable): label = "Health insurance premiums without Medicare Part B premiums" unit = USD definition_period = YEAR + documentation = ( + "Annual person-paid health insurance premiums excluding Medicare Part B, " + "pretax payroll deductions, and employer-paid premiums. Pretax payroll " + "payments belong only in pre_tax_health_insurance_premiums. The pretax " + "and non-pretax sets are disjoint; total person-paid premiums equal " + "pretax payments plus the applicable non-pretax aggregate. This input is " + "the non-Medicare component of the medical expense premium aggregate." + ) uprating = "calibration.gov.hhs.cms.moop_per_capita" diff --git a/policyengine_us/variables/household/expense/health/long_term_health_insurance_premiums.py b/policyengine_us/variables/household/expense/health/long_term_health_insurance_premiums.py index 71a9825e4ef..27826f81fc4 100644 --- a/policyengine_us/variables/household/expense/health/long_term_health_insurance_premiums.py +++ b/policyengine_us/variables/household/expense/health/long_term_health_insurance_premiums.py @@ -7,4 +7,12 @@ class long_term_health_insurance_premiums(Variable): label = "Long-term health insurance premiums" unit = USD definition_period = YEAR + documentation = ( + "Annual person-paid long-term care insurance premiums excluding pretax " + "payroll deductions and employer-paid premiums. Pretax payroll payments " + "belong only in pre_tax_health_insurance_premiums. The pretax and non- " + "pretax sets are disjoint; total person-paid premiums equal pretax " + "payments plus the applicable non-pretax aggregate. This component must " + "be counted only once when included in a broader premium input." + ) uprating = "calibration.gov.hhs.cms.moop_per_capita" diff --git a/policyengine_us/variables/household/expense/health/medical_expense_health_insurance_premiums.py b/policyengine_us/variables/household/expense/health/medical_expense_health_insurance_premiums.py index dfb010bfed6..307305f4176 100644 --- a/policyengine_us/variables/household/expense/health/medical_expense_health_insurance_premiums.py +++ b/policyengine_us/variables/household/expense/health/medical_expense_health_insurance_premiums.py @@ -8,10 +8,13 @@ class medical_expense_health_insurance_premiums(Variable): unit = USD definition_period = YEAR documentation = ( - "Person-level health insurance premiums counted by statutory medical " - "expense definitions. Uses a direct premium input when supplied; " - "otherwise combines reported non-Medicare premiums with modeled " - "Medicare Part B premiums." + "Person-level non-pretax health insurance premiums for medical expense " + "definitions. Uses a nonzero direct health_insurance_premiums input; " + "otherwise combines non-Medicare premiums with modeled Medicare Part B " + "premiums for enrollees. Excludes the disjoint " + "pre_tax_health_insurance_premiums input. Consumers whose rules count " + "pretax payroll payments add that separate input once; tax deductions " + "that exclude them use this aggregate alone." ) def formula(person, period, parameters): diff --git a/policyengine_us/variables/household/expense/health/medicare_part_b_premiums_reported.py b/policyengine_us/variables/household/expense/health/medicare_part_b_premiums_reported.py index 500970c641d..09a07b4c37e 100644 --- a/policyengine_us/variables/household/expense/health/medicare_part_b_premiums_reported.py +++ b/policyengine_us/variables/household/expense/health/medicare_part_b_premiums_reported.py @@ -6,5 +6,14 @@ class medicare_part_b_premiums_reported(Variable): entity = Person label = "Medicare Part B premiums (reported)" definition_period = YEAR + documentation = ( + "Annual reported person-paid Medicare Part B premiums excluding pretax " + "payroll deductions and employer-paid premiums. Pretax payroll payments " + "belong only in pre_tax_health_insurance_premiums. The pretax and non- " + "pretax sets are disjoint; total person-paid premiums equal pretax " + "payments plus the applicable non-pretax aggregate. A reported component " + "must not be added again when already included in a broader premium " + "input." + ) unit = USD uprating = "calibration.gov.hhs.cms.moop_per_capita" diff --git a/policyengine_us/variables/household/expense/health/other_health_insurance_premiums.py b/policyengine_us/variables/household/expense/health/other_health_insurance_premiums.py index 1f90a92ff02..e409c2e03a9 100644 --- a/policyengine_us/variables/household/expense/health/other_health_insurance_premiums.py +++ b/policyengine_us/variables/household/expense/health/other_health_insurance_premiums.py @@ -9,7 +9,11 @@ class other_health_insurance_premiums(Variable): definition_period = YEAR uprating = "calibration.gov.hhs.cms.moop_per_capita" documentation = ( - "Person-level health insurance premiums not otherwise represented by " - "modeled Marketplace, CHIP, Medicaid, Medicare Part A or Part B " - "premiums, or the Part D IRMAA surcharge." + "Annual person-paid health insurance premiums not otherwise represented " + "by modeled Marketplace, CHIP, Medicaid, Medicare Part A or Part B " + "premiums, or the Part D IRMAA surcharge. Excludes pretax payroll " + "deductions and employer-paid premiums. Pretax payroll payments belong " + "only in pre_tax_health_insurance_premiums. The pretax and non-pretax " + "sets are disjoint; total person-paid premiums equal their sum, with " + "modeled non-pretax components counted once." ) diff --git a/policyengine_us/variables/household/expense/health/pre_tax_health_insurance_premiums.py b/policyengine_us/variables/household/expense/health/pre_tax_health_insurance_premiums.py index 57064157628..7bc65818fe5 100644 --- a/policyengine_us/variables/household/expense/health/pre_tax_health_insurance_premiums.py +++ b/policyengine_us/variables/household/expense/health/pre_tax_health_insurance_premiums.py @@ -7,9 +7,11 @@ class pre_tax_health_insurance_premiums(Variable): label = "Pre-tax health insurance premiums" unit = USD documentation = ( - "Health insurance premiums paid through pre-tax payroll deductions. " - "This excludes Medicare Part B premiums and other post-tax medical " - "out-of-pocket premiums." + "Annual employee-paid health insurance premiums paid through pretax " + "payroll deductions. Supply these only here: health_insurance_premiums " + "and the applicable decomposed premium inputs exclude these payments. The " + "pretax and non-pretax sets are disjoint; total employee-paid premiums " + "equal their sum. Excludes employer-paid premiums." ) definition_period = YEAR uprating = "calibration.gov.hhs.cms.moop_per_capita" diff --git a/policyengine_us/variables/household/expense/health/self_employed_health_insurance_premiums.py b/policyengine_us/variables/household/expense/health/self_employed_health_insurance_premiums.py index 903b966d923..b548d91392b 100644 --- a/policyengine_us/variables/household/expense/health/self_employed_health_insurance_premiums.py +++ b/policyengine_us/variables/household/expense/health/self_employed_health_insurance_premiums.py @@ -6,7 +6,14 @@ class self_employed_health_insurance_premiums(Variable): entity = Person label = "Self-employed health insurance premiums" unit = USD - documentation = "Health insurance premiums for plans covering individuals who are not covered by any employer-sponsored health insurance." + documentation = ( + "Annual person-paid health insurance premiums for plans covering " + "individuals not covered by employer-sponsored health insurance. Excludes " + "pretax payroll deductions and employer-paid premiums; pretax payroll " + "payments belong only in pre_tax_health_insurance_premiums. The pretax " + "and non-pretax sets are disjoint. Defaults to the direct non-pretax " + "health_insurance_premiums input for self-employed people." + ) definition_period = YEAR defined_for = "is_self_employed" adds = ["health_insurance_premiums"] diff --git a/policyengine_us/variables/input/employer_sponsored_insurance_premiums.py b/policyengine_us/variables/input/employer_sponsored_insurance_premiums.py index 15cb910e830..88ba15e3676 100644 --- a/policyengine_us/variables/input/employer_sponsored_insurance_premiums.py +++ b/policyengine_us/variables/input/employer_sponsored_insurance_premiums.py @@ -7,7 +7,7 @@ class employer_sponsored_insurance_premiums(Variable): label = "Employer-sponsored insurance premiums" documentation = ( "Annual employer-paid health insurance premiums. CBO treats this " - "as part of household market income." + "as part of household market income. Excludes employee-paid premiums, both pretax payroll deductions in pre_tax_health_insurance_premiums and non-pretax payments in the other premium inputs." ) definition_period = YEAR unit = USD From e44a8983d8af9f61b4b3ebfa849c8f4c0b3489da Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 07:07:38 -0400 Subject: [PATCH 02/15] Count disjoint payroll premiums according to each consumer rule --- .../hub-premium-input-contract-1010.fixed.md | 1 + .../crfb/surtax/increased_base/sources.yaml | 11 +- .../tests/core/test_premium_input_contract.py | 195 ++++++++++++++++++ ...caid_medically_needy_medical_expenses.yaml | 38 ++++ .../gov/hud/hud_medical_expenses.yaml | 38 ++++ ...eneral_relief_countable_income_person.yaml | 19 ++ .../ccs/income/mo_ccs_adjusted_income.yaml | 21 ++ .../nd_renters_refund_income.yaml | 58 ++++++ .../nj_medical_expense_deduction.yaml | 55 +++++ .../pa/dhs/ccw/pa_ccw_adjusted_income.yaml | 22 ++ .../snap_allowable_medical_expenses.yaml | 29 +++ ...m_unit_medical_out_of_pocket_expenses.yaml | 34 +++ .../tests/policy/contrib/crfb/agi_surtax.yaml | 55 +++++ ...dicaid_medically_needy_medical_expenses.py | 13 +- .../variables/gov/hud/hud_medical_expenses.py | 13 +- ..._general_relief_countable_income_person.py | 4 + .../dese/ccs/income/mo_ccs_adjusted_income.py | 13 +- .../nd_renters_refund_income.py | 7 + .../nj_medical_expense_deduction.py | 13 +- .../dhs/ccw/income/pa_ccw_medical_expenses.py | 11 +- .../snap_allowable_medical_expenses.py | 6 +- .../spm_unit_health_insurance_premiums.py | 9 +- ...spm_unit_medical_out_of_pocket_expenses.py | 5 +- 23 files changed, 654 insertions(+), 16 deletions(-) create mode 100644 changelog.d/hub-premium-input-contract-1010.fixed.md create mode 100644 policyengine_us/tests/core/test_premium_input_contract.py create mode 100644 policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/categories/medically_needy/medicaid_medically_needy_medical_expenses.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/hud/hud_medical_expenses.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/usda/snap/income/deductions/snap_allowable_medical_expenses.yaml diff --git a/changelog.d/hub-premium-input-contract-1010.fixed.md b/changelog.d/hub-premium-input-contract-1010.fixed.md new file mode 100644 index 00000000000..09085c9df45 --- /dev/null +++ b/changelog.d/hub-premium-input-contract-1010.fixed.md @@ -0,0 +1 @@ +Document disjoint pretax and non-pretax health premium inputs, include employee-paid pretax premiums in applicable benefit and New Jersey medical deductions, exclude pretax premiums from North Dakota renter-refund income, and correct CRFB broad-income premium addbacks. diff --git a/policyengine_us/parameters/gov/contrib/crfb/surtax/increased_base/sources.yaml b/policyengine_us/parameters/gov/contrib/crfb/surtax/increased_base/sources.yaml index a978ed74f69..43a2f2c3e28 100644 --- a/policyengine_us/parameters/gov/contrib/crfb/surtax/increased_base/sources.yaml +++ b/policyengine_us/parameters/gov/contrib/crfb/surtax/increased_base/sources.yaml @@ -27,8 +27,13 @@ values: - foreign_earned_income_exclusion # Tax-free bond interest - tax_exempt_interest_income - # Employer-provided health insurance (premiums) - - health_insurance_premiums + # Employer-provided health insurance: employer-paid premiums and the + # employee's pretax payroll contribution. Including the employee's + # pretax contribution is inferred from the broad income definition and + # IRS Rev. Rul. 2002-3's treatment of salary-reduction premiums as + # employer-provided coverage; after-tax premiums use income in AGI. + - employer_sponsored_insurance_premiums + - pre_tax_health_insurance_premiums metadata: unit: list @@ -37,3 +42,5 @@ metadata: reference: - title: "Committee for a Responsible Federal Budget, Break Glass: A Plan for the Next Economic Shock, footnote 2" href: https://www.crfb.org/papers/break-glass-plan-next-economic-shock + - title: IRS Revenue Ruling 2002-3, salary-reduction health insurance premiums + href: https://www.irs.gov/pub/irs-drop/rr-02-3.pdf diff --git a/policyengine_us/tests/core/test_premium_input_contract.py b/policyengine_us/tests/core/test_premium_input_contract.py new file mode 100644 index 00000000000..be193f8ab57 --- /dev/null +++ b/policyengine_us/tests/core/test_premium_input_contract.py @@ -0,0 +1,195 @@ +"""Premium payment methods are disjoint inputs with consumer-specific treatment. + +Moving employee-paid premiums from non-pretax payments to pretax payroll +deductions leaves total medical spending unchanged. SNAP, HUD, Medicaid +spenddown, child-care deductions, Orange County General Relief and New Jersey +medical expenses count both payment methods. The CRFB broad income base adds +back the pretax premium excluded from federal wages. Michigan's wage exclusion +and non-pretax premium deduction likewise leave household resources unchanged. +North Dakota excludes pretax premiums from income and deducts non-pretax +premiums as medical expenses, also leaving renters' refund income unchanged. + +Federal taxable and FICA wages, ACA/Medicaid MAGI, and tax deductions that +exclude pretax premiums distinguish the payment methods. Program premiums and +eligibility are held fixed here: changing MAGI can otherwise legitimately change +Marketplace, CHIP or Medicaid premiums, obscuring the spending invariant. + +Each generated batch uses fresh vectorized simulations across several states; +no calculated arrays are shared between the two situations or across examples. +""" + +import numpy as np +from hypothesis import HealthCheck, example, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + + +YEAR = 2026 +MONTH = "2026-01" +STATES = ("CA", "MO", "PA", "NJ", "OH", "MI", "ND") +SURTAX_ON = { + "gov.contrib.crfb.surtax.in_effect": {"2000-01-01.2100-12-31": True}, + "gov.contrib.crfb.surtax.increased_base.in_effect": {"2000-01-01.2100-12-31": True}, +} + + +@st.composite +def premium_transfers(draw): + total = draw(st.integers(0, 12_000)) + pretax = draw(st.integers(0, total)) + transfer = draw(st.integers(0, total - pretax)) + return total, pretax, transfer + + +def _situation(batch, moved): + people = {} + tax_units = {} + spm_units = {} + households = {} + for row, (total, initial_pretax, transfer) in enumerate(batch): + pretax = initial_pretax + (transfer if moved else 0) + non_pretax = total - pretax + for state in STATES: + key = f"{state}_{row}" + people[key] = { + "age": {YEAR: 40}, + "is_tax_unit_head": {YEAR: True}, + "employment_income": {YEAR: 250_000}, + "pre_tax_health_insurance_premiums": {YEAR: pretax}, + # These are alternative representations used by different + # consumers, not additive premiums within the non-pretax set. + "health_insurance_premiums": {YEAR: non_pretax}, + "health_insurance_premiums_without_medicare_part_b": {YEAR: non_pretax}, + "other_health_insurance_premiums": {YEAR: non_pretax}, + "other_medical_expenses": {YEAR: 300}, + "over_the_counter_health_expenses": {YEAR: 100}, + "medicare_enrolled": {YEAR: False}, + "is_medicare_eligible": {YEAR: False}, + "medicare_part_a_premium": {YEAR: 0}, + "medicare_part_b_premium": {YEAR: 0}, + "income_adjusted_part_d_premium_surcharge": {YEAR: 0}, + "employer_sponsored_insurance_premiums": {YEAR: 0}, + "self_employed_health_insurance_ald_person": {YEAR: 0}, + "general_assistance": {YEAR: 0}, + "ca_oc_general_relief_countable_earned_income": {MONTH: 5_000}, + "ca_oc_general_relief_gross_unearned_income": {MONTH: 0}, + "ca_oc_general_relief_receives_other_cash_assistance": {MONTH: False}, + } + tax_units[key] = { + "members": [key], + "chip_premium": {YEAR: 0}, + "medicaid_premium": {YEAR: 0}, + "marketplace_net_premium": {YEAR: 0}, + "above_the_line_deductions": {YEAR: 0}, + "self_employed_health_insurance_ald": {YEAR: 0}, + "dependents_self_employed_health_insurance_ald": {YEAR: 0}, + "medical_expense_deduction": {YEAR: 0}, + "tax_unit_itemizes": {YEAR: False}, + "taxable_income": {YEAR: 300_000}, + "nj_agi": {YEAR: 50_000}, + "oh_employer_subsidized_health_plan_eligible": {YEAR: True}, + } + spm_units[key] = { + "members": [key], + "tanf": {YEAR: 0}, + "mo_ccs_countable_income": {MONTH: 5_000}, + "pa_ccw_countable_income": {YEAR: 60_000}, + "pa_ccw_stepparent_deduction": {MONTH: 0}, + } + households[key] = { + "members": [key], + "state_code": {YEAR: state}, + "in_oc": {YEAR: state == "CA"}, + } + return { + "people": people, + "tax_units": tax_units, + "spm_units": spm_units, + "households": households, + } + + +@settings( + max_examples=8, + deadline=None, + suppress_health_check=[HealthCheck.too_slow], +) +@example(batch=[(0, 0, 0), (12_000, 0, 12_000), (12_000, 6_000, 6_000)]) +@example(batch=[(12_000, 12_000, 0), (1, 0, 1)]) +@given(batch=st.lists(premium_transfers(), min_size=1, max_size=3)) +def test_premium_transfer_respects_each_consumers_tax_treatment(batch): + baseline = Simulation(situation=_situation(batch, False), reform=SURTAX_ON) + moved = Simulation(situation=_situation(batch, True), reform=SURTAX_ON) + transfer = np.repeat([row[2] for row in batch], len(STATES)) + total = np.repeat([row[0] for row in batch], len(STATES)) + states = np.tile(STATES, len(batch)) + + np.testing.assert_allclose( + baseline.calculate("spm_unit_medical_out_of_pocket_expenses", YEAR), + total + 400, + rtol=0, + atol=0.02, + ) + # North Dakota starts from raw market wages, excludes the pretax + # contribution once, and deducts only the remaining medical expenses. + np.testing.assert_allclose( + baseline.calculate("nd_renters_refund_income", YEAR), + (250_000 - total - 300) * (states == "ND"), + rtol=0, + atol=0.02, + ) + + invariant_consumers = ( + ("spm_unit_health_insurance_premiums", YEAR), + ("spm_unit_medical_out_of_pocket_expenses", YEAR), + ("snap_allowable_medical_expenses", YEAR), + ("hud_medical_expenses", YEAR), + ("medicaid_medically_needy_medical_expenses", YEAR), + ("mo_ccs_adjusted_income", MONTH), + ("pa_ccw_medical_expenses", YEAR), + ("pa_ccw_adjusted_income", YEAR), + ("ca_oc_general_relief_countable_income_person", MONTH), + ("nj_medical_expense_deduction", YEAR), + ("mi_household_resources", YEAR), + ("nd_renters_refund_income", YEAR), + ("agi_surtax", YEAR), + ) + for variable, period in invariant_consumers: + np.testing.assert_allclose( + moved.calculate(variable, period), + baseline.calculate(variable, period), + rtol=0, + atol=0.02, + err_msg=variable, + ) + + tax_status_sensitive_consumers = ( + "irs_employment_income", + "payroll_tax_gross_wages", + "adjusted_gross_income", + "aca_magi", + "medicaid_magi", + "medicaid_magi_person", + "medical_expense_health_insurance_premiums", + ) + for variable in tax_status_sensitive_consumers: + np.testing.assert_allclose( + baseline.calculate(variable, YEAR) - moved.calculate(variable, YEAR), + transfer, + rtol=0, + atol=0.02, + err_msg=variable, + ) + + for variable, state in ( + ("oh_insured_unreimbursed_medical_care_expense_amount", "OH"), + ("mo_qualified_health_insurance_premiums", "MO"), + ): + np.testing.assert_allclose( + baseline.calculate(variable, YEAR) - moved.calculate(variable, YEAR), + transfer * (states == state), + rtol=0, + atol=0.02, + err_msg=variable, + ) diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/categories/medically_needy/medicaid_medically_needy_medical_expenses.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/categories/medically_needy/medicaid_medically_needy_medical_expenses.yaml new file mode 100644 index 00000000000..2cc8f19d7ee --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/categories/medically_needy/medicaid_medically_needy_medical_expenses.yaml @@ -0,0 +1,38 @@ +- name: Medicaid spenddown expenses include disjoint non-pretax and payroll premiums. + period: 2026 + input: + health_insurance_premiums: 1_800 + pre_tax_health_insurance_premiums: 2_400 + other_medical_expenses: 300 + medicare_enrolled: false + output: + medicaid_medically_needy_medical_expenses: 4_500 + +- name: Medicaid adds payroll premiums to decomposed non-pretax Medicare premiums. + period: 2026 + input: + health_insurance_premiums_without_medicare_part_b: 600 + medicare_part_b_premium: 1_200 + medicare_enrolled: true + pre_tax_health_insurance_premiums: 2_400 + other_medical_expenses: 300 + output: + medicaid_medically_needy_medical_expenses: 4_500 + +- name: Employee payroll premiums count toward Medicaid medically needy spenddown. + period: 2026 + input: + is_disabled: true + meets_ssi_disability_criteria: true + is_in_medicaid_medically_needy_category: true + is_adult_for_medicaid: false + is_optional_senior_or_disabled_for_medicaid: false + ssi_countable_income: 12_000 + ssi_countable_resources: 1_000 + state_code: MA + health_insurance_premiums: 2_000 + pre_tax_health_insurance_premiums: 4_000 + medicare_enrolled: false + output: + medicaid_medically_needy_medical_expenses: 6_000 + is_medically_needy_for_medicaid: true diff --git a/policyengine_us/tests/policy/baseline/gov/hud/hud_medical_expenses.yaml b/policyengine_us/tests/policy/baseline/gov/hud/hud_medical_expenses.yaml new file mode 100644 index 00000000000..b46ec776815 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/hud/hud_medical_expenses.yaml @@ -0,0 +1,38 @@ +- name: HUD medical expenses sum disjoint premiums across household members. + period: 2026 + input: + people: + head: + health_insurance_premiums: 1_800 + pre_tax_health_insurance_premiums: 2_400 + other_medical_expenses: 300 + medicare_enrolled: false + spouse: + health_insurance_premiums: 600 + pre_tax_health_insurance_premiums: 1_200 + other_medical_expenses: 200 + medicare_enrolled: false + spm_units: + spm_unit: + members: [head, spouse] + output: + hud_medical_expenses: 6_500 + +- name: HUD adds payroll premiums to decomposed non-pretax Medicare premiums. + period: 2026 + input: + health_insurance_premiums_without_medicare_part_b: 600 + medicare_part_b_premium: 1_200 + medicare_enrolled: true + pre_tax_health_insurance_premiums: 2_400 + other_medical_expenses: 300 + output: + hud_medical_expenses: 4_500 + +- name: HUD medical expenses include premiums paid entirely through payroll. + period: 2026 + input: + pre_tax_health_insurance_premiums: 2_400 + medicare_enrolled: false + output: + hud_medical_expenses: 2_400 diff --git a/policyengine_us/tests/policy/baseline/gov/local/ca/oc/general_relief/income/ca_oc_general_relief_countable_income_person.yaml b/policyengine_us/tests/policy/baseline/gov/local/ca/oc/general_relief/income/ca_oc_general_relief_countable_income_person.yaml index f2ebaf09e15..329bc59713f 100644 --- a/policyengine_us/tests/policy/baseline/gov/local/ca/oc/general_relief/income/ca_oc_general_relief_countable_income_person.yaml +++ b/policyengine_us/tests/policy/baseline/gov/local/ca/oc/general_relief/income/ca_oc_general_relief_countable_income_person.yaml @@ -37,3 +37,22 @@ output: # Earned 1,000 x (1 - 0.2) = 800; less 100 spousal support = 700. ca_oc_general_relief_countable_income_person: 700 + +- name: Pretax and after-tax premiums reduce countable gross earned income once. + period: 2026-01 + input: + employment_income: 1_000 * 12 + health_insurance_premiums: 40 * 12 + pre_tax_health_insurance_premiums: 60 * 12 + in_oc: true + output: + ca_oc_general_relief_countable_income_person: 700 + +- name: Moving all premiums to pretax payroll leaves countable income unchanged. + period: 2026-01 + input: + employment_income: 1_000 * 12 + pre_tax_health_insurance_premiums: 100 * 12 + in_oc: true + output: + ca_oc_general_relief_countable_income_person: 700 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mo/dese/ccs/income/mo_ccs_adjusted_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/mo/dese/ccs/income/mo_ccs_adjusted_income.yaml index 10ac417a1b4..30c47ef1f99 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mo/dese/ccs/income/mo_ccs_adjusted_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mo/dese/ccs/income/mo_ccs_adjusted_income.yaml @@ -61,3 +61,24 @@ output: # Countable 500/mo; premiums 1,000/mo; max(500 - 1,000, 0) = 0. mo_ccs_adjusted_income: 0 + +- name: Pretax and after-tax premiums are each deducted once from gross wages. + period: 2026-01 + input: + state_code: MO + employment_income: 36_000 + health_insurance_premiums: 2_400 + pre_tax_health_insurance_premiums: 3_600 + output: + mo_ccs_countable_income: 3_000 + mo_ccs_adjusted_income: 2_500 + +- name: Moving all premiums to pretax payroll leaves adjusted income unchanged. + period: 2026-01 + input: + state_code: MO + employment_income: 36_000 + pre_tax_health_insurance_premiums: 6_000 + output: + mo_ccs_countable_income: 3_000 + mo_ccs_adjusted_income: 2_500 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/tax/property/renters_refund/nd_renters_refund_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/tax/property/renters_refund/nd_renters_refund_income.yaml index 189fd53d517..671e226e15a 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nd/tax/property/renters_refund/nd_renters_refund_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/tax/property/renters_refund/nd_renters_refund_income.yaml @@ -62,3 +62,61 @@ state_code: ND output: nd_renters_refund_income: 12_000 + +- name: Case 4, after-tax premiums reduce renter refund income as medical expenses. + absolute_error_margin: 0.01 + period: 2025 + input: + age: 40 + state_code: ND + employment_income: 40_000 + health_insurance_premiums: 6_000 + pre_tax_health_insurance_premiums: 0 + output: + market_income: 40_000 + itemized_medical_expenses: 6_000 + nd_renters_refund_income: 34_000 + +- name: Case 5, mixed premiums reduce renter refund income once through each disjoint route. + absolute_error_margin: 0.01 + period: 2025 + input: + age: 40 + state_code: ND + employment_income: 40_000 + health_insurance_premiums: 3_000 + pre_tax_health_insurance_premiums: 3_000 + output: + market_income: 40_000 + irs_employment_income: 37_000 + itemized_medical_expenses: 3_000 + nd_renters_refund_income: 34_000 + +- name: Case 6, pretax premiums reduce renter refund income as excluded wage income. + absolute_error_margin: 0.01 + period: 2025 + input: + age: 40 + state_code: ND + employment_income: 40_000 + health_insurance_premiums: 0 + pre_tax_health_insurance_premiums: 6_000 + output: + market_income: 40_000 + irs_employment_income: 34_000 + itemized_medical_expenses: 0 + nd_renters_refund_income: 34_000 + +- name: Case 7, employer-paid premiums do not reduce renter refund income. + absolute_error_margin: 0.01 + period: 2025 + input: + age: 40 + state_code: ND + employment_income: 40_000 + health_insurance_premiums: 0 + pre_tax_health_insurance_premiums: 0 + employer_sponsored_insurance_premiums: 6_000 + output: + market_income: 40_000 + nd_renters_refund_income: 40_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nj/tax/income/deductions/nj_medical_expense_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/nj/tax/income/deductions/nj_medical_expense_deduction.yaml index ddcd2cb333c..85a9e3443cf 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nj/tax/income/deductions/nj_medical_expense_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nj/tax/income/deductions/nj_medical_expense_deduction.yaml @@ -55,3 +55,58 @@ # child's 1,000 of self-employed health insurance is deducted too. nj_agi: 50_000 nj_medical_expense_deduction: 1_000 + +- name: New Jersey allows after-tax premiums above two percent of gross income + period: 2025 + absolute_error_margin: 0.01 + input: + age: 40 + employment_income: 40_000 + health_insurance_premiums: 6_000 + pre_tax_health_insurance_premiums: 0 + state_code: NJ + output: + nj_agi: 40_000 + nj_medical_expense_deduction: 5_200 + +- name: New Jersey counts disjoint payroll and after-tax premiums once each + period: 2025 + absolute_error_margin: 0.01 + input: + age: 40 + employment_income: 40_000 + health_insurance_premiums: 3_000 + pre_tax_health_insurance_premiums: 3_000 + state_code: NJ + output: + nj_agi: 40_000 + irs_employment_income: 37_000 + nj_medical_expense_deduction: 5_200 + +- name: New Jersey allows federally pretax payroll premiums included in state wages + period: 2025 + absolute_error_margin: 0.01 + input: + age: 40 + employment_income: 40_000 + health_insurance_premiums: 0 + pre_tax_health_insurance_premiums: 6_000 + state_code: NJ + output: + nj_agi: 40_000 + irs_employment_income: 34_000 + nj_medical_expense_deduction: 5_200 + +- name: Employer-paid premiums are not employee medical expenses in New Jersey + period: 2025 + absolute_error_margin: 0.01 + input: + age: 40 + employment_income: 40_000 + health_insurance_premiums: 0 + pre_tax_health_insurance_premiums: 0 + employer_sponsored_insurance_premiums: 6_000 + state_code: NJ + output: + nj_agi: 40_000 + nj_medical_expense_deduction: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/pa/dhs/ccw/pa_ccw_adjusted_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/pa/dhs/ccw/pa_ccw_adjusted_income.yaml index be942e60178..2424d08202e 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/pa/dhs/ccw/pa_ccw_adjusted_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/pa/dhs/ccw/pa_ccw_adjusted_income.yaml @@ -141,3 +141,25 @@ # Adjusted monthly: 5,000 - 587 = 4,413 # Annual: 4,413 * 12 = 52,956 pa_ccw_adjusted_income: 52_956 + +- name: Pretax and after-tax premiums both count toward the medical deduction. + period: 2026 + input: + state_code: PA + employment_income: 36_000 + health_insurance_premiums: 2_400 + pre_tax_health_insurance_premiums: 3_600 + output: + pa_ccw_medical_expenses: 6_000 + # Gross wages 36,000 less premiums 6,000 above the 3,600 threshold. + pa_ccw_adjusted_income: 33_600 + +- name: Moving all premiums to pretax payroll leaves the medical deduction unchanged. + period: 2026 + input: + state_code: PA + employment_income: 36_000 + pre_tax_health_insurance_premiums: 6_000 + output: + pa_ccw_medical_expenses: 6_000 + pa_ccw_adjusted_income: 33_600 diff --git a/policyengine_us/tests/policy/baseline/gov/usda/snap/income/deductions/snap_allowable_medical_expenses.yaml b/policyengine_us/tests/policy/baseline/gov/usda/snap/income/deductions/snap_allowable_medical_expenses.yaml new file mode 100644 index 00000000000..3e2d3b7f9e7 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/usda/snap/income/deductions/snap_allowable_medical_expenses.yaml @@ -0,0 +1,29 @@ +- name: SNAP medical expenses include disjoint non-pretax and payroll premiums. + period: 2026 + input: + health_insurance_premiums: 1_800 + pre_tax_health_insurance_premiums: 2_400 + other_medical_expenses: 300 + over_the_counter_health_expenses: 900 + medicare_enrolled: false + output: + snap_allowable_medical_expenses: 4_500 + +- name: SNAP adds payroll premiums to decomposed non-pretax Medicare premiums. + period: 2026 + input: + health_insurance_premiums_without_medicare_part_b: 600 + medicare_part_b_premium: 1_200 + medicare_enrolled: true + pre_tax_health_insurance_premiums: 2_400 + other_medical_expenses: 300 + output: + snap_allowable_medical_expenses: 4_500 + +- name: SNAP medical expenses include premiums paid entirely through payroll. + period: 2026 + input: + pre_tax_health_insurance_premiums: 2_400 + medicare_enrolled: false + output: + snap_allowable_medical_expenses: 2_400 diff --git a/policyengine_us/tests/policy/baseline/household/income/spm_unit/spm_unit_medical_out_of_pocket_expenses.yaml b/policyengine_us/tests/policy/baseline/household/income/spm_unit/spm_unit_medical_out_of_pocket_expenses.yaml index 0d5aee02b6f..86061b7a0d9 100644 --- a/policyengine_us/tests/policy/baseline/household/income/spm_unit/spm_unit_medical_out_of_pocket_expenses.yaml +++ b/policyengine_us/tests/policy/baseline/household/income/spm_unit/spm_unit_medical_out_of_pocket_expenses.yaml @@ -100,3 +100,37 @@ marketplace_net_premium: 5_000 spm_unit_health_insurance_premiums: 5_000 spm_unit_medical_out_of_pocket_expenses: 5_100 + +- name: SPM medical expenses include disjoint payroll and non-pretax premiums. + period: 2026 + input: + pre_tax_health_insurance_premiums: 2_400 + other_health_insurance_premiums: 600 + chip_premium: 0 + medicaid_premium: 0 + marketplace_net_premium: 0 + medicare_part_a_premium: 0 + medicare_part_b_premium: 0 + income_adjusted_part_d_premium_surcharge: 0 + other_medical_expenses: 300 + over_the_counter_health_expenses: 100 + output: + spm_unit_health_insurance_premiums: 3_000 + spm_unit_medical_out_of_pocket_expenses: 3_400 + +- name: Moving all premiums to pretax payroll leaves SPM medical expenses unchanged. + period: 2026 + input: + pre_tax_health_insurance_premiums: 3_000 + other_health_insurance_premiums: 0 + chip_premium: 0 + medicaid_premium: 0 + marketplace_net_premium: 0 + medicare_part_a_premium: 0 + medicare_part_b_premium: 0 + income_adjusted_part_d_premium_surcharge: 0 + other_medical_expenses: 300 + over_the_counter_health_expenses: 100 + output: + spm_unit_health_insurance_premiums: 3_000 + spm_unit_medical_out_of_pocket_expenses: 3_400 diff --git a/policyengine_us/tests/policy/contrib/crfb/agi_surtax.yaml b/policyengine_us/tests/policy/contrib/crfb/agi_surtax.yaml index f505b72c1c1..d2c32a7336d 100644 --- a/policyengine_us/tests/policy/contrib/crfb/agi_surtax.yaml +++ b/policyengine_us/tests/policy/contrib/crfb/agi_surtax.yaml @@ -18,6 +18,61 @@ output: agi_surtax: 500 +- name: Expanded base restores employee pretax premiums and adds employer-paid premiums. + period: 2026 + reforms: policyengine_us.reforms.crfb.agi_surtax.agi_surtax_reform_object + input: + gov.contrib.crfb.surtax.in_effect: true + gov.contrib.crfb.surtax.increased_base.in_effect: true + employment_income: 150_000 + health_insurance_premiums: 4_000 + pre_tax_health_insurance_premiums: 6_000 + employer_sponsored_insurance_premiums: 10_000 + filing_status: SINGLE + output: + adjusted_gross_income: 144_000 + # 144,000 AGI + 6,000 pretax + 10,000 employer premiums = 160,000. + agi_surtax: 600 + +- name: Expanded base is unchanged when employee premiums are paid entirely after tax. + period: 2026 + reforms: policyengine_us.reforms.crfb.agi_surtax.agi_surtax_reform_object + input: + gov.contrib.crfb.surtax.in_effect: true + gov.contrib.crfb.surtax.increased_base.in_effect: true + employment_income: 150_000 + health_insurance_premiums: 10_000 + employer_sponsored_insurance_premiums: 10_000 + filing_status: SINGLE + output: + adjusted_gross_income: 150_000 + # 150,000 AGI + 10,000 employer premiums = the same 160,000 base. + agi_surtax: 600 + +- name: Expanded base does not add after-tax employee premiums to supplied AGI. + period: 2026 + reforms: policyengine_us.reforms.crfb.agi_surtax.agi_surtax_reform_object + input: + gov.contrib.crfb.surtax.in_effect: true + gov.contrib.crfb.surtax.increased_base.in_effect: true + adjusted_gross_income: 110_000 + health_insurance_premiums: 20_000 + filing_status: SINGLE + output: + agi_surtax: 100 + +- name: Expanded base restores pretax employee premiums excluded from supplied AGI. + period: 2026 + reforms: policyengine_us.reforms.crfb.agi_surtax.agi_surtax_reform_object + input: + gov.contrib.crfb.surtax.in_effect: true + gov.contrib.crfb.surtax.increased_base.in_effect: true + adjusted_gross_income: 110_000 + pre_tax_health_insurance_premiums: 20_000 + filing_status: SINGLE + output: + agi_surtax: 300 + - name: Joint couple below threshold period: 2026 reforms: policyengine_us.reforms.crfb.agi_surtax.agi_surtax_reform_object diff --git a/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/medically_needy/medicaid_medically_needy_medical_expenses.py b/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/medically_needy/medicaid_medically_needy_medical_expenses.py index cd95bdbce86..bc8392062c4 100644 --- a/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/medically_needy/medicaid_medically_needy_medical_expenses.py +++ b/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/medically_needy/medicaid_medically_needy_medical_expenses.py @@ -6,11 +6,20 @@ class medicaid_medically_needy_medical_expenses(Variable): entity = Person label = "Medicaid medically needy medical expenses" unit = USD - documentation = "Medical expenses used for Medicaid medically needy spenddown." + documentation = ( + "Medical expenses used for Medicaid medically needy spenddown. " + "Includes employee-paid health insurance premiums from pretax payroll " + "deductions and disjoint non-pretax amounts, excluding expenses payable " + "by third parties." + ) definition_period = YEAR - reference = "https://www.law.cornell.edu/cfr/text/42/part-435/subpart-D" + reference = ( + "https://www.ecfr.gov/current/title-42/section-435.831#p-435.831(d)", + "https://www.ecfr.gov/current/title-42/section-435.831#p-435.831(e)(1)", + ) adds = [ "medical_expense_health_insurance_premiums", + "pre_tax_health_insurance_premiums", "other_medical_expenses", ] diff --git a/policyengine_us/variables/gov/hud/hud_medical_expenses.py b/policyengine_us/variables/gov/hud/hud_medical_expenses.py index bea476968e1..af53ee69dbd 100644 --- a/policyengine_us/variables/gov/hud/hud_medical_expenses.py +++ b/policyengine_us/variables/gov/hud/hud_medical_expenses.py @@ -6,11 +6,20 @@ class hud_medical_expenses(Variable): entity = SPMUnit label = "HUD medical expenses" unit = USD - documentation = "Medical expenses considered in HUD adjusted income." + documentation = ( + "Medical expenses considered in HUD adjusted income, including " + "unreimbursed employee-paid health insurance premiums whether paid " + "through pretax payroll deductions or from non-pretax funds." + ) definition_period = YEAR - reference = "https://www.law.cornell.edu/cfr/text/24/5.611" + reference = ( + "https://www.ecfr.gov/current/title-24/section-5.603", + "https://www.ecfr.gov/current/title-24/section-5.611", + "https://www.hud.gov/sites/dfiles/OCHCO/documents/2023-27pihn.pdf#page=37", + ) adds = [ "medical_expense_health_insurance_premiums", + "pre_tax_health_insurance_premiums", "other_medical_expenses", ] diff --git a/policyengine_us/variables/gov/local/ca/oc/general_relief/income/ca_oc_general_relief_countable_income_person.py b/policyengine_us/variables/gov/local/ca/oc/general_relief/income/ca_oc_general_relief_countable_income_person.py index 66b1c325fff..5f9bdf0629c 100644 --- a/policyengine_us/variables/gov/local/ca/oc/general_relief/income/ca_oc_general_relief_countable_income_person.py +++ b/policyengine_us/variables/gov/local/ca/oc/general_relief/income/ca_oc_general_relief_countable_income_person.py @@ -24,11 +24,15 @@ def formula(person, period, parameters): "ca_oc_general_relief_gross_unearned_income", ], ) + # Section 70.2.o(4) and .p(1) allow medical insurance payments; + # 70.2.a(1) counts earnings diverted to a third party as gross income. + # Employee pretax premiums therefore remain deductible here. deductions = add( person, period, [ "health_insurance_premiums", + "pre_tax_health_insurance_premiums", "child_support_expense", "alimony_expense", ], diff --git a/policyengine_us/variables/gov/states/mo/dese/ccs/income/mo_ccs_adjusted_income.py b/policyengine_us/variables/gov/states/mo/dese/ccs/income/mo_ccs_adjusted_income.py index af0a1d0c7d1..e28cd45ad1e 100644 --- a/policyengine_us/variables/gov/states/mo/dese/ccs/income/mo_ccs_adjusted_income.py +++ b/policyengine_us/variables/gov/states/mo/dese/ccs/income/mo_ccs_adjusted_income.py @@ -8,12 +8,19 @@ class mo_ccs_adjusted_income(Variable): definition_period = MONTH unit = USD defined_for = StateCode.MO - reference = "https://www.law.cornell.edu/regulations/missouri/5-CSR-25-200-050" + reference = ( + "https://www.sos.mo.gov/CMSImages/AdRules/csr/current/5csr/5c25-200.pdf#page=2" + ) def formula(spm_unit, period, parameters): countable_income = spm_unit("mo_ccs_countable_income", period) # Medical insurance premiums are the only deduction from monthly gross # income (5 CSR 25-200.050(2); Manual sec. 5.7 Deductions from Gross - # Income). - premiums = add(spm_unit, period, ["health_insurance_premiums"]) + # Income). The rule includes premiums paid by household members, + # whether through pretax payroll deductions or after-tax payments. + premiums = add( + spm_unit, + period, + ["health_insurance_premiums", "pre_tax_health_insurance_premiums"], + ) return max_(countable_income - premiums, 0) diff --git a/policyengine_us/variables/gov/states/nd/tax/property/renters_refund/nd_renters_refund_income.py b/policyengine_us/variables/gov/states/nd/tax/property/renters_refund/nd_renters_refund_income.py index cf6e3da8f99..1a39ef11684 100644 --- a/policyengine_us/variables/gov/states/nd/tax/property/renters_refund/nd_renters_refund_income.py +++ b/policyengine_us/variables/gov/states/nd/tax/property/renters_refund/nd_renters_refund_income.py @@ -15,8 +15,15 @@ class nd_renters_refund_income(Variable): def formula(tax_unit, period, parameters): p = parameters(period).gov.states.nd.tax.property.renters_refund + # NDCC 57-02-08.1(5)(c) excludes income excluded by federal or state + # law. market_income includes wages before payroll deductions, so + # subtract the separate pretax premium input once from this base. + # These premiums are disjoint from itemized_medical_expenses, + # whose medical definition follows state income tax law. + excluded_premiums = add(tax_unit, period, ["pre_tax_health_insurance_premiums"]) return max_( add(tax_unit, period, p.income_sources) + - excluded_premiums - tax_unit("itemized_medical_expenses", period), 0, ) diff --git a/policyengine_us/variables/gov/states/nj/tax/income/deductions/nj_medical_expense_deduction.py b/policyengine_us/variables/gov/states/nj/tax/income/deductions/nj_medical_expense_deduction.py index 03bbb514baf..85b8b1584aa 100644 --- a/policyengine_us/variables/gov/states/nj/tax/income/deductions/nj_medical_expense_deduction.py +++ b/policyengine_us/variables/gov/states/nj/tax/income/deductions/nj_medical_expense_deduction.py @@ -8,6 +8,11 @@ class nj_medical_expense_deduction(Variable): unit = USD definition_period = YEAR defined_for = StateCode.NJ + reference = ( + "https://www.nj.gov/treasury/taxation/njit13.shtml", + "https://www.nj.gov/treasury/taxation/pdf/pubs/tb/tb39r.pdf", + "https://www.nj.gov/treasury/taxation/pdf/current/1040i.pdf#page=24", + ) def formula(tax_unit, period, parameters): p = parameters(period).gov.states.nj.tax.income.deductions.medical_expenses @@ -21,7 +26,13 @@ def formula(tax_unit, period, parameters): "dependents_self_employed_health_insurance_ald", ], ) - medical_expenses = tax_unit("itemized_medical_expenses", period) + # New Jersey includes salary-reduction cafeteria-plan premiums in + # taxable wages (TB-39(R)), so these employee-paid premiums also + # qualify for the medical expense deduction. The separate payroll + # input is disjoint from the after-tax premium inputs. + medical_expenses = tax_unit("itemized_medical_expenses", period) + add( + tax_unit, period, ["pre_tax_health_insurance_premiums"] + ) agi = tax_unit("nj_agi", period) floor = p.rate * agi applicable_medical_expenses = max_(0, medical_expenses - floor) diff --git a/policyengine_us/variables/gov/states/pa/dhs/ccw/income/pa_ccw_medical_expenses.py b/policyengine_us/variables/gov/states/pa/dhs/ccw/income/pa_ccw_medical_expenses.py index d8e0b7ad524..fc7562c66fb 100644 --- a/policyengine_us/variables/gov/states/pa/dhs/ccw/income/pa_ccw_medical_expenses.py +++ b/policyengine_us/variables/gov/states/pa/dhs/ccw/income/pa_ccw_medical_expenses.py @@ -6,12 +6,19 @@ class pa_ccw_medical_expenses(Variable): entity = SPMUnit label = "Pennsylvania CCW medical expenses" unit = USD - documentation = "Medical expenses deducted from Pennsylvania CCW income." + documentation = ( + "Medical expenses deducted from Pennsylvania CCW income, including " + "employee-paid health premiums paid through pretax payroll deductions " + "and after-tax payments." + ) definition_period = YEAR defined_for = StateCode.PA - reference = "https://www.pacodeandbulletin.gov/secure/pacode/data/055/chapter3042/055_3042.pdf#page=18" + reference = "https://www.pacodeandbulletin.gov/secure/pacode/data/055/chapter3042/055_3042.pdf#page=61" + # Appendix A Part I(A) counts wages before health insurance deductions; + # Part II(C) permits unreimbursed health care premiums without a tax test. adds = [ "medical_expense_health_insurance_premiums", + "pre_tax_health_insurance_premiums", "other_medical_expenses", ] diff --git a/policyengine_us/variables/gov/usda/snap/income/deductions/snap_allowable_medical_expenses.py b/policyengine_us/variables/gov/usda/snap/income/deductions/snap_allowable_medical_expenses.py index fddfa2f03a5..f9767827dd6 100644 --- a/policyengine_us/variables/gov/usda/snap/income/deductions/snap_allowable_medical_expenses.py +++ b/policyengine_us/variables/gov/usda/snap/income/deductions/snap_allowable_medical_expenses.py @@ -10,6 +10,7 @@ class snap_allowable_medical_expenses(Variable): reference = [ "https://www.law.cornell.edu/uscode/text/7/2014#e_5", "https://www.law.cornell.edu/cfr/text/7/273.9#d_3", + "https://www.ecfr.gov/current/title-7/section-273.9#p-273.9(d)(3)(iv)", ] documentation = ( "Medical expenses allowable for SNAP's excess medical expense " @@ -21,10 +22,13 @@ class snap_allowable_medical_expenses(Variable): "insurance premiums, Medicare premiums, cost sharing, medical " "supplies, transportation, and related services. Current modeling " "uses health insurance premiums and other medical expenses, excluding " - "general over-the-counter health expenses." + "general over-the-counter health expenses. Employee-paid premiums " + "include both pretax payroll deductions and the disjoint non-pretax " + "premium amounts." ) adds = [ "medical_expense_health_insurance_premiums", + "pre_tax_health_insurance_premiums", "other_medical_expenses", ] diff --git a/policyengine_us/variables/household/income/spm_unit/spm_unit_health_insurance_premiums.py b/policyengine_us/variables/household/income/spm_unit/spm_unit_health_insurance_premiums.py index 6be89db1ae8..9d31a40245b 100644 --- a/policyengine_us/variables/household/income/spm_unit/spm_unit_health_insurance_premiums.py +++ b/policyengine_us/variables/household/income/spm_unit/spm_unit_health_insurance_premiums.py @@ -7,14 +7,21 @@ class spm_unit_health_insurance_premiums(Variable): label = "SPM unit health insurance premiums" unit = USD definition_period = YEAR + reference = ( + "https://www2.census.gov/programs-surveys/supplemental-poverty-measure/datasets/spm/spm_techdoc.pdf#page=18", + "https://www2.census.gov/library/working-papers/2026/adrm/cbsm/rsm2026-04.pdf#page=65", + ) documentation = ( "Health insurance premium expenses for an SPM unit, combining a " "data-imputed other premium component with modeled premium components " "that can respond to policy reforms, including Medicare Part A and " - "Part B premiums and the Part D IRMAA surcharge paid out of pocket." + "Part B premiums and the Part D IRMAA surcharge paid out of pocket. " + "Includes employee-paid pretax payroll premiums in addition to the " + "disjoint non-pretax premium components; excludes employer contributions." ) adds = [ + "pre_tax_health_insurance_premiums", "other_health_insurance_premiums", "chip_premium", "medicaid_premium", diff --git a/policyengine_us/variables/household/income/spm_unit/spm_unit_medical_out_of_pocket_expenses.py b/policyengine_us/variables/household/income/spm_unit/spm_unit_medical_out_of_pocket_expenses.py index f06025f553f..ba4c6c0cd64 100644 --- a/policyengine_us/variables/household/income/spm_unit/spm_unit_medical_out_of_pocket_expenses.py +++ b/policyengine_us/variables/household/income/spm_unit/spm_unit_medical_out_of_pocket_expenses.py @@ -10,8 +10,9 @@ class spm_unit_medical_out_of_pocket_expenses(Variable): documentation = ( "Total medical out-of-pocket expenses at the SPM unit level, " "combining health insurance premiums with non-premium medical " - "expenses. Health insurance premiums include other health insurance " - "premiums plus modeled Marketplace, CHIP, Medicaid, and Medicare " + "expenses. Health insurance premiums include pretax payroll premiums " + "and disjoint other health insurance premiums plus modeled " + "Marketplace, CHIP, Medicaid, and Medicare " "Part A and Part B premiums net of Medicare Savings Program coverage " "where modeled, plus the Part D IRMAA surcharge. " "Non-premium expenses include other medical expenses and " From 2344a1e3579f6e878501faddaecc55d97f3f7778 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 07:15:21 -0400 Subject: [PATCH 03/15] Keep generated premium households in independent family groups --- policyengine_us/tests/core/test_premium_input_contract.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/policyengine_us/tests/core/test_premium_input_contract.py b/policyengine_us/tests/core/test_premium_input_contract.py index be193f8ab57..d604a9d355e 100644 --- a/policyengine_us/tests/core/test_premium_input_contract.py +++ b/policyengine_us/tests/core/test_premium_input_contract.py @@ -107,6 +107,8 @@ def _situation(batch, moved): "tax_units": tax_units, "spm_units": spm_units, "households": households, + "marital_units": {key: {"members": [key]} for key in people}, + "families": {key: {"members": [key]} for key in people}, } From 0073f9c82cabeb5240e38e35f41b0cce0be9769c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 07:23:18 -0400 Subject: [PATCH 04/15] Reuse reference policy safely in premium transfer property --- .../tests/core/test_premium_input_contract.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/policyengine_us/tests/core/test_premium_input_contract.py b/policyengine_us/tests/core/test_premium_input_contract.py index d604a9d355e..e1e1615ab98 100644 --- a/policyengine_us/tests/core/test_premium_input_contract.py +++ b/policyengine_us/tests/core/test_premium_input_contract.py @@ -23,6 +23,8 @@ from hypothesis import strategies as st from policyengine_us import Simulation +from policyengine_us.reforms.crfb.agi_surtax import agi_surtax_reform_object +from policyengine_us.system import system YEAR = 2026 @@ -121,8 +123,16 @@ def _situation(batch, moved): @example(batch=[(12_000, 12_000, 0), (1, 0, 1)]) @given(batch=st.lists(premium_transfers(), min_size=1, max_size=3)) def test_premium_transfer_respects_each_consumers_tax_treatment(batch): - baseline = Simulation(situation=_situation(batch, False), reform=SURTAX_ON) - moved = Simulation(situation=_situation(batch, True), reform=SURTAX_ON) + # Supplying the read-only reference system avoids rebuilding the country + # for every generated example. Simulation clones it before applying these + # reforms, giving each situation private policy state and result arrays. + reform = (SURTAX_ON, agi_surtax_reform_object) + baseline = Simulation( + situation=_situation(batch, False), tax_benefit_system=system, reform=reform + ) + moved = Simulation( + situation=_situation(batch, True), tax_benefit_system=system, reform=reform + ) transfer = np.repeat([row[2] for row in batch], len(STATES)) total = np.repeat([row[0] for row in batch], len(STATES)) states = np.tile(STATES, len(batch)) From 1d142d9efb57456c6bf7dd83be4cafc84765398a Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 07:59:06 -0400 Subject: [PATCH 05/15] Exclude qualified premium salary reductions from SSI wages --- .../hub-premium-input-contract-1010.fixed.md | 2 +- .../tests/core/test_premium_input_contract.py | 22 ++++++-- ...caid_medically_needy_medical_expenses.yaml | 18 +++---- .../gov/ssa/ssi/income/ssi_earned_income.yaml | 53 +++++++++++++++++++ ...dicaid_medically_needy_medical_expenses.py | 12 +++-- .../eligibility/income/ssi_earned_income.py | 19 ++++++- 6 files changed, 106 insertions(+), 20 deletions(-) diff --git a/changelog.d/hub-premium-input-contract-1010.fixed.md b/changelog.d/hub-premium-input-contract-1010.fixed.md index 09085c9df45..df91bf17b85 100644 --- a/changelog.d/hub-premium-input-contract-1010.fixed.md +++ b/changelog.d/hub-premium-input-contract-1010.fixed.md @@ -1 +1 @@ -Document disjoint pretax and non-pretax health premium inputs, include employee-paid pretax premiums in applicable benefit and New Jersey medical deductions, exclude pretax premiums from North Dakota renter-refund income, and correct CRFB broad-income premium addbacks. +Document disjoint pretax and non-pretax health premium inputs, include employee-paid pretax premiums in applicable benefit and New Jersey medical deductions, exclude pretax premiums from SSI wages and North Dakota renter-refund income, and correct CRFB broad-income premium addbacks. diff --git a/policyengine_us/tests/core/test_premium_input_contract.py b/policyengine_us/tests/core/test_premium_input_contract.py index e1e1615ab98..d30f3302e71 100644 --- a/policyengine_us/tests/core/test_premium_input_contract.py +++ b/policyengine_us/tests/core/test_premium_input_contract.py @@ -1,8 +1,8 @@ """Premium payment methods are disjoint inputs with consumer-specific treatment. Moving employee-paid premiums from non-pretax payments to pretax payroll -deductions leaves total medical spending unchanged. SNAP, HUD, Medicaid -spenddown, child-care deductions, Orange County General Relief and New Jersey +deductions leaves total medical spending unchanged. SNAP, HUD, +child-care deductions, Orange County General Relief and New Jersey medical expenses count both payment methods. The CRFB broad income base adds back the pretax premium excluded from federal wages. Michigan's wage exclusion and non-pretax premium deduction likewise leave household resources unchanged. @@ -10,7 +10,9 @@ premiums as medical expenses, also leaving renters' refund income unchanged. Federal taxable and FICA wages, ACA/Medicaid MAGI, and tax deductions that -exclude pretax premiums distinguish the payment methods. Program premiums and +exclude pretax premiums distinguish the payment methods. SSI also excludes +qualified salary-reduction premiums from wages before its earned-income +disregard. Program premiums and eligibility are held fixed here: changing MAGI can otherwise legitimately change Marketplace, CHIP or Medicaid premiums, obscuring the spending invariant. @@ -57,6 +59,7 @@ def _situation(batch, moved): people[key] = { "age": {YEAR: 40}, "is_tax_unit_head": {YEAR: True}, + "is_ssi_aged_blind_disabled": {YEAR: True}, "employment_income": {YEAR: 250_000}, "pre_tax_health_insurance_premiums": {YEAR: pretax}, # These are alternative representations used by different @@ -157,7 +160,6 @@ def test_premium_transfer_respects_each_consumers_tax_treatment(batch): ("spm_unit_medical_out_of_pocket_expenses", YEAR), ("snap_allowable_medical_expenses", YEAR), ("hud_medical_expenses", YEAR), - ("medicaid_medically_needy_medical_expenses", YEAR), ("mo_ccs_adjusted_income", MONTH), ("pa_ccw_medical_expenses", YEAR), ("pa_ccw_adjusted_income", YEAR), @@ -179,11 +181,13 @@ def test_premium_transfer_respects_each_consumers_tax_treatment(batch): tax_status_sensitive_consumers = ( "irs_employment_income", "payroll_tax_gross_wages", + "ssi_earned_income", "adjusted_gross_income", "aca_magi", "medicaid_magi", "medicaid_magi_person", "medical_expense_health_insurance_premiums", + "medicaid_medically_needy_medical_expenses", ) for variable in tax_status_sensitive_consumers: np.testing.assert_allclose( @@ -194,6 +198,16 @@ def test_premium_transfer_respects_each_consumers_tax_treatment(batch): err_msg=variable, ) + # These categorically eligible individuals remain above the earned-income + # exclusions. SSI's 50% earned-income disregard halves the wage change. + np.testing.assert_allclose( + baseline.calculate("ssi_countable_income", YEAR) + - moved.calculate("ssi_countable_income", YEAR), + transfer / 2, + rtol=0, + atol=0.02, + ) + for variable, state in ( ("oh_insured_unreimbursed_medical_care_expense_amount", "OH"), ("mo_qualified_health_insurance_premiums", "MO"), diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/categories/medically_needy/medicaid_medically_needy_medical_expenses.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/categories/medically_needy/medicaid_medically_needy_medical_expenses.yaml index 2cc8f19d7ee..7cf936802de 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/categories/medically_needy/medicaid_medically_needy_medical_expenses.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/categories/medically_needy/medicaid_medically_needy_medical_expenses.yaml @@ -1,4 +1,4 @@ -- name: Medicaid spenddown expenses include disjoint non-pretax and payroll premiums. +- name: SSI-based spenddown expenses do not repeat the pretax wage exclusion. period: 2026 input: health_insurance_premiums: 1_800 @@ -6,9 +6,9 @@ other_medical_expenses: 300 medicare_enrolled: false output: - medicaid_medically_needy_medical_expenses: 4_500 + medicaid_medically_needy_medical_expenses: 2_100 -- name: Medicaid adds payroll premiums to decomposed non-pretax Medicare premiums. +- name: SSI-based spenddown retains decomposed non-pretax Medicare premiums. period: 2026 input: health_insurance_premiums_without_medicare_part_b: 600 @@ -17,9 +17,9 @@ pre_tax_health_insurance_premiums: 2_400 other_medical_expenses: 300 output: - medicaid_medically_needy_medical_expenses: 4_500 + medicaid_medically_needy_medical_expenses: 2_100 -- name: Employee payroll premiums count toward Medicaid medically needy spenddown. +- name: Kansas spenddown does not deduct pretax premiums again from countable income. period: 2026 input: is_disabled: true @@ -27,12 +27,12 @@ is_in_medicaid_medically_needy_category: true is_adult_for_medicaid: false is_optional_senior_or_disabled_for_medicaid: false - ssi_countable_income: 12_000 + ssi_countable_income: 11_000 ssi_countable_resources: 1_000 - state_code: MA + state_code: KS health_insurance_premiums: 2_000 pre_tax_health_insurance_premiums: 4_000 medicare_enrolled: false output: - medicaid_medically_needy_medical_expenses: 6_000 - is_medically_needy_for_medicaid: true + medicaid_medically_needy_medical_expenses: 2_000 + is_medically_needy_for_medicaid: false diff --git a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/income/ssi_earned_income.yaml b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/income/ssi_earned_income.yaml index cc9acb9d8db..a8185d00ffe 100644 --- a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/income/ssi_earned_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/income/ssi_earned_income.yaml @@ -18,3 +18,56 @@ sstb_self_employment_income: 100_000 output: ssi_earned_income: 100_000 + +- name: After-tax health premiums remain part of SSI wages. + period: 2025 + input: + employment_income: 12_000 + health_insurance_premiums: 1_200 + output: + ssi_earned_income: 12_000 + +- name: Pretax payroll health premiums are excluded from SSI wages. + period: 2025 + input: + employment_income: 12_000 + pre_tax_health_insurance_premiums: 1_200 + output: + ssi_earned_income: 10_800 + +- name: Only the pretax portion of disjoint premiums is excluded from SSI wages. + period: 2025 + input: + employment_income: 12_000 + health_insurance_premiums: 600 + pre_tax_health_insurance_premiums: 600 + output: + ssi_earned_income: 11_400 + +- name: Excess pretax premiums cannot reduce SSI self-employment income. + period: 2025 + input: + employment_income: 1_000 + self_employment_income: 500 + sstb_self_employment_income: 250 + pre_tax_health_insurance_premiums: 2_000 + output: + ssi_earned_income: 750 + +- name: Traditional retirement deferrals remain SSI wages after excluding health premiums. + period: 2025 + input: + employment_income: 12_000 + traditional_401k_contributions: 2_400 + traditional_403b_contributions: 1_200 + pre_tax_health_insurance_premiums: 1_200 + output: + ssi_earned_income: 10_800 + +- name: Employer-paid premiums do not reduce SSI wages. + period: 2025 + input: + employment_income: 12_000 + employer_sponsored_insurance_premiums: 1_200 + output: + ssi_earned_income: 12_000 diff --git a/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/medically_needy/medicaid_medically_needy_medical_expenses.py b/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/medically_needy/medicaid_medically_needy_medical_expenses.py index bc8392062c4..e4702532c46 100644 --- a/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/medically_needy/medicaid_medically_needy_medical_expenses.py +++ b/policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/medically_needy/medicaid_medically_needy_medical_expenses.py @@ -7,19 +7,21 @@ class medicaid_medically_needy_medical_expenses(Variable): label = "Medicaid medically needy medical expenses" unit = USD documentation = ( - "Medical expenses used for Medicaid medically needy spenddown. " - "Includes employee-paid health insurance premiums from pretax payroll " - "deductions and disjoint non-pretax amounts, excluding expenses payable " - "by third parties." + "Annual expense proxy for SSI-based Medicaid medically needy spenddown. " + "Includes non-pretax health premiums and other medical expenses. " + "Qualified pretax payroll premiums are excluded through SSI earnings " + "rather than deducted again here. State-specific income and expense " + "sequencing, including states using gross-income payroll deductions, " + "is not modeled by this proxy." ) definition_period = YEAR reference = ( "https://www.ecfr.gov/current/title-42/section-435.831#p-435.831(d)", "https://www.ecfr.gov/current/title-42/section-435.831#p-435.831(e)(1)", + "https://khap.kdhe.ks.gov/KEESM/July_2026_Output/7000/7500-Determination-of-Financial-Eligibility.htm", ) adds = [ "medical_expense_health_insurance_premiums", - "pre_tax_health_insurance_premiums", "other_medical_expenses", ] diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/ssi_earned_income.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/ssi_earned_income.py index 8b84426daf1..5c493c0a7c8 100644 --- a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/ssi_earned_income.py +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/ssi_earned_income.py @@ -7,5 +7,22 @@ class ssi_earned_income(Variable): label = "SSI earned income" unit = USD definition_period = YEAR + reference = "https://secure.ssa.gov/poms.nsf/lnx/0500820102" + documentation = ( + "Earned income for SSI, excluding health insurance premiums paid " + "through pretax payroll salary reductions under a qualified " + "cafeteria plan. The disjoint after-tax premium inputs do not " + "reduce SSI wages." + ) - adds = "gov.ssa.ssi.income.sources.earned" + def formula(person, period, parameters): + sources = parameters(period).gov.ssa.ssi.income.sources.earned + earned_income = add(person, period, sources) + if "employment_income" not in sources: + return earned_income + # POMS SI 00820.102C.2 excludes salary-reduction premiums from + # wages; C.4 counts ordinary payroll deductions. Limit the exclusion + # to wages so it cannot reduce self-employment income. + wages = max_(person("employment_income", period), 0) + pretax_premiums = max_(person("pre_tax_health_insurance_premiums", period), 0) + return earned_income - min_(wages, pretax_premiums) From 37f5e57c75b5b40d122ace365c50eb0d18fb9df3 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 08:25:25 -0400 Subject: [PATCH 06/15] Preserve gross work earnings for the SSI SGA test --- .../tests/core/test_premium_input_contract.py | 4 ++- .../ssa/ssi/income/ssi_engaged_in_sga.yaml | 32 +++++++++++++++++++ .../eligibility/income/ssi_engaged_in_sga.py | 20 +++++++++++- 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/policyengine_us/tests/core/test_premium_input_contract.py b/policyengine_us/tests/core/test_premium_input_contract.py index d30f3302e71..311ca53eaf6 100644 --- a/policyengine_us/tests/core/test_premium_input_contract.py +++ b/policyengine_us/tests/core/test_premium_input_contract.py @@ -12,7 +12,8 @@ Federal taxable and FICA wages, ACA/Medicaid MAGI, and tax deductions that exclude pretax premiums distinguish the payment methods. SSI also excludes qualified salary-reduction premiums from wages before its earned-income -disregard. Program premiums and +disregard. The SGA work-earnings test retains payroll premium deductions. +Program premiums and eligibility are held fixed here: changing MAGI can otherwise legitimately change Marketplace, CHIP or Medicaid premiums, obscuring the spending invariant. @@ -168,6 +169,7 @@ def test_premium_transfer_respects_each_consumers_tax_treatment(batch): ("mi_household_resources", YEAR), ("nd_renters_refund_income", YEAR), ("agi_surtax", YEAR), + ("ssi_engaged_in_sga", YEAR), ) for variable, period in invariant_consumers: np.testing.assert_allclose( diff --git a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/income/ssi_engaged_in_sga.yaml b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/income/ssi_engaged_in_sga.yaml index 4e0cf90c2e8..ecc98e6faa3 100644 --- a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/income/ssi_engaged_in_sga.yaml +++ b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/income/ssi_engaged_in_sga.yaml @@ -21,3 +21,35 @@ is_blind: true output: ssi_engaged_in_sga: false + +- name: Pretax health premiums remain in SGA earnings across the threshold. + period: 2026 + input: + employment_income: 21_600 + pre_tax_health_insurance_premiums: 2_400 + is_blind: false + output: + # SSI payment wages are 1,600 monthly; SGA earnings remain 1,800. + ssi_earned_income: 19_200 + ssi_engaged_in_sga: true + +- name: After-tax health premiums also remain in SGA earnings. + period: 2026 + input: + employment_income: 21_600 + health_insurance_premiums: 2_400 + is_blind: false + output: + ssi_earned_income: 21_600 + ssi_engaged_in_sga: true + +- name: Excess pretax premiums cannot raise SGA earnings above gross wages. + period: 2026 + input: + employment_income: 12_000 + pre_tax_health_insurance_premiums: 24_000 + is_blind: false + output: + # Only 12,000 of excluded wages is restored, below the SGA threshold. + ssi_earned_income: 0 + ssi_engaged_in_sga: false diff --git a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/ssi_engaged_in_sga.py b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/ssi_engaged_in_sga.py index 5f41d4d0bca..d116dabb369 100644 --- a/policyengine_us/variables/gov/ssa/ssi/eligibility/income/ssi_engaged_in_sga.py +++ b/policyengine_us/variables/gov/ssa/ssi/eligibility/income/ssi_engaged_in_sga.py @@ -6,10 +6,28 @@ class ssi_engaged_in_sga(Variable): entity = Person label = "Income less than the SGA limit" definition_period = YEAR - reference = "https://www.ssa.gov/OP_Home/cfr20/416/416-0971.htm" + reference = ( + "https://www.ssa.gov/OP_Home/cfr20/416/416-0971.htm", + "https://secure.ssa.gov/poms.nsf/lnx/0410505010", + ) + documentation = ( + "Whether non-blind work earnings exceed the SGA limit. Payroll " + "health insurance premiums remain in these earnings, including " + "pretax salary reductions excluded from SSI payment income." + ) def formula(person, period, parameters): income = person("ssi_earned_income", period) + # POMS DI 10505.010A counts payroll insurance premiums for SGA. + # Restore exactly the wage-limited premium exclusion made by + # ssi_earned_income, preserving the prior gross work-earnings base. + sources = parameters(period).gov.ssa.ssi.income.sources.earned + if "employment_income" in sources: + wages = max_(person("employment_income", period), 0) + pretax_premiums = max_( + person("pre_tax_health_insurance_premiums", period), 0 + ) + income = income + min_(wages, pretax_premiums) monthly_income = income / MONTHS_IN_YEAR p = parameters(period).gov.ssa.sga From 5a036d2c4079722e642b6458cd201eb284dbe4a1 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 09:26:47 -0400 Subject: [PATCH 07/15] Point premium consumer citations to the relevant PDF pages --- .../gov/contrib/crfb/surtax/increased_base/sources.yaml | 4 ++-- .../income/ca_oc_general_relief_countable_income_person.py | 4 ++-- .../nj/tax/income/deductions/nj_medical_expense_deduction.py | 4 ++-- .../gov/states/pa/dhs/ccw/income/pa_ccw_medical_expenses.py | 3 ++- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/policyengine_us/parameters/gov/contrib/crfb/surtax/increased_base/sources.yaml b/policyengine_us/parameters/gov/contrib/crfb/surtax/increased_base/sources.yaml index 43a2f2c3e28..0aebd7562d2 100644 --- a/policyengine_us/parameters/gov/contrib/crfb/surtax/increased_base/sources.yaml +++ b/policyengine_us/parameters/gov/contrib/crfb/surtax/increased_base/sources.yaml @@ -42,5 +42,5 @@ metadata: reference: - title: "Committee for a Responsible Federal Budget, Break Glass: A Plan for the Next Economic Shock, footnote 2" href: https://www.crfb.org/papers/break-glass-plan-next-economic-shock - - title: IRS Revenue Ruling 2002-3, salary-reduction health insurance premiums - href: https://www.irs.gov/pub/irs-drop/rr-02-3.pdf + - title: "IRS Revenue Ruling 2002-3, salary-reduction health insurance premiums #page 1-2" + href: https://www.irs.gov/pub/irs-drop/rr-02-3.pdf#page=1 diff --git a/policyengine_us/variables/gov/local/ca/oc/general_relief/income/ca_oc_general_relief_countable_income_person.py b/policyengine_us/variables/gov/local/ca/oc/general_relief/income/ca_oc_general_relief_countable_income_person.py index 5f9bdf0629c..9155d19ebf9 100644 --- a/policyengine_us/variables/gov/local/ca/oc/general_relief/income/ca_oc_general_relief_countable_income_person.py +++ b/policyengine_us/variables/gov/local/ca/oc/general_relief/income/ca_oc_general_relief_countable_income_person.py @@ -8,8 +8,8 @@ class ca_oc_general_relief_countable_income_person(Variable): unit = USD definition_period = MONTH defined_for = "in_oc" - # Sections 70.2.o through 70.2.q (pp. 4-5). - reference = "https://www.ssa.ocgov.com/sites/ssa/files/2025-03/Income.pdf#page=4" + # Sections 70.2.a(1), .o(4), and .p(1): PDF pages 1, 4. + reference = "https://www.ssa.ocgov.com/sites/ssa/files/2025-03/Income.pdf#page=1" def formula(person, period, parameters): # NOTE: Section 70.2.p also allows mandatory federal/state income tax diff --git a/policyengine_us/variables/gov/states/nj/tax/income/deductions/nj_medical_expense_deduction.py b/policyengine_us/variables/gov/states/nj/tax/income/deductions/nj_medical_expense_deduction.py index 85b8b1584aa..39df98ce8ef 100644 --- a/policyengine_us/variables/gov/states/nj/tax/income/deductions/nj_medical_expense_deduction.py +++ b/policyengine_us/variables/gov/states/nj/tax/income/deductions/nj_medical_expense_deduction.py @@ -10,8 +10,8 @@ class nj_medical_expense_deduction(Variable): defined_for = StateCode.NJ reference = ( "https://www.nj.gov/treasury/taxation/njit13.shtml", - "https://www.nj.gov/treasury/taxation/pdf/pubs/tb/tb39r.pdf", - "https://www.nj.gov/treasury/taxation/pdf/current/1040i.pdf#page=24", + "https://www.nj.gov/treasury/taxation/pdf/pubs/tb/tb39r.pdf#page=1", + "https://www.nj.gov/treasury/taxation/pdf/current/1040i.pdf#page=25", ) def formula(tax_unit, period, parameters): diff --git a/policyengine_us/variables/gov/states/pa/dhs/ccw/income/pa_ccw_medical_expenses.py b/policyengine_us/variables/gov/states/pa/dhs/ccw/income/pa_ccw_medical_expenses.py index fc7562c66fb..e2a50d71760 100644 --- a/policyengine_us/variables/gov/states/pa/dhs/ccw/income/pa_ccw_medical_expenses.py +++ b/policyengine_us/variables/gov/states/pa/dhs/ccw/income/pa_ccw_medical_expenses.py @@ -13,7 +13,8 @@ class pa_ccw_medical_expenses(Variable): ) definition_period = YEAR defined_for = StateCode.PA - reference = "https://www.pacodeandbulletin.gov/secure/pacode/data/055/chapter3042/055_3042.pdf#page=61" + # PDF pages 59, 61. + reference = "https://www.pacodeandbulletin.gov/secure/pacode/data/055/chapter3042/055_3042.pdf#page=59" # Appendix A Part I(A) counts wages before health insurance deductions; # Part II(C) permits unreimbursed health care premiums without a tax test. From a50b1569d4f44e722c3c08b6c43987aa65697275 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 09:31:16 -0400 Subject: [PATCH 08/15] Isolate the Kansas premium spenddown regression from SSI coverage --- .../medicaid_medically_needy_medical_expenses.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/categories/medically_needy/medicaid_medically_needy_medical_expenses.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/categories/medically_needy/medicaid_medically_needy_medical_expenses.yaml index 7cf936802de..b7d9c6b0007 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/categories/medically_needy/medicaid_medically_needy_medical_expenses.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/categories/medically_needy/medicaid_medically_needy_medical_expenses.yaml @@ -27,6 +27,8 @@ is_in_medicaid_medically_needy_category: true is_adult_for_medicaid: false is_optional_senior_or_disabled_for_medicaid: false + # Isolate spenddown from the separate SSI-recipient Medicaid pathway. + is_ssi_recipient_for_medicaid: false ssi_countable_income: 11_000 ssi_countable_resources: 1_000 state_code: KS @@ -35,4 +37,5 @@ medicare_enrolled: false output: medicaid_medically_needy_medical_expenses: 2_000 + is_ssi_recipient_for_medicaid: false is_medically_needy_for_medicaid: false From 59690fa3132c37646998e00104fdb3ce06f3994a Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 11:37:03 -0400 Subject: [PATCH 09/15] Exercise Kansas premium spenddown from raw wage inputs --- .../medicaid_medically_needy_medical_expenses.yaml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/categories/medically_needy/medicaid_medically_needy_medical_expenses.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/categories/medically_needy/medicaid_medically_needy_medical_expenses.yaml index b7d9c6b0007..4cf3c9c0b0f 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/categories/medically_needy/medicaid_medically_needy_medical_expenses.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/categories/medically_needy/medicaid_medically_needy_medical_expenses.yaml @@ -22,20 +22,23 @@ - name: Kansas spenddown does not deduct pretax premiums again from countable income. period: 2026 input: - is_disabled: true - meets_ssi_disability_criteria: true + age: 65 is_in_medicaid_medically_needy_category: true is_adult_for_medicaid: false is_optional_senior_or_disabled_for_medicaid: false # Isolate spenddown from the separate SSI-recipient Medicaid pathway. is_ssi_recipient_for_medicaid: false - ssi_countable_income: 11_000 + employment_income: 27_020 ssi_countable_resources: 1_000 state_code: KS health_insurance_premiums: 2_000 pre_tax_health_insurance_premiums: 4_000 medicare_enrolled: false output: + # (27,020 - 4,000 - 12 * (20 + 65)) / 2 = 11,000. + is_ssi_aged_blind_disabled: true + ssi_earned_income: 23_020 + ssi_countable_income: 11_000 medicaid_medically_needy_medical_expenses: 2_000 is_ssi_recipient_for_medicaid: false is_medically_needy_for_medicaid: false From f523c2a0724805cf258d904da5b642324eb88feb Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 13:26:08 -0400 Subject: [PATCH 10/15] Revert CRFB premium addback change for separate methodology review --- .../hub-premium-input-contract-1010.fixed.md | 2 +- .../crfb/surtax/increased_base/sources.yaml | 11 +--- .../tests/core/test_premium_input_contract.py | 22 ++------ .../tests/policy/contrib/crfb/agi_surtax.yaml | 55 ------------------- 4 files changed, 7 insertions(+), 83 deletions(-) diff --git a/changelog.d/hub-premium-input-contract-1010.fixed.md b/changelog.d/hub-premium-input-contract-1010.fixed.md index df91bf17b85..4064b85560a 100644 --- a/changelog.d/hub-premium-input-contract-1010.fixed.md +++ b/changelog.d/hub-premium-input-contract-1010.fixed.md @@ -1 +1 @@ -Document disjoint pretax and non-pretax health premium inputs, include employee-paid pretax premiums in applicable benefit and New Jersey medical deductions, exclude pretax premiums from SSI wages and North Dakota renter-refund income, and correct CRFB broad-income premium addbacks. +Document disjoint pretax and non-pretax health premium inputs, include employee-paid pretax premiums in applicable benefit and New Jersey medical deductions, exclude pretax premiums from SSI wages and North Dakota renter-refund income. diff --git a/policyengine_us/parameters/gov/contrib/crfb/surtax/increased_base/sources.yaml b/policyengine_us/parameters/gov/contrib/crfb/surtax/increased_base/sources.yaml index 0aebd7562d2..a978ed74f69 100644 --- a/policyengine_us/parameters/gov/contrib/crfb/surtax/increased_base/sources.yaml +++ b/policyengine_us/parameters/gov/contrib/crfb/surtax/increased_base/sources.yaml @@ -27,13 +27,8 @@ values: - foreign_earned_income_exclusion # Tax-free bond interest - tax_exempt_interest_income - # Employer-provided health insurance: employer-paid premiums and the - # employee's pretax payroll contribution. Including the employee's - # pretax contribution is inferred from the broad income definition and - # IRS Rev. Rul. 2002-3's treatment of salary-reduction premiums as - # employer-provided coverage; after-tax premiums use income in AGI. - - employer_sponsored_insurance_premiums - - pre_tax_health_insurance_premiums + # Employer-provided health insurance (premiums) + - health_insurance_premiums metadata: unit: list @@ -42,5 +37,3 @@ metadata: reference: - title: "Committee for a Responsible Federal Budget, Break Glass: A Plan for the Next Economic Shock, footnote 2" href: https://www.crfb.org/papers/break-glass-plan-next-economic-shock - - title: "IRS Revenue Ruling 2002-3, salary-reduction health insurance premiums #page 1-2" - href: https://www.irs.gov/pub/irs-drop/rr-02-3.pdf#page=1 diff --git a/policyengine_us/tests/core/test_premium_input_contract.py b/policyengine_us/tests/core/test_premium_input_contract.py index 311ca53eaf6..74a17f8b161 100644 --- a/policyengine_us/tests/core/test_premium_input_contract.py +++ b/policyengine_us/tests/core/test_premium_input_contract.py @@ -3,8 +3,7 @@ Moving employee-paid premiums from non-pretax payments to pretax payroll deductions leaves total medical spending unchanged. SNAP, HUD, child-care deductions, Orange County General Relief and New Jersey -medical expenses count both payment methods. The CRFB broad income base adds -back the pretax premium excluded from federal wages. Michigan's wage exclusion +medical expenses count both payment methods. Michigan's wage exclusion and non-pretax premium deduction likewise leave household resources unchanged. North Dakota excludes pretax premiums from income and deducts non-pretax premiums as medical expenses, also leaving renters' refund income unchanged. @@ -26,17 +25,12 @@ from hypothesis import strategies as st from policyengine_us import Simulation -from policyengine_us.reforms.crfb.agi_surtax import agi_surtax_reform_object from policyengine_us.system import system YEAR = 2026 MONTH = "2026-01" STATES = ("CA", "MO", "PA", "NJ", "OH", "MI", "ND") -SURTAX_ON = { - "gov.contrib.crfb.surtax.in_effect": {"2000-01-01.2100-12-31": True}, - "gov.contrib.crfb.surtax.increased_base.in_effect": {"2000-01-01.2100-12-31": True}, -} @st.composite @@ -127,16 +121,9 @@ def _situation(batch, moved): @example(batch=[(12_000, 12_000, 0), (1, 0, 1)]) @given(batch=st.lists(premium_transfers(), min_size=1, max_size=3)) def test_premium_transfer_respects_each_consumers_tax_treatment(batch): - # Supplying the read-only reference system avoids rebuilding the country - # for every generated example. Simulation clones it before applying these - # reforms, giving each situation private policy state and result arrays. - reform = (SURTAX_ON, agi_surtax_reform_object) - baseline = Simulation( - situation=_situation(batch, False), tax_benefit_system=system, reform=reform - ) - moved = Simulation( - situation=_situation(batch, True), tax_benefit_system=system, reform=reform - ) + # Reusing the reference system avoids rebuilding the country per example. + baseline = Simulation(situation=_situation(batch, False), tax_benefit_system=system) + moved = Simulation(situation=_situation(batch, True), tax_benefit_system=system) transfer = np.repeat([row[2] for row in batch], len(STATES)) total = np.repeat([row[0] for row in batch], len(STATES)) states = np.tile(STATES, len(batch)) @@ -168,7 +155,6 @@ def test_premium_transfer_respects_each_consumers_tax_treatment(batch): ("nj_medical_expense_deduction", YEAR), ("mi_household_resources", YEAR), ("nd_renters_refund_income", YEAR), - ("agi_surtax", YEAR), ("ssi_engaged_in_sga", YEAR), ) for variable, period in invariant_consumers: diff --git a/policyengine_us/tests/policy/contrib/crfb/agi_surtax.yaml b/policyengine_us/tests/policy/contrib/crfb/agi_surtax.yaml index d2c32a7336d..f505b72c1c1 100644 --- a/policyengine_us/tests/policy/contrib/crfb/agi_surtax.yaml +++ b/policyengine_us/tests/policy/contrib/crfb/agi_surtax.yaml @@ -18,61 +18,6 @@ output: agi_surtax: 500 -- name: Expanded base restores employee pretax premiums and adds employer-paid premiums. - period: 2026 - reforms: policyengine_us.reforms.crfb.agi_surtax.agi_surtax_reform_object - input: - gov.contrib.crfb.surtax.in_effect: true - gov.contrib.crfb.surtax.increased_base.in_effect: true - employment_income: 150_000 - health_insurance_premiums: 4_000 - pre_tax_health_insurance_premiums: 6_000 - employer_sponsored_insurance_premiums: 10_000 - filing_status: SINGLE - output: - adjusted_gross_income: 144_000 - # 144,000 AGI + 6,000 pretax + 10,000 employer premiums = 160,000. - agi_surtax: 600 - -- name: Expanded base is unchanged when employee premiums are paid entirely after tax. - period: 2026 - reforms: policyengine_us.reforms.crfb.agi_surtax.agi_surtax_reform_object - input: - gov.contrib.crfb.surtax.in_effect: true - gov.contrib.crfb.surtax.increased_base.in_effect: true - employment_income: 150_000 - health_insurance_premiums: 10_000 - employer_sponsored_insurance_premiums: 10_000 - filing_status: SINGLE - output: - adjusted_gross_income: 150_000 - # 150,000 AGI + 10,000 employer premiums = the same 160,000 base. - agi_surtax: 600 - -- name: Expanded base does not add after-tax employee premiums to supplied AGI. - period: 2026 - reforms: policyengine_us.reforms.crfb.agi_surtax.agi_surtax_reform_object - input: - gov.contrib.crfb.surtax.in_effect: true - gov.contrib.crfb.surtax.increased_base.in_effect: true - adjusted_gross_income: 110_000 - health_insurance_premiums: 20_000 - filing_status: SINGLE - output: - agi_surtax: 100 - -- name: Expanded base restores pretax employee premiums excluded from supplied AGI. - period: 2026 - reforms: policyengine_us.reforms.crfb.agi_surtax.agi_surtax_reform_object - input: - gov.contrib.crfb.surtax.in_effect: true - gov.contrib.crfb.surtax.increased_base.in_effect: true - adjusted_gross_income: 110_000 - pre_tax_health_insurance_premiums: 20_000 - filing_status: SINGLE - output: - agi_surtax: 300 - - name: Joint couple below threshold period: 2026 reforms: policyengine_us.reforms.crfb.agi_surtax.agi_surtax_reform_object From 023cdb00080ee4848a6a0c195052047ed2215ac7 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 13:32:15 -0400 Subject: [PATCH 11/15] Cover pretax premium exclusions across SSI income consumers --- .../tests/core/test_premium_input_contract.py | 55 ++++++++---- ..._income_deemed_from_ineligible_parent.yaml | 33 +++++++ ..._income_deemed_from_ineligible_spouse.yaml | 36 ++++++++ .../income/msp_countable_income.yaml | 39 ++++++++ .../income/deemed/from_ineligible_parent.yaml | 31 +++++++ .../income/deemed/from_ineligible_spouse.yaml | 36 ++++++++ .../ssi_ineligible_child_allocation.yaml | 13 +++ ...or_disabled_working_student_exclusion.yaml | 36 ++++++++ .../ct/dss/ssp/premium_input_contract.yaml | 88 +++++++++++++++++++ 9 files changed, 351 insertions(+), 16 deletions(-) create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ct/dss/ssp/premium_input_contract.yaml diff --git a/policyengine_us/tests/core/test_premium_input_contract.py b/policyengine_us/tests/core/test_premium_input_contract.py index 74a17f8b161..7c33e9e9004 100644 --- a/policyengine_us/tests/core/test_premium_input_contract.py +++ b/policyengine_us/tests/core/test_premium_input_contract.py @@ -4,7 +4,8 @@ deductions leaves total medical spending unchanged. SNAP, HUD, child-care deductions, Orange County General Relief and New Jersey medical expenses count both payment methods. Michigan's wage exclusion -and non-pretax premium deduction likewise leave household resources unchanged. +and non-pretax premium deduction likewise leave household resources unchanged +with other benefit amounts held fixed. North Dakota excludes pretax premiums from income and deducts non-pretax premiums as medical expenses, also leaving renters' refund income unchanged. @@ -46,11 +47,12 @@ def _situation(batch, moved): tax_units = {} spm_units = {} households = {} + marital_units = {} for row, (total, initial_pretax, transfer) in enumerate(batch): pretax = initial_pretax + (transfer if moved else 0) non_pretax = total - pretax - for state in STATES: - key = f"{state}_{row}" + for state, size in ((state, size) for state in STATES for size in (1, 2)): + key = f"{state}_{size}_{row}" people[key] = { "age": {YEAR: 40}, "is_tax_unit_head": {YEAR: True}, @@ -72,12 +74,24 @@ def _situation(batch, moved): "employer_sponsored_insurance_premiums": {YEAR: 0}, "self_employed_health_insurance_ald_person": {YEAR: 0}, "general_assistance": {YEAR: 0}, + # Hold other benefits fixed for the Michigan invariant. + "ssi": {YEAR: 0}, "ca_oc_general_relief_countable_earned_income": {MONTH: 5_000}, "ca_oc_general_relief_gross_unearned_income": {MONTH: 0}, "ca_oc_general_relief_receives_other_cash_assistance": {MONTH: False}, } + members = [key] + if size == 2: + spouse = f"{key}_spouse" + people[spouse] = { + **people[key], + "is_tax_unit_head": {YEAR: False}, + "is_tax_unit_spouse": {YEAR: True}, + } + members.append(spouse) + marital_units[key] = {"members": members} tax_units[key] = { - "members": [key], + "members": members, "chip_premium": {YEAR: 0}, "medicaid_premium": {YEAR: 0}, "marketplace_net_premium": {YEAR: 0}, @@ -91,14 +105,14 @@ def _situation(batch, moved): "oh_employer_subsidized_health_plan_eligible": {YEAR: True}, } spm_units[key] = { - "members": [key], + "members": members, "tanf": {YEAR: 0}, "mo_ccs_countable_income": {MONTH: 5_000}, "pa_ccw_countable_income": {YEAR: 60_000}, "pa_ccw_stepparent_deduction": {MONTH: 0}, } households[key] = { - "members": [key], + "members": members, "state_code": {YEAR: state}, "in_oc": {YEAR: state == "CA"}, } @@ -107,8 +121,10 @@ def _situation(batch, moved): "tax_units": tax_units, "spm_units": spm_units, "households": households, - "marital_units": {key: {"members": [key]} for key in people}, - "families": {key: {"members": [key]} for key in people}, + "marital_units": marital_units, + "families": { + key: {"members": unit["members"]} for key, unit in marital_units.items() + }, } @@ -121,16 +137,21 @@ def _situation(batch, moved): @example(batch=[(12_000, 12_000, 0), (1, 0, 1)]) @given(batch=st.lists(premium_transfers(), min_size=1, max_size=3)) def test_premium_transfer_respects_each_consumers_tax_treatment(batch): + # Each batch contains both single adults and two-person married units. # Reusing the reference system avoids rebuilding the country per example. baseline = Simulation(situation=_situation(batch, False), tax_benefit_system=system) moved = Simulation(situation=_situation(batch, True), tax_benefit_system=system) - transfer = np.repeat([row[2] for row in batch], len(STATES)) - total = np.repeat([row[0] for row in batch], len(STATES)) - states = np.tile(STATES, len(batch)) + sizes = np.tile((1, 2), len(batch) * len(STATES)) + transfer = np.repeat([row[2] for row in batch], len(STATES) * 2) + total = np.repeat([row[0] for row in batch], len(STATES) * 2) * sizes + states = np.tile(np.repeat(STATES, 2), len(batch)) + person_transfer = np.repeat(transfer, sizes) + person_states = np.repeat(states, sizes) + unit_transfer = transfer * sizes np.testing.assert_allclose( baseline.calculate("spm_unit_medical_out_of_pocket_expenses", YEAR), - total + 400, + total + 400 * sizes, rtol=0, atol=0.02, ) @@ -138,7 +159,7 @@ def test_premium_transfer_respects_each_consumers_tax_treatment(batch): # contribution once, and deducts only the remaining medical expenses. np.testing.assert_allclose( baseline.calculate("nd_renters_refund_income", YEAR), - (250_000 - total - 300) * (states == "ND"), + (250_000 * sizes - total - 300 * sizes) * (states == "ND"), rtol=0, atol=0.02, ) @@ -180,7 +201,9 @@ def test_premium_transfer_respects_each_consumers_tax_treatment(batch): for variable in tax_status_sensitive_consumers: np.testing.assert_allclose( baseline.calculate(variable, YEAR) - moved.calculate(variable, YEAR), - transfer, + person_transfer + if system.variables[variable].entity.key == "person" + else unit_transfer, rtol=0, atol=0.02, err_msg=variable, @@ -191,7 +214,7 @@ def test_premium_transfer_respects_each_consumers_tax_treatment(batch): np.testing.assert_allclose( baseline.calculate("ssi_countable_income", YEAR) - moved.calculate("ssi_countable_income", YEAR), - transfer / 2, + person_transfer / 2, rtol=0, atol=0.02, ) @@ -202,7 +225,7 @@ def test_premium_transfer_respects_each_consumers_tax_treatment(batch): ): np.testing.assert_allclose( baseline.calculate(variable, YEAR) - moved.calculate(variable, YEAR), - transfer * (states == state), + person_transfer * (person_states == state), rtol=0, atol=0.02, err_msg=variable, diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/deemed/from_ineligible_parent/medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/deemed/from_ineligible_parent/medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent.yaml index 985a44b41e4..679373427d4 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/deemed/from_ineligible_parent/medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/deemed/from_ineligible_parent/medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent.yaml @@ -19,6 +19,39 @@ output: medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: [6_006, 0] +- name: Optional Medicaid parental deeming inherits the SSI exclusion for qualified pretax premiums. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + recipient: + age: 12 + is_ssi_disabled: true + sibling: + age: 14 + employment_income: 2_400 + pre_tax_health_insurance_premiums: 1_200 + parent: + age: 40 + is_parent: true + employment_income: 36_000 + pre_tax_health_insurance_premiums: 3_600 + tax_units: + tax_unit: + members: [recipient, sibling, parent] + households: + household: + members: [recipient, sibling, parent] + state_code: NC + output: + # 42 USC 1396a(m) uses SSI methodology for this optional pathway. + # POMS SI 00820.102C.2: parent wages 32,400; sibling wages 1,200. + # 20 CFR 416.1165(b), (d): sibling allocation = 4,596, so + # (32,400 - 4,596 - 20 * 12 - 65 * 12) / 2 - 967 * 12 = 1,788 deemed. + # Under (e)(1), apply child's general exclusion: 1,788 - 20 * 12 = 1,548. + medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: [1_788, 0, 0] + medicaid_optional_senior_or_disabled_countable_income: [1_548, 0, 0] + # Missouri MHDC deems only a natural or adoptive parent's income, never a # stepparent's (DSS MHABD Manual ยง 0805.020.15), and divides the deemed # amount equally among the disabled children applying. Parent status diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/deemed/from_ineligible_spouse/medicaid_optional_senior_or_disabled_income_deemed_from_ineligible_spouse.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/deemed/from_ineligible_spouse/medicaid_optional_senior_or_disabled_income_deemed_from_ineligible_spouse.yaml index 10e06a9d943..1724495a64d 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/deemed/from_ineligible_spouse/medicaid_optional_senior_or_disabled_income_deemed_from_ineligible_spouse.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/deemed/from_ineligible_spouse/medicaid_optional_senior_or_disabled_income_deemed_from_ineligible_spouse.yaml @@ -84,3 +84,39 @@ # 1,450 - 20 = 1,430 monthly, or 17,160 annually. medicaid_optional_senior_or_disabled_countable_income: 17_160 is_optional_senior_or_disabled_income_eligible: false + +- name: Optional Medicaid spouse deeming inherits the SSI exclusion for qualified pretax premiums. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + recipient: + age: 70 + is_tax_unit_head: true + spouse: + age: 60 + is_tax_unit_spouse: true + employment_income: 24_000 + pre_tax_health_insurance_premiums: 2_400 + child: + age: 14 + employment_income: 2_400 + pre_tax_health_insurance_premiums: 1_200 + tax_units: + tax_unit: + members: [recipient, spouse, child] + marital_units: + marital_unit: + members: [recipient, spouse] + households: + household: + members: [recipient, spouse, child] + state_code: NC + output: + # 42 USC 1396a(m) uses SSI methodology for this optional pathway. + # POMS SI 00820.102C.2: spouse wages 21,600; child wages 1,200. + # 20 CFR 416.1163(b), (d): allocation = (1,450 - 967) * 12 + # - 1,200 = 4,596; remaining spouse wages = 17,004 > 5,796. + # (17,004 - 20 * 12 - 65 * 12) / 2 = 7,992 deemed and countable. + medicaid_optional_senior_or_disabled_income_deemed_from_ineligible_spouse: [7_992, 0, 0] + medicaid_optional_senior_or_disabled_countable_income: [7_992, 0, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/income/msp_countable_income.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/income/msp_countable_income.yaml index 6172d39ff87..1c823197a3b 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/income/msp_countable_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/income/msp_countable_income.yaml @@ -74,3 +74,42 @@ # Person2 own income also $12,000 earned, same exclusions => $457.50/mo. # Both under 100% FPL (~$1,255/mo) so person1 still qualifies for QMB. msp_countable_income: [457.50, 457.50] + +- name: MSP excludes qualified salary-reduction health premiums before SSI income exclusions. + period: 2025-01 + absolute_error_margin: 0.01 + input: + age: 70 + employment_income: 12_000 + pre_tax_health_insurance_premiums: 2_400 + output: + # 42 USC 1396d(p)(1)(B) uses SSI income methodology. + # POMS SI 00820.102C.2: (12,000 - 2,400) / 12 = 800 SSI wages. + # 20 CFR 416.1112(c)(4), (5), (7): (800 - 20 - 65) / 2 = 357.50. + msp_countable_income: 357.50 + +- name: MSP combines both Medicare spouses' wages after excluding their pretax premiums. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 68 + employment_income: 12_000 + pre_tax_health_insurance_premiums: 2_400 + person2: + age: 66 + employment_income: 24_000 + pre_tax_health_insurance_premiums: 3_600 + tax_units: + tax_unit: + members: [person1, person2] + marital_units: + marital_unit: + members: [person1, person2] + output: + # POMS SI 00820.102C.2 removes each spouse's salary reduction. + # 42 USC 1396d(p)(1)(B) and 20 CFR 416.1160(a)(1) combine the wages: + # (12,000 - 2,400 + 24,000 - 3,600) / 12 = 2,500 monthly. + # Apply the couple's exclusions once: (2,500 - 20 - 65) / 2 = 1,207.50. + msp_countable_income: [1_207.50, 1_207.50] diff --git a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent.yaml b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent.yaml index 745c14c4690..085e5931a1c 100644 --- a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent.yaml +++ b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent.yaml @@ -74,3 +74,34 @@ members: [jacks_mother, jacks_father] output: ssi_countable_income: [372, 0, 0, 0, 0] # 31 * 12 + +- name: Parental deeming excludes parent's and sibling's pretax premiums before allocations. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + recipient: + age: 12 + is_ssi_disabled: true + sibling: + age: 14 + employment_income: 2_400 + pre_tax_health_insurance_premiums: 1_200 + parent: + age: 40 + is_parent: true + employment_income: 36_000 + pre_tax_health_insurance_premiums: 3_600 + tax_units: + tax_unit: + members: [recipient, sibling, parent] + output: + # POMS SI 00820.102C.2: parent wages 32,400; sibling wages 1,200. + # 20 CFR 416.1165(b): sibling allocation = (1,450 - 967) * 12 + # - 1,200 = 4,596, leaving parent wages of 27,804. + # Under (d), (27,804 - 20 * 12 - 65 * 12) / 2 - 967 * 12 = 1,788. + # Under (e)(1), child's countable unearned income = 1,788 - 20 * 12 = 1,548. + ssi_earned_income: [0, 1_200, 32_400] + ssi_ineligible_child_allocation: [0, 4_596, 0] + ssi_unearned_income_deemed_from_ineligible_parent: [1_788, 0, 0] + ssi_countable_income: [1_548, 0, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse.yaml b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse.yaml index 5f286c46b0c..64125417f99 100644 --- a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse.yaml +++ b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_spouse.yaml @@ -57,3 +57,39 @@ ssi_countable_income: [1_776, 0] # 148 * 12 # Example 4 skipped as it relates to sponsored aliens. + +- name: Spousal deeming excludes spouse and child's qualified pretax premiums before allocations. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + recipient: + age: 70 + is_tax_unit_head: true + spouse: + age: 60 + is_tax_unit_spouse: true + employment_income: 24_000 + pre_tax_health_insurance_premiums: 2_400 + child: + age: 14 + employment_income: 2_400 + pre_tax_health_insurance_premiums: 1_200 + tax_units: + tax_unit: + members: [recipient, spouse, child] + marital_units: + marital_unit: + members: [recipient, spouse] + output: + # POMS SI 00820.102C.2: spouse wages 21,600; child wages 1,200. + # 20 CFR 416.1163(b): child allocation = (1,450 - 967) * 12 + # - 1,200 = 4,596; spouse's remaining wages = 21,600 - 4,596 = 17,004. + # Under (d)(2), 17,004 > 5,796 and couple exclusions apply: + # (17,004 - 20 * 12 - 65 * 12) / 2 = 7,992. + ssi_earned_income: [0, 21_600, 1_200] + ssi_ineligible_child_allocation: [0, 0, 4_596] + ssi_earned_income_deemed_from_ineligible_spouse: [17_004, 0, 0] + is_ssi_spousal_deeming_applies: [true, false, false] + ssi_income_deemed_from_ineligible_spouse: [7_992, 0, 0] + ssi_countable_income: [7_992, 0, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/ssi_ineligible_child_allocation.yaml b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/ssi_ineligible_child_allocation.yaml index cf2a42e0de8..76164837620 100644 --- a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/ssi_ineligible_child_allocation.yaml +++ b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/ssi_ineligible_child_allocation.yaml @@ -4,3 +4,16 @@ age: 14 output: ssi_ineligible_child_allocation: 5_040 # (1,261 - 841) * 12 + +- name: Ineligible child's pretax premiums do not reduce the child allocation as wages. + period: 2025 + absolute_error_margin: 0.01 + input: + age: 14 + employment_income: 2_400 + pre_tax_health_insurance_premiums: 1_200 + output: + # POMS SI 00820.102C.2: wages for SSI = 2,400 - 1,200 = 1,200. + # 20 CFR 416.1163(b)(1)-(2): (1,450 - 967) * 12 - 1,200 = 4,596. + ssi_earned_income: 1_200 + ssi_ineligible_child_allocation: 4_596 diff --git a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/ssi_blind_or_disabled_working_student_exclusion.yaml b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/ssi_blind_or_disabled_working_student_exclusion.yaml index a576a272015..ec5dda6aec9 100644 --- a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/ssi_blind_or_disabled_working_student_exclusion.yaml +++ b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/ssi_blind_or_disabled_working_student_exclusion.yaml @@ -63,3 +63,39 @@ ssi_earned_income: 20_000 output: ssi_blind_or_disabled_working_student_exclusion: 433.33 # 5,200/12 + +- name: Student earned-income exclusion applies to wages after qualified pretax premiums. + period: 2025 + absolute_error_margin: 0.01 + input: + age: 20 + is_disabled: true + is_ssi_disabled: true + is_full_time_student: true + employment_income: 9_600 + pre_tax_health_insurance_premiums: 2_400 + output: + # POMS SI 00820.102C.2: SSI wages = 9,600 - 2,400 = 7,200. + # 20 CFR 416.1112(c)(3): min(600, 2,350) * 12 = 7,200, + # below the 2025 annual cap of 9,460. No earned income remains. + ssi_earned_income: 7_200 + ssi_blind_or_disabled_working_student_exclusion: 7_200 + ssi_countable_income: 0 + +- name: Student with pretax premiums reaches the annual exclusion cap before standard exclusions. + period: 2025 + absolute_error_margin: 0.01 + input: + age: 20 + is_disabled: true + is_ssi_disabled: true + is_full_time_student: true + employment_income: 18_000 + pre_tax_health_insurance_premiums: 2_400 + output: + # POMS SI 00820.102C.2: SSI wages = 18,000 - 2,400 = 15,600. + # 20 CFR 416.1112(c)(3)-(7): student exclusion is capped at 9,460, + # then (15,600 - 9,460 - 20 * 12 - 65 * 12) / 2 = 2,560. + ssi_earned_income: 15_600 + ssi_blind_or_disabled_working_student_exclusion: 9_460 + ssi_countable_income: 2_560 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ct/dss/ssp/premium_input_contract.yaml b/policyengine_us/tests/policy/baseline/gov/states/ct/dss/ssp/premium_input_contract.yaml new file mode 100644 index 00000000000..b654cf7d663 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ct/dss/ssp/premium_input_contract.yaml @@ -0,0 +1,88 @@ +# Salary-reduction premiums are excluded from inherited SSI earned income: +# https://secure.ssa.gov/poms.nsf/lnx/0500820102 (SI 00820.102C.2). +# CT earned and unearned disregard rules: UPM 5030.10 and 5030.15; +# https://www.ssa.gov/policy/docs/progdesc/ssi_st_asst/2011/ct.html +# Current disregard and need amounts: +# https://portal.ct.gov/dss/-/media/departments-and-agencies/dss/fact-sheets-and-issue-briefs/fact-sheets/dss-program-standards-chart-effective-010126.pdf + +- name: Connecticut SSP applies its earned disregard after the inherited pretax exclusion + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person: + age: 70 + immigration_status: CITIZEN + ssi_countable_resources: 500 + employment_income: 7_200 + pre_tax_health_insurance_premiums: 1_200 + rent: 12_000 + marital_units: + marital_unit: + members: [person] + tax_units: + tax_unit: + members: [person] + spm_units: + spm_unit: + members: [person] + households: + household: + members: [person] + state_code: CT + output: + ssi_earned_income: 500 # $600 - $100. + ssi: 786.50 # $994 - ($500 - $20 - $65)/2. + ct_ssp_gross_income: 1_286.50 # $500 SSI wages + $786.50 SSI. + ct_ssp_earned_income_disregard: 282.50 # $65 + ($500 - $65)/2. + ct_ssp_unearned_income_disregard: 562 + # ($786.50 - $562) + ($500 - $282.50) = $442. + ct_ssp_countable_income: 442 + ct_ssp_need_standard: 597.35 # $400 shelter + $197.35 personal needs. + ct_ssp_income_eligible: true + ct_ssp_eligible_person: true + ct_ssp_person: 155.35 # $597.35 - $442. + ct_ssp: 155.35 + +- name: Connecticut SSP blind earned disregard uses earnings after the pretax exclusion + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person: + age: 50 + is_blind: true + immigration_status: CITIZEN + ssi_countable_resources: 500 + employment_income: 3_600 + pre_tax_health_insurance_premiums: 1_200 + ct_ssp_lives_with_others: true + ct_ssp_has_therapeutic_diet: true + rent: 6_000 + marital_units: + marital_unit: + members: [person] + tax_units: + tax_unit: + members: [person] + spm_units: + spm_unit: + members: [person] + households: + household: + members: [person] + state_code: CT + output: + ssi_earned_income: 200 # $300 - $100. + ssi: 936.50 # $994 - ($200 - $20 - $65)/2. + ct_ssp_gross_income: 1_136.50 # $200 SSI wages + $936.50 SSI. + ct_ssp_earned_income_disregard: 142.50 # $85 + ($200 - $85)/2. + ct_ssp_unearned_income_disregard: 629.90 + # ($936.50 - $629.90) + ($200 - $142.50) = $364.10. + ct_ssp_countable_income: 364.10 + # $200 shelter + $197.35 personal needs + $36.20 therapeutic diet. + ct_ssp_need_standard: 433.55 + ct_ssp_income_eligible: true + ct_ssp_eligible_person: true + ct_ssp_person: 69.45 # $433.55 - $364.10. + ct_ssp: 69.45 From de43bf5f610e4219e7f48d4e2ad19352c69cccfe Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 13:32:29 -0400 Subject: [PATCH 12/15] Preserve gross premium income for Minnesota MSA eligibility and guardian fees --- .../hub-premium-input-contract-1010.fixed.md | 2 +- .../mn/dhs/msa/premium_input_contract.yaml | 144 ++++++++++++++++++ .../mn_msa_gross_income_eligible.py | 15 +- .../eligibility/mn_msa_net_income_eligible.py | 3 + .../mn/dhs/msa/income/mn_msa_gross_income.py | 45 ++++++ .../gov/states/mn/dhs/msa/mn_msa_person.py | 3 + .../mn/dhs/msa/payment/mn_msa_guardian_fee.py | 15 +- 7 files changed, 198 insertions(+), 29 deletions(-) create mode 100644 policyengine_us/tests/policy/baseline/gov/states/mn/dhs/msa/premium_input_contract.yaml create mode 100644 policyengine_us/variables/gov/states/mn/dhs/msa/income/mn_msa_gross_income.py diff --git a/changelog.d/hub-premium-input-contract-1010.fixed.md b/changelog.d/hub-premium-input-contract-1010.fixed.md index 4064b85560a..b8f2ac1c020 100644 --- a/changelog.d/hub-premium-input-contract-1010.fixed.md +++ b/changelog.d/hub-premium-input-contract-1010.fixed.md @@ -1 +1 @@ -Document disjoint pretax and non-pretax health premium inputs, include employee-paid pretax premiums in applicable benefit and New Jersey medical deductions, exclude pretax premiums from SSI wages and North Dakota renter-refund income. +Document disjoint pretax and non-pretax health premium inputs, include employee-paid pretax premiums in applicable benefit and New Jersey medical deductions, exclude pretax premiums from SSI wages and North Dakota renter-refund income. Preserve Minnesota Supplemental Aid gross income for eligibility and guardian fees while applying SSI salary-reduction exclusions to countable income. diff --git a/policyengine_us/tests/policy/baseline/gov/states/mn/dhs/msa/premium_input_contract.yaml b/policyengine_us/tests/policy/baseline/gov/states/mn/dhs/msa/premium_input_contract.yaml new file mode 100644 index 00000000000..ae994f08338 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mn/dhs/msa/premium_input_contract.yaml @@ -0,0 +1,144 @@ +# SSI salary-reduction treatment: POMS SI 00820.102C.2. +# MSA countable exclusions: Minn. Stat. 256D.435 subds. 4a, 5, and 6. +# Gross income: Minn. Stat. 256D.35 subd. 10; Minn. R. 9500.1206 subp. 15a. +# Guardian cap: Minn. Stat. 256D.44 subd. 5(d). +# https://secure.ssa.gov/poms.nsf/lnx/0500820102 +# https://www.revisor.mn.gov/statutes/cite/256D.435 +# https://www.revisor.mn.gov/statutes/cite/256D.35 +# https://www.revisor.mn.gov/rules/9500.1206/ +# https://www.revisor.mn.gov/statutes/cite/256D.44 + +- name: MSA excludes pretax premiums from countable income and restores them for guardian fees + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person: + age: 70 + immigration_status: CITIZEN + ssi_countable_resources: 500 + employment_income: 20_400 # $1,700/month before salary reduction. + pre_tax_health_insurance_premiums: 1_200 + social_security: 3_000 + mn_msa_has_guardian: true + marital_units: + marital_unit: + members: [person] + tax_units: + tax_unit: + members: [person] + spm_units: + spm_unit: + members: [person] + households: + household: + members: [person] + state_code: MN + output: + ssi_earned_income: 1_600 # $1,700 - $100. + # Countable = ($1,600 - $65)/2 + ($250 - $20) = $997.50/month. + ssi_countable_income: 997.50 + ssi: 0 # Countable $997.50 exceeds the $994 FBR. + mn_msa_gross_income: 1_950 # $1,700 gross wages + $250 Social Security. + mn_msa_gross_income_eligible: true # $1,950 <= 3 * $994. + mn_msa_guardian_fee: 97.50 # min(5% * $1,950, $100). + mn_msa_net_income_eligible: true + mn_msa_eligible_person: true + mn_msa_assistance_standard: 1_055 + mn_msa_person: 155 # $1,055 + $97.50 - $997.50. + mn_msa: 155 + +- name: MSA SSI track substitutes the FBR and keeps pretax premiums in guardian gross income + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person: + age: 70 + immigration_status: CITIZEN + ssi_countable_resources: 500 + employment_income: 7_200 + pre_tax_health_insurance_premiums: 1_200 + mn_msa_has_guardian: true + marital_units: + marital_unit: + members: [person] + tax_units: + tax_unit: + members: [person] + spm_units: + spm_unit: + members: [person] + households: + household: + members: [person] + state_code: MN + output: + ssi_earned_income: 500 # $600 - $100. + ssi: 786.50 # $994 - ($500 - $20 - $65)/2. + # CM 0017 uses the full FBR: gross = $600 + $994, rather than the SSI check. + mn_msa_gross_income: 1_594 + mn_msa_gross_income_eligible: true + mn_msa_guardian_fee: 79.70 # 5% * $1,594. + mn_msa_net_income_eligible: true + mn_msa_eligible_person: true + mn_msa_person: 160.70 # $1,055 + $79.70 - ($994 - $20). + mn_msa: 160.70 + +- name: MSA gross test includes pretax premiums at the individual limit + period: 2026-01 + input: + age: 70 + state_code: MN + employment_income: 35_784 + pre_tax_health_insurance_premiums: 1_200 + output: + ssi_earned_income: 2_882 # $2,982 - $100. + ssi: 0 + mn_msa_gross_income: 2_982 + mn_msa_gross_income_eligible: true # $2,982 = 3 * $994. + +- name: MSA gross test includes pretax premiums one dollar above the individual limit + period: 2026-01 + input: + age: 70 + state_code: MN + employment_income: 35_796 + pre_tax_health_insurance_premiums: 1_200 + output: + ssi_earned_income: 2_883 # $2,983 - $100. + ssi: 0 + mn_msa_gross_income: 2_983 + mn_msa_gross_income_eligible: false # $2,983 > 3 * $994. + +- name: MSA gross couple limit includes both spouses salary reductions + period: 2026-01 + input: + people: + person1: + age: 70 + employment_income: 35_796 + pre_tax_health_insurance_premiums: 1_200 + person2: + age: 68 + employment_income: 35_784 + pre_tax_health_insurance_premiums: 1_200 + marital_units: + marital_unit: + members: [person1, person2] + tax_units: + tax_unit: + members: [person1, person2] + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MN + output: + ssi_earned_income: [2_883, 2_882] + ssi: [0, 0] + mn_msa_gross_income: [2_983, 2_982] + # Both are aged: $5,965 gross exceeds the couple limit 6 * $994 = $5,964. + mn_msa_gross_income_eligible: [false, false] diff --git a/policyengine_us/variables/gov/states/mn/dhs/msa/eligibility/mn_msa_gross_income_eligible.py b/policyengine_us/variables/gov/states/mn/dhs/msa/eligibility/mn_msa_gross_income_eligible.py index e13a80161b3..27de6fc0bc3 100644 --- a/policyengine_us/variables/gov/states/mn/dhs/msa/eligibility/mn_msa_gross_income_eligible.py +++ b/policyengine_us/variables/gov/states/mn/dhs/msa/eligibility/mn_msa_gross_income_eligible.py @@ -22,20 +22,7 @@ def formula(person, period, parameters): ssi_individual_fbr = parameters(period).gov.ssa.ssi.amount.individual categorically_eligible = person("is_ssi_aged_blind_disabled", period.this_year) both_eligible = person.marital_unit.sum(categorically_eligible) == 2 - income = add( - person, - period, - [ - "ssi_earned_income", - "ssi_unearned_income", - "ssi_earned_income_deemed_from_ineligible_spouse", - "ssi_unearned_income_deemed_from_ineligible_spouse", - "ssi_unearned_income_deemed_from_ineligible_parent", - ], - ) - ssi = person("ssi", period) - ssi_fbr = person("ssi_amount_if_eligible", period) - gross = income + where(ssi > 0, ssi_fbr, 0) + gross = person("mn_msa_gross_income", period) au_income = where(both_eligible, person.marital_unit.sum(gross), gross) cap = ssi_individual_fbr * where( both_eligible, p.couple_fbr_multiplier, p.individual_fbr_multiplier diff --git a/policyengine_us/variables/gov/states/mn/dhs/msa/eligibility/mn_msa_net_income_eligible.py b/policyengine_us/variables/gov/states/mn/dhs/msa/eligibility/mn_msa_net_income_eligible.py index 7543e4d17a8..bd8a807be09 100644 --- a/policyengine_us/variables/gov/states/mn/dhs/msa/eligibility/mn_msa_net_income_eligible.py +++ b/policyengine_us/variables/gov/states/mn/dhs/msa/eligibility/mn_msa_net_income_eligible.py @@ -11,6 +11,7 @@ class mn_msa_net_income_eligible(Variable): definition_period = MONTH defined_for = StateCode.MN reference = ( + "https://www.revisor.mn.gov/statutes/cite/256D.435", "https://www.revisor.mn.gov/statutes/cite/256D.44", "https://www.dhs.state.mn.us/main/groups/county_access/documents/pub/mndhs-073585.pdf#page=2", ) @@ -26,6 +27,8 @@ def formula(person, period, parameters): # (federal SSI already consumed the $65 + 1/2 earned disregards). # Non-SSI track recipients get the standard $20 + $65 + 1/2 # disregards on actual earned and unearned income. + # Section 256D.435 Subd. 4a adopts SSI income exclusions, including + # the salary-reduction premium exclusion already in ssi_earned_income. # FLA-D recipients are exempt from the $20 disregard so the formula # collapses to FBR + unearned vs PNA. arrangement = person("mn_msa_payment_category", period) diff --git a/policyengine_us/variables/gov/states/mn/dhs/msa/income/mn_msa_gross_income.py b/policyengine_us/variables/gov/states/mn/dhs/msa/income/mn_msa_gross_income.py new file mode 100644 index 00000000000..1364f7e2e1c --- /dev/null +++ b/policyengine_us/variables/gov/states/mn/dhs/msa/income/mn_msa_gross_income.py @@ -0,0 +1,45 @@ +from policyengine_us.model_api import * + + +class mn_msa_gross_income(Variable): + value_type = float + entity = Person + label = "Minnesota Supplemental Aid gross income" + unit = USD + definition_period = MONTH + defined_for = StateCode.MN + reference = ( + "https://www.revisor.mn.gov/statutes/cite/256D.35#stat.256D.35.10", + "https://www.revisor.mn.gov/rules/9500.1206/#rule.9500.1206.15a", + "https://www.dhs.state.mn.us/main/idcplg?IdcService=GET_DYNAMIC_CONVERSION&RevisionSelectionMethod=LatestReleased&dDocName=cm_0017", + ) + + def formula(person, period, parameters): + income = add( + person, + period, + [ + "ssi_earned_income", + "ssi_unearned_income", + "ssi_earned_income_deemed_from_ineligible_spouse", + "ssi_unearned_income_deemed_from_ineligible_spouse", + "ssi_unearned_income_deemed_from_ineligible_parent", + ], + ) + # MSA gross income is measured before deductions and disregards. + # Its net-income calculation adopts SSI exclusions (256D.435), + # but the gross test and guardian-fee cap must restore the employee's + # salary-reduction premium excluded from their own SSI wages. + # Keep the SSI financial-responsibility rules for deemed income. + sources = parameters(period).gov.ssa.ssi.income.sources.earned + if "employment_income" in sources: + wages = max_(person("employment_income", period.this_year), 0) + premiums = max_( + person("pre_tax_health_insurance_premiums", period.this_year), 0 + ) + income += min_(wages, premiums) / MONTHS_IN_YEAR + + # CM 0017 substitutes the full FBR for an SSI recipient's payment. + ssi = person("ssi", period) + ssi_fbr = person("ssi_amount_if_eligible", period) + return income + where(ssi > 0, ssi_fbr, 0) diff --git a/policyengine_us/variables/gov/states/mn/dhs/msa/mn_msa_person.py b/policyengine_us/variables/gov/states/mn/dhs/msa/mn_msa_person.py index 997a757ec09..4019169214c 100644 --- a/policyengine_us/variables/gov/states/mn/dhs/msa/mn_msa_person.py +++ b/policyengine_us/variables/gov/states/mn/dhs/msa/mn_msa_person.py @@ -12,6 +12,7 @@ class mn_msa_person(Variable): definition_period = MONTH defined_for = "mn_msa_eligible_person" reference = ( + "https://www.revisor.mn.gov/statutes/cite/256D.435", "https://www.revisor.mn.gov/statutes/cite/256D.44", "https://www.house.mn.gov/hrd/pubs/pap_MSA.pdf#page=2", "https://www.dhs.state.mn.us/main/groups/county_access/documents/pub/mndhs-073585.pdf#page=4", @@ -24,6 +25,8 @@ def formula(person, period, parameters): # mn_msa_net_income_eligible). Keep the two formulas in sync when # changing income treatment. # MSA inherits federal SSI's $20 / $65 / 1/2 disregards (CM 0018.18). + # Section 256D.435 Subd. 4a also adopts SSI income exclusions; + # ssi_earned_income already excludes salary-reduction premiums. # The SSI track substitutes the federal SSI FBR for the recipient's # post-disregard SSI payment so MSA tops up against the FBR rather # than the federal payment. FLA-D recipients are exempt from the $20 diff --git a/policyengine_us/variables/gov/states/mn/dhs/msa/payment/mn_msa_guardian_fee.py b/policyengine_us/variables/gov/states/mn/dhs/msa/payment/mn_msa_guardian_fee.py index 87d919c7654..1cb654a81cd 100644 --- a/policyengine_us/variables/gov/states/mn/dhs/msa/payment/mn_msa_guardian_fee.py +++ b/policyengine_us/variables/gov/states/mn/dhs/msa/payment/mn_msa_guardian_fee.py @@ -22,20 +22,7 @@ def formula(person, period, parameters): # the marital unit before applying the cap. p = parameters(period).gov.states.mn.dhs.msa.special_needs.guardian_fee has_guardian = person("mn_msa_has_guardian", period) - income = add( - person, - period, - [ - "ssi_earned_income", - "ssi_unearned_income", - "ssi_earned_income_deemed_from_ineligible_spouse", - "ssi_unearned_income_deemed_from_ineligible_spouse", - "ssi_unearned_income_deemed_from_ineligible_parent", - ], - ) - ssi = person("ssi", period) - ssi_fbr = person("ssi_amount_if_eligible", period) - gross = income + where(ssi > 0, ssi_fbr, 0) + gross = person("mn_msa_gross_income", period) unit_gross = person.marital_unit.sum(gross) unit_fee = min_(unit_gross * p.rate, p.max_amount) return has_guardian * unit_fee From 16ab034b9f6981dec8a7beec27b4cd6bab5eebf8 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 13:52:31 -0400 Subject: [PATCH 13/15] Define unmarried parental income fixtures with singleton marital units --- ...unearned_income_deemed_from_ineligible_parent.yaml | 11 +++++++++++ .../income/deemed/from_ineligible_parent.yaml | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/deemed/from_ineligible_parent/medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/deemed/from_ineligible_parent/medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent.yaml index 679373427d4..fe17f4085ea 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/deemed/from_ineligible_parent/medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/deemed/from_ineligible_parent/medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent.yaml @@ -39,6 +39,15 @@ tax_units: tax_unit: members: [recipient, sibling, parent] + # A parent and children are not a married couple. Explicit singleton + # units prevent default grouping from also applying spousal deeming. + marital_units: + recipient_unit: + members: [recipient] + sibling_unit: + members: [sibling] + parent_unit: + members: [parent] households: household: members: [recipient, sibling, parent] @@ -50,6 +59,8 @@ # (32,400 - 4,596 - 20 * 12 - 65 * 12) / 2 - 967 * 12 = 1,788 deemed. # Under (e)(1), apply child's general exclusion: 1,788 - 20 * 12 = 1,548. medicaid_optional_senior_or_disabled_unearned_income_deemed_from_ineligible_parent: [1_788, 0, 0] + is_ssi_spousal_deeming_applies: [false, false, false] + medicaid_optional_senior_or_disabled_income_deemed_from_ineligible_spouse: [0, 0, 0] medicaid_optional_senior_or_disabled_countable_income: [1_548, 0, 0] # Missouri MHDC deems only a natural or adoptive parent's income, never a diff --git a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent.yaml b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent.yaml index 085e5931a1c..96f3e7376a9 100644 --- a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent.yaml +++ b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/deemed/from_ineligible_parent.yaml @@ -95,6 +95,15 @@ tax_units: tax_unit: members: [recipient, sibling, parent] + # A parent and children are not a married couple. Explicit singleton + # units prevent default grouping from also applying spousal deeming. + marital_units: + recipient_unit: + members: [recipient] + sibling_unit: + members: [sibling] + parent_unit: + members: [parent] output: # POMS SI 00820.102C.2: parent wages 32,400; sibling wages 1,200. # 20 CFR 416.1165(b): sibling allocation = (1,450 - 967) * 12 @@ -104,4 +113,6 @@ ssi_earned_income: [0, 1_200, 32_400] ssi_ineligible_child_allocation: [0, 4_596, 0] ssi_unearned_income_deemed_from_ineligible_parent: [1_788, 0, 0] + is_ssi_spousal_deeming_applies: [false, false, false] + ssi_income_deemed_from_ineligible_spouse: [0, 0, 0] ssi_countable_income: [1_548, 0, 0] From 70c8e5a8df3d876e9e4d3543abf7506a03639224 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 14:32:28 -0400 Subject: [PATCH 14/15] Clarify the statutory basis for MSA gross premium treatment --- .../baseline/gov/states/mn/dhs/msa/premium_input_contract.yaml | 3 ++- .../gov/states/mn/dhs/msa/income/mn_msa_gross_income.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/mn/dhs/msa/premium_input_contract.yaml b/policyengine_us/tests/policy/baseline/gov/states/mn/dhs/msa/premium_input_contract.yaml index ae994f08338..4b95f69554c 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mn/dhs/msa/premium_input_contract.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mn/dhs/msa/premium_input_contract.yaml @@ -1,6 +1,7 @@ # SSI salary-reduction treatment: POMS SI 00820.102C.2. # MSA countable exclusions: Minn. Stat. 256D.435 subds. 4a, 5, and 6. -# Gross income: Minn. Stat. 256D.35 subd. 10; Minn. R. 9500.1206 subp. 15a. +# MSA gross income: Minn. Stat. 256D.35 subd. 10. Rule 9500.1206 subp. 15a +# describes GA conventions, adopted for non-SSI MSA recipients by CM 0017. # Guardian cap: Minn. Stat. 256D.44 subd. 5(d). # https://secure.ssa.gov/poms.nsf/lnx/0500820102 # https://www.revisor.mn.gov/statutes/cite/256D.435 diff --git a/policyengine_us/variables/gov/states/mn/dhs/msa/income/mn_msa_gross_income.py b/policyengine_us/variables/gov/states/mn/dhs/msa/income/mn_msa_gross_income.py index 1364f7e2e1c..a6438e3c287 100644 --- a/policyengine_us/variables/gov/states/mn/dhs/msa/income/mn_msa_gross_income.py +++ b/policyengine_us/variables/gov/states/mn/dhs/msa/income/mn_msa_gross_income.py @@ -8,6 +8,8 @@ class mn_msa_gross_income(Variable): unit = USD definition_period = MONTH defined_for = StateCode.MN + # Section 256D.35 directly defines MSA gross income. Rule 9500.1206 + # describes GA conventions, which CM 0017 adopts for non-SSI recipients. reference = ( "https://www.revisor.mn.gov/statutes/cite/256D.35#stat.256D.35.10", "https://www.revisor.mn.gov/rules/9500.1206/#rule.9500.1206.15a", From c7d270970e2c2ea8067277d9e9faf558a3d4b316 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 17:51:26 -0400 Subject: [PATCH 15/15] Supply SSI disability criteria in student premium fixtures --- .../ssi_blind_or_disabled_working_student_exclusion.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/ssi_blind_or_disabled_working_student_exclusion.yaml b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/ssi_blind_or_disabled_working_student_exclusion.yaml index ec5dda6aec9..2e3b42da0f3 100644 --- a/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/ssi_blind_or_disabled_working_student_exclusion.yaml +++ b/policyengine_us/tests/policy/baseline/gov/ssa/ssi/eligibility/income/ssi_blind_or_disabled_working_student_exclusion.yaml @@ -71,6 +71,7 @@ age: 20 is_disabled: true is_ssi_disabled: true + meets_ssi_disability_criteria: true is_full_time_student: true employment_income: 9_600 pre_tax_health_insurance_premiums: 2_400 @@ -79,6 +80,7 @@ # 20 CFR 416.1112(c)(3): min(600, 2,350) * 12 = 7,200, # below the 2025 annual cap of 9,460. No earned income remains. ssi_earned_income: 7_200 + is_ssi_blind_or_disabled_working_student_exclusion_eligible: true ssi_blind_or_disabled_working_student_exclusion: 7_200 ssi_countable_income: 0 @@ -89,6 +91,7 @@ age: 20 is_disabled: true is_ssi_disabled: true + meets_ssi_disability_criteria: true is_full_time_student: true employment_income: 18_000 pre_tax_health_insurance_premiums: 2_400 @@ -97,5 +100,6 @@ # 20 CFR 416.1112(c)(3)-(7): student exclusion is capped at 9,460, # then (15,600 - 9,460 - 20 * 12 - 65 * 12) / 2 = 2,560. ssi_earned_income: 15_600 + is_ssi_blind_or_disabled_working_student_exclusion_eligible: true ssi_blind_or_disabled_working_student_exclusion: 9_460 ssi_countable_income: 2_560