Skip to content
Draft
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/co-and-cs-payment-floor-3549.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Apply Colorado's $13 AND-CS payment floor from January 2026, and count income for every AND-CS client under 9 CCR 2503-5 3.549: no unearned disregard for SSI-only clients, spousal and parental deeming without SSI's threshold, and the exemption for spouses and parents on SSI, Old Age Pension or Medicaid.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
description: Colorado does not deem income to an Aid to the Needy Disabled-Colorado Supplement client from a spouse or parent who receives Adult Financial grant payments, SSI or Medicaid with income no greater than the SSI limit, if this is true.

values:
# Before March 2020, 3.545(F) excluded only the income from those benefits;
# the recipient's other income was deemed.
2002-01-01: false
2020-03-01: true

metadata:
unit: bool
label: Colorado AND-CS deeming exemption for assistance recipients
reference:
- title: 9 CCR 2503-5, Section 3.549(F)
href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=95
- title: 9 CCR 2503-5, Section 3.549(F) (effective March 1, 2020)
href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=8577#page=77
- title: 9 CCR 2503-5, Section 3.545(F) (effective January 1, 2020)
href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=8443#page=63
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
description: Colorado deducts this amount from monthly gross earned income when counting income for the Aid to the Needy Disabled-Colorado Supplement.

values:
2002-01-01: 65

metadata:
unit: currency-USD
period: month
label: Colorado AND-CS earned income disregard
reference:
- title: "9 CCR 2503-5, Section 3.549(A)(1)(a), (C)(2) #page 93-94"
href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=93
- title: SSA State Assistance Programs for SSI Recipients, January 2002 - Colorado (income exclusions same as federal)
href: https://www.ssa.gov/policy/docs/progdesc/ssi_st_asst/2002/co.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
description: Colorado counts this share of earned income above the flat disregard for the Aid to the Needy Disabled-Colorado Supplement.

values:
2002-01-01: 0.5

metadata:
unit: /1
label: Colorado AND-CS countable share of earned income
reference:
- title: "9 CCR 2503-5, Section 3.549(A)(1)(b), (C)(3) #page 94"
href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=94
- title: SSA State Assistance Programs for SSI Recipients, January 2002 - Colorado (income exclusions same as federal)
href: https://www.ssa.gov/policy/docs/progdesc/ssi_st_asst/2002/co.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
description: Colorado disregards this amount of unearned income other than SSI, shared by a married couple, for the Aid to the Needy Disabled-Colorado Supplement.

values:
2002-01-01: 20

metadata:
unit: currency-USD
period: month
label: Colorado AND-CS unearned income disregard
reference:
- title: "9 CCR 2503-5, Section 3.549(A)(2)(c), (D)(2) #page 94-95"
href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=94
- title: SSA State Assistance Programs for SSI Recipients, January 2002 - Colorado (income exclusions same as federal)
href: https://www.ssa.gov/policy/docs/progdesc/ssi_st_asst/2002/co.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
description: Colorado raises an Aid to the Needy Disabled-Colorado Supplement grant below this amount to it with a state supplementary payment.

values:
# Before 2026 Colorado met the SSI pass-along requirement by maintaining
# total expenditures, with irregular lump-sum payments and no payment floor.
2002-01-01: 0
2026-01-01: 13

metadata:
unit: currency-USD
period: month
label: Colorado AND-CS payment floor standard
reference:
- title: 9 CCR 2503-5, Section 3.546(E), AND-CS payment floor standard
href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=92
- title: 9 CCR 2503-5, Section 3.546(E) (emergency rule effective January 1, 2026)
href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12318#page=74
- title: 9 CCR 2503-5, Section 3.546(C) (effective March 2, 2025), MOE payments without a payment floor
href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=11861#page=73
- title: "Colorado State Board of Human Services, Adult Financial Payment Floor and COLA 2026, rulemaking packet 2025-00596 #page 4,9"
href: https://www.sos.state.co.us/CCR/Upload/AGORequestEmergency/RegulatoryAnalysisAttachment2025-00596.pdf#page=4
- title: SSA State Assistance Programs for SSI Recipients, January 2011 - Colorado (passalong method)
href: https://www.ssa.gov/policy/docs/progdesc/ssi_st_asst/2011/co.html
291 changes: 291 additions & 0 deletions policyengine_us/tests/core/test_co_and_cs_invariants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
"""Property tests: Colorado AND-CS countable income, deeming and payment floor.

9 CCR 2503-5 3.549(A) counts an AND-CS client's own income; 3.549(C)-(D)
deem part of a spouse's income to the client, except under 3.549(F) when the
spouse receives SSI, Old Age Pension or Medicaid with income within the SSI
limit; 3.520.781(A) makes a client whose total countable income (SSI
included) equals or exceeds the grant standard ineligible; and 3.546(E),
3.549(B) raise a grant below the $13 payment floor standard to it.

Each case is a disabled client aged 18-59 in their own household, with
pension, interest and earnings, optionally receiving outside shelter
support, and either single or married to a spouse who is not disabled and
has earnings and a pension. Each married case appears four times: (A) the
spouse is enrolled in Medicaid, (B) the spouse is not, (C) as B with extra
spousal earnings, and (D) as B with the tax-unit roles swapped. Income can
be high enough that the client gets no SSI; 3.546 then makes them no AND-CS
client, and countable income, defined only for clients, is compared only
between arrangements in which the client is one. For 2026:

1. Payment: the supplement is the grant standard less total countable income
when that is positive, raised to the floor if below it, and zero otherwise;
so it is zero or between the floor and the standard.
2. Singles: since 2012 the grant standard has equaled the SSI individual
benefit rate, and for a client 3.549(A) never counts less than SSI does,
so a single client in their own household is never paid. Where the client has no
earnings or at least $20 of unearned income other than SSI, 3.549(A) and
SSI's exclusions (20 CFR 416.1112, 416.1124) count the same income.
3. Exemption: Medicaid enrollment of a spouse within the SSI limit can only
lower the client's countable income and raise the supplement.
4. Monotonicity: for a client without in-kind support, more earnings for a
non-exempt spouse never lower the client's countable income or raise the
supplement. (In-kind support is excluded: 3.520.79(B)(6) charges a client
the ISM SSA charges, and SSA's presumed maximum value halves once spousal
deeming starts, so crossing SSI's deeming threshold lowers the client's
ISM by about as much as the deemed earnings rise.)
5. Role invariance: the client's countable income and supplement do not
depend on which spouse is the tax-unit head.

YAML states the worked cases; Python is needed to compare arrangements of
the same household in one simulation.
"""

import numpy as np
import pytest

from policyengine_us import Simulation

YEAR = 2026
TOLERANCE = 0.01
ARRANGEMENTS = "ABCD"


def build_situation(cases):
people, groups = (
{},
{
name: {}
for name in (
"tax_units",
"marital_units",
"spm_units",
"families",
"households",
)
},
)
for i, c in enumerate(cases):
for arrangement in ARRANGEMENTS if c["married"] else "A":
key = f"{arrangement}{i}"
client, spouse = f"client_{key}", f"spouse_{key}"
people[client] = {
"age": {YEAR: c["client_age"]},
"is_disabled": {YEAR: True},
"meets_ssi_disability_criteria": {YEAR: True},
"pension_income": {YEAR: c["client_pension"]},
"interest_income": {YEAR: c["client_interest"]},
"employment_income": {YEAR: c["client_earnings"]},
"ssi_receives_outside_shelter_support": {YEAR: c["shelter_support"]},
# Set for everyone: an input for some people gives the others
# its default (false), not its formula.
"co_state_supplement_medicaid_enrolled": {YEAR: True},
}
members = [client]
if c["married"]:
extra = c["extra_spouse_earnings"] if arrangement == "C" else 0
people[spouse] = {
"age": {YEAR: c["spouse_age"]},
"employment_income": {YEAR: c["spouse_earnings"] + extra},
"pension_income": {YEAR: c["spouse_pension"]},
"co_state_supplement_medicaid_enrolled": {YEAR: arrangement == "A"},
}
members = [spouse, client] if arrangement == "D" else [client, spouse]
head, partner = members
people[head]["is_tax_unit_head"] = {YEAR: True}
people[head]["is_tax_unit_spouse"] = {YEAR: False}
people[partner]["is_tax_unit_head"] = {YEAR: False}
people[partner]["is_tax_unit_spouse"] = {YEAR: True}
for name, group in groups.items():
group[f"{name}_{key}"] = {"members": members}
groups["households"][f"households_{key}"]["state_code"] = {YEAR: "CO"}
return {"people": people, **groups}


def calculate(cases):
"""Arrays indexed by person name for the client of every arrangement."""
simulation = Simulation(situation=build_situation(cases))
names = list(simulation.populations["person"].ids)
results = {}
for variable, period in (
("co_state_supplement", YEAR),
("co_state_supplement_countable_income", YEAR),
("co_state_supplement_eligible", f"{YEAR}-01"),
("ssi", YEAR),
("ssi_countable_income", YEAR),
):
values = np.asarray(simulation.calculate(variable, period))
results[variable] = dict(zip(names, values))
parameters = simulation.tax_benefit_system.parameters(f"{YEAR}-01")
p = parameters.gov.states.co.ssa.state_supplement
return results, p.grant_standard, p.payment_floor


def assert_properties(cases):
results, standard, floor = calculate(cases)
supplement = results["co_state_supplement"]
countable = results["co_state_supplement_countable_income"]
eligible = results["co_state_supplement_eligible"]
ssi = results["ssi"]
for i, c in enumerate(cases):
for arrangement in ARRANGEMENTS if c["married"] else "A":
client = f"client_{arrangement}{i}"
# Monthly amounts: the inputs and the program are constant
# within the year.
total = (ssi[client] + countable[client]) / 12
paid = supplement[client] / 12
# 1. Payment rule and bounds.
if eligible[client] and total < standard - TOLERANCE:
expected = max(standard - total, floor)
assert paid == pytest.approx(expected, abs=TOLERANCE), c
assert floor - TOLERANCE <= paid <= standard + TOLERANCE, c
elif eligible[client] and total > standard + TOLERANCE:
assert paid == 0, c
client = f"client_A{i}"
if not c["married"]:
# 2. Singles.
assert supplement[client] == 0, c
if eligible[client]:
federal = results["ssi_countable_income"][client]
assert countable[client] >= federal - TOLERANCE, c
other_unearned = c["client_pension"] + c["client_interest"]
if c["client_earnings"] == 0 or other_unearned >= 20 * 12:
assert countable[client] == pytest.approx(federal, abs=TOLERANCE), c
continue
a, b, c_, d = (f"client_{x}{i}" for x in ARRANGEMENTS)
# The spouse's Medicaid and the head role do not change the client's
# SSI, so A, B and D have the same clients.
assert eligible[a] == eligible[b] == eligible[d], c
# 3. The Medicaid exemption only lowers countable income.
if eligible[b]:
assert countable[a] <= countable[b] + TOLERANCE, c
assert supplement[a] >= supplement[b] - TOLERANCE, c
# 4. More non-exempt spousal earnings never lower countable income,
# for a client who stays one and has no in-kind support.
if not c["shelter_support"]:
if eligible[b] and eligible[c_]:
assert countable[c_] >= countable[b] - TOLERANCE, c
assert supplement[c_] <= supplement[b] + TOLERANCE, c
# 5. Roles.
assert countable[d] == pytest.approx(countable[b], abs=TOLERANCE), c
assert supplement[d] == pytest.approx(supplement[b], abs=TOLERANCE), c


def test_worked_example():
"""co_state_supplement YAML Case 29: the client has no income and the
spouse earns $1,500/month. With the spouse on Medicaid, SSI pays 783.50
and 3.549(F) deems nothing: 994 - 783.50 = 210.50/month. Without
Medicaid, 3.549(C) deems (1,500 - 65) / 2 = 717.50 and the client is not
eligible."""
case = {
"client_age": 40,
"client_pension": 0,
"client_interest": 0,
"client_earnings": 0,
"shelter_support": False,
"married": True,
"spouse_age": 42,
"spouse_earnings": 18_000,
"spouse_pension": 0,
"extra_spouse_earnings": 1_200,
}
results, _, _ = calculate([case])
supplement = results["co_state_supplement"]
countable = results["co_state_supplement_countable_income"]
assert supplement["client_A0"] == pytest.approx(2_526, abs=TOLERANCE)
assert countable["client_A0"] == pytest.approx(0, abs=TOLERANCE)
assert supplement["client_B0"] == 0
assert countable["client_B0"] == pytest.approx(8_610, abs=TOLERANCE)
assert_properties([case])


def test_clients_who_leave_ssi():
"""Review counterexamples: a single client whose $1,025/month of
unearned income ends SSI, and a married client whose spouse's extra
earnings end SSI. Neither is then an AND-CS client, so neither has
countable income to compare."""
single = {
"client_age": 40,
"client_pension": 12_000,
"client_interest": 300,
"client_earnings": 0,
"shelter_support": False,
"married": False,
"spouse_age": 40,
"spouse_earnings": 0,
"spouse_pension": 0,
"extra_spouse_earnings": 1,
}
married = {
**single,
"client_pension": 0,
"client_interest": 0,
"married": True,
"spouse_earnings": 30_000,
"extra_spouse_earnings": 12_000,
}
results, _, _ = calculate([single, married])
assert results["ssi"]["client_A0"] == 0
assert results["ssi"]["client_C1"] == 0
assert results["ssi"]["client_B1"] > 0
assert_properties([single, married])


def test_ism_basis_switch_at_ssi_deeming_threshold():
"""Found by Hypothesis: a client with outside shelter support whose
spouse's income crosses SSI's spousal deeming threshold. SSA's presumed
maximum value drops from 994 / 3 + 20 = 351.33 to (1,491 / 3 + 20) / 2 =
258.50, and 3.520.79(B)(6) charges the client that ISM. The spouse's
deemed earnings rise by (250.58 - 65) / 2 = 92.79, so countable income
falls by $0.04 a month: no breach of 3.549, which deems every dollar."""
case = {
"client_age": 18,
"client_pension": 0,
"client_interest": 0,
"client_earnings": 0,
"shelter_support": True,
"married": True,
"spouse_age": 18,
"spouse_earnings": 1,
"spouse_pension": 2_958,
"extra_spouse_earnings": 3_006,
}
results, _, _ = calculate([case])
countable = results["co_state_supplement_countable_income"]
assert countable["client_B0"] == pytest.approx(6_934, abs=1)
assert countable["client_C0"] < countable["client_B0"]
assert_properties([case])


try:
import hypothesis
import hypothesis.strategies as st
except ImportError: # pragma: no cover
hypothesis = None

if hypothesis is not None:
# Each example builds a Simulation; on a loaded runner input generation
# can trip the too_slow health check, which says nothing about the model.
SLOW = [hypothesis.HealthCheck.too_slow]

def amount(high):
return st.one_of(st.just(0), st.integers(min_value=1, max_value=high))

@st.composite
def case(draw):
return {
"client_age": draw(st.integers(min_value=18, max_value=59)),
"client_pension": draw(amount(12_000)),
"client_interest": draw(amount(300)),
"client_earnings": draw(amount(20_000)),
"shelter_support": draw(st.booleans()),
"married": draw(st.booleans()),
"spouse_age": draw(st.integers(min_value=18, max_value=59)),
"spouse_earnings": draw(amount(30_000)),
"spouse_pension": draw(amount(12_000)),
"extra_spouse_earnings": draw(st.integers(min_value=1, max_value=12_000)),
}

@hypothesis.settings(max_examples=15, deadline=None, suppress_health_check=SLOW)
@hypothesis.given(st.lists(case(), min_size=1, max_size=8))
def test_properties(cases):
assert_properties(cases)
Loading