diff --git a/changelog.d/medicare-part-d-lis.added.md b/changelog.d/medicare-part-d-lis.added.md new file mode 100644 index 00000000000..b37702d66bd --- /dev/null +++ b/changelog.d/medicare-part-d-lis.added.md @@ -0,0 +1 @@ +Model the Medicare Part D low-income subsidy (Extra Help): statutory eligibility valued at the Trustees' average subsidy per enrollee, and count it as a CBO means-tested transfer. diff --git a/policyengine_us/parameters/calibration/gov/hhs/medicare/part_d/low_income_subsidy_per_recipient.yaml b/policyengine_us/parameters/calibration/gov/hhs/medicare/part_d/low_income_subsidy_per_recipient.yaml new file mode 100644 index 00000000000..5e9211e701e --- /dev/null +++ b/policyengine_us/parameters/calibration/gov/hhs/medicare/part_d/low_income_subsidy_per_recipient.yaml @@ -0,0 +1,59 @@ +description: The Medicare Trustees report and project these Part D low-income subsidy payments per low-income subsidy enrollee. +metadata: + unit: currency-USD + period: year + label: Medicare Part D low-income subsidy per recipient + # From 2036, each value is the 2035 value times the growth in Medicare + # spending per enrollee, as for the per-enrollee series beside it. + uprating: calibration.gov.hhs.medicare.per_capita_cost + reference: + # Table IV.B9, "Low-income subsidy", "Subsidy amount": incurred payments + # per low-income subsidy enrollee, beside the subsidy's enrollment (in + # millions). Enrollment times the amount gives Table IV.B10's "Low-income + # subsidy" payments, e.g. 14.4 million x $1,254 = $18.1 billion in 2026. + # 2016 to 2035 come from the 2026 report (file page 158); 2015, which that + # table no longer shows, from the 2025 report (file page 158). The two + # reports differ by $1 to $2 in 2021 to 2023 (revised data). 2026 Medicare + # Trustees Report, transmitted 2026-06-09, retrieved 2026-10-11, sha256 + # ffa56b9137006872300b0346149eae1613d09a172b6ba118aad48e66dfc48fa8; 2025 + # report sha256 + # 1b3de7a4faeee5ad42059e10740603182b716a5fa7b53b30d360b45b9e474d95. + # The average includes enrollees in institutions, whom CBO removes along + # with their benefits (Habib 2018, file page 52). CBO's adjusted 2015 + # totals, $22.6 billion for 10.7 million recipients (file page 51), give + # $2,112 per recipient, the same as this table's 2015 average; for 2016 + # CBO's $26.5 billion (from its own baseline) for 11.1 million gives + # $2,387 against $2,126 here. + - title: 2026 Annual Report of the Boards of Trustees of the Federal Hospital Insurance and Federal Supplementary Medical Insurance Trust Funds, Table IV.B9 + href: https://www.cms.gov/oact/tr/2026#page=158 + - title: 2025 Annual Report of the Boards of Trustees of the Federal Hospital Insurance and Federal Supplementary Medical Insurance Trust Funds, Table IV.B9 + href: https://www.cms.gov/oact/tr/2025#page=158 + - title: Habib, How CBO Adjusts for Survey Underreporting of Transfer Income in Its Distributional Analyses, CBO Working Paper 2018-07, Appendix C + href: https://www.cbo.gov/system/files/2018-07/54234-workingpaper.pdf#page=51 +values: + # Historical. + 2015-01-01: 2_112 + 2016-01-01: 2_126 + 2017-01-01: 2_156 + 2018-01-01: 2_204 + 2019-01-01: 2_273 + 2020-01-01: 2_507 + 2021-01-01: 2_646 + 2022-01-01: 2_873 + 2023-01-01: 3_092 + 2024-01-01: 2_833 + # 2025 is the first year of the full Inflation Reduction Act benefit + # redesign (2026 report, file page 119), which moved spending from the + # subsidy to the direct subsidy (Table IV.B9). + 2025-01-01: 1_226 + # Trustees' intermediate estimates. + 2026-01-01: 1_254 + 2027-01-01: 1_384 + 2028-01-01: 1_459 + 2029-01-01: 1_579 + 2030-01-01: 1_833 + 2031-01-01: 1_922 + 2032-01-01: 1_956 + 2033-01-01: 1_982 + 2034-01-01: 2_014 + 2035-01-01: 2_076 diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_d/low_income_subsidy/eligibility/income_limit.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_d/low_income_subsidy/eligibility/income_limit.yaml new file mode 100644 index 00000000000..7aa3fcd667a --- /dev/null +++ b/policyengine_us/parameters/gov/hhs/medicare/part_d/low_income_subsidy/eligibility/income_limit.yaml @@ -0,0 +1,15 @@ +description: Medicare limits the Part D low-income subsidy to people whose countable income is below this share of the federal poverty guideline for their family size. +values: + 2006-01-01: 1.5 + +metadata: + unit: /1 + period: year + label: Medicare Part D low-income subsidy income limit + reference: + - title: 42 U.S.C. § 1395w-114(a)(3)(A)(ii) - Subsidy eligible individual defined + href: https://www.law.cornell.edu/uscode/text/42/1395w-114#a_3_A_ii + - title: 42 CFR § 423.773(a)(1) - Subsidy eligible individual + href: https://www.ecfr.gov/current/title-42/chapter-IV/subchapter-B/part-423/subpart-P/section-423.773#p-423.773(a)(1) + - title: SSA POMS HI 03020.055 - Income Limits for Subsidy Eligibility + href: https://secure.ssa.gov/poms.nsf/lnx/0603020055 diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_d/low_income_subsidy/eligibility/ineligible_jurisdictions.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_d/low_income_subsidy/eligibility/ineligible_jurisdictions.yaml new file mode 100644 index 00000000000..150028aac86 --- /dev/null +++ b/policyengine_us/parameters/gov/hhs/medicare/part_d/low_income_subsidy/eligibility/ineligible_jurisdictions.yaml @@ -0,0 +1,25 @@ +description: Medicare limits the Part D low-income subsidy to residents of the 50 states and the District of Columbia, so residents of these other jurisdictions are ineligible. +values: + 2006-01-01: + # Territories and freely associated states. + - GU + - MP + - PR + - PW + - VI + # Armed Forces mail codes, which are addresses abroad. + - AA + - AE + - AP + +metadata: + unit: list + period: year + label: Medicare Part D low-income subsidy ineligible jurisdictions + reference: + - title: 42 U.S.C. § 1395w-114(a)(3)(F) - Treatment of territorial residents + href: https://www.law.cornell.edu/uscode/text/42/1395w-114#a_3_F + - title: 42 CFR § 423.772 - Definitions (State) + href: https://www.ecfr.gov/current/title-42/chapter-IV/subchapter-B/part-423/subpart-P/section-423.772 + - title: 20 CFR § 418.3010(b)(11) - Definitions (State) + href: https://www.ecfr.gov/current/title-20/chapter-III/part-418/subpart-A/section-418.3010#p-418.3010(b)(11) diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_d/low_income_subsidy/eligibility/resources/burial_exclusion.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_d/low_income_subsidy/eligibility/resources/burial_exclusion.yaml new file mode 100644 index 00000000000..d6a04e3f272 --- /dev/null +++ b/policyengine_us/parameters/gov/hhs/medicare/part_d/low_income_subsidy/eligibility/resources/burial_exclusion.yaml @@ -0,0 +1,18 @@ +description: Medicare excludes this amount per person, for the applicant and a spouse living with them, from countable resources as funds expected to be used for burial under the Part D low-income subsidy. +values: + 2006-01-01: 1_500 + +metadata: + unit: currency-USD + period: year + label: Medicare Part D low-income subsidy burial funds exclusion + reference: + # SSA applies the exclusion unless the applicant says none of the + # resources will go to funeral or burial expenses, without asking for the + # funds' value or designation (POMS HI 03030.020 and HI 03030.025). + - title: 20 CFR § 418.3425(j) - Burial funds resource exclusion + href: https://www.ecfr.gov/current/title-20/chapter-III/part-418/subpart-D/section-418.3425#p-418.3425(j) + - title: SSA POMS HI 03030.020 - Resource Exclusions + href: https://secure.ssa.gov/poms.nsf/lnx/0603030020 + - title: SSA POMS HI 03030.025 - Resource Limits for Subsidy Eligibility + href: https://secure.ssa.gov/poms.nsf/lnx/0603030025 diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_d/low_income_subsidy/eligibility/resources/limit/couple.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_d/low_income_subsidy/eligibility/resources/limit/couple.yaml new file mode 100644 index 00000000000..fd07ec6bf55 --- /dev/null +++ b/policyengine_us/parameters/gov/hhs/medicare/part_d/low_income_subsidy/eligibility/resources/limit/couple.yaml @@ -0,0 +1,40 @@ +description: Medicare limits the Part D low-income subsidy to married people living with their spouse whose combined countable resources do not exceed this amount, before the burial funds exclusion. +values: + 2006-01-01: 20_000 + 2007-01-01: 20_410 + 2008-01-01: 20_970 + 2009-01-01: 22_010 + 2011-01-01: 22_260 + 2012-01-01: 23_120 + 2013-01-01: 23_580 + 2014-01-01: 23_860 + 2015-01-01: 24_250 + 2017-01-01: 24_600 + 2018-01-01: 25_150 + 2019-01-01: 25_720 + 2020-01-01: 26_160 + 2021-01-01: 26_520 + 2022-01-01: 27_950 + 2023-01-01: 30_240 + 2024-01-01: 31_360 + 2025-01-01: 32_130 + 2026-01-01: 33_100 + +metadata: + unit: currency-USD + period: year + label: Medicare Part D low-income subsidy couple resource limit + reference: + # The statute's alternative resource standard: $20,000 in 2006, raised + # each year by the September CPI-U and rounded to $10. Before 2024 it was + # the limit for the partial subsidy (the full subsidy had a lower limit); + # from 2024 it is the limit for the full subsidy, the only one left. SSA's + # table lists both pre-2024 limits; these are the higher ones. SSA's table + # excludes the $3,000 couple burial funds exclusion, which is a separate + # parameter. + - title: 42 U.S.C. § 1395w-114(a)(3)(E)(i) - Alternative resource standard + href: https://www.law.cornell.edu/uscode/text/42/1395w-114#a_3_E_i + - title: 42 CFR § 423.773(b)(2)(iii) and (d)(2) - Resource thresholds + href: https://www.ecfr.gov/current/title-42/chapter-IV/subchapter-B/part-423/subpart-P/section-423.773#p-423.773(d)(2) + - title: SSA POMS HI 03030.025 - Resource Limits for Subsidy Eligibility + href: https://secure.ssa.gov/poms.nsf/lnx/0603030025 diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_d/low_income_subsidy/eligibility/resources/limit/individual.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_d/low_income_subsidy/eligibility/resources/limit/individual.yaml new file mode 100644 index 00000000000..54fc4010afd --- /dev/null +++ b/policyengine_us/parameters/gov/hhs/medicare/part_d/low_income_subsidy/eligibility/resources/limit/individual.yaml @@ -0,0 +1,40 @@ +description: Medicare limits the Part D low-income subsidy to unmarried people whose countable resources do not exceed this amount, before the burial funds exclusion. +values: + 2006-01-01: 10_000 + 2007-01-01: 10_210 + 2008-01-01: 10_490 + 2009-01-01: 11_010 + 2011-01-01: 11_140 + 2012-01-01: 11_570 + 2013-01-01: 11_800 + 2014-01-01: 11_940 + 2015-01-01: 12_140 + 2017-01-01: 12_320 + 2018-01-01: 12_600 + 2019-01-01: 12_890 + 2020-01-01: 13_110 + 2021-01-01: 13_290 + 2022-01-01: 14_010 + 2023-01-01: 15_160 + 2024-01-01: 15_720 + 2025-01-01: 16_100 + 2026-01-01: 16_590 + +metadata: + unit: currency-USD + period: year + label: Medicare Part D low-income subsidy individual resource limit + reference: + # The statute's alternative resource standard: $10,000 in 2006, raised + # each year by the September CPI-U and rounded to $10. Before 2024 it was + # the limit for the partial subsidy (the full subsidy had a lower limit); + # from 2024 it is the limit for the full subsidy, the only one left. SSA's + # table lists both pre-2024 limits; these are the higher ones. SSA's table + # excludes the $1,500 burial funds exclusion, which is a separate + # parameter. + - title: 42 U.S.C. § 1395w-114(a)(3)(E)(i) - Alternative resource standard + href: https://www.law.cornell.edu/uscode/text/42/1395w-114#a_3_E_i + - title: 42 CFR § 423.773(b)(2)(iii) and (d)(2) - Resource thresholds + href: https://www.ecfr.gov/current/title-42/chapter-IV/subchapter-B/part-423/subpart-P/section-423.773#p-423.773(d)(2) + - title: SSA POMS HI 03030.025 - Resource Limits for Subsidy Eligibility + href: https://secure.ssa.gov/poms.nsf/lnx/0603030025 diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_d/low_income_subsidy/eligibility/resources/sources.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_d/low_income_subsidy/eligibility/resources/sources.yaml new file mode 100644 index 00000000000..e85f02bcbf8 --- /dev/null +++ b/policyengine_us/parameters/gov/hhs/medicare/part_d/low_income_subsidy/eligibility/resources/sources.yaml @@ -0,0 +1,20 @@ +description: Medicare counts these assets as resources under the Part D low-income subsidy. +values: + 2006-01-01: + - bank_account_assets + - stock_assets + - bond_assets + +metadata: + unit: list + period: year + label: Medicare Part D low-income subsidy countable resource sources + reference: + # SSA counts liquid resources (cash, financial accounts, stocks, bonds, + # mutual funds, retirement accounts and similar) and equity in real + # property other than the home. The model has no retirement account or + # non-home real property inputs. + - title: 20 CFR § 418.3405 - What types of resources do we count? + href: https://www.ecfr.gov/current/title-20/chapter-III/part-418/subpart-D/section-418.3405 + - title: 42 CFR § 423.772 - Definitions (resources) + href: https://www.ecfr.gov/current/title-42/chapter-IV/subchapter-B/part-423/subpart-P/section-423.772 diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_d/low_income_subsidy/income/excluded_sources.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_d/low_income_subsidy/income/excluded_sources.yaml new file mode 100644 index 00000000000..18053652968 --- /dev/null +++ b/policyengine_us/parameters/gov/hhs/medicare/part_d/low_income_subsidy/income/excluded_sources.yaml @@ -0,0 +1,15 @@ +description: Medicare excludes these Supplemental Security Income unearned income sources from countable income under the Part D low-income subsidy. +values: + 2006-01-01: + - dividend_income + - interest_income + +metadata: + unit: list + period: year + label: Medicare Part D low-income subsidy excluded income sources + reference: + - title: 20 CFR § 418.3350(d) - What types of unearned income do we not count? + href: https://www.ecfr.gov/current/title-20/chapter-III/part-418/subpart-D/section-418.3350#p-418.3350(d) + - title: SSA POMS HI 03020.050 - Unearned Income Exclusions + href: https://secure.ssa.gov/poms.nsf/lnx/0603020050 diff --git a/policyengine_us/parameters/gov/household/cbo_means_tested_transfers.yaml b/policyengine_us/parameters/gov/household/cbo_means_tested_transfers.yaml index f8081bc5a9d..b32ee470846 100644 --- a/policyengine_us/parameters/gov/household/cbo_means_tested_transfers.yaml +++ b/policyengine_us/parameters/gov/household/cbo_means_tested_transfers.yaml @@ -11,6 +11,7 @@ values: - reduced_price_school_meals - spm_unit_energy_subsidy - general_assistance + - part_d_lis metadata: unit: list @@ -19,3 +20,7 @@ metadata: reference: - title: Congressional Budget Office, Reconciling the Official Poverty Measure and CBO’s Distributional Analysis of Household Income, Including In-Kind Transfers href: https://www.cbo.gov/publication/61157 + # CBO counts the Medicare Part D low-income subsidy as a means-tested + # transfer, not as Medicare (see cbo_medicare_benefit). + - title: Congressional Budget Office, Sensitivity of the Distribution of Household Income to the Treatment of Health Insurance From 1979 to 2021 (January 2026) + href: https://www.cbo.gov/publication/61910 diff --git a/policyengine_us/programs.yaml b/policyengine_us/programs.yaml index 05cb1d13747..8df4779c589 100644 --- a/policyengine_us/programs.yaml +++ b/policyengine_us/programs.yaml @@ -1117,6 +1117,23 @@ programs: adjustments; gross Part B premium calibration should use the enrollment-gated gross_medicare_part_b_premium_if_enrolled variable. + - id: medicare_part_d_lis + name: Medicare Part D Extra Help + full_name: Medicare Part D Low-Income Subsidy (Extra Help) + category: Healthcare + agency: HHS + status: partial + coverage: US + variable: part_d_lis + parameter_prefix: gov.hhs.medicare.part_d.low_income_subsidy + verified_start_year: 2015 + notes: >- + Models eligibility (countable income below 150% of the poverty guideline, + resource limits, and deemed eligibility for Medicaid and SSI recipients + and Medicare Savings Program enrollees) and values the subsidy at the + Medicare Trustees' average payment per subsidy enrollee, as CBO does; + the premium and cost-sharing subsidies are not computed per person. + # --- HUD programs --- - id: section_8 name: Section 8 diff --git a/policyengine_us/tests/core/test_part_d_lis.py b/policyengine_us/tests/core/test_part_d_lis.py new file mode 100644 index 00000000000..632dacf8660 --- /dev/null +++ b/policyengine_us/tests/core/test_part_d_lis.py @@ -0,0 +1,444 @@ +"""Invariants of the Medicare Part D low-income subsidy (``part_d_lis``). + +The subsidy goes to Medicare enrollees in the 50 states and DC who are deemed +eligible (Medicaid enrollees, SSI recipients, Medicare Savings Program +enrollees) or whose countable income is below 150% of the poverty guideline +for their family size and whose countable resources are within the limit +plus $1,500 per person for burial funds (42 U.S.C. 1395w-114(a)(3); 42 CFR +423.773; 20 CFR 418 subpart D). Each enrollee is valued at the Medicare +Trustees' average subsidy per subsidy enrollee, as CBO does. + +Parameters: + +1. Transcription: ``low_income_subsidy_per_recipient`` equals Table IV.B9's + "Subsidy amount" (2026 report; the 2025 report for 2015). +2. Conservation: IV.B9 enrollment times the amount returns Table IV.B10's + low-income subsidy payments, within the cells' rounding. +3. Differential: the same payments over Table V.B3's total enrollment equal + ``part_d_low_income_subsidy_per_enrollee``, the series + ``cbo_medicare_benefit`` subtracts, built from different cells. +4. Indexing: each resource limit is the previous year's limit raised by the + September-to-September change in the CPI-U (not seasonally adjusted), + never lowered, and rounded to the nearest $10, from the statute's 2006 + amounts. SSA's published 2015 couple limit is $10 below the rule; the + parameter keeps the published amount (an intended exception). +5. Bounds (Hypothesis): the amount per recipient is positive and below + Medicare spending per enrollee, and the limits never fall, in every year + from 2015 to 2100. + +Households (a seeded sample in one shared simulation, each household also +run with more Social Security, more interest and more assets): + +6. Countable income equals the rule restated here: the couple's SSI + income without interest and dividends, less $20 a month from unearned + income (the rest from earnings), $65 a month of earnings and half the + remaining earnings. +7. Interest and dividends never change countable income or eligibility. +8. More Social Security never lowers countable income or makes an income + ineligible person income eligible; more assets never make a resource + ineligible person resource eligible. +9. Eligibility equals the restated rule, and the benefit is either zero or + the year's amount per recipient, positive exactly for enrollees, who + are all eligible. +10. Spouses get the same countable income, family size and test results. +11. Differential: for people without interest or dividends, countable income + equals 12 times ``msp_countable_income``, which applies the same SSI + exclusions through a separate formula (single people, and couples where + both spouses are Medicare eligible). +""" + +import numpy as np +import pytest +from hypothesis import given +from hypothesis import strategies as st + +from policyengine_us import Simulation +from policyengine_us.system import system + +P = system.parameters +PER_RECIPIENT = "calibration.gov.hhs.medicare.part_d.low_income_subsidy_per_recipient" +LIS = P.gov.hhs.medicare.part_d.low_income_subsidy + +# 2026 Medicare Trustees Report (sha256 ffa56b91...8fa8), Table IV.B9 (file +# page 158): low-income subsidy enrollment (millions) and subsidy amount ($). +# 2015 from the 2025 report (sha256 1b3de7a4...4d95), Table IV.B9 (page 158). +IV_B9 = { + 2015: (12.1, 2_112), + 2016: (12.4, 2_126), + 2017: (12.7, 2_156), + 2018: (12.9, 2_204), + 2019: (13.1, 2_273), + 2020: (13.1, 2_507), + 2021: (13.2, 2_646), + 2022: (13.7, 2_873), + 2023: (14.3, 3_092), + 2024: (14.5, 2_833), + 2025: (14.2, 1_226), + 2026: (14.4, 1_254), + 2027: (14.8, 1_384), + 2028: (15.2, 1_459), + 2029: (15.5, 1_579), + 2030: (15.8, 1_833), + 2031: (16.0, 1_922), + 2032: (16.2, 1_956), + 2033: (16.4, 1_982), + 2034: (16.6, 2_014), + 2035: (16.7, 2_076), +} +# Table IV.B10 (file page 159), low-income subsidy, $ billions, incurred; +# 2015 from the 2025 report's Table IV.B10 (page 159). +IV_B10 = { + **{2015: 25.6, 2016: 26.4, 2017: 27.3, 2018: 28.5, 2019: 29.7, 2020: 33.0}, + **{2021: 35.0, 2022: 39.4, 2023: 44.2, 2024: 41.2, 2025: 17.4, 2026: 18.1}, + **{2027: 20.5, 2028: 22.1, 2029: 24.5, 2030: 28.9, 2031: 30.7, 2032: 31.7}, + **{2033: 32.4, 2034: 33.3, 2035: 34.8}, +} +# Table V.B3 (file page 196), total enrollment (HI and/or SMI), thousands. +V_B3_TOTAL = dict( + zip( + range(2015, 2036), + (55_589, 57_073, 58_683, 60_020, 61_535, 62_887, 63_980, 65_166) + + (66_606, 68_066, 69_289, 70_743, 72_322, 74_026, 75_436, 76_598) + + (77_521, 78_327, 79_058, 79_827, 80_632), + ) +) +# BLS CPI-U, U.S. city average, all items, not seasonally adjusted +# (CUUR0000SA0), September of each year, retrieved 2026-10-11 from the BLS +# public API. (pe-us's gov.bls.cpi.cpi_u holds seasonally adjusted values +# from 2024, so it is not used here.) +CPI_U_SEPTEMBER = { + **{2004: 189.9, 2005: 198.8, 2006: 202.9, 2007: 208.490, 2008: 218.783}, + **{2009: 215.969, 2010: 218.439, 2011: 226.889, 2012: 231.407}, + **{2013: 234.149, 2014: 238.031, 2015: 237.945, 2016: 241.428}, + **{2017: 246.819, 2018: 252.439, 2019: 256.759, 2020: 260.280}, + **{2021: 274.310, 2022: 296.808, 2023: 307.789, 2024: 315.301}, + **{2025: 324.800}, +} +STATUTE_2006 = {"individual": 10_000, "couple": 20_000} +PUBLISHED_EXCEPTIONS = {("couple", 2015): 24_250} # the rule gives 24,260 + + +def _per_recipient(year): + return P.calibration.gov.hhs.medicare.part_d.low_income_subsidy_per_recipient( + f"{year}-01-01" + ) + + +@pytest.mark.parametrize("year", sorted(IV_B9)) +def test_per_recipient_transcribes_table_iv_b9(year): + assert _per_recipient(year) == IV_B9[year][1] + + +@pytest.mark.parametrize("year", sorted(IV_B9)) +def test_enrollment_times_amount_returns_table_iv_b10(year): + enrollment, amount = IV_B9[year] + rebuilt = enrollment * 1e6 * _per_recipient(year) / 1e9 + # Rounding: total to $0.05B, enrollment to 0.05M x amount, amount to $0.5. + tolerance = 0.05 + 0.05e6 * amount / 1e9 + 0.5 * enrollment * 1e6 / 1e9 + assert abs(rebuilt - IV_B10[year]) <= tolerance + + +@pytest.mark.parametrize("year", sorted(IV_B9)) +def test_recipients_times_amount_matches_per_enrollee_series(year): + per_enrollee = ( + P.calibration.gov.hhs.medicare.part_d_low_income_subsidy_per_enrollee( + f"{year}-01-01" + ) + ) + enrollment, _ = IV_B9[year] + rebuilt = enrollment * 1e6 * _per_recipient(year) / (V_B3_TOTAL[year] * 1e3) + # Enrollment rounded to 0.1 million is at worst 0.42% off (2015, 12.1). + assert rebuilt == pytest.approx(per_enrollee, rel=0.005) + + +def _round_to_10(x): + return int(np.floor(x / 10 + 0.5)) * 10 + + +@pytest.mark.parametrize("kind", ["individual", "couple"]) +def test_resource_limits_follow_the_cpi_rule(kind): + limit = getattr(LIS.eligibility.resources.limit, kind) + assert limit("2006-01-01") == STATUTE_2006[kind] + for year in range(2007, 2027): + growth = CPI_U_SEPTEMBER[year - 1] / CPI_U_SEPTEMBER[year - 2] + expected = _round_to_10(limit(f"{year - 1}-01-01") * max(growth, 1)) + expected = PUBLISHED_EXCEPTIONS.get((kind, year), expected) + assert limit(f"{year}-01-01") == expected, year + + +@given(st.integers(2015, 2100)) +def test_amount_positive_and_below_spending(year): + amount = _per_recipient(year) + spending = P.calibration.gov.hhs.medicare.per_capita_cost(f"{year}-01-01") + assert 0 < amount < spending + + +@given(st.integers(2007, 2100)) +def test_resource_limits_never_fall(year): + for kind in ["individual", "couple"]: + limit = getattr(LIS.eligibility.resources.limit, kind) + assert limit(f"{year}-01-01") >= limit(f"{year - 1}-01-01") + + +# Households. +YEAR = 2026 +N = 300 +SEED = 20261011 +STATES = ["TX", "CA", "NY", "AK", "HI", "FL", "PR", "DC"] +VARIANTS = ["base", "more_ss", "more_interest", "more_assets"] + + +def _households(): + rng = np.random.default_rng(SEED) + out = [] + for _ in range(N): + married = bool(rng.random() < 0.5) + + def person(): + return dict( + social_security=int(rng.choice([0, 6_000, 12_000, 18_000, 26_000])), + employment_income=int(rng.choice([0, 0, 0, 4_000, 15_000])), + interest_income=int(rng.choice([0, 0, 300, 4_000])), + dividend_income=int(rng.choice([0, 0, 1_000])), + bank_account_assets=int(rng.choice([0, 3_000, 9_000, 16_000, 40_000])), + stock_assets=int(rng.choice([0, 0, 5_000])), + medicare_enrolled=bool(rng.random() < 0.9), + medicaid_enrolled=bool(rng.random() < 0.15), + ssi=int(rng.choice([0, 0, 0, 0, 2_400])), + msp_eligible=bool(rng.random() < 0.15), + takes_up_part_d_lis_if_eligible=bool(rng.random() < 0.7), + ) + + out.append( + dict( + state=str(rng.choice(STATES)), + dependents=int(rng.choice([0, 0, 0, 1, 2])), + members=[person()] + ([person()] if married else []), + ) + ) + return out + + +HOUSEHOLDS = _households() + + +def _variant(member, variant, is_head): + member = dict(member) + if is_head and variant == "more_ss": + member["social_security"] += 3_000 + if is_head and variant == "more_interest": + member["interest_income"] += 5_000 + member["dividend_income"] += 2_000 + if is_head and variant == "more_assets": + member["bank_account_assets"] += 5_000 + return member + + +def _situation(households, variants, inputs=True): + people, marital_units, tax_units, spm_units, units = {}, {}, {}, {}, {} + rows = [] # (household index, variant, member index, person name) + for v in variants: + for i, h in enumerate(households): + names = [] + for j, member in enumerate(h["members"]): + name = f"{v}_{i}_{j}" + record = _variant(member, v, j == 0) + if not inputs: + record = { + k: record[k] + for k in ( + "social_security", + "employment_income", + "interest_income", + "dividend_income", + ) + } + people[name] = { + "age": {YEAR: 70}, + **{k: {YEAR: val} for k, val in record.items()}, + } + names.append(name) + rows.append((i, v, j, name)) + spouses = list(names) + dependents = [] + for k in range(h["dependents"]): + name = f"{v}_{i}_d{k}" + people[name] = { + "age": {YEAR: 10}, + "is_tax_unit_dependent": {YEAR: True}, + } + marital_units[f"mu_{name}"] = {"members": [name]} + dependents.append(name) + marital_units[f"mu_{v}_{i}"] = {"members": spouses} + members = spouses + dependents + tax_units[f"tu_{v}_{i}"] = {"members": members} + spm_units[f"spm_{v}_{i}"] = {"members": members} + units[f"hh_{v}_{i}"] = { + "members": members, + "state_code": {YEAR: h["state"]}, + } + situation = { + "people": people, + "marital_units": marital_units, + "tax_units": tax_units, + "spm_units": spm_units, + "households": units, + } + return situation, rows + + +@pytest.fixture(scope="module") +def sample(): + situation, rows = _situation(HOUSEHOLDS, VARIANTS) + sim = Simulation(situation=situation) + names = list(situation["people"]) + index = {name: k for k, name in enumerate(names)} + variables = [ + "part_d_lis", + "part_d_lis_enrolled", + "part_d_lis_eligible", + "part_d_lis_deemed_eligible", + "part_d_lis_income_eligible", + "part_d_lis_resource_eligible", + "part_d_lis_countable_income", + "part_d_lis_family_size", + ] + values = {v: sim.calculate(v, YEAR) for v in variables} + return rows, index, values + + +def _restated_countable(members): + earned = sum(m["employment_income"] for m in members) / 12 + unearned = sum(m["social_security"] for m in members) / 12 + general = min(20, unearned) + countable_unearned = unearned - general + countable_earned = max(earned - 65 - (20 - general), 0) / 2 + return 12 * (countable_unearned + countable_earned) + + +def _fpg(state, size): + fpg = P.gov.hhs.fpg(f"{YEAR}-01-01") + group = state if state in ("AK", "HI", "PR") else "CONTIGUOUS_US" + return fpg.first_person[group] + fpg.additional_person[group] * (size - 1) + + +def _value(values, index, variable, name): + return values[variable][index[name]] + + +def test_countable_income_matches_restated_rule(sample): + rows, index, values = sample + for i, v, _, name in rows: + members = [ + _variant(m, v, j == 0) for j, m in enumerate(HOUSEHOLDS[i]["members"]) + ] + expected = _restated_countable(members) + actual = _value(values, index, "part_d_lis_countable_income", name) + assert actual == pytest.approx(expected, abs=0.01), (i, v) + + +def test_interest_and_dividends_change_nothing(sample): + rows, index, values = sample + for i, v, j, name in rows: + if v != "more_interest": + continue + base = f"base_{i}_{j}" + for variable in [ + "part_d_lis_countable_income", + "part_d_lis_income_eligible", + "part_d_lis_eligible", + "part_d_lis", + ]: + assert _value(values, index, variable, name) == _value( + values, index, variable, base + ), (i, variable) + + +def test_more_income_or_assets_never_helps(sample): + rows, index, values = sample + for i, v, j, name in rows: + base = f"base_{i}_{j}" + if v == "more_ss": + assert _value(values, index, "part_d_lis_countable_income", name) >= ( + _value(values, index, "part_d_lis_countable_income", base) + ) + if _value(values, index, "part_d_lis_income_eligible", name): + assert _value(values, index, "part_d_lis_income_eligible", base), i + if v == "more_assets": + if _value(values, index, "part_d_lis_resource_eligible", name): + assert _value(values, index, "part_d_lis_resource_eligible", base), i + + +def test_eligibility_and_benefit_match_restated_rules(sample): + rows, index, values = sample + amount = _per_recipient(YEAR) + excluded = list(LIS.eligibility.ineligible_jurisdictions(f"{YEAR}-01-01")) + limits = LIS.eligibility.resources(f"{YEAR}-01-01") + for i, v, j, name in rows: + h = HOUSEHOLDS[i] + members = [_variant(m, v, k == 0) for k, m in enumerate(h["members"])] + me = members[j] + couple = len(members) + size = couple + h["dependents"] + assert _value(values, index, "part_d_lis_family_size", name) == size + income_ok = _restated_countable(members) < 1.5 * _fpg(h["state"], size) + resources = sum(m["bank_account_assets"] + m["stock_assets"] for m in members) + limit = limits.limit.couple if couple == 2 else limits.limit.individual + resource_ok = resources <= limit + limits.burial_exclusion * couple + deemed = me["medicaid_enrolled"] or me["ssi"] > 0 or me["msp_eligible"] + eligible = ( + me["medicare_enrolled"] + and h["state"] not in excluded + and (deemed or (income_ok and resource_ok)) + ) + automatic = me["medicaid_enrolled"] or me["ssi"] > 0 + enrolled = eligible and (automatic or me["takes_up_part_d_lis_if_eligible"]) + assert _value(values, index, "part_d_lis_income_eligible", name) == income_ok + assert _value(values, index, "part_d_lis_resource_eligible", name) == ( + resource_ok + ) + assert _value(values, index, "part_d_lis_eligible", name) == eligible, i + assert _value(values, index, "part_d_lis_enrolled", name) == enrolled, i + benefit = _value(values, index, "part_d_lis", name) + assert benefit == (amount if enrolled else 0), i + + +def test_spouses_agree(sample): + rows, index, values = sample + for i, v, j, name in rows: + if j != 1: + continue + head = f"{v}_{i}_0" + for variable in [ + "part_d_lis_countable_income", + "part_d_lis_family_size", + "part_d_lis_income_eligible", + "part_d_lis_resource_eligible", + ]: + assert _value(values, index, variable, name) == _value( + values, index, variable, head + ), (i, variable) + + +def test_countable_income_matches_msp_countable_income(): + # No interest or dividends, everyone 70 and so Medicare eligible: the + # MSP formula then counts the same income the same way, through its + # couple branch for married couples and its personal branch otherwise. + households = [ + { + **h, + "members": [ + {**m, "interest_income": 0, "dividend_income": 0} for m in h["members"] + ], + } + for h in HOUSEHOLDS + ] + situation, rows = _situation(households, ["base"], inputs=False) + sim = Simulation(situation=situation) + names = list(situation["people"]) + lis = sim.calculate("part_d_lis_countable_income", YEAR) + msp = sim.calculate("msp_countable_income", f"{YEAR}-01") + medicare_eligible = sim.calculate("is_medicare_eligible", YEAR) + adults = np.array([not name.split("_")[-1].startswith("d") for name in names]) + assert medicare_eligible[adults].all() + np.testing.assert_allclose(lis[adults], 12 * msp[adults], atol=0.01) diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/part_d/low_income_subsidy/integration.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/part_d/low_income_subsidy/integration.yaml new file mode 100644 index 00000000000..a424e9ad85a --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/part_d/low_income_subsidy/integration.yaml @@ -0,0 +1,187 @@ +# Households computed from their inputs, through the Medicare Savings +# Program and the subsidy's own income and resource tests. 2026 guidelines: +# $15,960 for one person and $21,640 for two (contiguous states); the +# subsidy limit is 150% of them: $23,940 and $32,460. Medicare Savings +# Program QI limit: 135%, $21,546 for one person and $29,214 for two. + +- name: Single retiree deemed eligible through a Medicare Savings Program. + period: 2026 + input: + people: + person: + age: 70 + social_security: 20_000 + bank_account_assets: 5_000 + households: + household: + members: [person] + state_code: TX + output: + # Medicare Savings Program countable income (20,000 / 12 - 20) x 12 = + # 19,760 is above the SLMB limit (120%, 19,152) and below the QI limit, + # with resources under the $9,950 limit. + msp_eligible: true + part_d_lis_deemed_eligible: true + part_d_lis_countable_income: 19_760 + part_d_lis_income_eligible: true + part_d_lis_resource_eligible: true + part_d_lis_eligible: true + part_d_lis_enrolled: true + part_d_lis: 1_254 + +- name: Interest is excluded for the subsidy but not for the Medicare Savings Program. + period: 2026 + input: + people: + person: + age: 70 + social_security: 20_000 + interest_income: 5_000 + bank_account_assets: 15_000 + households: + household: + members: [person] + state_code: TX + output: + # Medicare Savings Program countable income (25,000 / 12 - 20) x 12 = + # 24,760 is over the QI limit, and $15,000 is over its $9,950 resource + # limit. The subsidy excludes interest: 19,760 < 23,940, and $15,000 <= + # 16,590 + 1,500. + msp_eligible: false + part_d_lis_deemed_eligible: false + part_d_lis_countable_income: 19_760 + part_d_lis_eligible: true + part_d_lis: 1_254 + +- name: Single retiree with resources over the limit is not eligible. + period: 2026 + input: + people: + person: + age: 68 + social_security: 22_000 + bank_account_assets: 50_000 + households: + household: + members: [person] + state_code: TX + output: + # Countable income (22,000 / 12 - 20) x 12 = 21,760 is under 23,940, but + # $50,000 is over $18,090. + part_d_lis_income_eligible: true + part_d_lis_resource_eligible: false + part_d_lis_deemed_eligible: false + part_d_lis_eligible: false + part_d_lis: 0 + +- name: Married couple eligible through the income and resource tests. + period: 2026 + input: + people: + spouse1: + age: 70 + social_security: 18_000 + bank_account_assets: 20_000 + spouse2: + age: 70 + social_security: 12_000 + bank_account_assets: 10_000 + marital_units: + marital_unit: + members: [spouse1, spouse2] + tax_units: + tax_unit: + members: [spouse1, spouse2] + spm_units: + spm_unit: + members: [spouse1, spouse2] + households: + household: + members: [spouse1, spouse2] + state_code: TX + output: + # Combined countable income (30,000 / 12 - 20) x 12 = 29,760 is over the + # couple QI limit (29,214) but under 32,460; combined resources $30,000 + # are under 33,100 + 3,000 = 36,100. + msp_eligible: [false, false] + part_d_lis_family_size: [2, 2] + part_d_lis_countable_income: [29_760, 29_760] + part_d_lis_income_eligible: [true, true] + part_d_lis_resource_eligible: [true, true] + part_d_lis_eligible: [true, true] + part_d_lis: [1_254, 1_254] + +- name: Not taking it up leaves an applicant-path eligible person without it. + period: 2026 + input: + people: + person: + age: 70 + social_security: 20_000 + interest_income: 5_000 + bank_account_assets: 15_000 + takes_up_part_d_lis_if_eligible: false + households: + household: + members: [person] + state_code: TX + output: + part_d_lis_eligible: true + part_d_lis_enrolled: false + part_d_lis: 0 + +- name: 2023, a person in the former partial-subsidy range gets the average. + period: 2023 + input: + people: + person: + age: 70 + social_security: 20_500 + bank_account_assets: 12_000 + households: + household: + members: [person] + state_code: TX + output: + # Countable income (20,500 / 12 - 20) x 12 = 20,260: above 135% (19,683) + # and below 150% (21,870) of the 2023 guideline ($14,580). Resources + # $12,000 are above the 2023 full-subsidy limit (9,090 + 1,500 = 10,590) + # and within the partial limit (15,160 + 1,500 = 16,660). + part_d_lis_countable_income: 20_260 + part_d_lis_eligible: true + part_d_lis: 3_092 + +- name: Under-65 Medicare enrollee with a disability on SSI. + period: 2026 + input: + people: + person: + age: 50 + is_disabled: true + medicare_enrolled: true + ssi: 6_000 + bank_account_assets: 1_000 + households: + household: + members: [person] + state_code: TX + output: + part_d_lis_deemed_eligible: true + part_d_lis_enrolled: true + part_d_lis: 1_254 + +- name: Person aged 70 not enrolled in Medicare gets nothing. + period: 2026 + input: + people: + person: + age: 70 + medicare_enrolled: false + social_security: 10_000 + households: + household: + members: [person] + state_code: TX + output: + part_d_lis_eligible: false + part_d_lis: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis.yaml new file mode 100644 index 00000000000..6b00b321bd7 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis.yaml @@ -0,0 +1,60 @@ +# Each enrollee gets the Trustees' average subsidy per low-income subsidy +# enrollee (2026 report, Table IV.B9; 2015 from the 2025 report). + +- name: Enrollee in 2026. + period: 2026 + input: + people: + person: + part_d_lis_enrolled: true + output: + part_d_lis: 1_254 + +- name: Enrollee in 2015. + period: 2015 + input: + people: + person: + part_d_lis_enrolled: true + output: + part_d_lis: 2_112 + +- name: Enrollee in 2024, before the Inflation Reduction Act redesign. + period: 2024 + input: + people: + person: + part_d_lis_enrolled: true + output: + part_d_lis: 2_833 + +- name: Enrollee in 2035, the last Trustees year. + period: 2035 + input: + people: + person: + part_d_lis_enrolled: true + output: + part_d_lis: 2_076 + +- name: Not enrolled. + period: 2026 + input: + people: + person: + part_d_lis_enrolled: false + output: + part_d_lis: 0 + +- name: Two enrollees in one household get the same average. + period: 2026 + input: + people: + spouse1: + part_d_lis_enrolled: true + spouse2: + part_d_lis_enrolled: true + child: + part_d_lis_enrolled: false + output: + part_d_lis: [1_254, 1_254, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_countable_income.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_countable_income.yaml new file mode 100644 index 00000000000..f21127b4dbf --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_countable_income.yaml @@ -0,0 +1,79 @@ +# Countable income: SSI income of the person and a spouse living with them, +# without interest and dividends; the $20 general exclusion and the $65 and +# one-half earned income exclusions apply once to the couple's income +# (20 CFR 418.3310, 418.3325, 418.3350). + +- name: Single person, Social Security and interest; interest excluded. + period: 2026 + input: + people: + person: + age: 70 + social_security: 18_000 + interest_income: 1_000 + dividend_income: 600 + output: + # Unearned 18,000 + 1,000 + 600 - 1,600 excluded = 18,000 a year, + # 1,500 a month; less the $20 general exclusion = 1,480 a month. + # 1,480 x 12 = 17,760. + part_d_lis_countable_income: 17_760 + +- name: Married couple, exclusions applied once to combined income. + period: 2026 + input: + people: + spouse1: + age: 70 + employment_income: 12_000 + spouse2: + age: 67 + social_security: 6_000 + dividend_income: 500 + marital_units: + marital_unit: + members: [spouse1, spouse2] + output: + # Combined unearned 6,000 a year = 500 a month, less $20 = 480. + # Combined earned 12,000 a year = 1,000 a month, less $65 = 935, half + # excluded = 467.50. Monthly 947.50 x 12 = 11,370 for each spouse. + part_d_lis_countable_income: [11_370, 11_370] + +- name: Spouse's income counts even when only one spouse is a Medicare enrollee. + period: 2026 + input: + people: + spouse1: + age: 70 + medicare_enrolled: true + social_security: 12_000 + spouse2: + age: 60 + medicare_enrolled: false + employment_income: 30_000 + marital_units: + marital_unit: + members: [spouse1, spouse2] + output: + # Unearned 1,000 a month less $20 = 980. Earned 2,500 a month less $65 = + # 2,435, half = 1,217.50. Monthly 2,197.50 x 12 = 26,370. + part_d_lis_countable_income: [26_370, 26_370] + +- name: Earned income only; the unused $20 general exclusion applies to earnings. + period: 2026 + input: + people: + person: + age: 66 + employment_income: 6_000 + output: + # 500 a month less $20 less $65 = 415, half = 207.50; x 12 = 2,490. + part_d_lis_countable_income: 2_490 + +- name: No income. + period: 2026 + input: + people: + person: + age: 66 + output: + part_d_lis_countable_income: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_deemed_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_deemed_eligible.yaml new file mode 100644 index 00000000000..9f76a8f12ab --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_deemed_eligible.yaml @@ -0,0 +1,47 @@ +# Full-benefit dual eligibles, SSI recipients and Medicare Savings Program +# enrollees are treated as eligible for the full subsidy (42 CFR +# 423.773(c)(1)). + +- name: Medicaid enrollee is deemed eligible. + period: 2026 + input: + people: + person: + medicaid_enrolled: true + ssi: 0 + msp_eligible: false + output: + part_d_lis_deemed_eligible: true + +- name: SSI recipient is deemed eligible. + period: 2026 + input: + people: + person: + medicaid_enrolled: false + ssi: 1_200 + msp_eligible: false + output: + part_d_lis_deemed_eligible: true + +- name: Medicare Savings Program eligible person is deemed eligible. + period: 2026 + input: + people: + person: + medicaid_enrolled: false + ssi: 0 + msp_eligible: true + output: + part_d_lis_deemed_eligible: true + +- name: None of the three is not deemed eligible. + period: 2026 + input: + people: + person: + medicaid_enrolled: false + ssi: 0 + msp_eligible: false + output: + part_d_lis_deemed_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_eligible.yaml new file mode 100644 index 00000000000..aa46e6d9793 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_eligible.yaml @@ -0,0 +1,82 @@ +# Eligible: a Medicare enrollee in the 50 states or DC who is deemed eligible +# or meets both the income and resource tests (42 U.S.C. 1395w-114(a)(3)). + +- name: Meets the income and resource tests. + period: 2026 + input: + people: + person: + medicare_enrolled: true + part_d_lis_deemed_eligible: false + part_d_lis_income_eligible: true + part_d_lis_resource_eligible: true + output: + part_d_lis_eligible: true + +- name: Income eligible but resources over the limit. + period: 2026 + input: + people: + person: + medicare_enrolled: true + part_d_lis_deemed_eligible: false + part_d_lis_income_eligible: true + part_d_lis_resource_eligible: false + output: + part_d_lis_eligible: false + +- name: Deemed eligible with income and resources over the limits. + period: 2026 + input: + people: + person: + medicare_enrolled: true + part_d_lis_deemed_eligible: true + part_d_lis_income_eligible: false + part_d_lis_resource_eligible: false + output: + part_d_lis_eligible: true + +- name: Not enrolled in Medicare. + period: 2026 + input: + people: + person: + medicare_enrolled: false + part_d_lis_deemed_eligible: true + part_d_lis_income_eligible: true + part_d_lis_resource_eligible: true + output: + part_d_lis_eligible: false + +- name: Puerto Rico residents are not eligible. + period: 2026 + input: + people: + person: + medicare_enrolled: true + part_d_lis_deemed_eligible: true + part_d_lis_income_eligible: true + part_d_lis_resource_eligible: true + households: + household: + members: [person] + state_code: PR + output: + part_d_lis_eligible: false + +- name: District of Columbia residents are eligible. + period: 2026 + input: + people: + person: + medicare_enrolled: true + part_d_lis_deemed_eligible: false + part_d_lis_income_eligible: true + part_d_lis_resource_eligible: true + households: + household: + members: [person] + state_code: DC + output: + part_d_lis_eligible: true diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_enrolled.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_enrolled.yaml new file mode 100644 index 00000000000..e86f88cc3d6 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_enrolled.yaml @@ -0,0 +1,61 @@ +# Medicaid enrollees and SSI recipients are enrolled without an application; +# other eligible people are enrolled if they take up the subsidy. + +- name: Medicaid enrollee who would not apply is enrolled anyway. + period: 2026 + input: + people: + person: + part_d_lis_eligible: true + medicaid_enrolled: true + ssi: 0 + takes_up_part_d_lis_if_eligible: false + output: + part_d_lis_enrolled: true + +- name: SSI recipient who would not apply is enrolled anyway. + period: 2026 + input: + people: + person: + part_d_lis_eligible: true + medicaid_enrolled: false + ssi: 1_200 + takes_up_part_d_lis_if_eligible: false + output: + part_d_lis_enrolled: true + +- name: Other eligible person who does not take it up. + period: 2026 + input: + people: + person: + part_d_lis_eligible: true + medicaid_enrolled: false + ssi: 0 + takes_up_part_d_lis_if_eligible: false + output: + part_d_lis_enrolled: false + +- name: Other eligible person who takes it up. + period: 2026 + input: + people: + person: + part_d_lis_eligible: true + medicaid_enrolled: false + ssi: 0 + takes_up_part_d_lis_if_eligible: true + output: + part_d_lis_enrolled: true + +- name: Medicaid enrollee who is not eligible (not in Medicare) is not enrolled. + period: 2026 + input: + people: + person: + part_d_lis_eligible: false + medicaid_enrolled: true + ssi: 0 + output: + part_d_lis_enrolled: false diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_family_size.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_family_size.yaml new file mode 100644 index 00000000000..d0bcd98f082 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_family_size.yaml @@ -0,0 +1,57 @@ +# Family size: the person, a spouse living with them, and relatives who +# depend on them for at least half of their support (42 CFR 423.772). + +- name: Single person living alone. + period: 2026 + input: + people: + person: + age: 70 + output: + part_d_lis_family_size: 1 + +- name: Married couple supporting a grandchild. + period: 2026 + input: + people: + head: + age: 70 + spouse: + age: 68 + grandchild: + age: 12 + is_tax_unit_dependent: true + marital_units: + couple: + members: [head, spouse] + grandchild_unit: + members: [grandchild] + tax_units: + tax_unit: + members: [head, spouse, grandchild] + output: + # The couple's family is 3. The grandchild is not a head or spouse, so + # its own family size is 1. + part_d_lis_family_size: [3, 3, 1] + +- name: Parent who is a dependent of an adult child. + period: 2026 + input: + people: + child: + age: 45 + parent: + age: 80 + is_tax_unit_dependent: true + marital_units: + child_unit: + members: [child] + parent_unit: + members: [parent] + tax_units: + tax_unit: + members: [child, parent] + output: + # The parent depends on the child, so the parent's family is just the + # parent; the child's family includes the dependent parent. + part_d_lis_family_size: [2, 1] diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_income_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_income_eligible.yaml new file mode 100644 index 00000000000..44769a1e266 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_income_eligible.yaml @@ -0,0 +1,62 @@ +# Countable income must be below 150% of the poverty guideline for the +# family size (42 U.S.C. 1395w-114(a)(3)(A)(ii)). 2026 guidelines: $15,960 +# for one person in the contiguous states plus $5,680 per additional person; +# Alaska $19,950 plus $7,100. + +- name: Single person just below 150% of the guideline. + period: 2026 + input: + people: + person: + part_d_lis_countable_income: 23_939 + part_d_lis_family_size: 1 + output: + # 1.5 x 15,960 = 23,940. + part_d_lis_income_eligible: true + +- name: Single person exactly at 150% of the guideline is not below it. + period: 2026 + input: + people: + person: + part_d_lis_countable_income: 23_940 + part_d_lis_family_size: 1 + output: + part_d_lis_income_eligible: false + +- name: Family of three uses the three-person guideline. + period: 2026 + input: + people: + person: + part_d_lis_countable_income: 40_000 + part_d_lis_family_size: 3 + output: + # 1.5 x (15,960 + 2 x 5,680) = 1.5 x 27,320 = 40,980. + part_d_lis_income_eligible: true + +- name: Alaska uses its own guideline. + period: 2026 + input: + people: + person: + part_d_lis_countable_income: 29_000 + part_d_lis_family_size: 1 + households: + household: + members: [person] + state_code: AK + output: + # 1.5 x 19,950 = 29,925; the contiguous-state limit would be 23,940. + part_d_lis_income_eligible: true + +- name: 2023, income from 135% to 150% of the guideline qualified for the partial subsidy. + period: 2023 + input: + people: + person: + part_d_lis_countable_income: 21_000 + part_d_lis_family_size: 1 + output: + # 2023: 1.35 x 14,580 = 19,683 < 21,000 < 1.5 x 14,580 = 21,870. + part_d_lis_income_eligible: true diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_resource_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_resource_eligible.yaml new file mode 100644 index 00000000000..c12e0858819 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_resource_eligible.yaml @@ -0,0 +1,63 @@ +# Countable resources of the person and a spouse living with them must not +# exceed the limit plus $1,500 per person for burial funds (POMS HI +# 03030.025, 20 CFR 418.3425(j)). + +- name: Single person at the 2026 limit with the burial exclusion. + period: 2026 + input: + people: + person: + bank_account_assets: 18_090 + output: + # 16,590 + 1,500 = 18,090. + part_d_lis_resource_eligible: true + +- name: Single person one dollar over the 2026 limit. + period: 2026 + input: + people: + person: + bank_account_assets: 18_091 + output: + part_d_lis_resource_eligible: false + +- name: Married couple at the 2026 couple limit, resources combined. + period: 2026 + input: + people: + spouse1: + bank_account_assets: 20_000 + spouse2: + stock_assets: 10_100 + bond_assets: 6_000 + marital_units: + marital_unit: + members: [spouse1, spouse2] + output: + # 20,000 + 10,100 + 6,000 = 36,100 = 33,100 + 2 x 1,500. + part_d_lis_resource_eligible: [true, true] + +- name: Married couple one dollar over the 2026 couple limit. + period: 2026 + input: + people: + spouse1: + bank_account_assets: 20_001 + spouse2: + stock_assets: 16_100 + marital_units: + marital_unit: + members: [spouse1, spouse2] + output: + part_d_lis_resource_eligible: [false, false] + +- name: 2023 uses the higher (partial subsidy) limit. + period: 2023 + input: + people: + person: + bank_account_assets: 16_660 + output: + # 15,160 + 1,500 = 16,660, CMS's published 2023 partial subsidy limit. + # The 2023 full subsidy limit was 9,090 + 1,500 = 10,590. + part_d_lis_resource_eligible: true diff --git a/policyengine_us/tests/policy/baseline/household/income/household/cbo_household_income.yaml b/policyengine_us/tests/policy/baseline/household/income/household/cbo_household_income.yaml index d52fdf17f34..19c6830df95 100644 --- a/policyengine_us/tests/policy/baseline/household/income/household/cbo_household_income.yaml +++ b/policyengine_us/tests/policy/baseline/household/income/household/cbo_household_income.yaml @@ -59,6 +59,8 @@ reduced_price_school_meals: 1 spm_unit_energy_subsidy: 9 general_assistance: 6 + part_d_lis: 11 + msp: 100 wic: 100 head_start: 100 early_head_start: 100 @@ -66,7 +68,9 @@ household_state_benefits: 100 household_health_benefits: 100 output: - cbo_household_means_tested_transfers: 55 + # The Medicare Part D low-income subsidy counts; Medicare Savings + # Programs do not. + cbo_household_means_tested_transfers: 66 - name: CBO after-tax income uses federal taxes only and keeps negative income tax period: 2025 @@ -95,3 +99,30 @@ cbo_household_means_tested_transfers: 11_000 cbo_household_federal_taxes: 3_252 cbo_household_income_after_transfers_and_taxes: 45_085 + +- name: CBO income after transfers adds the Part D low-income subsidy + period: 2026 + input: + age: 70 + medicare_enrolled: true + social_security: 20_000 + interest_income: 5_000 + bank_account_assets: 15_000 + state_code: TX + medicaid: 0 + chip: 0 + snap: 0 + housing_assistance: 0 + ssi: 0 + tanf: 0 + free_school_meals: 0 + reduced_price_school_meals: 0 + spm_unit_energy_subsidy: 0 + general_assistance: 0 + output: + # Eligible through the subsidy's own tests (countable income 19,760 < + # 23,940, resources 15,000 <= 18,090), valued at the 2026 Trustees + # average of $1,254 per subsidy enrollee, the only means-tested + # transfer here. + part_d_lis: 1_254 + cbo_household_means_tested_transfers: 1_254 diff --git a/policyengine_us/variables/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis.py b/policyengine_us/variables/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis.py new file mode 100644 index 00000000000..aa3d928ddea --- /dev/null +++ b/policyengine_us/variables/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis.py @@ -0,0 +1,28 @@ +from policyengine_us.model_api import * + + +class part_d_lis(Variable): + value_type = float + entity = Person + label = "Medicare Part D low-income subsidy" + documentation = ( + "Value of the Part D low-income subsidy (Extra Help) as the " + "Congressional Budget Office counts it: the Medicare Trustees' " + "average subsidy payment per subsidy enrollee, the same for every " + "enrollee. Most of the subsidy pays cost sharing, which depends on " + "each enrollee's drug use, which the model does not have." + ) + unit = USD + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/42/1395w-114", + "https://www.cbo.gov/publication/61910", + "https://www.cbo.gov/system/files/2018-07/54234-workingpaper.pdf#page=46", + "https://www.cms.gov/oact/tr/2026#page=158", + ) + defined_for = "part_d_lis_enrolled" + + def formula(person, period, parameters): + p = parameters(period).calibration.gov.hhs.medicare.part_d + enrolled = person("part_d_lis_enrolled", period) + return enrolled * p.low_income_subsidy_per_recipient diff --git a/policyengine_us/variables/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_countable_income.py b/policyengine_us/variables/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_countable_income.py new file mode 100644 index 00000000000..686665bd8a5 --- /dev/null +++ b/policyengine_us/variables/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_countable_income.py @@ -0,0 +1,40 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.income._apply_ssi_exclusions import ( + _apply_ssi_exclusions, +) + + +class part_d_lis_countable_income(Variable): + value_type = float + entity = Person + label = "Medicare Part D low-income subsidy countable income" + documentation = ( + "Annual countable income for the Part D low-income subsidy: the " + "Supplemental Security Income definition of income, for the person " + "and a spouse living with them, without in-kind support and " + "maintenance or interest and dividends. The $20 general, $65 earned " + "and one-half earned income exclusions apply once to the combined " + "income." + ) + unit = USD + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/42/1395w-114#a_3_C", + "https://www.ecfr.gov/current/title-42/chapter-IV/subchapter-B/part-423/subpart-P/section-423.772", + "https://www.ecfr.gov/current/title-20/chapter-III/part-418/subpart-D/section-418.3310", + "https://www.ecfr.gov/current/title-20/chapter-III/part-418/subpart-D/section-418.3325", + "https://www.ecfr.gov/current/title-20/chapter-III/part-418/subpart-D/section-418.3350", + "https://secure.ssa.gov/poms.nsf/lnx/0603020050", + ) + + def formula(person, period, parameters): + p = parameters(period).gov.hhs.medicare.part_d.low_income_subsidy.income + earned = person("ssi_earned_income", period) + unearned = person("ssi_unearned_income", period) - add( + person, period, p.excluded_sources + ) + # A spouse's income counts whether or not the spouse applies or is + # eligible, and the exclusions apply once to the couple's income. + couple_earned = person.marital_unit.sum(earned) + couple_unearned = max_(person.marital_unit.sum(unearned), 0) + return _apply_ssi_exclusions(couple_earned, couple_unearned, parameters, period) diff --git a/policyengine_us/variables/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_deemed_eligible.py b/policyengine_us/variables/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_deemed_eligible.py new file mode 100644 index 00000000000..2dfdc01bafd --- /dev/null +++ b/policyengine_us/variables/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_deemed_eligible.py @@ -0,0 +1,24 @@ +from policyengine_us.model_api import * + + +class part_d_lis_deemed_eligible(Variable): + value_type = bool + entity = Person + label = "Deemed eligible for the Medicare Part D low-income subsidy" + documentation = ( + "Treated as eligible for the full subsidy without an income or " + "resource test: full-benefit dual eligibles (Medicaid enrollees), " + "Supplemental Security Income recipients, and people eligible for a " + "Medicare Savings Program (QMB, SLMB or QI)." + ) + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/42/1395w-114#a_3_B_v", + "https://www.ecfr.gov/current/title-42/chapter-IV/subchapter-B/part-423/subpart-P/section-423.773#p-423.773(c)", + ) + + def formula(person, period, parameters): + medicaid = person("medicaid_enrolled", period) + ssi = add(person, period, ["ssi"]) > 0 + msp = add(person, period, ["msp_eligible"]) > 0 + return medicaid | ssi | msp diff --git a/policyengine_us/variables/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_eligible.py b/policyengine_us/variables/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_eligible.py new file mode 100644 index 00000000000..7e07fb76b40 --- /dev/null +++ b/policyengine_us/variables/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_eligible.py @@ -0,0 +1,30 @@ +from policyengine_us.model_api import * + + +class part_d_lis_eligible(Variable): + value_type = bool + entity = Person + label = "Eligible for the Medicare Part D low-income subsidy" + documentation = ( + "A Medicare enrollee living in the 50 states or the District of " + "Columbia who is deemed eligible or meets the income and resource " + "tests. Before 2024 people with income from 135% to 150% of the " + "poverty guideline, or resources between the lower and higher " + "limits, got a partial subsidy; this covers both." + ) + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/42/1395w-114#a_3", + "https://www.ecfr.gov/current/title-42/chapter-IV/subchapter-B/part-423/subpart-P/section-423.773", + "https://www.ecfr.gov/current/title-20/chapter-III/part-418/subpart-D/section-418.3101", + ) + defined_for = "medicare_enrolled" + + def formula(person, period, parameters): + p = parameters(period).gov.hhs.medicare.part_d.low_income_subsidy.eligibility + state_code = person.household("state_code_str", period) + in_state = ~np.isin(state_code, p.ineligible_jurisdictions) + deemed = person("part_d_lis_deemed_eligible", period) + income_eligible = person("part_d_lis_income_eligible", period) + resource_eligible = person("part_d_lis_resource_eligible", period) + return in_state & (deemed | (income_eligible & resource_eligible)) diff --git a/policyengine_us/variables/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_enrolled.py b/policyengine_us/variables/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_enrolled.py new file mode 100644 index 00000000000..a1cadfb0488 --- /dev/null +++ b/policyengine_us/variables/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_enrolled.py @@ -0,0 +1,25 @@ +from policyengine_us.model_api import * + + +class part_d_lis_enrolled(Variable): + value_type = bool + entity = Person + label = "Enrolled in the Medicare Part D low-income subsidy" + documentation = ( + "Medicare enrolls Medicaid enrollees and Supplemental Security Income " + "recipients in a Part D plan with the subsidy without an application. " + "Other eligible people receive it if they take it up." + ) + definition_period = YEAR + reference = ( + "https://www.ecfr.gov/current/title-42/chapter-IV/subchapter-B/part-423/subpart-P/section-423.773#p-423.773(c)(2)", + "https://www.ecfr.gov/current/title-20/chapter-III/part-418/subpart-D/section-418.3101", + "https://www.ecfr.gov/current/title-42/chapter-IV/subchapter-B/part-423/subpart-B/section-423.34", + ) + defined_for = "part_d_lis_eligible" + + def formula(person, period, parameters): + medicaid = person("medicaid_enrolled", period) + ssi = add(person, period, ["ssi"]) > 0 + takes_up = person("takes_up_part_d_lis_if_eligible", period) + return medicaid | ssi | takes_up diff --git a/policyengine_us/variables/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_family_size.py b/policyengine_us/variables/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_family_size.py new file mode 100644 index 00000000000..9e5a9bf72b8 --- /dev/null +++ b/policyengine_us/variables/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_family_size.py @@ -0,0 +1,24 @@ +from policyengine_us.model_api import * + + +class part_d_lis_family_size(Variable): + value_type = int + entity = Person + label = "Medicare Part D low-income subsidy family size" + documentation = ( + "The person, a spouse living with them, and relatives living with " + "them who depend on the person or spouse for at least half of their " + "support, approximated by the dependents of the tax unit the person " + "heads or is the spouse in." + ) + definition_period = YEAR + reference = ( + "https://www.ecfr.gov/current/title-42/chapter-IV/subchapter-B/part-423/subpart-P/section-423.772", + "https://secure.ssa.gov/poms.nsf/lnx/0603020055", + ) + + def formula(person, period, parameters): + couple_size = person.marital_unit.nb_persons() + head_or_spouse = person("is_tax_unit_head_or_spouse", period) + dependents = person.tax_unit("tax_unit_dependents", period) + return couple_size + where(head_or_spouse, dependents, 0) diff --git a/policyengine_us/variables/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_income_eligible.py b/policyengine_us/variables/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_income_eligible.py new file mode 100644 index 00000000000..37e1475241a --- /dev/null +++ b/policyengine_us/variables/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_income_eligible.py @@ -0,0 +1,23 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.hhs.tax_unit_fpg import fpg + + +class part_d_lis_income_eligible(Variable): + value_type = bool + entity = Person + label = "Income eligible for the Medicare Part D low-income subsidy" + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/42/1395w-114#a_3_A_ii", + "https://www.ecfr.gov/current/title-42/chapter-IV/subchapter-B/part-423/subpart-P/section-423.773#p-423.773(a)(1)", + "https://secure.ssa.gov/poms.nsf/lnx/0603020055", + ) + + def formula(person, period, parameters): + p = parameters(period).gov.hhs.medicare.part_d.low_income_subsidy.eligibility + family_size = person("part_d_lis_family_size", period) + state_group = person.household("state_group_str", period) + income_limit = p.income_limit * fpg( + family_size, state_group, period, parameters + ) + return person("part_d_lis_countable_income", period) < income_limit diff --git a/policyengine_us/variables/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_resource_eligible.py b/policyengine_us/variables/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_resource_eligible.py new file mode 100644 index 00000000000..fe7e0580493 --- /dev/null +++ b/policyengine_us/variables/gov/hhs/medicare/part_d/low_income_subsidy/part_d_lis_resource_eligible.py @@ -0,0 +1,29 @@ +from policyengine_us.model_api import * + + +class part_d_lis_resource_eligible(Variable): + value_type = bool + entity = Person + label = "Resource eligible for the Medicare Part D low-income subsidy" + documentation = ( + "Countable resources of the person and a spouse living with them do " + "not exceed the individual or couple limit plus the burial funds " + "exclusion for each of them." + ) + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/42/1395w-114#a_3_E", + "https://www.ecfr.gov/current/title-42/chapter-IV/subchapter-B/part-423/subpart-P/section-423.773#p-423.773(d)(2)", + "https://www.ecfr.gov/current/title-20/chapter-III/part-418/subpart-D/section-418.3410", + "https://www.ecfr.gov/current/title-20/chapter-III/part-418/subpart-D/section-418.3425", + "https://secure.ssa.gov/poms.nsf/lnx/0603030025", + ) + + def formula(person, period, parameters): + p = parameters( + period + ).gov.hhs.medicare.part_d.low_income_subsidy.eligibility.resources + resources = person.marital_unit.sum(add(person, period, p.sources)) + couple_size = person.marital_unit.nb_persons() + limit = where(couple_size == 2, p.limit.couple, p.limit.individual) + return resources <= limit + p.burial_exclusion * couple_size diff --git a/policyengine_us/variables/gov/hhs/medicare/part_d/low_income_subsidy/takes_up_part_d_lis_if_eligible.py b/policyengine_us/variables/gov/hhs/medicare/part_d/low_income_subsidy/takes_up_part_d_lis_if_eligible.py new file mode 100644 index 00000000000..f1a359189e5 --- /dev/null +++ b/policyengine_us/variables/gov/hhs/medicare/part_d/low_income_subsidy/takes_up_part_d_lis_if_eligible.py @@ -0,0 +1,18 @@ +from policyengine_us.model_api import * + + +class takes_up_part_d_lis_if_eligible(Variable): + value_type = bool + entity = Person + label = ( + "Whether an eligible person enrolls in the Medicare Part D low-income subsidy" + ) + documentation = ( + "Whether an eligible person who is neither a Medicaid enrollee nor a " + "Supplemental Security Income recipient applies for the subsidy and " + "enrolls in a Part D plan. Medicare Savings Program enrollees are " + "deemed eligible, but the model has Medicare Savings Program " + "eligibility, not enrollment, so this flag covers them too." + ) + definition_period = YEAR + default_value = True