From 2c307bb153a403cbf87b29025f1118291d732403 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 11 Nov 2026 14:04:07 -0500 Subject: [PATCH 1/7] Read monthly SSI support in Maryland and California benefits Co-Authored-By: Claude Opus 5.5 --- .../ssi-monthly-ism-state-readers.fixed.md | 1 + .../core/test_ssi_monthly_state_readers.py | 203 ++++++++++++++++++ .../state_supplement/ca_state_supplement.yaml | 112 ++++++++++ .../dhs/fia/paa/md_paa_countable_income.yaml | 89 ++++++++ .../ca_state_supplement_person.py | 5 +- .../md/dhs/fia/paa/md_paa_countable_income.py | 6 +- 6 files changed, 411 insertions(+), 5 deletions(-) create mode 100644 changelog.d/ssi-monthly-ism-state-readers.fixed.md create mode 100644 policyengine_us/tests/core/test_ssi_monthly_state_readers.py diff --git a/changelog.d/ssi-monthly-ism-state-readers.fixed.md b/changelog.d/ssi-monthly-ism-state-readers.fixed.md new file mode 100644 index 00000000000..783916874ee --- /dev/null +++ b/changelog.d/ssi-monthly-ism-state-readers.fixed.md @@ -0,0 +1 @@ +Corrected Maryland PAA income and California split SSI/CAPI supplements to count each month's SSI in-kind support and maintenance. diff --git a/policyengine_us/tests/core/test_ssi_monthly_state_readers.py b/policyengine_us/tests/core/test_ssi_monthly_state_readers.py new file mode 100644 index 00000000000..b58960f9184 --- /dev/null +++ b/policyengine_us/tests/core/test_ssi_monthly_state_readers.py @@ -0,0 +1,203 @@ +"""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) diff --git a/policyengine_us/tests/policy/baseline/gov/states/ca/cdss/state_supplement/ca_state_supplement.yaml b/policyengine_us/tests/policy/baseline/gov/states/ca/cdss/state_supplement/ca_state_supplement.yaml index 85dc3480892..17a56f9945d 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ca/cdss/state_supplement/ca_state_supplement.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ca/cdss/state_supplement/ca_state_supplement.yaml @@ -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 food ISM 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 September food ISM 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 excludes food ISM from the split couple's December 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] diff --git a/policyengine_us/tests/policy/baseline/gov/states/md/dhs/fia/paa/md_paa_countable_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/md/dhs/fia/paa/md_paa_countable_income.yaml index b66a6235b4a..64d34b19668 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/md/dhs/fia/paa/md_paa_countable_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/md/dhs/fia/paa/md_paa_countable_income.yaml @@ -45,3 +45,92 @@ # $2,500 monthly SSDI - $20 general exclusion. ssi_countable_income: 0 md_paa_countable_income: 2_480 + +- name: Maryland counts January food support in January PAA income. + period: 2024-01 + absolute_error_margin: 0.01 + input: + people: + person: + age: + 2024: 70 + ssi_receives_food_from_others: + 2024: true + md_paa_living_arrangement: + 2024: CARE_HOME_LEVEL_C + households: + household: + members: [person] + state_code: + 2024: MD + output: + # ISM = 943 / 3 + 20; countable = ISM - 20 = 314.333333. + ssi_in_kind_support_and_maintenance: 334.333333 + md_paa_countable_income: 314.333333 + # Imputed SSI = 943 - 314.333333 = 628.666667. + md_paa_imputed_federal_ssi: 628.666667 + # PAA = 1,173 + 98 - 314.333333 - 628.666667 = 328. + md_paa: 328 + +- name: Maryland counts September food support in November PAA income. + period: 2024-11 + absolute_error_margin: 0.01 + input: + people: + person: + age: + 2024: 70 + ssi_receives_food_from_others: + 2024: true + md_paa_living_arrangement: + 2024: CARE_HOME_LEVEL_C + households: + household: + members: [person] + state_code: + 2024: MD + output: + # November retains food ISM from September under SSI's payment timing. + ssi_in_kind_support_and_maintenance: 334.333333 + md_paa_countable_income: 314.333333 + md_paa_imputed_federal_ssi: 628.666667 + # PAA = 1,173 + 102 - 943 = 332 after the July PNA increase. + md_paa: 332 + +- name: Maryland excludes food support from December PAA income. + period: 2024-12 + absolute_error_margin: 0.01 + input: + people: + person: + age: + 2024: 70 + ssi_receives_food_from_others: + 2024: true + md_paa_living_arrangement: + 2024: CARE_HOME_LEVEL_C + households: + household: + members: [person] + state_code: + 2024: MD + output: + # October food no longer counts, so December ISM and countable income = 0. + ssi_in_kind_support_and_maintenance: 0 + md_paa_countable_income: 0 + md_paa_imputed_federal_ssi: 943 + # PAA = 1,173 + 102 - 0 - 943 = 332. + md_paa: 332 + +- name: Maryland sums monthly countable income without carrying December's unused exclusion. + period: 2024 + absolute_error_margin: 0.01 + input: + age: 70 + state_code: MD + ssi_receives_food_from_others: true + output: + # 11 * (943 / 3 + 20) = 3,677.666667 gross ISM. + ssi_in_kind_support_and_maintenance: 3_677.666667 + # 11 * (943 / 3) + 0 = 3,457.666667 countable income. + md_paa_countable_income: 3_457.666667 diff --git a/policyengine_us/variables/gov/states/ca/cdss/state_supplement/ca_state_supplement_person.py b/policyengine_us/variables/gov/states/ca/cdss/state_supplement/ca_state_supplement_person.py index f4aaf8ae212..edac0d2ff7d 100644 --- a/policyengine_us/variables/gov/states/ca/cdss/state_supplement/ca_state_supplement_person.py +++ b/policyengine_us/variables/gov/states/ca/cdss/state_supplement/ca_state_supplement_person.py @@ -89,8 +89,9 @@ def formula(person, period, parameters): ) # NOTE: in-kind support uses the couple's presumed maximum value here, # because SSI still treats the pair as a couple. - unearned = person("ssi_unearned_income", period.this_year) + person( - "ssi_in_kind_support_and_maintenance", period.this_year + unearned = ( + person("ssi_unearned_income", period.this_year) + + person("ssi_in_kind_support_and_maintenance", period) * MONTHS_IN_YEAR ) # _apply_ssi_exclusions takes and returns annual amounts. own_countable_income = ( diff --git a/policyengine_us/variables/gov/states/md/dhs/fia/paa/md_paa_countable_income.py b/policyengine_us/variables/gov/states/md/dhs/fia/paa/md_paa_countable_income.py index b32ee9f66b5..ee39adf3623 100644 --- a/policyengine_us/variables/gov/states/md/dhs/fia/paa/md_paa_countable_income.py +++ b/policyengine_us/variables/gov/states/md/dhs/fia/paa/md_paa_countable_income.py @@ -1,6 +1,6 @@ from policyengine_us.model_api import * from policyengine_us.variables.gov.ssa.ssi.eligibility.income._apply_ssi_exclusions import ( - _apply_ssi_exclusions, + _apply_ssi_exclusions_monthly, ) @@ -9,7 +9,7 @@ class md_paa_countable_income(Variable): entity = Person label = "Maryland PAA countable income" unit = USD - definition_period = YEAR + definition_period = MONTH defined_for = StateCode.MD reference = ( "https://regs.maryland.gov/us/md/exec/comar/07.03.07.07", @@ -37,7 +37,7 @@ def formula(person, period, parameters): "ssi_in_kind_support_and_maintenance", ], ) - personal_countable = _apply_ssi_exclusions( + personal_countable = _apply_ssi_exclusions_monthly( adjusted_earned, unearned, parameters, From ab7d62f905d718269071b7ff1ebc4a7219ec2fa5 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 13 Nov 2026 18:25:09 -0500 Subject: [PATCH 2/7] Read Maryland PAA's couple status for the month md_paa_countable_income became monthly on this branch, and #10820 made _ssi_couple_income combine a couple's income by each month's status. The formula still halved that income by December's status, so in a year with a part-year facility stay each spouse counted the couple's whole income in the months before it. both_eligible now reads the same month. Case 4 now expects 973.75 each in June, 1,447.50 and 480 in July, and 14,527.50 and 8,722.50 for 2025. Co-Authored-By: Claude Opus 5.5 --- .../dhs/fia/paa/md_paa_countable_income.yaml | 28 +++++++++++-------- .../md/dhs/fia/paa/md_paa_countable_income.py | 11 +++----- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/md/dhs/fia/paa/md_paa_countable_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/md/dhs/fia/paa/md_paa_countable_income.yaml index 8c7e20c1f63..ffb80190a30 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/md/dhs/fia/paa/md_paa_countable_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/md/dhs/fia/paa/md_paa_countable_income.yaml @@ -46,7 +46,7 @@ ssi_countable_income: 0 md_paa_countable_income: 2_480 -- name: Case 4, a spouse's facility stay from July counts by December's status for the whole year, as an annual approximation. +- name: Case 4, a spouse's facility stay from July separates the couple's income from July. period: 2025 absolute_error_margin: 0.01 input: @@ -79,19 +79,23 @@ ssi_couple_computation_applies: 2025-06: [true, true] 2025-07: [false, false] - # This annual formula applies December's status, separate, to the whole - # year, so each spouse counts their own income in all twelve months, - # including January to June, when SSI combines the couple's income. - # person1: (1,000 - 20) + (1,000 - 65) / 2 = 980 + 467.50 = 1,447.50, - # x 12 = 17,370. person2: (500 - 20) x 12 = 5,760. + # January to June each spouse counts half of the couple's combined income + # (20 CFR 416.1802(b)(1)): earned 1,000 and unearned 1,000 + 500 = 1,500 a + # month, so (1,500 - 20) + (1,000 - 65) / 2 = 1,480 + 467.50 = 1,947.50, + # and 1,947.50 / 2 = 973.75 each. + # July to December each spouse counts their own income (20 CFR + # 416.414(b)(3)): person1 (1,000 - 20) + (1,000 - 65) / 2 = 1,447.50; + # person2 500 - 20 = 480. # Both spouses are aged, so neither is an ineligible spouse and no # income is deemed. - # Summing the monthly ssi_marital_* variables instead would combine the - # couple's income through June. Unearned, 1,500 a month: person1 - # 15,000 - 240 + 5,610 = 20,370 and person2 12,000 - 240 = 11,760. - # Earned, 1,000 a month: person2 gets 6,000 a year, so - # 5,760 + (500 - 65) / 2 x 12 = 8,370. - md_paa_countable_income: [17_370, 5_760] + # Year: person1 6 x 973.75 + 6 x 1,447.50 = 5,842.50 + 8,685 = 14,527.50; + # person2 6 x 973.75 + 6 x 480 = 5,842.50 + 2,880 = 8,722.50. The two sum + # to 23,250 = 6 x 1,947.50 + 6 x (1,447.50 + 480), the couple's income + # net of exclusions. + md_paa_countable_income: + 2025-06: [973.75, 973.75] + 2025-07: [1_447.50, 480] + 2025: [14_527.50, 8_722.50] - name: Maryland counts January food support in January PAA income. period: 2024-01 diff --git a/policyengine_us/variables/gov/states/md/dhs/fia/paa/md_paa_countable_income.py b/policyengine_us/variables/gov/states/md/dhs/fia/paa/md_paa_countable_income.py index ab783fe9448..5c998fefb13 100644 --- a/policyengine_us/variables/gov/states/md/dhs/fia/paa/md_paa_countable_income.py +++ b/policyengine_us/variables/gov/states/md/dhs/fia/paa/md_paa_countable_income.py @@ -26,12 +26,9 @@ def formula(person, period, parameters): # exclusions, but it must count income for every PAA eligibility # pathway. Using ssi_countable_income would zero out SSDI-only # recipients unless an input also marks them as SSI-disabled. - # NOTE: this annual formula applies one couple status to the whole - # year (both_eligible below), so it combines the spouses' annual - # income by that same status. ssi_marital_earned_income and - # ssi_marital_unearned_income are monthly and follow each month's - # status; their annual totals would not match it in a year with a - # part-year facility stay. + # _ssi_couple_income and both_eligible read the same month's couple + # status, so a month in which one spouse stays in a medical facility + # counts each spouse's own income (20 CFR 416.414(b)(3)). earned = _ssi_couple_income(person, period, "ssi_earned_income") student_exclusion = person( "ssi_blind_or_disabled_working_student_exclusion", period @@ -49,7 +46,7 @@ def formula(person, period, parameters): period, ) - both_eligible = person("ssi_couple_computation_applies", period.this_year) + both_eligible = person("ssi_couple_computation_applies", period) # COMAR 07.03.07.07 counts the individual's own income and has no # spousal deeming, so people who fail SSI's citizen or # qualified-noncitizen test count their own income alone, including From 6fcadb58c25f7763d452a8918c2aa1158d6d7273 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 13 Nov 2026 18:25:18 -0500 Subject: [PATCH 3/7] Apply California's exclusions to the month and test ISM under $20 ca_state_supplement_person passes the month's income to _apply_ssi_exclusions_monthly in place of scaling the month's ISM to a year and back. A new property test gives ISM of $0 to $19, where the unused part of the $20 exclusion reduces earnings, and checks Maryland against COMAR 07.03.07.08(A) month by month. Three Maryland case names now say which month's SSI in-kind support they read. Co-Authored-By: Claude Opus 5.5 --- .../core/test_ssi_monthly_state_readers.py | 44 +++++++++++++++++++ .../dhs/fia/paa/md_paa_countable_income.yaml | 6 +-- .../ca_state_supplement_person.py | 11 +++-- 3 files changed, 52 insertions(+), 9 deletions(-) diff --git a/policyengine_us/tests/core/test_ssi_monthly_state_readers.py b/policyengine_us/tests/core/test_ssi_monthly_state_readers.py index b58960f9184..6949b70a62b 100644 --- a/policyengine_us/tests/core/test_ssi_monthly_state_readers.py +++ b/policyengine_us/tests/core/test_ssi_monthly_state_readers.py @@ -201,3 +201,47 @@ def test_constant_ism_keeps_the_annual_exclusion_result( 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): + # COMAR 07.03.07.08(A) disregards $20 of unearned income, then $65 and + # one-half of the remaining earned income, in any month. PAA Manual 900 + # applies the part of the $20 that unearned income under $20 leaves + # unused to earnings. + # 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 + ) diff --git a/policyengine_us/tests/policy/baseline/gov/states/md/dhs/fia/paa/md_paa_countable_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/md/dhs/fia/paa/md_paa_countable_income.yaml index ffb80190a30..d2e9cfc66b1 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/md/dhs/fia/paa/md_paa_countable_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/md/dhs/fia/paa/md_paa_countable_income.yaml @@ -97,7 +97,7 @@ 2025-07: [1_447.50, 480] 2025: [14_527.50, 8_722.50] -- name: Maryland counts January food support in January PAA income. +- name: Maryland counts January's SSI in-kind support in January PAA income. period: 2024-01 absolute_error_margin: 0.01 input: @@ -123,7 +123,7 @@ # PAA = 1,173 + 98 - 314.333333 - 628.666667 = 328. md_paa: 328 -- name: Maryland counts September food support in November PAA income. +- name: Maryland counts November's SSI in-kind support in November PAA income. period: 2024-11 absolute_error_margin: 0.01 input: @@ -148,7 +148,7 @@ # PAA = 1,173 + 102 - 943 = 332 after the July PNA increase. md_paa: 332 -- name: Maryland excludes food support from December PAA income. +- name: Maryland counts no SSI in-kind support in December 2024 PAA income. period: 2024-12 absolute_error_margin: 0.01 input: diff --git a/policyengine_us/variables/gov/states/ca/cdss/state_supplement/ca_state_supplement_person.py b/policyengine_us/variables/gov/states/ca/cdss/state_supplement/ca_state_supplement_person.py index edac0d2ff7d..5e4cf980210 100644 --- a/policyengine_us/variables/gov/states/ca/cdss/state_supplement/ca_state_supplement_person.py +++ b/policyengine_us/variables/gov/states/ca/cdss/state_supplement/ca_state_supplement_person.py @@ -4,7 +4,7 @@ ssi_state_supplement, ) from policyengine_us.variables.gov.ssa.ssi.eligibility.income._apply_ssi_exclusions import ( - _apply_ssi_exclusions, + _apply_ssi_exclusions_monthly, ) from policyengine_us.variables.gov.ssa.ssi.eligibility.resources.deemed._ssi_spouses import ( _ssi_couple_total, @@ -89,13 +89,12 @@ def formula(person, period, parameters): ) # NOTE: in-kind support uses the couple's presumed maximum value here, # because SSI still treats the pair as a couple. + ism = person("ssi_in_kind_support_and_maintenance", period) unearned = ( - person("ssi_unearned_income", period.this_year) - + person("ssi_in_kind_support_and_maintenance", period) * MONTHS_IN_YEAR + person("ssi_unearned_income", period.this_year) / MONTHS_IN_YEAR + ism ) - # _apply_ssi_exclusions takes and returns annual amounts. - own_countable_income = ( - _apply_ssi_exclusions(earned, unearned, parameters, period) / MONTHS_IN_YEAR + own_countable_income = _apply_ssi_exclusions_monthly( + earned / MONTHS_IN_YEAR, unearned, parameters, period ) ssi_countable_income = person("ssi_countable_income", period) couple_income = _ssi_couple_total( From dc3fc47165bfe85018c1d876f78d3f638ccfbdbe Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 13 Nov 2026 18:25:38 -0500 Subject: [PATCH 4/7] Name Maryland's monthly couple status in the changelog Co-Authored-By: Claude Opus 5.5 --- changelog.d/ssi-monthly-ism-state-readers.fixed.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/ssi-monthly-ism-state-readers.fixed.md b/changelog.d/ssi-monthly-ism-state-readers.fixed.md index 783916874ee..8352fd0eeef 100644 --- a/changelog.d/ssi-monthly-ism-state-readers.fixed.md +++ b/changelog.d/ssi-monthly-ism-state-readers.fixed.md @@ -1 +1 @@ -Corrected Maryland PAA income and California split SSI/CAPI supplements to count each month's SSI in-kind support and maintenance. +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, and apply each month's SSI couple status to Maryland PAA income. From f53c22729a63bb8f499fcb5067206946a493aff9 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 13 Nov 2026 18:35:47 -0500 Subject: [PATCH 5/7] Name the month's SSI in-kind support in California's split-couple cases Co-Authored-By: Claude Opus 5.5 --- .../ca/cdss/state_supplement/ca_state_supplement.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/ca/cdss/state_supplement/ca_state_supplement.yaml b/policyengine_us/tests/policy/baseline/gov/states/ca/cdss/state_supplement/ca_state_supplement.yaml index 17a56f9945d..130345be6dd 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ca/cdss/state_supplement/ca_state_supplement.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ca/cdss/state_supplement/ca_state_supplement.yaml @@ -834,7 +834,7 @@ # (POMS SI 02001.010 A.2). ca_state_supplement_person: [453.86, 0] -- name: California counts January food ISM for the SSI member of a split SSI and CAPI couple. +- 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: @@ -873,7 +873,7 @@ # SSP = ceil_cent(1,182.94 - 471.666667 - 471.666667) = 239.61. ca_state_supplement_person: [239.61, 0] -- name: California counts September food ISM in the split couple's November supplement. +- 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: @@ -909,7 +909,7 @@ ssi: [471.666667, 0] ca_state_supplement_person: [239.61, 0] -- name: California excludes food ISM from the split couple's December supplement. +- 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: From f03789a370b9363abbb09501d30aa7cfab807aa2 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 14 Nov 2026 15:39:29 -0500 Subject: [PATCH 6/7] Credit PAA Manual 900 and check California's December payment The small-ISM test's reference comment now credits PAA Manual 900 with deducting the $20 from unearned income first and moving the unused part to earnings, and cites COMAR 07.03.07.08(A) for the monthly disregards. The test also checks California's payment in every month, December included, against the aged standard less SSI and the month's countable income. Co-Authored-By: Claude Opus 5.5 --- .../core/test_ssi_monthly_state_readers.py | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/policyengine_us/tests/core/test_ssi_monthly_state_readers.py b/policyengine_us/tests/core/test_ssi_monthly_state_readers.py index 6949b70a62b..271c224362e 100644 --- a/policyengine_us/tests/core/test_ssi_monthly_state_readers.py +++ b/policyengine_us/tests/core/test_ssi_monthly_state_readers.py @@ -204,10 +204,11 @@ def test_constant_ism_keeps_the_annual_exclusion_result( def _maryland_reference_countable(monthly_earned, monthly_unearned): - # COMAR 07.03.07.08(A) disregards $20 of unearned income, then $65 and - # one-half of the remaining earned income, in any month. PAA Manual 900 - # applies the part of the $20 that unearned income under $20 leaves - # unused to earnings. + # 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 @@ -245,3 +246,20 @@ def test_state_readers_keep_a_small_ism_exclusion_in_its_month( 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 + ) From cecda1e28c7ef2612ebe58deff57febcae966188 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 14 Nov 2026 16:48:54 -0500 Subject: [PATCH 7/7] Read the month's deemed spouse income in Maryland PAA income Since #10837, ssi_income_deemed_from_ineligible_spouse_monthly holds each month's income deemed from an ineligible spouse, with the claimant's in-kind support in both budgets, and SSI's monthly readers read it. md_paa_countable_income becomes monthly on this branch, so it reads that month's amount too. It no longer takes one-twelfth of the year's deemed income. A new case gives the claimant shelter support in July alone and checks January, July and the year. Co-Authored-By: Claude Fable 5.2 --- .../ssi-monthly-ism-state-readers.fixed.md | 2 +- .../dhs/fia/paa/md_paa_countable_income.yaml | 52 +++++++++++++++++++ .../md/dhs/fia/paa/md_paa_countable_income.py | 5 +- 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/changelog.d/ssi-monthly-ism-state-readers.fixed.md b/changelog.d/ssi-monthly-ism-state-readers.fixed.md index 8352fd0eeef..4d67fa4f3bf 100644 --- a/changelog.d/ssi-monthly-ism-state-readers.fixed.md +++ b/changelog.d/ssi-monthly-ism-state-readers.fixed.md @@ -1 +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, and apply each month's SSI couple status to Maryland PAA income. +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. diff --git a/policyengine_us/tests/policy/baseline/gov/states/md/dhs/fia/paa/md_paa_countable_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/md/dhs/fia/paa/md_paa_countable_income.yaml index d2e9cfc66b1..9cd1f185d58 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/md/dhs/fia/paa/md_paa_countable_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/md/dhs/fia/paa/md_paa_countable_income.yaml @@ -185,3 +185,55 @@ ssi_in_kind_support_and_maintenance: 3_677.666667 # 11 * (943 / 3) + 0 = 3,457.666667 countable income. md_paa_countable_income: 3_457.666667 + +- name: Maryland adds the month's income deemed from an ineligible spouse to that month's PAA income. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + ssi_receives_outside_shelter_support: + 2026-01: false + 2026-07: true + 2026-08: false + spouse: + age: 60 + employment_income: 24_000 + tax_units: + couple: + members: [claimant, spouse] + marital_units: + couple: + members: [claimant, spouse] + households: + home: + members: [claimant, spouse] + state_code: MD + output: + # The spouse's $2,000 of monthly wages exceeds 1,491 - 994 = 497, so SSA + # combines the spouses' incomes (20 CFR 416.1163(d)(2)). The claimant + # receives shelter support in July alone, at the presumed maximum value + # of 1,491 / 6 + 10 = 258.50 (POMS SI 01320.150 B.1.a). + # https://secure.ssa.gov/poms.nsf/lnx/0501320150 + # The deemed income is the couple's countable income less the + # claimant's own. + # January: the couple counts (2,000 - 20 - 65) / 2 = 957.50 and the + # claimant alone counts 0, which leaves 957.50. + # July: the couple counts (258.50 - 20) + (2,000 - 65) / 2 = 238.50 + + # 967.50 = 1,206 and the claimant alone counts 258.50 - 20 = 238.50, + # which leaves 967.50. + ssi_income_deemed_from_ineligible_spouse_monthly: + 2026-01: [957.50, 0] + 2026-07: [967.50, 0] + # The claimant counts own income plus that month's deemed income: + # 0 + 957.50 = 957.50 in January and 238.50 + 967.50 = 1,206 in July. + # One-twelfth of the year's deemed income, (11 x 957.50 + 967.50) / 12 = + # 958.33, would give 958.33 in January and 1,196.83 in July. + # The spouse counts own wages: (2,000 - 20 - 65) / 2 = 957.50 a month. + # Year: claimant 11 x 957.50 + 1,206 = 11,738.50; spouse 12 x 957.50 = + # 11,490. + md_paa_countable_income: + 2026-01: [957.50, 957.50] + 2026-07: [1_206, 957.50] + 2026: [11_738.50, 11_490] diff --git a/policyengine_us/variables/gov/states/md/dhs/fia/paa/md_paa_countable_income.py b/policyengine_us/variables/gov/states/md/dhs/fia/paa/md_paa_countable_income.py index 5c998fefb13..1cec1b9fe3f 100644 --- a/policyengine_us/variables/gov/states/md/dhs/fia/paa/md_paa_countable_income.py +++ b/policyengine_us/variables/gov/states/md/dhs/fia/paa/md_paa_countable_income.py @@ -58,9 +58,12 @@ def formula(person, period, parameters): is_citizen = immigration_status == immigration_status.possible_values.CITIZEN is_qualified_noncitizen = person("is_ssi_qualified_noncitizen", period) meets_ssi_immigration_test = is_citizen | is_qualified_noncitizen + # The deemed amount comes from the same month as the income above: + # ssi_income_deemed_from_ineligible_spouse_monthly counts that month's + # in-kind support in the couple's budget and in the claimant's. spousal_deemed = where( meets_ssi_immigration_test, - person("ssi_income_deemed_from_ineligible_spouse", period), + person("ssi_income_deemed_from_ineligible_spouse_monthly", period), 0, ) return where(