diff --git a/changelog.d/medicare-part-b-payers.fixed.md b/changelog.d/medicare-part-b-payers.fixed.md new file mode 100644 index 00000000000..ef988006336 --- /dev/null +++ b/changelog.d/medicare-part-b-payers.fixed.md @@ -0,0 +1 @@ +Stop charging the Medicare Part B premium to Part A-only enrollees (new medicare_part_b_enrolled input) and to SSI recipients whose Part B premium a state Medicaid buy-in pays, counted month by month and including 209(b) SSI recipients who qualify for Medicaid; Medicaid eligibility counts the premium before the buy-in. diff --git a/policyengine_us/parameters/gov/states/wa/dor/property_tax_exemption/senior_disabled/income/deductions/sources.yaml b/policyengine_us/parameters/gov/states/wa/dor/property_tax_exemption/senior_disabled/income/deductions/sources.yaml index 9a0d644369a..6175a31ff01 100644 --- a/policyengine_us/parameters/gov/states/wa/dor/property_tax_exemption/senior_disabled/income/deductions/sources.yaml +++ b/policyengine_us/parameters/gov/states/wa/dor/property_tax_exemption/senior_disabled/income/deductions/sources.yaml @@ -3,7 +3,7 @@ values: 2008-01-01: # RCW 84.36.383(2)(a) — prescription drugs - prescription_expense - # RCW 84.36.383(2)(c) — Medicare premiums (Part B, net of MSP coverage). The statute authorizes Medicare premiums only, not general private health insurance, so we use medicare_part_b_premium rather than the broader medical_expense_health_insurance_premiums. + # RCW 84.36.383(2)(c) — Medicare premiums (Part B, net of state-paid MSP and SSI buy-in coverage). The statute authorizes Medicare premiums only, not general private health insurance, so we use medicare_part_b_premium rather than the broader medical_expense_health_insurance_premiums. - medicare_part_b_premium # RCW 84.36.383(2)(e) DME / prosthetics, (h) nebulizers, (j) ostomic items, (k) insulin, (l) kidney dialysis devices, (m) disposable drug-delivery devices — covered as an aggregate by other_medical_expenses. - other_medical_expenses 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 dd7d9a1821c..68dbd86aa42 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 @@ -12,7 +12,7 @@ period: 2026 input: health_insurance_premiums_without_medicare_part_b: 600 - medicare_part_b_premium: 1_200 + medicare_part_b_premium_before_buy_in: 1_200 medicare_enrolled: true pre_tax_health_insurance_premiums: 2_400 other_medical_expenses: 300 diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/medicaid_optional_senior_or_disabled_countable_income.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/medicaid_optional_senior_or_disabled_countable_income.yaml index a0b2f1b20f4..e60af53f66e 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/medicaid_optional_senior_or_disabled_countable_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/income/medicaid_optional_senior_or_disabled_countable_income.yaml @@ -154,6 +154,20 @@ # (1,000 - 20 - 202.90) x 12 = 9,325.20. medicaid_optional_senior_or_disabled_countable_income: 9_325.2 +- name: California deducts no Part B premium for a Part A-only Medicare enrollee. + period: 2026 + absolute_error_margin: 0.01 + input: + age: 70 + medicare_enrolled: true + medicare_part_b_enrolled: false + ssi_marital_unearned_income: 12_000 + state_code: CA + output: + # No Part B enrollment, so no Part B premium to disregard: + # (1,000 - 20) x 12 = 11,760. + medicaid_optional_senior_or_disabled_countable_income: 11_760 + - name: California applies one 20-dollar deduction to a couple's combined unearned income. period: 2026 absolute_error_margin: 0.01 @@ -574,7 +588,7 @@ age: 70 medicare_enrolled: true social_security_retirement: 17_400 - medicare_part_b_premium: 1_626 + medicare_part_b_premium_before_buy_in: 1_626 health_insurance_premiums_without_medicare_part_b: 1_200 state_code: CA output: diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/medicaid_part_b_buy_in_premium_coverage.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/medicaid_part_b_buy_in_premium_coverage.yaml new file mode 100644 index 00000000000..3a04d40b070 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/medicaid_part_b_buy_in_premium_coverage.yaml @@ -0,0 +1,173 @@ +- name: SSI recipient in a Section 1634 state gets the standard premium paid. + period: 2025 + input: + age: 70 + taxable_interest_income: 30_000 + receives_ssi: true + state_code: CA + output: + medicaid_part_b_buy_in_premium_coverage: 2_220 + +- name: A Medicare enrollee without SSI gets no buy-in coverage. + period: 2025 + input: + age: 70 + taxable_interest_income: 30_000 + state_code: CA + output: + medicaid_part_b_buy_in_premium_coverage: 0 + +- name: A Part A-only SSI recipient has no Part B premium to pay. + period: 2025 + input: + age: 70 + taxable_interest_income: 30_000 + receives_ssi: true + medicare_part_b_enrolled: false + state_code: CA + output: + medicaid_part_b_buy_in_premium_coverage: 0 + +- name: A 209(b) SSI recipient who spends down to Medicaid gets the standard premium paid. + # Connecticut is a 209(b) state. Medical expenses of 40,000 plus the 2,220 + # premium before the buy-in exceed countable income (at most + # 30,000 - 240 = 29,760), so the 42 CFR 435.121 spend-down makes the + # person categorically needy and in buy-in group 1 (42 CFR 407.42(b)(1)(i)). + period: 2025 + input: + age: 70 + taxable_interest_income: 30_000 + other_medical_expenses: 40_000 + receives_ssi: true + state_code: CT + output: + is_209b_ssi_recipient_for_medicaid: true + medicaid_enrolled: true + medicaid_part_b_buy_in_premium_coverage: 2_220 + medicare_part_b_premium: 0 + +- name: The Part B premium before the buy-in decides a 209(b) spend-down. + # Countable income 23,580 and limit 10,016. 23,580 - 12,000 = 11,580 is over + # the limit; 23,580 - 12,000 - 2,220 = 9,360 is under it. + period: 2025 + input: + age: 70 + taxable_interest_income: 30_000 + other_medical_expenses: 12_000 + receives_ssi: true + state_code: CT + output: + medicaid_optional_senior_or_disabled_countable_income: 23_580 + medicaid_medically_needy_medical_expenses: 14_220 + is_209b_ssi_recipient_for_medicaid: true + medicaid_part_b_buy_in_premium_coverage: 2_220 + medicare_part_b_premium: 0 + +- name: A 209(b) spend-down that lands on the limit qualifies. + # Countable income 23,580 and limit 0.64 x 15,650 = 10,016. + # 23,580 - 11,344 - 2,220 = 10,016, at the limit; 11,343 would leave 10,017. + period: 2025 + input: + age: 70 + taxable_interest_income: 30_000 + other_medical_expenses: 11_344 + receives_ssi: true + state_code: CT + output: + medicaid_medically_needy_medical_expenses: 13_564 + is_209b_ssi_recipient_for_medicaid: true + medicaid_part_b_buy_in_premium_coverage: 2_220 + medicare_part_b_premium: 0 + +- name: A 209(b) SSI recipient with SSI in December only gets one month of buy-in. + # 185 x 12 = 2,220 base premium; one month of SSI buys in 185, leaving + # 2,035. is_209b_ssi_recipient_for_medicaid reads December's SSI. + period: 2025 + input: + age: 70 + taxable_interest_income: 40_000 + other_medical_expenses: 40_000 + receives_ssi: + 2025-01: false + 2025-02: false + 2025-03: false + 2025-04: false + 2025-05: false + 2025-06: false + 2025-07: false + 2025-08: false + 2025-09: false + 2025-10: false + 2025-11: false + 2025-12: true + state_code: CT + output: + is_209b_ssi_recipient_for_medicaid: true + medicaid_part_b_buy_in_premium_coverage: 185 + medicare_part_b_premium: 2_035 + +- name: SSI in December only buys in one month, not the whole year. + # 2026 base premium 202.90 x 12 = 2,434.80. The buy-in runs month by month + # (42 USC 1395v(d)(2)(C)): 202.90, leaving 2,231.90 for the enrollee. + period: 2026 + absolute_error_margin: 0.01 + input: + age: 70 + taxable_interest_income: 30_000 + receives_ssi: + 2026-01: false + 2026-02: false + 2026-03: false + 2026-04: false + 2026-05: false + 2026-06: false + 2026-07: false + 2026-08: false + 2026-09: false + 2026-10: false + 2026-11: false + 2026-12: true + state_code: TX + output: + medicaid_part_b_buy_in_premium_coverage: 202.9 + medicare_part_b_premium: 2_231.9 + +- name: SSI in January only still buys in that month. + # 42 USC 1395v(d)(3)(A): coverage ends after SSI ends, so January's month + # counts: 202.90 buy-in, 2,231.90 for the enrollee. + period: 2026 + absolute_error_margin: 0.01 + input: + age: 70 + taxable_interest_income: 30_000 + receives_ssi: + 2026-01: true + 2026-02: false + 2026-03: false + 2026-04: false + 2026-05: false + 2026-06: false + 2026-07: false + 2026-08: false + 2026-09: false + 2026-10: false + 2026-11: false + 2026-12: false + state_code: TX + output: + medicaid_part_b_buy_in_premium_coverage: 202.9 + medicare_part_b_premium: 2_231.9 + +- name: A modeled SSI recipient gets the buy-in without reported SSI. + # No income, so SSI pays the 2025 federal benefit rate: 967 x 12 = 11,604. + # Switching MSP off isolates the buy-in: 185 x 12 = 2,220. + period: 2025 + input: + age: 70 + msp_income_eligible: false + msp_asset_eligible: false + state_code: CA + output: + ssi: 11_604 + medicaid_part_b_buy_in_premium_coverage: 2_220 + medicare_part_b_premium: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/medicare_part_b_enrolled.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/medicare_part_b_enrolled.yaml new file mode 100644 index 00000000000..2b23fe4bea8 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/medicare_part_b_enrolled.yaml @@ -0,0 +1,31 @@ +- name: A Medicare enrollee is in Part B by default. + period: 2025 + input: + age: 70 + output: + medicare_enrolled: true + medicare_part_b_enrolled: true + +- name: A person not enrolled in Medicare is not in Part B. + period: 2025 + input: + age: 70 + medicare_enrolled: false + output: + medicare_part_b_enrolled: false + +- name: A person under 65 without Medicare is not in Part B. + period: 2025 + input: + age: 64 + output: + medicare_part_b_enrolled: false + +- name: A Part A-only enrollee is entered as not in Part B. + period: 2025 + input: + age: 70 + medicare_part_b_enrolled: false + output: + medicare_enrolled: true + medicare_part_b_enrolled: false diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/medicare_part_b_premium.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/medicare_part_b_premium.yaml index 5ad0e7b72d0..98b889769ed 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/medicare_part_b_premium.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/medicare_part_b_premium.yaml @@ -10,6 +10,7 @@ is_medicare_eligible: true msp_income_eligible: false msp_asset_eligible: false + ssi: 0 # Not an SSI recipient, so no state buy-in. output: medicare_part_b_premium: 2_220 # $185 * 12 months @@ -25,6 +26,7 @@ is_medicare_eligible: true msp_income_eligible: false msp_asset_eligible: false + ssi: 0 # Not an SSI recipient, so no state buy-in. output: medicare_part_b_premium: 4_440 # $370 * 12 months @@ -40,6 +42,7 @@ is_medicare_eligible: true msp_income_eligible: false msp_asset_eligible: false + ssi: 0 # Not an SSI recipient, so no state buy-in. output: medicare_part_b_premium: 7_546.8 # $628.90 * 12 months @@ -55,6 +58,7 @@ is_medicare_eligible: true msp_income_eligible: false msp_asset_eligible: false + ssi: 0 # Not an SSI recipient, so no state buy-in. output: medicare_part_b_premium: 3_108 # $259 * 12 months @@ -70,6 +74,7 @@ is_medicare_eligible: true msp_income_eligible: false msp_asset_eligible: false + ssi: 0 # Not an SSI recipient, so no state buy-in. output: medicare_part_b_premium: 2_220 # $185 * 12 months @@ -85,6 +90,7 @@ is_medicare_eligible: true msp_income_eligible: false msp_asset_eligible: false + ssi: 0 # Not an SSI recipient, so no state buy-in. output: medicare_part_b_premium: 7_546.8 # $628.90 * 12 months @@ -127,6 +133,7 @@ medicare_enrolled: true msp_income_eligible: false msp_asset_eligible: false + ssi: 0 # Not an SSI recipient, so no state buy-in. output: medicare_part_b_premium: 2_434.8 @@ -143,6 +150,7 @@ medicare_enrolled: true msp_income_eligible: false msp_asset_eligible: false + ssi: 0 # Not an SSI recipient, so no state buy-in. output: medicare_part_b_premium: 3_409.2 @@ -159,6 +167,7 @@ medicare_enrolled: true msp_income_eligible: false msp_asset_eligible: false + ssi: 0 # Not an SSI recipient, so no state buy-in. output: medicare_part_b_premium: 4_869.6 @@ -175,6 +184,7 @@ medicare_enrolled: true msp_income_eligible: false msp_asset_eligible: false + ssi: 0 # Not an SSI recipient, so no state buy-in. output: medicare_part_b_premium: 7_790.4 @@ -191,5 +201,84 @@ medicare_enrolled: true msp_income_eligible: false msp_asset_eligible: false + ssi: 0 # Not an SSI recipient, so no state buy-in. output: medicare_part_b_premium: 8_278.8 + +- name: A Part A-only enrollee pays no Part B premium. + # $40,000 of interest is above the 135% FPL MSP limit, so with Part B + # this person would pay the $185 standard premium: $2,220 a year. + period: 2025 + input: + age: 70 + taxable_interest_income: 40_000 + medicare_part_b_enrolled: false + state_code: TX + output: + medicare_enrolled: true + msp_part_b_premium_coverage: 0 + gross_medicare_part_b_premium_if_enrolled: 0 + medicare_part_b_premium: 0 + medical_expense_health_insurance_premiums: 0 + +- name: The same person enrolled in Part B pays the standard premium. + period: 2025 + input: + age: 70 + taxable_interest_income: 40_000 + state_code: TX + output: + medicare_part_b_enrolled: true + msp_part_b_premium_coverage: 0 + medicaid_part_b_buy_in_premium_coverage: 0 + gross_medicare_part_b_premium_if_enrolled: 2_220 + medicare_part_b_premium: 2_220 + medical_expense_health_insurance_premiums: 2_220 + +- name: A Part A-only enrollee's other premiums still count. + period: 2025 + input: + age: 70 + taxable_interest_income: 40_000 + medicare_part_b_enrolled: false + health_insurance_premiums_without_medicare_part_b: 1_500 + state_code: TX + output: + medicare_part_b_premium: 0 + medical_expense_health_insurance_premiums: 1_500 + +- name: The state buy-in pays the standard premium for an SSI recipient above the MSP limits. + # Reported SSI receipt in California, a Section 1634 state, gives Medicaid + # as an SSI recipient; $30,000 of interest is above 135% FPL, so no MSP. + period: 2025 + input: + age: 70 + taxable_interest_income: 30_000 + receives_ssi: true + state_code: CA + output: + msp_part_b_premium_coverage: 0 + medicaid_part_b_buy_in_premium_coverage: 2_220 + medicare_part_b_premium: 0 + medical_expense_health_insurance_premiums: 0 + +- name: MSP and buy-in coverage of the same premium are not added twice. + period: 2025 + input: + age: 70 + gross_medicare_part_b_premium: 4_440 + msp_part_b_premium_coverage: 2_220 + medicaid_part_b_buy_in_premium_coverage: 2_220 + output: + # The income-related amount above the standard premium stays: + # 4,440 - 2,220 = 2,220. + medicare_part_b_premium: 2_220 + +- name: Coverage above the gross premium leaves no negative premium. + period: 2025 + input: + age: 70 + gross_medicare_part_b_premium: 2_220 + msp_part_b_premium_coverage: 3_000 + output: + medicare_part_b_premium: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/test_part_b_msp_offset.py b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/test_part_b_msp_offset.py index 29ecb083de7..0d59d486787 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/test_part_b_msp_offset.py +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/test_part_b_msp_offset.py @@ -1,3 +1,6 @@ +"""Random draws check the paid premium's bounds across premium, coverage and +enrollment values together; the YAML cases pin single points.""" + import pytest from policyengine_us import CountryTaxBenefitSystem, Simulation @@ -256,6 +259,8 @@ def test_medicare_part_b_premium_does_not_depend_on_calculation_order(): "base_part_b_premium": {"2025": 2_220, "2026": 2_220}, "msp_income_eligible": no_msp_eligibility, "msp_asset_eligible": no_msp_eligibility, + # Not an SSI recipient, so no state buy-in. + "ssi": {"2025": 0, "2026": 0}, } }, "households": {"household": {"members": ["person"]}}, @@ -415,3 +420,50 @@ def test_medicare_irmaa_magi_two_years_prior_input_drives_irmaa(): assert sim.calculate("gross_medicare_part_b_premium", PERIOD)[0] == pytest.approx( 7_546.8 ) + + +def test_part_b_premium_paid_stays_between_zero_and_gross_premium(): + # Property over random premiums, state-paid amounts and Part B enrollment: + # what the enrollee pays is never negative and never above the gross + # premium, and a Part A-only enrollee pays nothing. One vectorized + # simulation covers every draw. + import numpy as np + + rng = np.random.default_rng(10634) + n = 200 + gross = rng.uniform(0, 10_000, n).round(2) + msp = rng.choice([0, 1, 2], n) * rng.uniform(0, 6_000, n).round(2) / 2 + buy_in = rng.choice([0, 1], n) * rng.uniform(0, 6_000, n).round(2) + part_b = rng.random(n) < 0.8 + names = [f"p{i}" for i in range(n)] + people = { + name: { + "age": {PERIOD: 70}, + "medicare_enrolled": {PERIOD: True}, + "medicare_part_b_enrolled": {PERIOD: bool(part_b[i])}, + "gross_medicare_part_b_premium": {PERIOD: float(gross[i])}, + "msp_part_b_premium_coverage": {PERIOD: float(msp[i])}, + "medicaid_part_b_buy_in_premium_coverage": {PERIOD: float(buy_in[i])}, + } + for i, name in enumerate(names) + } + groups = {name: {"members": [name]} for name in names} + sim = Simulation( + tax_benefit_system=SYSTEM, + situation={ + "people": people, + "households": {f"h_{k}": v for k, v in groups.items()}, + "tax_units": {f"t_{k}": v for k, v in groups.items()}, + "spm_units": {f"s_{k}": v for k, v in groups.items()}, + "families": {f"f_{k}": v for k, v in groups.items()}, + "marital_units": {f"m_{k}": v for k, v in groups.items()}, + }, + ) + paid = sim.calculate("medicare_part_b_premium", PERIOD) + modeled_gross = sim.calculate("gross_medicare_part_b_premium", PERIOD) + + assert (paid >= 0).all() + assert (paid <= modeled_gross).all() + assert (paid[~part_b] == 0).all() + # The exact netting (a person covered by both pathways is covered once) + # is pinned by the overlap case in medicare_part_b_premium.yaml. diff --git a/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/subtractions/id_health_insurance_premiums_subtraction.yaml b/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/subtractions/id_health_insurance_premiums_subtraction.yaml index 64e8371a130..6382ca2f2ee 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/subtractions/id_health_insurance_premiums_subtraction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/id/tax/income/subtractions/id_health_insurance_premiums_subtraction.yaml @@ -529,6 +529,23 @@ id_deductions: [20_000, 20_000] id_taxable_income: [8_250, 8_250] +- name: No subtraction when a Medicare Savings Program pays the Part B premium + # Idaho Code 63-3022P covers premiums the taxpayer paid. $12,000 of Social + # Security ($1,000 a month) is below the 135% FPL MSP limit, so the MSP + # pays the $185 standard premium for all 12 months and the filer pays none. + period: 2025 + input: + age: 70 + social_security_retirement: 12_000 + state_code: ID + output: + medicare_part_b_enrolled: true + gross_medicare_part_b_premium: 2_220 + msp_part_b_premium_coverage: 2_220 + medicare_part_b_premium: 0 + id_qualified_health_insurance_premiums: 0 + id_health_insurance_premiums_subtraction: 0 + - name: A supplied deduction below the person amounts takes medical premiums first period: 2025 input: 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 e4702532c46..402dd115165 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 @@ -22,6 +22,6 @@ class medicaid_medically_needy_medical_expenses(Variable): ) adds = [ - "medical_expense_health_insurance_premiums", + "medicaid_eligibility_health_insurance_premiums", "other_medical_expenses", ] diff --git a/policyengine_us/variables/gov/hhs/medicaid/income/medicaid_optional_senior_or_disabled_countable_income.py b/policyengine_us/variables/gov/hhs/medicaid/income/medicaid_optional_senior_or_disabled_countable_income.py index 1a21e9444f0..d21bb002f98 100644 --- a/policyengine_us/variables/gov/hhs/medicaid/income/medicaid_optional_senior_or_disabled_countable_income.py +++ b/policyengine_us/variables/gov/hhs/medicaid/income/medicaid_optional_senior_or_disabled_countable_income.py @@ -72,18 +72,20 @@ def formula(person, period, parameters): # "regardless of how the premium is paid": swap the person-paid Part B # in the shared aggregate for the individual's full Part B premium, the # base premium plus any IRMAA (42 USC 1395r(i)), whoever pays it. - # NOTE: medicare_enrolled (Part A and/or Part B) stands in for Part B - # enrollment, as in medical_expense_health_insurance_premiums. + # Only a Part B enrollee (medicare_part_b_enrolled) has a Part B + # premium to disregard. # NOTE: ACWDL 20-24 Example 5 also deducts a non-applicant spouse's # premiums. Spousal income here follows SSI deeming (20 CFR 416.1163), # so that premium is not modeled, nor is MC 176's maintenance need # deduction for the spouse (lines 19 and 21; issue 10543). - medicare_enrolled = person("medicare_enrolled", period) - paid_premiums = person("medical_expense_health_insurance_premiums", period) - paid_part_b = person("medicare_part_b_premium", period) * medicare_enrolled - full_part_b = ( - person("gross_medicare_part_b_premium", period) * medicare_enrolled + # Premiums are counted before the Medicaid Part B buy-in, which starts + # only once Medicaid finds the person eligible (42 USC 1395v(d)(2)(C)). + part_b_enrolled = person("medicare_part_b_enrolled", period) + paid_premiums = person("medicaid_eligibility_health_insurance_premiums", period) + paid_part_b = ( + person("medicare_part_b_premium_before_buy_in", period) * part_b_enrolled ) + full_part_b = person("gross_medicare_part_b_premium", period) * part_b_enrolled disregards_part_b = p.disregards_medicare_part_b_premium[state].astype(bool) own_premiums = where( disregards_part_b, diff --git a/policyengine_us/variables/gov/hhs/medicare/eligibility/part_b/gross_medicare_part_b_premium_if_enrolled.py b/policyengine_us/variables/gov/hhs/medicare/eligibility/part_b/gross_medicare_part_b_premium_if_enrolled.py index 0dfc72bc864..a9728fbc186 100644 --- a/policyengine_us/variables/gov/hhs/medicare/eligibility/part_b/gross_medicare_part_b_premium_if_enrolled.py +++ b/policyengine_us/variables/gov/hhs/medicare/eligibility/part_b/gross_medicare_part_b_premium_if_enrolled.py @@ -7,7 +7,7 @@ class gross_medicare_part_b_premium_if_enrolled(Variable): label = "Gross Medicare Part B premium if enrolled" unit = USD definition_period = YEAR - defined_for = "medicare_enrolled" + defined_for = "medicare_part_b_enrolled" reference = "https://www.medicare.gov/basics/costs/medicare-costs#partB" documentation = ( "Annual Medicare Part B premium for enrolled beneficiaries before " diff --git a/policyengine_us/variables/gov/hhs/medicare/eligibility/part_b/medicaid_part_b_buy_in_premium_coverage.py b/policyengine_us/variables/gov/hhs/medicare/eligibility/part_b/medicaid_part_b_buy_in_premium_coverage.py new file mode 100644 index 00000000000..f8d1d3d31c0 --- /dev/null +++ b/policyengine_us/variables/gov/hhs/medicare/eligibility/part_b/medicaid_part_b_buy_in_premium_coverage.py @@ -0,0 +1,59 @@ +from policyengine_us.model_api import * + + +class medicaid_part_b_buy_in_premium_coverage(Variable): + value_type = float + entity = Person + unit = USD + label = "Medicare Part B premium paid by a state Medicaid buy-in for SSI recipients" + definition_period = YEAR + defined_for = "medicare_part_b_enrolled" + reference = ( + "https://www.law.cornell.edu/uscode/text/42/1395v#d", + "https://www.ecfr.gov/current/title-42/section-407.40#p-407.40(c)(5)", + "https://www.ecfr.gov/current/title-42/section-407.42#p-407.42(b)(1)(i)", + "https://www.ecfr.gov/current/title-42/section-408.28", + "https://secure.ssa.gov/poms.nsf/lnx/0600815001", + ) + documentation = ( + "Annual standard Part B premium a state pays under its Part B buy-in " + "agreement for an enrollee who has Medicaid as an SSI recipient, " + "counted month by month in the months the person receives SSI. " + "Medicare Savings Program coverage is msp_part_b_premium_coverage. " + "SSI recipients in Section 1634 and SSI-criteria states are covered, " + "and so are SSI recipients who qualify for Medicaid under a Section " + "209(b) state's more restrictive criteria, since 42 CFR " + "407.42(b)(1)(i) includes SSI recipients covered as categorically " + "needy. Other full-benefit Medicaid groups, such as the medically " + "needy, are not covered here: whether a state's buy-in agreement " + "includes them varies by state. As with Medicare Savings Program " + "coverage, any income-related monthly adjustment amount stays with " + "the enrollee." + ) + + def formula(person, period, parameters): + # The Section 1634 and SSI-criteria test of is_ssi_recipient_for_medicaid. + state = person.household("state_code_str", period) + categories = parameters(period).gov.hhs.medicaid.eligibility.categories + is_covered = categories.ssi_recipient.is_covered[state].astype(bool) + classified = person("medicaid_ssi_recipient_state_classification", period) + classifications = classified.possible_values + covered_classification = (classified == classifications.SECTION_1634) | ( + classified == classifications.SSI_CRITERIA + ) + # The Section 209(b) test reads Medicaid countable income and + # medically needy expenses, which count the premium before this + # buy-in (medicare_part_b_premium_before_buy_in), so there is no cycle. + # It tests eligibility, with no take-up step. + ssi_medicaid_state = (is_covered & covered_classification) | person( + "is_209b_ssi_recipient_for_medicaid", period + ) + # 42 USC 1395v(d)(2)(C) and (d)(3)(A): buy-in coverage runs month by + # month while the person receives SSI. + monthly_premium = person("base_part_b_premium", period) / MONTHS_IN_YEAR + coverage = 0 + for month in period.get_subperiods(MONTH): + receives_ssi = (person("ssi", month) > 0) | person("receives_ssi", month) + covered = receives_ssi & ssi_medicaid_state + coverage += where(covered, monthly_premium, 0) + return coverage diff --git a/policyengine_us/variables/gov/hhs/medicare/eligibility/part_b/medicare_part_b_enrolled.py b/policyengine_us/variables/gov/hhs/medicare/eligibility/part_b/medicare_part_b_enrolled.py new file mode 100644 index 00000000000..3a49d938e06 --- /dev/null +++ b/policyengine_us/variables/gov/hhs/medicare/eligibility/part_b/medicare_part_b_enrolled.py @@ -0,0 +1,24 @@ +from policyengine_us.model_api import * + + +class medicare_part_b_enrolled(Variable): + value_type = bool + entity = Person + label = "Medicare Part B enrolled" + documentation = ( + "Whether the person is enrolled in Medicare Part B (supplementary " + "medical insurance). Part B is a separate enrollment from Part A, so " + "a Medicare enrollee can hold Part A only and owe no Part B premium. " + "Defaults to true for every Medicare enrollee (medicare_enrolled); " + "set it to false for a Part A-only enrollee." + ) + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/42/1395o#a", + "https://www.ecfr.gov/current/title-42/section-407.10", + ) + + def formula(person, period, parameters): + # Every Medicare enrollee is taken to hold Part B unless entered + # otherwise. + return person("medicare_enrolled", period) diff --git a/policyengine_us/variables/gov/hhs/medicare/eligibility/part_b/medicare_part_b_premium.py b/policyengine_us/variables/gov/hhs/medicare/eligibility/part_b/medicare_part_b_premium.py index b6d75b0ed52..b61a01ed1a3 100644 --- a/policyengine_us/variables/gov/hhs/medicare/eligibility/part_b/medicare_part_b_premium.py +++ b/policyengine_us/variables/gov/hhs/medicare/eligibility/part_b/medicare_part_b_premium.py @@ -7,11 +7,21 @@ class medicare_part_b_premium(Variable): label = "Medicare Part B premium" unit = USD definition_period = YEAR - defined_for = "medicare_enrolled" + defined_for = "medicare_part_b_enrolled" reference = "https://www.medicare.gov/basics/costs/medicare-costs#partB" - documentation = "Annual Medicare Part B premium paid out of pocket by the enrollee, net of Medicare Savings Program coverage." + documentation = ( + "Annual Medicare Part B premium paid out of pocket by a Part B " + "enrollee, net of the standard premium a state pays through a " + "Medicare Savings Program or a Medicaid buy-in for SSI recipients. " + "Never below zero or above the gross premium." + ) def formula(person, period, parameters): gross_premium = person("gross_medicare_part_b_premium", period) - msp_coverage = person("msp_part_b_premium_coverage", period) - return max_(gross_premium - msp_coverage, 0) + # Both pathways pay the same standard premium, so a person covered + # by both is covered once. + state_paid = max_( + person("msp_part_b_premium_coverage", period), + person("medicaid_part_b_buy_in_premium_coverage", period), + ) + return max_(gross_premium - state_paid, 0) diff --git a/policyengine_us/variables/gov/hhs/medicare/eligibility/part_b/medicare_part_b_premium_before_buy_in.py b/policyengine_us/variables/gov/hhs/medicare/eligibility/part_b/medicare_part_b_premium_before_buy_in.py new file mode 100644 index 00000000000..0ccc80a50ca --- /dev/null +++ b/policyengine_us/variables/gov/hhs/medicare/eligibility/part_b/medicare_part_b_premium_before_buy_in.py @@ -0,0 +1,27 @@ +from policyengine_us.model_api import * + + +class medicare_part_b_premium_before_buy_in(Variable): + value_type = float + entity = Person + label = "Medicare Part B premium before the Medicaid buy-in" + unit = USD + definition_period = YEAR + defined_for = "medicare_part_b_enrolled" + reference = ( + "https://www.ecfr.gov/current/title-42/section-407.42", + "https://www.law.cornell.edu/uscode/text/42/1395v#d", + ) + documentation = ( + "Annual Medicare Part B premium a Part B enrollee owes, net of " + "Medicare Savings Program coverage but before a state's Medicaid " + "buy-in for SSI recipients. Buy-in coverage starts only once Medicaid " + "finds the person eligible, so Medicaid eligibility tests count this " + "premium. Reading it instead of medicare_part_b_premium keeps those " + "tests from depending on the buy-in they determine." + ) + + def formula(person, period, parameters): + gross_premium = person("gross_medicare_part_b_premium", period) + msp_coverage = person("msp_part_b_premium_coverage", period) + return max_(gross_premium - msp_coverage, 0) diff --git a/policyengine_us/variables/gov/states/ok/dhs/liheap/income/ok_liheap_income_deductions.py b/policyengine_us/variables/gov/states/ok/dhs/liheap/income/ok_liheap_income_deductions.py index d3e8fcc49c0..f0e0c5e6833 100644 --- a/policyengine_us/variables/gov/states/ok/dhs/liheap/income/ok_liheap_income_deductions.py +++ b/policyengine_us/variables/gov/states/ok/dhs/liheap/income/ok_liheap_income_deductions.py @@ -38,7 +38,7 @@ def formula(person, period, parameters): # by analogy and without its $35 threshold: health premiums paid after # tax or through payroll, the whole Medicare Part B premium, and other # medical costs. The Part B premium counts only for a member with - # medicare_enrolled, and a nonzero health_insurance_premiums input + # medicare_part_b_enrolled, and a nonzero health_insurance_premiums input # replaces the non-Medicare and Part B split. medical_expenses = max_(person("snap_allowable_medical_expenses", period), 0) medical_deduction = medical_expenses * medical_eligible diff --git a/policyengine_us/variables/household/expense/health/medicaid_eligibility_health_insurance_premiums.py b/policyengine_us/variables/household/expense/health/medicaid_eligibility_health_insurance_premiums.py new file mode 100644 index 00000000000..2da4f6b9698 --- /dev/null +++ b/policyengine_us/variables/household/expense/health/medicaid_eligibility_health_insurance_premiums.py @@ -0,0 +1,33 @@ +from policyengine_us.model_api import * + + +class medicaid_eligibility_health_insurance_premiums(Variable): + value_type = float + entity = Person + label = "Health insurance premiums counted for Medicaid eligibility" + unit = USD + definition_period = YEAR + reference = ( + "https://www.ecfr.gov/current/title-42/section-435.121#p-435.121(f)", + "https://www.ecfr.gov/current/title-42/section-435.831", + ) + documentation = ( + "Person-level non-pretax health insurance premiums that Medicaid " + "eligibility tests deduct, such as 209(b) and medically needy " + "spend-down. Matches medical_expense_health_insurance_premiums but " + "counts the Medicare Part B premium before the state's Medicaid " + "buy-in (medicare_part_b_premium_before_buy_in), because the buy-in " + "starts only once Medicaid finds the person eligible." + ) + + def formula(person, period, parameters): + direct = person("health_insurance_premiums", period) + non_medicare = person( + "health_insurance_premiums_without_medicare_part_b", period + ) + part_b_enrolled = person("medicare_part_b_enrolled", period) + medicare_part_b = ( + person("medicare_part_b_premium_before_buy_in", period) * part_b_enrolled + ) + decomposed = non_medicare + medicare_part_b + return where(direct != 0, direct, decomposed) 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 307305f4176..847e3b998ee 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 @@ -11,7 +11,7 @@ class medical_expense_health_insurance_premiums(Variable): "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 " + "premiums for Part B 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." @@ -22,7 +22,7 @@ def formula(person, period, parameters): non_medicare = person( "health_insurance_premiums_without_medicare_part_b", period ) - medicare_enrolled = person("medicare_enrolled", period) - medicare_part_b = person("medicare_part_b_premium", period) * medicare_enrolled + part_b_enrolled = person("medicare_part_b_enrolled", period) + medicare_part_b = person("medicare_part_b_premium", period) * part_b_enrolled decomposed = non_medicare + medicare_part_b return where(direct != 0, direct, decomposed) 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 ba4c6c0cd64..1c62cf1bd4b 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 @@ -13,8 +13,9 @@ class spm_unit_medical_out_of_pocket_expenses(Variable): "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. " + "Part A and Part B premiums net of Medicare Savings Program and " + "Medicaid buy-in coverage where modeled, plus the Part D IRMAA " + "surcharge. " "Non-premium expenses include other medical expenses and " "over-the-counter health expenses." )