From 896a087747dca3a7366a870002cb584d7cf0d814 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 15:20:13 -0400 Subject: [PATCH 1/2] Charge the Part D income-related premium only to Part D enrollees income_adjusted_part_d_premium_surcharge was defined for medicare_enrolled (Part A and/or Part B), so every Medicare enrollee above the IRMAA thresholds paid it, including people whose drug coverage is not Part D (retiree drug subsidy plans, FEHB, TRICARE for Life, VA) and people with no drug coverage. 42 U.S.C. 1395w-113(a)(7) raises the Part D beneficiary premium, which only Part D enrollees pay. Adds takes_up_medicare_part_d_if_eligible (input, default true) and medicare_part_d_enrolled (Medicare enrollees who take up Part D; 42 U.S.C. 1395w-101(a)(3)(A)), and gates the surcharge on the latter. With the default, results are unchanged until microdata supply the input. Co-Authored-By: Claude Opus 5.5 --- .../medicare-part-d-enrollment.fixed.md | 1 + ...ome_adjusted_part_d_premium_surcharge.yaml | 38 ++++++++ .../eligibility/medicare_part_d_enrolled.yaml | 38 ++++++++ .../hhs/medicare/test_part_d_enrollment.py | 94 +++++++++++++++++++ ...ncome_adjusted_part_d_premium_surcharge.py | 2 +- .../part_d/medicare_part_d_enrolled.py | 16 ++++ .../takes_up_medicare_part_d_if_eligible.py | 18 ++++ 7 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 changelog.d/medicare-part-d-enrollment.fixed.md create mode 100644 policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/medicare_part_d_enrolled.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/hhs/medicare/test_part_d_enrollment.py create mode 100644 policyengine_us/variables/gov/hhs/medicare/eligibility/part_d/medicare_part_d_enrolled.py create mode 100644 policyengine_us/variables/gov/hhs/medicare/takes_up_medicare_part_d_if_eligible.py diff --git a/changelog.d/medicare-part-d-enrollment.fixed.md b/changelog.d/medicare-part-d-enrollment.fixed.md new file mode 100644 index 00000000000..19d06b14022 --- /dev/null +++ b/changelog.d/medicare-part-d-enrollment.fixed.md @@ -0,0 +1 @@ +Charge the Medicare Part D income-related monthly adjustment amount only to Part D enrollees, through a new takes_up_medicare_part_d_if_eligible input that defaults to true. diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/income_adjusted_part_d_premium_surcharge.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/income_adjusted_part_d_premium_surcharge.yaml index 66ce09857a6..2f5aea8cd68 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/income_adjusted_part_d_premium_surcharge.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/income_adjusted_part_d_premium_surcharge.yaml @@ -129,3 +129,41 @@ medicare_irmaa_magi_two_years_prior: 500_000 output: income_adjusted_part_d_premium_surcharge: 1_029.6 + +- name: Case 11, 2026 Medicare enrollee above the IRMAA threshold without Part D. + period: 2026 + input: + age: 66 + filing_status: SINGLE + adjusted_gross_income: + 2024: 109_001 + tax_exempt_interest_income: + 2024: 0 + takes_up_medicare_part_d_if_eligible: false + output: + income_adjusted_part_d_premium_surcharge: 0 + +- name: Case 12, 2026 joint couple in the third tier where only one spouse has Part D. + period: 2026 + input: + people: + head: + age: 67 + spouse: + age: 67 + takes_up_medicare_part_d_if_eligible: false + tax_units: + tax_unit: + members: [head, spouse] + filing_status: JOINT + adjusted_gross_income: + 2024: 342_001 + households: + household: + members: [head, spouse] + output: + medicare_part_d_enrolled: [true, false] + # $60.40 a month for the enrolled spouse; none for the other. + income_adjusted_part_d_premium_surcharge: [724.8, 0] + # Part B premium and IRMAA ($202.90 + $324.60 a month) are unaffected. + gross_medicare_part_b_premium: [6_330, 6_330] diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/medicare_part_d_enrolled.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/medicare_part_d_enrolled.yaml new file mode 100644 index 00000000000..e9b074ce8d0 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/medicare_part_d_enrolled.yaml @@ -0,0 +1,38 @@ +- name: Case 1, a Medicare enrollee takes up Part D by default. + period: 2026 + input: + age: 70 + output: + medicare_part_d_enrolled: true + +- name: Case 2, a Medicare enrollee without Part D. + period: 2026 + input: + age: 70 + takes_up_medicare_part_d_if_eligible: false + output: + medicare_part_d_enrolled: false + +- name: Case 3, no Part D without Part A or Part B. + period: 2026 + input: + age: 70 + takes_up_medicare_if_eligible: false + output: + medicare_part_d_enrolled: false + +- name: Case 4, no Part D below Medicare age without disability entitlement. + period: 2026 + input: + age: 50 + output: + medicare_part_d_enrolled: false + +- name: Case 5, disability entitlement under 65 opens Part D. + period: 2026 + input: + age: 50 + social_security_disability: 20_000 + months_receiving_social_security_disability: 24 + output: + medicare_part_d_enrolled: true diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/test_part_d_enrollment.py b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/test_part_d_enrollment.py new file mode 100644 index 00000000000..9f70680bcee --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/test_part_d_enrollment.py @@ -0,0 +1,94 @@ +"""The Part D income-related premium applies only to Part D enrollees. + +42 U.S.C. 1395w-113(a)(7) raises the Part D beneficiary premium, which only +Part D enrollees pay. Invariants checked for every drawn set of people: +- the surcharge is never negative and is zero for anyone not in Part D; +- for Part D enrollees it equals the surcharge when every Medicare enrollee + takes up Part D, so the enrollment gate removes charges and changes none; +- Part D enrollment implies Medicare enrollment, which implies eligibility; +- Part D take-up leaves the Part B premium unchanged. + +Each example builds two small simulations on the shared model system; no +extra tax-benefit system stays resident. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +FILING_STATUSES = [ + "SINGLE", + "JOINT", + "HEAD_OF_HOUSEHOLD", + "SURVIVING_SPOUSE", + "SEPARATE", +] +# Years with brackets of their own. +YEARS = [2024, 2025, 2026] + +PERSON = st.fixed_dictionaries( + { + "age": st.integers(min_value=55, max_value=95), + "takes_up_medicare": st.booleans(), + "takes_up_part_d": st.booleans(), + # MAGI is stored as float32; draw values it can hold. + "magi": st.floats(min_value=0, max_value=1_500_000, width=32), + "filing_status": st.sampled_from(FILING_STATUSES), + } +) + + +def simulate(people, year, everyone_takes_up_part_d): + situation = {"people": {}, "tax_units": {}, "households": {}} + for i, person in enumerate(people): + name = f"person_{i}" + situation["people"][name] = { + "age": {year: person["age"]}, + "takes_up_medicare_if_eligible": {year: person["takes_up_medicare"]}, + "takes_up_medicare_part_d_if_eligible": { + year: everyone_takes_up_part_d or person["takes_up_part_d"] + }, + } + situation["tax_units"][f"tax_unit_{i}"] = { + "members": [name], + "filing_status": {year: person["filing_status"]}, + "medicare_irmaa_magi_two_years_prior": {year: person["magi"]}, + } + situation["households"][f"household_{i}"] = {"members": [name]} + return Simulation(situation=situation) + + +@settings( + max_examples=25, + deadline=None, + suppress_health_check=[HealthCheck.too_slow], +) +@given( + people=st.lists(PERSON, min_size=1, max_size=8), + year=st.sampled_from(YEARS), +) +def test_part_d_surcharge_applies_only_to_part_d_enrollees(people, year): + sim = simulate(people, year, everyone_takes_up_part_d=False) + everyone = simulate(people, year, everyone_takes_up_part_d=True) + + surcharge = sim.calculate("income_adjusted_part_d_premium_surcharge", year) + surcharge_if_all_enroll = everyone.calculate( + "income_adjusted_part_d_premium_surcharge", year + ) + part_d = sim.calculate("medicare_part_d_enrolled", year) + enrolled = sim.calculate("medicare_enrolled", year) + eligible = sim.calculate("is_medicare_eligible", year) + takes_up_part_d = np.array([person["takes_up_part_d"] for person in people]) + + assert (surcharge >= 0).all() + assert (surcharge[~part_d] == 0).all() + np.testing.assert_array_equal(surcharge[part_d], surcharge_if_all_enroll[part_d]) + np.testing.assert_array_equal(part_d, enrolled & takes_up_part_d) + assert (~part_d | enrolled).all() + assert (~enrolled | eligible).all() + np.testing.assert_array_equal( + sim.calculate("gross_medicare_part_b_premium", year), + everyone.calculate("gross_medicare_part_b_premium", year), + ) diff --git a/policyengine_us/variables/gov/hhs/medicare/eligibility/part_d/income_adjusted_part_d_premium_surcharge.py b/policyengine_us/variables/gov/hhs/medicare/eligibility/part_d/income_adjusted_part_d_premium_surcharge.py index b4813a80226..dbbebc84eb4 100644 --- a/policyengine_us/variables/gov/hhs/medicare/eligibility/part_d/income_adjusted_part_d_premium_surcharge.py +++ b/policyengine_us/variables/gov/hhs/medicare/eligibility/part_d/income_adjusted_part_d_premium_surcharge.py @@ -7,7 +7,7 @@ class income_adjusted_part_d_premium_surcharge(Variable): label = "Medicare Part D premium surcharge (income-adjusted)" unit = USD definition_period = YEAR - defined_for = "medicare_enrolled" + defined_for = "medicare_part_d_enrolled" reference = "https://www.law.cornell.edu/cfr/text/42/423.286" documentation = "Annualized Medicare Part D IRMAA surcharge under 42 U.S.C. 1395w-113(a)(7). This is the federally set income-related premium adjustment amount, paid in addition to the beneficiary's underlying Part D plan premium. Annual bracket values come from CMS fact sheets." diff --git a/policyengine_us/variables/gov/hhs/medicare/eligibility/part_d/medicare_part_d_enrolled.py b/policyengine_us/variables/gov/hhs/medicare/eligibility/part_d/medicare_part_d_enrolled.py new file mode 100644 index 00000000000..eb9b49c8205 --- /dev/null +++ b/policyengine_us/variables/gov/hhs/medicare/eligibility/part_d/medicare_part_d_enrolled.py @@ -0,0 +1,16 @@ +from policyengine_us.model_api import * + + +class medicare_part_d_enrolled(Variable): + value_type = bool + entity = Person + label = "Enrolled in Medicare Part D" + documentation = ( + "Whether the person is enrolled in Medicare Part D. Only a Part D " + "eligible individual, one entitled to Part A or enrolled in Part B " + "(medicare_enrolled), can enroll." + ) + definition_period = YEAR + reference = "https://www.law.cornell.edu/uscode/text/42/1395w-101#a_3_A" + defined_for = "medicare_enrolled" + adds = ["takes_up_medicare_part_d_if_eligible"] diff --git a/policyengine_us/variables/gov/hhs/medicare/takes_up_medicare_part_d_if_eligible.py b/policyengine_us/variables/gov/hhs/medicare/takes_up_medicare_part_d_if_eligible.py new file mode 100644 index 00000000000..a31236373a2 --- /dev/null +++ b/policyengine_us/variables/gov/hhs/medicare/takes_up_medicare_part_d_if_eligible.py @@ -0,0 +1,18 @@ +from policyengine_us.model_api import * + + +class takes_up_medicare_part_d_if_eligible(Variable): + value_type = bool + entity = Person + label = "Whether a Medicare enrollee takes up Medicare Part D" + documentation = ( + "Whether a person enrolled in Medicare also enrolls in Part D drug " + "coverage: a stand-alone prescription drug plan, a Medicare " + "Advantage plan with drug coverage, or an employer group waiver plan. " + "People whose drug coverage comes instead from a retiree drug subsidy " + "plan, the Federal Employees Health Benefits Program, TRICARE for " + "Life or the VA are not enrolled in Part D. Microdata assign this " + "input; it defaults to true." + ) + definition_period = YEAR + default_value = True From 0470a2567f36740d2c9716bedcd4e33348e27275 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 15:23:01 -0400 Subject: [PATCH 2/2] Place the Part D enrollment cases beside the Medicare non-enrollment case Co-Authored-By: Claude Opus 5.5 --- ...ome_adjusted_part_d_premium_surcharge.yaml | 76 +++++++++---------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/income_adjusted_part_d_premium_surcharge.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/income_adjusted_part_d_premium_surcharge.yaml index 2f5aea8cd68..5abbc603df1 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/income_adjusted_part_d_premium_surcharge.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/income_adjusted_part_d_premium_surcharge.yaml @@ -47,6 +47,44 @@ output: income_adjusted_part_d_premium_surcharge: 0 +- name: unit test 4a - 2026 Medicare enrollee above the IRMAA threshold without Part D. + period: 2026 + input: + age: 66 + filing_status: SINGLE + adjusted_gross_income: + 2024: 109_001 + tax_exempt_interest_income: + 2024: 0 + takes_up_medicare_part_d_if_eligible: false + output: + income_adjusted_part_d_premium_surcharge: 0 + +- name: unit test 4b - 2026 joint couple in the third tier where only one spouse has Part D. + period: 2026 + input: + people: + head: + age: 67 + spouse: + age: 67 + takes_up_medicare_part_d_if_eligible: false + tax_units: + tax_unit: + members: [head, spouse] + filing_status: JOINT + adjusted_gross_income: + 2024: 342_001 + households: + household: + members: [head, spouse] + output: + medicare_part_d_enrolled: [true, false] + # $60.40 a month for the enrolled spouse; none for the other. + income_adjusted_part_d_premium_surcharge: [724.8, 0] + # Part B premium and IRMAA ($202.90 + $324.60 a month) are unaffected. + gross_medicare_part_b_premium: [6_330, 6_330] + - name: Case 5, 2026 single filer at the IRMAA threshold. period: 2026 input: @@ -129,41 +167,3 @@ medicare_irmaa_magi_two_years_prior: 500_000 output: income_adjusted_part_d_premium_surcharge: 1_029.6 - -- name: Case 11, 2026 Medicare enrollee above the IRMAA threshold without Part D. - period: 2026 - input: - age: 66 - filing_status: SINGLE - adjusted_gross_income: - 2024: 109_001 - tax_exempt_interest_income: - 2024: 0 - takes_up_medicare_part_d_if_eligible: false - output: - income_adjusted_part_d_premium_surcharge: 0 - -- name: Case 12, 2026 joint couple in the third tier where only one spouse has Part D. - period: 2026 - input: - people: - head: - age: 67 - spouse: - age: 67 - takes_up_medicare_part_d_if_eligible: false - tax_units: - tax_unit: - members: [head, spouse] - filing_status: JOINT - adjusted_gross_income: - 2024: 342_001 - households: - household: - members: [head, spouse] - output: - medicare_part_d_enrolled: [true, false] - # $60.40 a month for the enrolled spouse; none for the other. - income_adjusted_part_d_premium_surcharge: [724.8, 0] - # Part B premium and IRMAA ($202.90 + $324.60 a month) are unaffected. - gross_medicare_part_b_premium: [6_330, 6_330]