Skip to content
Open
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/ssi-monthly-ism-state-readers.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Count each month's SSI in-kind support and maintenance in Maryland PAA income and in California's supplement for a couple with one SSI and one CAPI spouse. Read each month's SSI couple status and income deemed from an ineligible spouse in Maryland PAA income.
265 changes: 265 additions & 0 deletions policyengine_us/tests/core/test_ssi_monthly_state_readers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
"""Check Maryland PAA and California SSP against changes to one month's ISM."""

import numpy as np
import pytest
from hypothesis import given, settings
from hypothesis import strategies as st

from policyengine_core.periods import period as make_period
from policyengine_us import Simulation
from policyengine_us.system import system
from policyengine_us.variables.gov.ssa.ssi.eligibility.income._apply_ssi_exclusions import (
_apply_ssi_exclusions,
)


YEAR = "2024"
MONTHS = tuple(f"{YEAR}-{month:02d}" for month in range(1, 13))
VARIABLES = (
"md_paa_countable_income",
"md_paa_imputed_federal_ssi",
"md_paa",
"ca_state_supplement_person",
)


def _situation(profiles, annual_earned, annual_unearned):
situation = {
group: {}
for group in (
"people",
"households",
"tax_units",
"marital_units",
"families",
"spm_units",
)
}
rows = {}
for name, profile in profiles.items():
for state, citizenship in (
("MD", "CITIZEN"),
("MD", "LEGAL_PERMANENT_RESIDENT"),
("CA", "CITIZEN"),
):
unit = f"{name}_{state}_{citizenship}"
person = f"{unit}_head"
members = [person]
rows[name, state, citizenship] = len(situation["people"])
values = {
"age": {YEAR: 70},
"is_tax_unit_head": {YEAR: True},
"immigration_status": {YEAR: citizenship},
"is_ssi_qualified_noncitizen": {YEAR: False},
"ssi_earned_income": {YEAR: annual_earned},
"ssi_unearned_income": {YEAR: annual_unearned},
"ssi_in_kind_support_and_maintenance": dict(zip(MONTHS, profile)),
}
if state == "MD":
values.update(
md_paa_eligible={YEAR: True},
md_paa_total_cost_of_care={YEAR: 18_000},
)
else:
# Hold federal SSI and the couple's income fixed to isolate
# the supplement's own-income reader for a CAPI spouse.
values.update(
ssi={YEAR: 7_200},
ssi_countable_income={YEAR: 0},
)
situation["people"][person] = values
if state == "CA":
spouse = f"{unit}_spouse"
members.append(spouse)
situation["people"][spouse] = {
"age": {YEAR: 68},
"is_tax_unit_spouse": {YEAR: True},
"immigration_status": {YEAR: "LEGAL_PERMANENT_RESIDENT"},
"ssi_qualifying_quarters_earnings": {YEAR: 39},
"ssi_countable_income": {YEAR: 0},
}
for group in (
"households",
"tax_units",
"marital_units",
"families",
"spm_units",
):
situation[group][unit] = {"members": members}
situation["households"][unit].update(
state_code={YEAR: state},
living_arrangements_allow_for_food_preparation={YEAR: True},
)
return situation, rows


def _monthly(simulation, variable):
return np.stack([simulation.calculate(variable, month) for month in MONTHS])


@settings(max_examples=8, deadline=None, database=None, derandomize=True)
@given(
ism=st.integers(20, 200),
extra=st.integers(1, 100),
annual_earned=st.integers(0, 1_200),
annual_unearned=st.integers(0, 2_400),
)
def test_state_readers_keep_a_december_ism_change_in_december(
ism, extra, annual_earned, annual_unearned
):
situation, rows = _situation(
{"constant": [ism] * 12, "changed": [ism] * 11 + [ism + extra]},
annual_earned,
annual_unearned,
)
simulation = Simulation(tax_benefit_system=system, situation=situation)
results = {name: _monthly(simulation, name) for name in VARIABLES}

for name, values in results.items():
assert np.all(values >= 0)
np.testing.assert_allclose(
simulation.calculate(name, YEAR), values.sum(axis=0), atol=0.01
)
for state, citizenship in (
("MD", "CITIZEN"),
("MD", "LEGAL_PERMANENT_RESIDENT"),
("CA", "CITIZEN"),
):
constant = rows["constant", state, citizenship]
changed = rows["changed", state, citizenship]
np.testing.assert_allclose(
values[:11, constant], values[:11, changed], atol=0.01
)

for citizenship in ("CITIZEN", "LEGAL_PERMANENT_RESIDENT"):
constant = rows["constant", "MD", citizenship]
changed = rows["changed", "MD", citizenship]
countable = results["md_paa_countable_income"][-1]
# ISM already uses the whole $20 exclusion, so each extra dollar
# increases this month's countable income by one dollar.
assert countable[changed] - countable[constant] == pytest.approx(
extra, abs=0.01
)
imputed = results["md_paa_imputed_federal_ssi"][-1]
payment = results["md_paa"][-1]
if citizenship == "CITIZEN":
assert imputed[constant] - imputed[changed] == pytest.approx(
extra, abs=0.01
)
# Federal SSI offsets the income change while both amounts
# remain below the federal benefit rate.
assert payment[changed] == pytest.approx(payment[constant], abs=0.01)
else:
assert imputed[constant] == imputed[changed] == 0
assert payment[constant] - payment[changed] == pytest.approx(
extra, abs=0.01
)

constant = rows["constant", "CA", "CITIZEN"]
changed = rows["changed", "CA", "CITIZEN"]
payment = results["ca_state_supplement_person"][-1]
assert payment[constant] - payment[changed] == pytest.approx(extra, abs=0.011)


@settings(max_examples=6, deadline=None, database=None, derandomize=True)
@given(
ism=st.integers(0, 400),
annual_earned=st.integers(0, 24_000),
annual_unearned=st.integers(0, 12_000),
)
def test_constant_ism_keeps_the_annual_exclusion_result(
ism, annual_earned, annual_unearned
):
situation, rows = _situation(
{"constant": [ism] * 12}, annual_earned, annual_unearned
)
simulation = Simulation(tax_benefit_system=system, situation=situation)
# Constant monthly income must retain the existing annual computation.
annual_countable = _apply_ssi_exclusions(
np.array([annual_earned]),
np.array([annual_unearned + 12 * ism]),
simulation.tax_benefit_system.parameters,
make_period(YEAR),
)[0]
countable = annual_countable / 12
for citizenship in ("CITIZEN", "LEGAL_PERMANENT_RESIDENT"):
index = rows["constant", "MD", citizenship]
np.testing.assert_allclose(
_monthly(simulation, "md_paa_countable_income")[:, index],
countable,
atol=0.01,
)

index = rows["constant", "CA", "CITIZEN"]
payment = _monthly(simulation, "ca_state_supplement_person")[:, index]
# POMS SI 01415.056 B.2.a gives the 2024 aged standard of $1,182.94.
# Federal administration rounds a positive balance and pays at least $1.
# https://secure.ssa.gov/apps10/poms.nsf/lnx/0501415056
balance = max(1_182.94 - 600 - countable, 0)
if balance == 0:
np.testing.assert_allclose(payment, 0, atol=0.01)
else:
assert np.all(payment >= min(balance, 1))
np.testing.assert_allclose(payment, max(balance, 1), atol=0.011)


def _maryland_reference_countable(monthly_earned, monthly_unearned):
# Maryland DHS deducts $20 from unearned income, then $65 and one-half
# of the remaining earned income. When unearned income falls short of
# $20, DHS applies the unused part to earnings (PAA Manual 900). Under
# COMAR 07.03.07.08(A), DHS takes these disregards "in any month".
# https://dhs.maryland.gov/documents/FIA/Manuals/Public%20Assistance%20to%20Adults%20%28PAA%29%20Manual/PAA%20900%20Calculation%20of%20Benefits%20rev%2011.22.docx
# https://regs.maryland.gov/us/md/exec/comar/07.03.07.08
general = min(20, monthly_unearned)
return monthly_unearned - general + max(monthly_earned - 65 - (20 - general), 0) / 2


@settings(max_examples=5, deadline=None, database=None, derandomize=True)
@given(
ism=st.integers(0, 19),
extra=st.integers(1, 19),
annual_earned=st.integers(0, 2_400),
)
def test_state_readers_keep_a_small_ism_exclusion_in_its_month(
ism, extra, annual_earned
):
# With ISM under $20 and no other unearned income, the unused part of
# the $20 exclusion reduces that month's earnings and no other month's.
profiles = {"constant": [ism] * 12, "changed": [ism] * 11 + [ism + extra]}
situation, rows = _situation(profiles, annual_earned, 0)
simulation = Simulation(tax_benefit_system=system, situation=situation)
countable = _monthly(simulation, "md_paa_countable_income")
supplement = _monthly(simulation, "ca_state_supplement_person")

for name, profile in profiles.items():
expected = [
_maryland_reference_countable(annual_earned / 12, monthly_ism)
for monthly_ism in profile
]
for citizenship in ("CITIZEN", "LEGAL_PERMANENT_RESIDENT"):
np.testing.assert_allclose(
countable[:, rows[name, "MD", citizenship]], expected, atol=0.01
)

constant = rows["constant", "CA", "CITIZEN"]
changed = rows["changed", "CA", "CITIZEN"]
np.testing.assert_allclose(
supplement[:11, constant], supplement[:11, changed], atol=0.01
)
# SSA applies the same exclusions to this person's own income in each
# month (20 CFR 416.1112(c)). In December, as in every other month, it
# pays the 2024 aged standard of $1,182.94, less $600 of SSI and that
# month's countable income, rounded up to the cent. For example, $10 of
# ISM and $200 of earnings leave 0 + (200 - 65 - 10) / 2 = $62.50
# countable and a $520.44 payment; a December ISM of $25 leaves
# (25 - 20) + (200 - 65) / 2 = $72.50 and a $510.44 payment.
for name, profile in profiles.items():
expected = [
1_182.94
- 600
- _maryland_reference_countable(annual_earned / 12, monthly_ism)
for monthly_ism in profile
]
np.testing.assert_allclose(
supplement[:, rows[name, "CA", "CITIZEN"]], expected, atol=0.011
)
Original file line number Diff line number Diff line change
Expand Up @@ -833,3 +833,115 @@
# 1,233.94 - 0 - (800.0833 - 20) = 453.8567, which rounds up to 453.86
# (POMS SI 02001.010 A.2).
ca_state_supplement_person: [453.86, 0]

- name: California counts January's SSI in-kind support for the SSI member of a split SSI and CAPI couple.
period: 2024-01
absolute_error_margin: 0.01
input:
people:
citizen:
age:
2024: 72
immigration_status:
2024: CITIZEN
ssi_receives_food_from_others:
2024: true
noncitizen:
age:
2024: 68
immigration_status:
2024: LEGAL_PERMANENT_RESIDENT
ssi_qualifying_quarters_earnings:
2024: 39
marital_units:
couple:
members: [citizen, noncitizen]
households:
household:
members: [citizen, noncitizen]
state_code:
2024: CA
living_arrangements_allow_for_food_preparation:
2024: true
output:
ca_capi_eligible_person: [false, true]
# Couple PMV = 1,415 / 3 + 20 = 491.666667 for the recipient.
ssi_in_kind_support_and_maintenance: [491.666667, 0]
# SSI = 1,415 / 2 - (491.666667 - 20) / 2 = 471.666667.
ssi: [471.666667, 0]
# Own income = 491.666667 - 20 = 471.666667.
# SSP = ceil_cent(1,182.94 - 471.666667 - 471.666667) = 239.61.
ca_state_supplement_person: [239.61, 0]

- name: California counts November's SSI in-kind support in the split couple's November supplement.
period: 2024-11
absolute_error_margin: 0.01
input:
people:
citizen:
age:
2024: 72
immigration_status:
2024: CITIZEN
ssi_receives_food_from_others:
2024: true
noncitizen:
age:
2024: 68
immigration_status:
2024: LEGAL_PERMANENT_RESIDENT
ssi_qualifying_quarters_earnings:
2024: 39
marital_units:
couple:
members: [citizen, noncitizen]
households:
household:
members: [citizen, noncitizen]
state_code:
2024: CA
living_arrangements_allow_for_food_preparation:
2024: true
output:
ca_capi_eligible_person: [false, true]
# November uses September food; the January arithmetic still applies.
ssi_in_kind_support_and_maintenance: [491.666667, 0]
ssi: [471.666667, 0]
ca_state_supplement_person: [239.61, 0]

- name: California counts no SSI in-kind support in the split couple's December 2024 supplement.
period: 2024-12
absolute_error_margin: 0.01
input:
people:
citizen:
age:
2024: 72
immigration_status:
2024: CITIZEN
ssi_receives_food_from_others:
2024: true
noncitizen:
age:
2024: 68
immigration_status:
2024: LEGAL_PERMANENT_RESIDENT
ssi_qualifying_quarters_earnings:
2024: 39
marital_units:
couple:
members: [citizen, noncitizen]
households:
household:
members: [citizen, noncitizen]
state_code:
2024: CA
living_arrangements_allow_for_food_preparation:
2024: true
output:
ca_capi_eligible_person: [false, true]
# October food no longer counts; SSI = 1,415 / 2 = 707.50.
ssi_in_kind_support_and_maintenance: [0, 0]
ssi: [707.50, 0]
# SSP = 1,182.94 - 707.50 - 0 = 475.44.
ca_state_supplement_person: [475.44, 0]
Loading
Loading