Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/fix-home-equity-limit-projection.fixed.md
Original file line number Diff line number Diff line change
@@ -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.
18 changes: 18 additions & 0 deletions policyengine_us/parameters/gov/bls/cpi/cpi_u_nsa_september.yaml
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,23 @@ 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:
unit: currency-USD
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
102 changes: 102 additions & 0 deletions policyengine_us/parameters/uprating_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
188 changes: 188 additions & 0 deletions policyengine_us/tests/core/test_medicaid_home_equity_limit_indexing.py
Original file line number Diff line number Diff line change
@@ -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 <year>" 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
)
Loading
Loading