diff --git a/changelog.d/fix-home-equity-limit-projection.fixed.md b/changelog.d/fix-home-equity-limit-projection.fixed.md new file mode 100644 index 00000000000..672ec8fab91 --- /dev/null +++ b/changelog.d/fix-home-equity-limit-projection.fixed.md @@ -0,0 +1 @@ +Project the Medicaid long-term care home equity limits with CMS's method (the September-to-September CPI-U increase rounded to 0.1%, effective each January 1) instead of stepping the agricultural limit each February 1, drop their unpublished 2027 and 2028 values, and correct the CMS bulletin citations. diff --git a/policyengine_us/parameters/gov/bls/cpi/cpi_u_nsa_september.yaml b/policyengine_us/parameters/gov/bls/cpi/cpi_u_nsa_september.yaml new file mode 100644 index 00000000000..738737b408f --- /dev/null +++ b/policyengine_us/parameters/gov/bls/cpi/cpi_u_nsa_september.yaml @@ -0,0 +1,18 @@ +description: The Bureau of Labor Statistics publishes this September value of the Consumer Price Index for All Urban Consumers, not seasonally adjusted, from which CMS takes the September-to-September percentage change it applies to the Medicaid long-term care home equity limits. +values: + # BLS series CUUR0000SA0 (U.S. city average, all items, 1982-84=100). + 2018-09-01: 252.439 + 2019-09-01: 256.759 + 2020-09-01: 260.28 + 2021-09-01: 274.31 + 2022-09-01: 296.808 + 2023-09-01: 307.789 + 2024-09-01: 315.301 + 2025-09-01: 324.8 +metadata: + unit: currency-USD + label: CPI-U, not seasonally adjusted, September + economy: false + reference: + - title: BLS | CPI-U, U.S. city average, all items, not seasonally adjusted (CUUR0000SA0) + href: https://data.bls.gov/timeseries/CUUR0000SA0 diff --git a/policyengine_us/parameters/gov/hhs/medicaid/eligibility/long_term_care/home_equity/agricultural_limit.yaml b/policyengine_us/parameters/gov/hhs/medicaid/eligibility/long_term_care/home_equity/agricultural_limit.yaml index d9995ce6432..f003e8e9355 100644 --- a/policyengine_us/parameters/gov/hhs/medicaid/eligibility/long_term_care/home_equity/agricultural_limit.yaml +++ b/policyengine_us/parameters/gov/hhs/medicaid/eligibility/long_term_care/home_equity/agricultural_limit.yaml @@ -2,26 +2,25 @@ description: The Department of Health and Human Services limits agricultural hom values: 2025-01-01: 1_097_000 2026-01-01: 1_130_000 - 2027-01-01: 1_159_000 - 2028-01-01: 1_189_000 metadata: unit: currency-USD period: year label: Medicaid long-term care agricultural home equity limit - uprating: - parameter: gov.bls.cpi.cpi_u - rounding: - type: nearest - interval: 1_000 + # Years after the last CMS-published value are projected in + # parameters/uprating_extensions.py (extend_medicaid_home_equity_limits) by + # CMS's method under 42 USC 1396p(f)(1)(C): the prior year's limit times the + # September-to-September CPI-U increase rounded to 0.1%, then rounded to the + # nearest $1,000, effective January 1. P.L. 119-21 section 71108 exempts this + # limit from the $1,000,000 cap on other homes from 2028. reference: - - title: Updated 2025 SSI and Spousal Impoverishment Standards - href: https://www.medicaid.gov/federal-policy-guidance/downloads/cib05282025.pdf - - title: 2026 SSI, Spousal Impoverishment, and Medicare Savings Program Resource Standards - href: https://www.medicaid.gov/federal-policy-guidance/downloads/cib04272026.pdf - - title: CMCS Informational Bulletin, Dec. 9, 2025 + - title: Updated 2025 SSI and Spousal Impoverishment Standards, CMCS Informational Bulletin, May 28, 2025 + href: https://www.medicaid.gov/federal-policy-guidance/downloads/cib05282025.pdf#page=2 + - title: Updated 2026 SSI and Spousal Impoverishment Standards, CMCS Informational Bulletin, April 27, 2026 + href: https://www.medicaid.gov/federal-policy-guidance/downloads/cib04272026.pdf#page=2 + - title: 2026 SSI, Spousal Impoverishment, and Medicare Savings Program Resource Standards, CMCS Informational Bulletin, December 9, 2025 href: https://www.medicaid.gov/federal-policy-guidance/downloads/cib12092025.pdf#page=3 - - title: CMCS Informational Bulletin, Nov. 18, 2025 - href: https://www.medicaid.gov/federal-policy-guidance/downloads/cib11182025.pdf#page=8 + - title: CMCS Informational Bulletin, November 18, 2025, Section 71108 - Revising Home Equity Limit + href: https://www.medicaid.gov/federal-policy-guidance/downloads/cib11182025.pdf#page=9 - title: H.R. 1, 119th Congress, Sec. 71108 - Revising home equity limit for determining eligibility for long-term care services under the Medicaid program href: https://www.congress.gov/bill/119th-congress/house-bill/1/text diff --git a/policyengine_us/parameters/gov/hhs/medicaid/eligibility/long_term_care/home_equity/limit.yaml b/policyengine_us/parameters/gov/hhs/medicaid/eligibility/long_term_care/home_equity/limit.yaml index 1e657c53b4e..118f0e15970 100644 --- a/policyengine_us/parameters/gov/hhs/medicaid/eligibility/long_term_care/home_equity/limit.yaml +++ b/policyengine_us/parameters/gov/hhs/medicaid/eligibility/long_term_care/home_equity/limit.yaml @@ -2,7 +2,9 @@ description: The Department of Health and Human Services limits non-agricultural values: 2025-01-01: 1_097_000 2026-01-01: 1_130_000 - 2027-01-01: 1_159_000 + # CMS has not published 2027 standards. Until then 2027 is projected in + # parameters/uprating_extensions.py (extend_medicaid_home_equity_limits) as + # the indexed maximum, which equals the agricultural limit before 2028. 2028-01-01: 1_000_000 metadata: @@ -10,11 +12,13 @@ metadata: period: year label: Medicaid long-term care non-agricultural home equity limit reference: - - title: Updated 2025 SSI and Spousal Impoverishment Standards - href: https://www.medicaid.gov/federal-policy-guidance/downloads/cib05282025.pdf - - title: 2026 SSI, Spousal Impoverishment, and Medicare Savings Program Resource Standards - href: https://www.medicaid.gov/federal-policy-guidance/downloads/cib04272026.pdf + - title: Updated 2025 SSI and Spousal Impoverishment Standards, CMCS Informational Bulletin, May 28, 2025 + href: https://www.medicaid.gov/federal-policy-guidance/downloads/cib05282025.pdf#page=2 + - title: Updated 2026 SSI and Spousal Impoverishment Standards, CMCS Informational Bulletin, April 27, 2026 + href: https://www.medicaid.gov/federal-policy-guidance/downloads/cib04272026.pdf#page=2 + - title: 42 USC 1396p(f)(1)(C) - Annual CPI-U indexing of the home equity limits + href: https://www.law.cornell.edu/uscode/text/42/1396p#f_1 - title: H.R. 1, 119th Congress, Sec. 71108 - Revising home equity limit for determining eligibility for long-term care services under the Medicaid program href: https://www.congress.gov/bill/119th-congress/house-bill/1/text - - title: CMCS Informational Bulletin, Nov. 18, 2025 - href: https://www.medicaid.gov/federal-policy-guidance/downloads/cib11182025.pdf#page=8 + - title: CMCS Informational Bulletin, November 18, 2025, Section 71108 - Revising Home Equity Limit + href: https://www.medicaid.gov/federal-policy-guidance/downloads/cib11182025.pdf#page=9 diff --git a/policyengine_us/parameters/uprating_extensions.py b/policyengine_us/parameters/uprating_extensions.py index 75d41b4268a..5e435c01d14 100644 --- a/policyengine_us/parameters/uprating_extensions.py +++ b/policyengine_us/parameters/uprating_extensions.py @@ -571,6 +571,105 @@ def extend_la_cpi_u_indexed_amounts(parameters: ParameterNode, end_year: int) -> ) +def get_cms_cpi_u_percentage_increase(cpi: ParameterNode, year: int) -> float: + """The CPI-U percentage increase CMS applies to ``year``'s home equity limits. + + 42 USC 1396p(f)(1)(C) increases the Medicaid long-term care home equity + limits "from year to year based on the percentage increase in the consumer + price index for all urban consumers ... rounded to the nearest $1,000". + CMS's annual SSI and spousal impoverishment bulletins report a "CPI + Increase" for each year to one decimal place: 1.7% for 2020, 1.4% for + 2021, 5.4% for 2022, 2.4% for 2025 and 3.0% for 2026. Each is the + September-to-September change in the not seasonally adjusted CPI-U ending + the September before the year, rounded to one decimal place. CMS applies + the rounded percentage: $893,000 x 1.014 = $905,502 became the published + 2021 maximum of $906,000, where the unrounded 1.371% would give $905,000. + A fall in the index is not an increase, so the percentage is never + negative. + + ``cpi`` is the ``gov.bls.cpi`` node. The published Septembers in + ``cpi_u_nsa_september`` are used when both are available. Otherwise both + Septembers come from the model's CPI-U series (seasonally adjusted from + January 2024) through ``get_projected_cpi_u_for_month``, so the two are + never mixed. + """ + september = cpi.cpi_u_nsa_september + published = {int(value.instant_str[:4]) for value in september.values_list} + if {year - 2, year - 1} <= published: + current = september(f"{year - 1}-09-01") + prior = september(f"{year - 2}-09-01") + else: + current = get_projected_cpi_u_for_month(cpi.cpi_u, instant(f"{year - 1}-09-01")) + prior = get_projected_cpi_u_for_month(cpi.cpi_u, instant(f"{year - 2}-09-01")) + change = 100 * (Decimal(repr(current)) / Decimal(repr(prior)) - 1) + percentage = change.quantize(Decimal("0.1"), rounding=ROUND_HALF_UP) + return float(max(percentage, 0)) + + +def adjust_home_equity_limit_for_inflation( + prior_amount: float, percentage: float +) -> float: + """Apply a 42 USC 1396p(f)(1)(C) increase, rounded to the nearest $1,000.""" + adjusted = Decimal(str(prior_amount)) * (100 + Decimal(str(percentage))) / 100 + thousands = (adjusted / 1_000).quantize(Decimal(1), rounding=ROUND_HALF_UP) + return float(thousands * 1_000) + + +# P.L. 119-21 section 71108(c): the $1,000,000 cap on non-agricultural homes +# applies beginning January 1, 2028. +HOME_EQUITY_NON_AGRICULTURAL_CAP_START_YEAR = 2028 + + +def extend_medicaid_home_equity_limits( + parameters: ParameterNode, end_year: int +) -> None: + """Project the CPI-indexed Medicaid long-term care home equity limits. + + Each year after the last CMS-published value, the agricultural home + equity limit becomes the prior year's limit increased by that year's + CPI-U percentage (see ``get_cms_cpi_u_percentage_increase``), rounded to + the nearest $1,000, effective January 1. Before P.L. 119-21 section 71108 + caps other homes at $1,000,000 beginning in 2028, the agricultural and + non-agricultural limits are the same indexed maximum, so each year before + the cap with no non-agricultural value in the YAML takes the agricultural + projection. Amounts encoded in the YAML take precedence for their own + years. + + Like the other projections in this module, the values are fixed when the + parameter tree is built. + """ + home_equity = parameters.gov.hhs.medicaid.eligibility.long_term_care.home_equity + cpi = parameters.gov.bls.cpi + agricultural = home_equity.agricultural_limit + first_projected_year = 1 + max( + int(value.instant_str[:4]) for value in agricultural.values_list + ) + for year in range(first_projected_year, end_year + 1): + agricultural.update( + period=f"year:{year}-01-01:1", + value=adjust_home_equity_limit_for_inflation( + agricultural(f"{year - 1}-01-01"), + get_cms_cpi_u_percentage_increase(cpi, year), + ), + ) + agricultural.update( + start=instant(f"{end_year}-01-01"), + value=agricultural(f"{end_year}-01-01"), + ) + non_agricultural = home_equity.limit + encoded_years = { + int(value.instant_str[:4]) for value in non_agricultural.values_list + } + for year in range( + min(encoded_years) + 1, HOME_EQUITY_NON_AGRICULTURAL_CAP_START_YEAR + ): + if year not in encoded_years: + non_agricultural.update( + period=f"year:{year}-01-01:1", + value=agricultural(f"{year}-01-01"), + ) + + def extend_wa_millionaires_standard_deduction( parameters: ParameterNode, end_year: int, @@ -1036,6 +1135,9 @@ def set_all_uprating_parameters(parameters: ParameterNode) -> ParameterNode: # above so projected Decembers are available. extend_la_cpi_u_indexed_amounts(parameters, end_year=END_YEAR) + # Medicaid long-term care home equity limits, CMS's rounded CPI-U method + extend_medicaid_home_equity_limits(parameters, end_year=END_YEAR) + # The limits on a dependent's standard deduction follow 26 U.S.C. # 63(c)(4)'s chained CPI-U schedule, computed from the statutory 1987 and # 1997 bases. Must run after the Chained CPI-U extension above so diff --git a/policyengine_us/tests/core/test_medicaid_home_equity_limit_indexing.py b/policyengine_us/tests/core/test_medicaid_home_equity_limit_indexing.py new file mode 100644 index 00000000000..81fb207e5d2 --- /dev/null +++ b/policyengine_us/tests/core/test_medicaid_home_equity_limit_indexing.py @@ -0,0 +1,188 @@ +"""Projection of the Medicaid long-term care home equity limits. + +42 USC 1396p(f)(1)(C) increases the limits from year to year by CPI-U, +rounded to the nearest $1,000. ``extend_medicaid_home_equity_limits`` projects +them the way CMS computes them. These tests check that method against CMS's +published bulletins, then check the projected parameter values. + +Published figures (CMS SSI and spousal impoverishment bulletins, chart page): +2020 cib062420-new p.2, 2021 cib060421 p.2, 2022 cib06022022_0 p.2, +2025 cib05282025 p.2, 2026 cib04272026 p.2. +""" + +from types import SimpleNamespace + +import pytest + +from policyengine_us import Simulation +from policyengine_core.parameters import ParameterNode + +from policyengine_us.parameters.uprating_extensions import ( + adjust_home_equity_limit_for_inflation, + extend_medicaid_home_equity_limits, + get_cms_cpi_u_percentage_increase, +) +from policyengine_us.system import system + +CPI = system.parameters.gov.bls.cpi +HOME_EQUITY = system.parameters.gov.hhs.medicaid.eligibility.long_term_care.home_equity + +# "CPI Increase for " in each bulletin's chart. +PUBLISHED_CPI_INCREASE = {2020: 1.7, 2021: 1.4, 2022: 5.4, 2025: 2.4, 2026: 3.0} +# (minimum, maximum) home equity limits in each bulletin's chart. +PUBLISHED_LIMITS = { + 2020: (595_000, 893_000), + 2021: (603_000, 906_000), + 2022: (636_000, 955_000), + 2025: (730_000, 1_097_000), + 2026: (752_000, 1_130_000), +} + + +@pytest.mark.parametrize("year, percentage", PUBLISHED_CPI_INCREASE.items()) +def test_cpi_increase_matches_cms(year, percentage): + assert get_cms_cpi_u_percentage_increase(CPI, year) == percentage + + +@pytest.mark.parametrize("start, end", [(2020, 2021), (2021, 2022), (2022, 2025)]) +@pytest.mark.parametrize("bound", [0, 1]) +def test_year_to_year_increases_reproduce_cms_limits(start, end, bound): + # 2021 separates the methods: $893,000 x 1.014 rounds to $906,000, while + # the unrounded 1.371% increase would give $905,000. 2022 -> 2025 chains + # through 2023 and 2024, whose bulletins are not cited here. + limit = PUBLISHED_LIMITS[start][bound] + for year in range(start + 1, end + 1): + limit = adjust_home_equity_limit_for_inflation( + limit, get_cms_cpi_u_percentage_increase(CPI, year) + ) + assert limit == PUBLISHED_LIMITS[end][bound] + + +class _Series: + """A minimal stand-in for a dated CPI parameter.""" + + def __init__(self, values): + self.values = values + self.values_list = [SimpleNamespace(instant_str=date) for date in values] + + def __call__(self, date): + return self.values[date] + + +@pytest.mark.parametrize("current, percentage", [(99.0, 0), (101.0, 1.0)]) +def test_a_falling_index_is_not_an_increase(current, percentage): + cpi = SimpleNamespace( + cpi_u_nsa_september=_Series({"2030-09-01": 100.0, "2031-09-01": current}), + cpi_u=None, + ) + assert get_cms_cpi_u_percentage_increase(cpi, 2032) == percentage + + +def test_published_limits_are_parameter_values(): + for year in (2025, 2026): + assert HOME_EQUITY.limit(f"{year}-01-01") == PUBLISHED_LIMITS[year][1] + assert ( + HOME_EQUITY.agricultural_limit(f"{year}-01-01") + == (PUBLISHED_LIMITS[year][1]) + ) + + +def test_agricultural_limit_is_projected_year_to_year_on_january_1(): + for year in range(2027, 2041): + current = HOME_EQUITY.agricultural_limit(f"{year}-01-01") + assert current == adjust_home_equity_limit_for_inflation( + HOME_EQUITY.agricultural_limit(f"{year - 1}-01-01"), + get_cms_cpi_u_percentage_increase(CPI, year), + ) + for month in ["02", "07", "12"]: + assert HOME_EQUITY.agricultural_limit(f"{year}-{month}-01") == current + + +def test_non_agricultural_limit_is_the_indexed_maximum_until_the_2028_cap(): + assert HOME_EQUITY.limit("2027-01-01") == HOME_EQUITY.agricultural_limit( + "2027-01-01" + ) + for year in range(2028, 2041): + assert HOME_EQUITY.limit(f"{year}-01-01") == 1_000_000 + assert HOME_EQUITY.agricultural_limit(f"{year}-01-01") > 1_000_000 + + +def test_chassis_screen_reads_each_years_agricultural_limit(): + # The projected limits move with the CPI-U forecast, so the boundaries are + # read from the parameters rather than written into a YAML case. + limit_2028 = HOME_EQUITY.agricultural_limit("2028-01-01") + limit_2029 = HOME_EQUITY.agricultural_limit("2029-01-01") + assert limit_2029 > limit_2028 + equity = {"above_2028_limit": limit_2028 + 1, "at_limit": limit_2029} + equity["above_limit"] = limit_2029 + 1 + people = { + name: { + "receives_medicaid_long_term_care_services": {"2029": True}, + "home_is_on_agricultural_land": {"2029": True}, + "home_equity": {"2029": value}, + } + for name, value in equity.items() + } + situation = {"people": people} + for group in ["tax_units", "spm_units", "families", "marital_units", "households"]: + situation[group] = {f"{name}_{group}": {"members": [name]} for name in people} + simulation = Simulation(situation=situation) + eligible = simulation.calculate( + "is_medicaid_long_term_care_home_equity_eligible", 2029 + ) + assert eligible.tolist() == [True, True, False] + + +@pytest.mark.parametrize("agricultural_published_through", [2026, 2027]) +def test_non_agricultural_fill_follows_its_own_coverage( + agricultural_published_through, +): + # Publishing the 2027 agricultural limit before the non-agricultural one + # must not leave the non-agricultural 2027 limit at its 2026 value. + agricultural = {"2025-01-01": 1_097_000, "2026-01-01": 1_130_000} + if agricultural_published_through == 2027: + agricultural["2027-01-01"] = 1_164_000 + non_agricultural = { + "2025-01-01": 1_097_000, + "2026-01-01": 1_130_000, + "2028-01-01": 1_000_000, + } + septembers = { + f"{year}-09-01": 100 * 1.03 ** (year - 2024) for year in range(2024, 2031) + } + parameters = ParameterNode( + data={ + "gov": { + "hhs": { + "medicaid": { + "eligibility": { + "long_term_care": { + "home_equity": { + "agricultural_limit": {"values": agricultural}, + "limit": {"values": non_agricultural}, + } + } + } + } + }, + "bls": { + "cpi": { + "cpi_u_nsa_september": {"values": septembers}, + "cpi_u": {"values": septembers}, + } + }, + } + } + ) + extend_medicaid_home_equity_limits(parameters, end_year=2030) + home_equity = parameters.gov.hhs.medicaid.eligibility.long_term_care.home_equity + assert home_equity.limit("2027-01-01") == home_equity.agricultural_limit( + "2027-01-01" + ) + assert home_equity.limit("2027-01-01") > 1_130_000 + assert home_equity.limit("2028-01-01") == 1_000_000 + assert home_equity.agricultural_limit( + "2028-01-01" + ) == adjust_home_equity_limit_for_inflation( + home_equity.agricultural_limit("2027-01-01"), 3.0 + ) diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/is_medicaid_long_term_care_home_equity_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/is_medicaid_long_term_care_home_equity_eligible.yaml index aeb4936abee..b6d51d2aee3 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/is_medicaid_long_term_care_home_equity_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicaid/eligibility/is_medicaid_long_term_care_home_equity_eligible.yaml @@ -22,11 +22,11 @@ output: is_medicaid_long_term_care_home_equity_eligible: false -- name: Case 3b, current-law home equity limit uprates in 2027. +- name: Case 3b, the home equity limit is indexed above its 2026 value in 2027. period: 2027 input: receives_medicaid_long_term_care_services: true - home_equity: 1_159_000 + home_equity: 1_130_001 output: is_medicaid_long_term_care_home_equity_eligible: true @@ -38,12 +38,12 @@ output: is_medicaid_long_term_care_home_equity_eligible: true -- name: Case 5, agricultural home at the agricultural home equity limit remains eligible. +- name: Case 5, agricultural home above the non-agricultural cap remains eligible. period: 2028 input: receives_medicaid_long_term_care_services: true home_is_on_agricultural_land: true - home_equity: 1_189_000 + home_equity: 1_000_001 output: is_medicaid_long_term_care_home_equity_eligible: true @@ -103,7 +103,7 @@ input: receives_medicaid_long_term_care_services: true home_is_on_agricultural_land: true - home_equity: 1_189_001 + home_equity: 2_000_000 output: is_medicaid_long_term_care_home_equity_eligible: false @@ -278,12 +278,12 @@ output: is_medicaid_long_term_care_home_equity_eligible: false -- name: Case 17, agricultural home equity limit continues to uprate after 2028. +- name: Case 17, agricultural home above the non-agricultural cap remains eligible after 2028. period: 2029 input: receives_medicaid_long_term_care_services: true home_is_on_agricultural_land: true - home_equity: 1_217_000 + home_equity: 1_130_001 output: is_medicaid_long_term_care_home_equity_eligible: true @@ -292,7 +292,7 @@ input: receives_medicaid_long_term_care_services: true home_is_on_agricultural_land: true - home_equity: 1_217_001 + home_equity: 2_000_000 output: is_medicaid_long_term_care_home_equity_eligible: false @@ -355,3 +355,29 @@ output: medicaid_home_equity_limit_family_exception: [false, false] is_medicaid_long_term_care_home_equity_eligible: [false, true] + +- name: Case 21, agricultural and non-agricultural homes bracket their own limits after 2028. + period: 2029 + input: + gov.hhs.medicaid.eligibility.long_term_care.home_equity.agricultural_limit: 1_200_000 + people: + agricultural_at_limit: + receives_medicaid_long_term_care_services: true + home_is_on_agricultural_land: true + home_equity: 1_200_000 + agricultural_above_limit: + receives_medicaid_long_term_care_services: true + home_is_on_agricultural_land: true + home_equity: 1_200_001 + other_at_cap: + receives_medicaid_long_term_care_services: true + home_equity: 1_000_000 + other_above_cap: + receives_medicaid_long_term_care_services: true + home_equity: 1_000_001 + households: + household: + members: + [agricultural_at_limit, agricultural_above_limit, other_at_cap, other_above_cap] + output: + is_medicaid_long_term_care_home_equity_eligible: [true, false, true, false]