From 9524da0f809c0bcc94570bdc43a53590bfffb90c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 12 Oct 2026 06:07:24 -0400 Subject: [PATCH 01/10] Pay CO, TX and NM SSI supplements to both members of an SSI couple The Colorado AND-CS, Texas Medicaid-facility and New Mexico adult residential care supplements gated on is_ssi_eligible_individual, which is false for the tax-unit-spouse member of an SSI couple. Each state's rule pays every SSI recipient on their own (9 CCR 2503-5 3.546-3.548; HHSC MEPD H-1500 and H-6000; 8.106.400.10(C) and 8.106.500.10 NMAC), so gate on SSI receipt instead, the repo's convention for SSI-recipient programs. Co-Authored-By: Claude Opus 5.5 --- ...plement-couple-spouse-eligibility.fixed.md | 1 + .../ssa/state_supplement/grant_standard.yaml | 2 +- .../state_supplement/co_state_supplement.yaml | 93 ++++++++++++ .../co_state_supplement_eligible.yaml | 115 +++++++++++++-- .../nm/hca/ssi_supplement/integration.yaml | 111 +++++++++++--- .../nm_ssi_state_supplement.yaml | 10 +- .../nm_ssi_state_supplement_eligible.yaml | 40 ++++-- .../nm_ssi_state_supplement_historical.yaml | 8 +- .../ssi_state_supplement/integration.yaml | 136 +++++++++++++++--- .../tx_ssi_state_supplement_eligible.yaml | 32 +++-- .../edge_cases/state/co/state_supplement.yaml | 12 +- .../co_state_supplement_eligible.py | 12 +- .../nm_ssi_state_supplement_eligible.py | 9 +- .../tx_ssi_state_supplement_eligible.py | 9 +- 14 files changed, 497 insertions(+), 93 deletions(-) create mode 100644 changelog.d/ssi-supplement-couple-spouse-eligibility.fixed.md diff --git a/changelog.d/ssi-supplement-couple-spouse-eligibility.fixed.md b/changelog.d/ssi-supplement-couple-spouse-eligibility.fixed.md new file mode 100644 index 00000000000..602b88b4c35 --- /dev/null +++ b/changelog.d/ssi-supplement-couple-spouse-eligibility.fixed.md @@ -0,0 +1 @@ +Pay the Colorado AND-CS, Texas SSI facility and New Mexico residential care supplements to SSI recipients, so both members of an SSI couple qualify instead of only the one labelled the eligible individual. diff --git a/policyengine_us/parameters/gov/states/co/ssa/state_supplement/grant_standard.yaml b/policyengine_us/parameters/gov/states/co/ssa/state_supplement/grant_standard.yaml index 9d104731d97..d35eed18446 100644 --- a/policyengine_us/parameters/gov/states/co/ssa/state_supplement/grant_standard.yaml +++ b/policyengine_us/parameters/gov/states/co/ssa/state_supplement/grant_standard.yaml @@ -38,4 +38,4 @@ metadata: - title: 9 CCR 2503-5, Section 3.546, AND-CS Program (effective 2023) href: https://www.coloradosos.gov/CCR/GenerateRulePdf.do?ruleVersionId=10694#page=73 - title: 9 CCR 2503-5, Section 3.546, AND-CS Program - href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=99 + href: https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=91 diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement.yaml index 31fd3ee0804..5a6a467da21 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement.yaml @@ -127,3 +127,96 @@ ssi: 0 output: co_state_supplement: 11_928 + +- name: Case 14, both members of a disabled SSI couple receive the supplement in 2026. + # SSA 2026 SSI couple rate: $1,491/month, or $745.50 for each spouse of a + # joint claim. 9 CCR 2503-5 3.546, 3.548(A)-(B): each spouse is an AND-CS + # client whose grant is the $994 standard less their own countable income, + # which includes their SSI (3.549(A)(2)(d)): 994 - 745.50 = 248.50/month, + # or 2,982/year. SSA's January 2011 Colorado table pays a couple the same + # way: combined standard $1,398 (2 x $699), state supplement $387 + # (1,398 - 1,011 federal couple rate). + absolute_error_margin: 0.1 + period: 2026 + input: + people: + head: + age: 50 + is_disabled: true + meets_ssi_disability_criteria: true + is_tax_unit_head: true + spouse: + age: 52 + is_disabled: true + meets_ssi_disability_criteria: true + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + ssi: [8_946, 8_946] + co_state_supplement: [2_982, 2_982] + +- name: Case 15, a disabled SSI couple with equal pensions each receive the supplement. + # Each spouse has a $300/month pension. Federal SSI: couple unearned income + # 600 - 20 general exclusion = 580; couple payment 1,491 - 580 = 911, or + # 455.50 each. 3.549(A)(2): each client's countable unearned income is + # 300 - 10 (the $20 disregard split between spouses) + 455.50 SSI added back + # = 745.50, so each grant is 994 - 745.50 = 248.50/month, or 2,982/year. + absolute_error_margin: 0.1 + period: 2026 + input: + people: + head: + age: 50 + is_disabled: true + meets_ssi_disability_criteria: true + pension_income: 3_600 + is_tax_unit_head: true + spouse: + age: 52 + is_disabled: true + meets_ssi_disability_criteria: true + pension_income: 3_600 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + ssi: [5_466, 5_466] + co_state_supplement: [2_982, 2_982] + +- name: Case 16, a disabled adult above the SSI resource limit receives nothing. + # $3,000 of countable resources exceeds the $2,000 SSI limit, so no SSI is + # paid. 3.546 limits AND-CS to clients receiving SSI, and 3.547(D) ends + # AND-CS when SSI ends, so the grant is 0 rather than the $994 standard. + absolute_error_margin: 0.1 + period: 2026 + input: + people: + person: + age: 45 + is_disabled: true + meets_ssi_disability_criteria: true + ssi_countable_resources: 3_000 + households: + household: + members: [person] + state_code: CO + output: + ssi: 0 + co_state_supplement: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_eligible.yaml index c9f2703f30f..6a42a8fd629 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_eligible.yaml @@ -1,7 +1,7 @@ - name: Case 1, ineligible senior over 60. period: 2023 input: - is_ssi_eligible_individual: true + ssi: 1_200 age: 60 is_blind: true is_ssi_disabled: true @@ -12,7 +12,7 @@ - name: Case 2, not blind or disabled. period: 2023 input: - is_ssi_eligible_individual: true + ssi: 1_200 age: 30 is_blind: false is_ssi_disabled: false @@ -20,10 +20,10 @@ output: co_state_supplement_eligible: false -- name: Case 3, not SSI eligible. +- name: Case 3, not receiving SSI. period: 2023 input: - is_ssi_eligible_individual: false + ssi: 0 age: 19 is_blind: true is_ssi_disabled: true @@ -31,10 +31,10 @@ output: co_state_supplement_eligible: false -- name: Case 4, SSI eligible and blind under 60. +- name: Case 4, receives SSI and blind under 60. period: 2023 input: - is_ssi_eligible_individual: true + ssi: 1_200 age: 59 is_blind: true is_ssi_disabled: false @@ -42,10 +42,10 @@ output: co_state_supplement_eligible: true -- name: Case 5, SSI eligible and disabled child. +- name: Case 5, receives SSI and disabled child. period: 2023 input: - is_ssi_eligible_individual: true + ssi: 1_200 age: 5 is_blind: false is_ssi_disabled: true @@ -56,7 +56,7 @@ - name: Case 6, historical eligibility in 2005. period: 2005 input: - is_ssi_eligible_individual: true + ssi: 1_200 age: 40 is_blind: false is_ssi_disabled: true @@ -67,10 +67,105 @@ - name: Case 7, age boundary at exactly 59. period: 2023 input: - is_ssi_eligible_individual: true + ssi: 1_200 age: 59 is_blind: false is_ssi_disabled: true state_code: CO output: co_state_supplement_eligible: true + +- name: Case 8, reported SSI receipt qualifies with zero computed SSI. + period: 2026-01 + input: + ssi: 0 + receives_ssi: true + age: 40 + is_blind: false + is_ssi_disabled: true + state_code: CO + output: + co_state_supplement_eligible: true + +- name: Case 9, both members of a disabled SSI couple are eligible. + # 9 CCR 2503-5 3.546: AND-CS is for clients "receiving SSI due to a + # disability or blindness"; 3.548(A): grants are "calculated on an + # individual basis with just one client per case". Each spouse of a joint + # SSI claim receives SSI, so each is an AND-CS client. + period: 2026-01 + input: + people: + head: + age: 50 + is_disabled: true + meets_ssi_disability_criteria: true + is_tax_unit_head: true + spouse: + age: 52 + is_disabled: true + meets_ssi_disability_criteria: true + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + co_state_supplement_eligible: [true, true] + +- name: Case 10, a disabled spouse who fails the SSI citizenship condition is not eligible. + # The tax-unit spouse is undocumented, so receives no SSI (8 USC 1611, + # 1612) and is not an AND-CS client. + period: 2026-01 + input: + people: + head: + age: 50 + is_disabled: true + meets_ssi_disability_criteria: true + is_tax_unit_head: true + spouse: + age: 52 + is_disabled: true + meets_ssi_disability_criteria: true + immigration_status_str: UNDOCUMENTED + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + people: + spouse: + ssi: 0 + co_state_supplement_eligible: false + +- name: Case 11, a disabled adult above the SSI resource limit is not eligible. + # $3,000 of countable resources exceeds the $2,000 SSI limit (20 CFR + # 416.1205), so no SSI is paid and 3.547(D) bars AND-CS. + period: 2026-01 + input: + people: + person: + age: 45 + is_disabled: true + meets_ssi_disability_criteria: true + ssi_countable_resources: 3_000 + households: + household: + members: [person] + state_code: CO + output: + ssi: 0 + co_state_supplement_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/nm/hca/ssi_supplement/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/nm/hca/ssi_supplement/integration.yaml index 387916d45ac..eba4e110bd0 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nm/hca/ssi_supplement/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nm/hca/ssi_supplement/integration.yaml @@ -2,6 +2,8 @@ # Tests the full eligibility and benefit calculation pipeline # Source: 8.106.500.10 NMAC - Payments to Adults in Residential Care # Program: $100/month flat payment for SSI recipients in licensed ARSCH facilities +# 8.106.400.10(C) NMAC: two SSI recipients who would be a family at home are +# two separate benefit groups in an ARSCH facility, so each spouse is paid. - name: Case 1, elderly SSI recipient in ARSCH facility receives supplement. period: 2025 @@ -10,7 +12,6 @@ people: person1: age: 70 - is_ssi_eligible_individual: true is_in_adult_residential_care: true households: household: @@ -19,7 +20,7 @@ output: people: person1: - # Eligibility: SSI-eligible (true) AND age >= 18 (70 >= 18) AND in ARSCH (true) + # Eligibility: SSI recipient (aged) AND age >= 18 (70 >= 18) AND in ARSCH (true) nm_ssi_state_supplement_eligible: true # Benefit: $100/month * 12 = $1,200/year (flat payment) nm_ssi_state_supplement: 1_200 @@ -31,7 +32,8 @@ people: person1: age: 35 - is_ssi_eligible_individual: true + is_disabled: true + meets_ssi_disability_criteria: true is_in_adult_residential_care: true households: household: @@ -40,24 +42,35 @@ output: people: person1: - # Eligibility: SSI-eligible (true) AND age >= 18 (35 >= 18) AND in ARSCH (true) + # Eligibility: SSI recipient (disabled) AND age >= 18 (35 >= 18) AND in ARSCH (true) nm_ssi_state_supplement_eligible: true # Benefit: $100/month * 12 = $1,200/year nm_ssi_state_supplement: 1_200 -- name: Case 3, SSI couple both in ARSCH facility each receive supplement. +- name: Case 3, married SSI couple both in ARSCH facility each receive supplement. + # Both spouses of a joint SSI claim. 2025 SSI couple rate: $1,450/month, or + # $725 for each spouse. 8.106.400.10(C) NMAC: two SSI recipients who would + # be a family at home are "two separate benefit groups" in an ARSCH + # facility, and 8.106.500.10(C)(2) NMAC pays each $100/month. SSA's January + # 2011 New Mexico table shows the couple supplement as $200 (2 x $100). period: 2025 absolute_error_margin: 0.01 input: people: person1: age: 68 - is_ssi_eligible_individual: true + is_tax_unit_head: true is_in_adult_residential_care: true person2: age: 66 - is_ssi_eligible_individual: true + is_tax_unit_spouse: true is_in_adult_residential_care: true + tax_units: + tax_unit: + members: [person1, person2] + marital_units: + marital_unit: + members: [person1, person2] households: household: members: [person1, person2] @@ -65,13 +78,17 @@ output: people: person1: - # Person 1: SSI-eligible, age 68 >= 18, in ARSCH + ssi_claim_is_joint: true + # $725/month * 12 = $8,700/year + ssi: 8_700 + # Person 1: SSI recipient, age 68 >= 18, in ARSCH nm_ssi_state_supplement_eligible: true # $100/month * 12 = $1,200/year per person nm_ssi_state_supplement: 1_200 person2: - # Person 2: SSI-eligible, age 66 >= 18, in ARSCH - # Per 8.106.400.10 NMAC: couples in ARSCH are two separate benefit groups + ssi_claim_is_joint: true + ssi: 8_700 + # Person 2: SSI recipient, age 66 >= 18, in ARSCH nm_ssi_state_supplement_eligible: true # $100/month * 12 = $1,200/year per person nm_ssi_state_supplement: 1_200 @@ -83,7 +100,6 @@ people: person1: age: 72 - is_ssi_eligible_individual: true is_in_adult_residential_care: false households: household: @@ -92,7 +108,7 @@ output: people: person1: - # Not in ARSCH facility: ineligible regardless of SSI status + # Not in ARSCH facility: ineligible regardless of SSI receipt nm_ssi_state_supplement_eligible: false nm_ssi_state_supplement: 0 @@ -103,7 +119,8 @@ people: person1: age: 18 - is_ssi_eligible_individual: true + is_disabled: true + meets_ssi_disability_criteria: true is_in_adult_residential_care: true households: household: @@ -124,11 +141,11 @@ people: person1: age: 60 - is_ssi_eligible_individual: true + is_disabled: true + meets_ssi_disability_criteria: true is_in_adult_residential_care: true person2: age: 45 - is_ssi_eligible_individual: false is_in_adult_residential_care: true households: household: @@ -137,11 +154,11 @@ output: people: person1: - # Person 1: SSI-eligible, age 60 >= 18, in ARSCH = eligible + # Person 1: SSI recipient (disabled), age 60 >= 18, in ARSCH = eligible nm_ssi_state_supplement_eligible: true nm_ssi_state_supplement: 1_200 person2: - # Person 2: NOT SSI-eligible = ineligible even though in ARSCH + # Person 2: not aged, blind or disabled, so no SSI = ineligible even though in ARSCH nm_ssi_state_supplement_eligible: false nm_ssi_state_supplement: 0 @@ -152,7 +169,6 @@ people: person1: age: 65 - is_ssi_eligible_individual: true is_in_adult_residential_care: true households: household: @@ -164,3 +180,62 @@ # Wrong state: defined_for = StateCode.NM excludes non-NM residents nm_ssi_state_supplement_eligible: false nm_ssi_state_supplement: 0 + +- name: Case 8, ARSCH resident whose spouse fails the SSI citizenship condition. + # The undocumented tax-unit spouse receives no SSI (8 USC 1611, 1612), so + # is not "a recipient of supplemental security income" under + # 8.106.500.10(A) NMAC. The citizen head still receives the supplement. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 68 + is_tax_unit_head: true + is_in_adult_residential_care: true + person2: + age: 66 + immigration_status_str: UNDOCUMENTED + is_tax_unit_spouse: true + is_in_adult_residential_care: true + tax_units: + tax_unit: + members: [person1, person2] + marital_units: + marital_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: NM + output: + people: + person1: + nm_ssi_state_supplement_eligible: true + nm_ssi_state_supplement: 1_200 + person2: + ssi: 0 + nm_ssi_state_supplement_eligible: false + nm_ssi_state_supplement: 0 + +- name: Case 9, ARSCH resident above the SSI resource limit receives nothing. + # $3,000 of countable resources exceeds the $2,000 SSI limit, so no SSI is + # paid and 8.106.500.10(A) NMAC bars the supplement. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + ssi_countable_resources: 3_000 + is_in_adult_residential_care: true + households: + household: + members: [person1] + state_code: NM + output: + people: + person1: + ssi: 0 + nm_ssi_state_supplement_eligible: false + nm_ssi_state_supplement: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nm/hca/ssi_supplement/nm_ssi_state_supplement.yaml b/policyengine_us/tests/policy/baseline/gov/states/nm/hca/ssi_supplement/nm_ssi_state_supplement.yaml index d101679d948..3d7d28619ee 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nm/hca/ssi_supplement/nm_ssi_state_supplement.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nm/hca/ssi_supplement/nm_ssi_state_supplement.yaml @@ -9,7 +9,7 @@ people: person1: age: 65 - is_ssi_eligible_individual: true + ssi: 9_000 is_in_adult_residential_care: true households: household: @@ -21,14 +21,14 @@ # $100/month * 12 months = $1,200/year nm_ssi_state_supplement: 1_200 -- name: Case 2, not SSI-eligible receives zero. +- name: Case 2, not an SSI recipient receives zero. period: 2025 absolute_error_margin: 0.01 input: people: person1: age: 65 - is_ssi_eligible_individual: false + ssi: 0 is_in_adult_residential_care: true households: household: @@ -46,7 +46,7 @@ people: person1: age: 65 - is_ssi_eligible_individual: true + ssi: 9_000 is_in_adult_residential_care: false households: household: @@ -64,7 +64,7 @@ people: person1: age: 17 - is_ssi_eligible_individual: true + ssi: 9_000 is_in_adult_residential_care: true households: household: diff --git a/policyengine_us/tests/policy/baseline/gov/states/nm/hca/ssi_supplement/nm_ssi_state_supplement_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/nm/hca/ssi_supplement/nm_ssi_state_supplement_eligible.yaml index 44eb3aa7c43..6a6840b34fd 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nm/hca/ssi_supplement/nm_ssi_state_supplement_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nm/hca/ssi_supplement/nm_ssi_state_supplement_eligible.yaml @@ -9,7 +9,7 @@ people: person1: age: 65 - is_ssi_eligible_individual: true + ssi: 9_000 is_in_adult_residential_care: true households: household: @@ -27,7 +27,7 @@ people: person1: age: 30 - is_ssi_eligible_individual: true + ssi: 9_000 is_in_adult_residential_care: true households: household: @@ -38,14 +38,14 @@ person1: nm_ssi_state_supplement_eligible: true -- name: Case 3, not SSI-eligible person in ARSCH facility in NM is ineligible. +- name: Case 3, person in ARSCH facility in NM who receives no SSI is ineligible. period: 2025 absolute_error_margin: 0.01 input: people: person1: age: 65 - is_ssi_eligible_individual: false + ssi: 0 is_in_adult_residential_care: true households: household: @@ -56,14 +56,14 @@ person1: nm_ssi_state_supplement_eligible: false -- name: Case 4, underage person is ineligible even if SSI-eligible and in ARSCH. +- name: Case 4, underage person is ineligible even if SSI recipient and in ARSCH. period: 2025 absolute_error_margin: 0.01 input: people: person1: age: 17 - is_ssi_eligible_individual: true + ssi: 9_000 is_in_adult_residential_care: true households: household: @@ -81,7 +81,7 @@ people: person1: age: 18 - is_ssi_eligible_individual: true + ssi: 9_000 is_in_adult_residential_care: true households: household: @@ -92,14 +92,14 @@ person1: nm_ssi_state_supplement_eligible: true -- name: Case 6, SSI-eligible adult NOT in ARSCH facility is ineligible. +- name: Case 6, SSI recipient adult NOT in ARSCH facility is ineligible. period: 2025 absolute_error_margin: 0.01 input: people: person1: age: 65 - is_ssi_eligible_individual: true + ssi: 9_000 is_in_adult_residential_care: false households: household: @@ -110,14 +110,14 @@ person1: nm_ssi_state_supplement_eligible: false -- name: Case 7, SSI-eligible adult in ARSCH but wrong state is ineligible. +- name: Case 7, SSI recipient adult in ARSCH but wrong state is ineligible. period: 2025 absolute_error_margin: 0.01 input: people: person1: age: 65 - is_ssi_eligible_individual: true + ssi: 9_000 is_in_adult_residential_care: true households: household: @@ -127,3 +127,21 @@ people: person1: nm_ssi_state_supplement_eligible: false + +- name: Case 8, reported SSI receipt qualifies a resident with zero computed SSI. + period: 2025-01 + input: + people: + person1: + age: 65 + ssi: 0 + receives_ssi: true + is_in_adult_residential_care: true + households: + household: + members: [person1] + state_code: NM + output: + people: + person1: + nm_ssi_state_supplement_eligible: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/nm/hca/ssi_supplement/nm_ssi_state_supplement_historical.yaml b/policyengine_us/tests/policy/baseline/gov/states/nm/hca/ssi_supplement/nm_ssi_state_supplement_historical.yaml index 0724d4aaa8a..99246699f84 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nm/hca/ssi_supplement/nm_ssi_state_supplement_historical.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nm/hca/ssi_supplement/nm_ssi_state_supplement_historical.yaml @@ -1,5 +1,5 @@ # Historical (2017-2019) New Mexico SSI state supplement amount. -# The optional state supplement for an SSI-eligible adult residing in a +# The optional state supplement for an SSI recipient adult residing in a # licensed adult residential care home has been a flat $100.00/month # ($1,200/year) continuously since July 2004 through at least 2025 (verified # against the SSA annual "State Assistance Programs for SSI Recipients" reports @@ -22,7 +22,7 @@ people: person1: age: 65 - is_ssi_eligible_individual: true + ssi: 9_000 is_in_adult_residential_care: true households: household: @@ -40,7 +40,7 @@ people: person1: age: 65 - is_ssi_eligible_individual: true + ssi: 9_000 is_in_adult_residential_care: true households: household: @@ -58,7 +58,7 @@ people: person1: age: 65 - is_ssi_eligible_individual: true + ssi: 9_000 is_in_adult_residential_care: true households: household: diff --git a/policyengine_us/tests/policy/baseline/gov/states/tx/hhsc/ssi_state_supplement/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/tx/hhsc/ssi_state_supplement/integration.yaml index 014f8e1eb49..358ed96957b 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/tx/hhsc/ssi_state_supplement/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/tx/hhsc/ssi_state_supplement/integration.yaml @@ -7,19 +7,23 @@ # - 2024 PNA: $75/month, Federal reduced SSI: $30/month individual # - Individual supplement: $45/month = $540/year # - No additional state income rules beyond federal SSI +# - Each spouse of a couple in a facility receives the reduced benefit ($60 +# for the couple, H-6000), so each receives the supplement # # Reference: Texas Human Resources Code Section 32.024(w) +# Reference: HHSC MEPD Handbook H-1500 and H-6000 # Reference: SSA State Assistance Programs for SSI Recipients - Texas -- name: Scenario 1, SSI-eligible elderly individual in Texas Medicaid facility. +- name: Scenario 1, elderly SSI recipient in Texas Medicaid facility. # 70-year-old SSI recipient in a nursing facility receives state supplement period: 2024 input: people: person1: age: 70 - is_ssi_eligible_individual: true is_in_medicaid_facility: true + ssi_lives_in_medical_treatment_facility: true + ssi_medicaid_pays_majority_of_care: true tax_units: tax_unit: members: [person1] @@ -31,20 +35,25 @@ members: [person1] state_code: TX output: - # Eligibility: SSI eligible + in Medicaid facility + TX = eligible + # Federal reduced SSI: $30/month * 12 = $360/year + ssi: 360 + # Eligibility: SSI recipient + in Medicaid facility + TX = eligible tx_ssi_state_supplement_eligible: true # Supplement: ($75 PNA - $30 federal reduced SSI) * 12 = $540/year tx_ssi_state_supplement: 540 -- name: Scenario 2, SSI-eligible disabled individual in Texas Medicaid facility. +- name: Scenario 2, disabled SSI recipient in Texas Medicaid facility. # 45-year-old disabled SSI recipient in ICF/IID period: 2024 input: people: person1: age: 45 - is_ssi_eligible_individual: true + is_disabled: true + meets_ssi_disability_criteria: true is_in_medicaid_facility: true + ssi_lives_in_medical_treatment_facility: true + ssi_medicaid_pays_majority_of_care: true tax_units: tax_unit: members: [person1] @@ -56,19 +65,19 @@ members: [person1] state_code: TX output: - # Eligibility: SSI eligible + in Medicaid facility + TX = eligible + ssi: 360 + # Eligibility: SSI recipient + in Medicaid facility + TX = eligible tx_ssi_state_supplement_eligible: true # Supplement: ($75 - $30) * 12 = $540/year tx_ssi_state_supplement: 540 -- name: Scenario 3, SSI-eligible individual NOT in Medicaid facility. +- name: Scenario 3, SSI recipient NOT in Medicaid facility. # SSI recipient living in community - no supplement period: 2024 input: people: person1: age: 70 - is_ssi_eligible_individual: true is_in_medicaid_facility: false tax_units: tax_unit: @@ -85,15 +94,16 @@ tx_ssi_state_supplement_eligible: false tx_ssi_state_supplement: 0 -- name: Scenario 4, person in Medicaid facility but NOT SSI eligible. - # Non-SSI recipient in nursing facility - no supplement +- name: Scenario 4, person in Medicaid facility but NOT an SSI recipient. + # 50-year-old who is neither aged, blind nor disabled receives no SSI period: 2024 input: people: person1: age: 50 - is_ssi_eligible_individual: false is_in_medicaid_facility: true + ssi_lives_in_medical_treatment_facility: true + ssi_medicaid_pays_majority_of_care: true tax_units: tax_unit: members: [person1] @@ -105,19 +115,21 @@ members: [person1] state_code: TX output: - # Not SSI eligible = not eligible for supplement + # No SSI = not eligible for supplement + ssi: 0 tx_ssi_state_supplement_eligible: false tx_ssi_state_supplement: 0 -- name: Scenario 5, SSI-eligible individual in Medicaid facility in another state. +- name: Scenario 5, SSI recipient in Medicaid facility in another state. # SSI recipient in facility but not in Texas period: 2024 input: people: person1: age: 68 - is_ssi_eligible_individual: true is_in_medicaid_facility: true + ssi_lives_in_medical_treatment_facility: true + ssi_medicaid_pays_majority_of_care: true tax_units: tax_unit: members: [person1] @@ -133,22 +145,35 @@ tx_ssi_state_supplement_eligible: false tx_ssi_state_supplement: 0 -- name: Scenario 6, couple both SSI-eligible in Texas Medicaid facility. - # Both members of a couple in a nursing facility +- name: Scenario 6, married SSI couple both in a Texas Medicaid facility. + # Both spouses of a joint SSI claim live in a nursing facility. H-6000: + # "SSA lowers the SSI payment standard to the reduced standard for a couple + # ($60)", i.e. $30 for each spouse, and "HHSC supplements the reduced SSI + # payment standard by $45 per month to ensure SSI recipients also have a $75 + # personal needs allowance." SSA's January 2011 Texas table: "Couples + # residing in this living arrangement for more than 30 days are treated as + # two individuals." period: 2024 input: people: person1: age: 72 - is_ssi_eligible_individual: true + is_tax_unit_head: true is_in_medicaid_facility: true + ssi_lives_in_medical_treatment_facility: true + ssi_medicaid_pays_majority_of_care: true person2: age: 70 - is_ssi_eligible_individual: true + is_tax_unit_spouse: true is_in_medicaid_facility: true + ssi_lives_in_medical_treatment_facility: true + ssi_medicaid_pays_majority_of_care: true tax_units: tax_unit: members: [person1, person2] + marital_units: + marital_unit: + members: [person1, person2] spm_units: spm_unit: members: [person1, person2] @@ -157,7 +182,10 @@ members: [person1, person2] state_code: TX output: - # Both persons are eligible + ssi_claim_is_joint: [true, true] + # Each spouse: $30/month * 12 = $360/year + ssi: [360, 360] + # Both spouses are eligible tx_ssi_state_supplement_eligible: [true, true] # Each person: ($75 - $30) * 12 = $540/year tx_ssi_state_supplement: [540, 540] @@ -169,8 +197,9 @@ people: person1: age: 75 - is_ssi_eligible_individual: true is_in_medicaid_facility: true + ssi_lives_in_medical_treatment_facility: true + ssi_medicaid_pays_majority_of_care: true tax_units: tax_unit: members: [person1] @@ -194,8 +223,9 @@ people: person1: age: 80 - is_ssi_eligible_individual: true is_in_medicaid_facility: true + ssi_lives_in_medical_treatment_facility: true + ssi_medicaid_pays_majority_of_care: true tax_units: tax_unit: members: [person1] @@ -211,3 +241,67 @@ # 2005 PNA: $45/month (set 2003-09-01), Federal reduced SSI: $30/month # Supplement: ($45 - $30) * 12 = $180/year tx_ssi_state_supplement: 180 + +- name: Scenario 9, facility resident whose spouse fails the SSI citizenship condition. + # The undocumented tax-unit spouse receives no SSI (8 USC 1611, 1612), so + # receives no Texas supplement. The citizen head still does. + period: 2024 + input: + people: + person1: + age: 72 + is_tax_unit_head: true + is_in_medicaid_facility: true + ssi_lives_in_medical_treatment_facility: true + ssi_medicaid_pays_majority_of_care: true + person2: + age: 70 + immigration_status_str: UNDOCUMENTED + is_tax_unit_spouse: true + is_in_medicaid_facility: true + ssi_lives_in_medical_treatment_facility: true + ssi_medicaid_pays_majority_of_care: true + tax_units: + tax_unit: + members: [person1, person2] + marital_units: + marital_unit: + members: [person1, person2] + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: TX + output: + ssi: [360, 0] + tx_ssi_state_supplement_eligible: [true, false] + tx_ssi_state_supplement: [540, 0] + +- name: Scenario 10, facility resident above the SSI resource limit. + # $3,000 of countable resources exceeds the $2,000 SSI limit, so the + # person is not an SSI recipient and H-1500 gives no state supplement. + period: 2024 + input: + people: + person1: + age: 80 + ssi_countable_resources: 3_000 + is_in_medicaid_facility: true + ssi_lives_in_medical_treatment_facility: true + ssi_medicaid_pays_majority_of_care: true + tax_units: + tax_unit: + members: [person1] + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + state_code: TX + output: + ssi: 0 + tx_ssi_state_supplement_eligible: false + tx_ssi_state_supplement: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/tx/hhsc/ssi_state_supplement/tx_ssi_state_supplement_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/tx/hhsc/ssi_state_supplement/tx_ssi_state_supplement_eligible.yaml index dd95373cc66..b57d90c2f64 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/tx/hhsc/ssi_state_supplement/tx_ssi_state_supplement_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/tx/hhsc/ssi_state_supplement/tx_ssi_state_supplement_eligible.yaml @@ -1,40 +1,52 @@ # Unit tests for Texas SSI State Supplement eligibility -# Eligible if: SSI eligible individual AND residing in a Medicaid-funded facility +# Eligible if: SSI recipient AND residing in a Medicaid-funded facility # AND in Texas -# Reference: Texas Human Resources Code Section 32.024(w) +# Reference: Texas Human Resources Code Section 32.024(w); HHSC MEPD Handbook +# H-1500 ("SSI recipients who live in an institutional setting and receive the +# $30 reduced federal benefit receive a state supplement") -- name: Case 1, SSI eligible and in Medicaid facility in Texas. +- name: Case 1, SSI recipient and in Medicaid facility in Texas. period: 2024 input: - is_ssi_eligible_individual: true + ssi: 360 is_in_medicaid_facility: true state_code: TX output: tx_ssi_state_supplement_eligible: true -- name: Case 2, SSI eligible but not in Medicaid facility. +- name: Case 2, SSI recipient but not in Medicaid facility. period: 2024 input: - is_ssi_eligible_individual: true + ssi: 360 is_in_medicaid_facility: false state_code: TX output: tx_ssi_state_supplement_eligible: false -- name: Case 3, not SSI eligible but in Medicaid facility. +- name: Case 3, not SSI recipient but in Medicaid facility. period: 2024 input: - is_ssi_eligible_individual: false + ssi: 0 is_in_medicaid_facility: true state_code: TX output: tx_ssi_state_supplement_eligible: false -- name: Case 4, neither SSI eligible nor in Medicaid facility. +- name: Case 4, neither SSI recipient nor in Medicaid facility. period: 2024 input: - is_ssi_eligible_individual: false + ssi: 0 is_in_medicaid_facility: false state_code: TX output: tx_ssi_state_supplement_eligible: false + +- name: Case 5, reported SSI receipt qualifies a facility resident with zero computed SSI. + period: 2024-01 + input: + ssi: 0 + receives_ssi: true + is_in_medicaid_facility: true + state_code: TX + output: + tx_ssi_state_supplement_eligible: true diff --git a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/state/co/state_supplement.yaml b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/state/co/state_supplement.yaml index f5176cf680d..6ae5d8e189c 100644 --- a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/state/co/state_supplement.yaml +++ b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/state/co/state_supplement.yaml @@ -1,6 +1,6 @@ # YAML regression-test fixture (policyengine-us partner edge cases). # CO AND-CS state supplement (9 CCR 2503-5 Section 3.546), 2026 parameters: -# ages 0-59, blind/disabled SSI-eligible; benefit = 994/month grant standard (2026) +# ages 0-59, blind/disabled SSI recipients; benefit = 994/month grant standard (2026) # minus (SSI received + SSI-countable income). The 2026 single SSI FBR # (994/mo) exceeds 967, so singles on full SSI get 0; members of an SSI # couple (couple FBR 1,491/mo -> 745.50 each) get a positive supplement. @@ -74,16 +74,14 @@ co_family_affordability_credit: 0.0 # Each member of the SSI couple receives 1,491 / 2 = 745.50/mo SSI; # supplement = (994 - 745.50) x 12 = 248.50 x 12 = 2,982/yr. - # The model pays it only to the SSI "eligible individual" (the head): - # co_state_supplement_eligible checks is_ssi_eligible_individual, - # which is false for the designated "eligible spouse". co_state_supplement: 2_982.0 co_oap: 0.0 spouse: co_family_affordability_credit: 0.0 - # The tax-unit spouse is the SSI "eligible spouse", not an "eligible - # individual", so the current model pays them no AND-CS supplement. - co_state_supplement: 0.0 + # The tax-unit spouse also receives SSI, and 9 CCR 2503-5 3.548(A) + # calculates AND-CS for each client individually, so the spouse + # receives the same 2,982/yr. + co_state_supplement: 2_982.0 co_oap: 0.0 - name: co_state_supplement_single_disabled_fully_offset_by_ssi (co) period: 2026 diff --git a/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_eligible.py b/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_eligible.py index f9c624c8887..ee2c39b882b 100644 --- a/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_eligible.py +++ b/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_eligible.py @@ -7,13 +7,21 @@ class co_state_supplement_eligible(Variable): label = "Colorado State Supplement Eligible" definition_period = MONTH defined_for = StateCode.CO + reference = ( + "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=91", + "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=92", + ) def formula(person, period, parameters): - ssi_eligible = person("is_ssi_eligible_individual", period.this_year) + # 9 CCR 2503-5 3.546 and 3.547(A), (D): AND-CS is for clients + # "receiving SSI due to a disability or blindness". 3.548(A) + # calculates grants "on an individual basis with just one client per + # case", so each member of an SSI couple qualifies on their own. + receives_ssi = (person("ssi", period) > 0) | person("receives_ssi", period) is_disabled = person("is_ssi_disabled", period.this_year) is_blind = person("is_blind", period.this_year) disabled_or_blind = is_disabled | is_blind age = person("age", period.this_year) p = parameters(period).gov.states.co.ssa.state_supplement in_age_range = p.age_range.calc(age) - return disabled_or_blind & ssi_eligible & in_age_range + return disabled_or_blind & receives_ssi & in_age_range diff --git a/policyengine_us/variables/gov/states/nm/hca/ssi_supplement/nm_ssi_state_supplement_eligible.py b/policyengine_us/variables/gov/states/nm/hca/ssi_supplement/nm_ssi_state_supplement_eligible.py index 7129f51da12..3e73c285359 100644 --- a/policyengine_us/variables/gov/states/nm/hca/ssi_supplement/nm_ssi_state_supplement_eligible.py +++ b/policyengine_us/variables/gov/states/nm/hca/ssi_supplement/nm_ssi_state_supplement_eligible.py @@ -9,13 +9,18 @@ class nm_ssi_state_supplement_eligible(Variable): defined_for = StateCode.NM reference = ( "https://srca.nm.gov/parts/title08/08.106.0500.html", + "https://srca.nm.gov/parts/title08/08.106.0400.html", "https://www.ssa.gov/policy/docs/progdesc/ssi_st_asst/2011/nm.html", ) def formula(person, period, parameters): - ssi_eligible = person("is_ssi_eligible_individual", period.this_year) + # 8.106.500.10(A) NMAC: "The individual must be a recipient of + # supplemental security income (SSI)". 8.106.400.10(C) NMAC: two SSI + # recipients who would be a family at home are two separate benefit + # groups in residential care, so each spouse receives the payment. + receives_ssi = (person("ssi", period) > 0) | person("receives_ssi", period) age = person("age", period.this_year) p = parameters(period).gov.states.nm.hca.ssi_supplement meets_age = age >= p.min_age in_residential_care = person("is_in_adult_residential_care", period) - return ssi_eligible & meets_age & in_residential_care + return receives_ssi & meets_age & in_residential_care diff --git a/policyengine_us/variables/gov/states/tx/hhsc/ssi_state_supplement/tx_ssi_state_supplement_eligible.py b/policyengine_us/variables/gov/states/tx/hhsc/ssi_state_supplement/tx_ssi_state_supplement_eligible.py index 2bcbdb97b9a..32a3838768d 100644 --- a/policyengine_us/variables/gov/states/tx/hhsc/ssi_state_supplement/tx_ssi_state_supplement_eligible.py +++ b/policyengine_us/variables/gov/states/tx/hhsc/ssi_state_supplement/tx_ssi_state_supplement_eligible.py @@ -10,9 +10,14 @@ class tx_ssi_state_supplement_eligible(Variable): reference = ( "https://statutes.capitol.texas.gov/Docs/HR/htm/HR.32.htm", "https://www.hhs.texas.gov/handbooks/medicaid-elderly-people-disabilities-handbook/h-6000-co-payment-ssi-cases", + "https://www.hhs.texas.gov/handbooks/medicaid-elderly-people-disabilities-handbook/h-1500-personal-needs-allowance", ) def formula(person, period, parameters): - ssi_eligible = person("is_ssi_eligible_individual", period.this_year) + # MEPD Handbook H-1500: "SSI recipients who live in an institutional + # setting and receive the $30 reduced federal benefit receive a state + # supplement." H-6000 gives a couple in a facility the $60 couple + # standard, so each spouse receives the reduced benefit and qualifies. + receives_ssi = (person("ssi", period) > 0) | person("receives_ssi", period) in_facility = person("is_in_medicaid_facility", period) - return ssi_eligible & in_facility + return receives_ssi & in_facility From 72ca048bfeb429a43989da243fe88e651451f586 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 12 Oct 2026 06:29:22 -0400 Subject: [PATCH 02/10] Add couple invariants for the CO, TX and NM SSI supplements One vectorized simulation of 48 married couples checks that eligibility is SSI receipt, that the result does not depend on which spouse is the tax-unit head, that two citizen spouses are treated alike, and that a spouse failing the SSI citizenship condition is never eligible. main fails the first three; the is_ssi_eligible_individual | is_ssi_eligible_spouse alternative fails the fourth. Co-Authored-By: Claude Opus 5.5 --- ..._ssi_state_supplement_couple_invariants.py | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 policyengine_us/tests/core/test_ssi_state_supplement_couple_invariants.py diff --git a/policyengine_us/tests/core/test_ssi_state_supplement_couple_invariants.py b/policyengine_us/tests/core/test_ssi_state_supplement_couple_invariants.py new file mode 100644 index 00000000000..c60f563ba96 --- /dev/null +++ b/policyengine_us/tests/core/test_ssi_state_supplement_couple_invariants.py @@ -0,0 +1,150 @@ +"""Invariants for the CO, TX and NM SSI supplements over married couples. + +Each state pays every SSI recipient on their own (9 CCR 2503-5 3.546-3.548; +HHSC MEPD Handbook H-1500 and H-6000; 8.106.400.10(C) and 8.106.500.10 NMAC), +so a spouse's supplement must not depend on which spouse holds the tax-unit +head role. The grid crosses state, role assignment, the second spouse's +citizenship, the first spouse's resources and an equal pension for both +spouses: 48 married couples (96 people) in one vectorized simulation. + +Checked for every person in the grid: + +1. Receipt: eligibility holds exactly when the person receives SSI (every + other program condition is met by construction, and no one reports SSI + receipt). +2. Role invariance: eligibility and the supplement are identical when the + head and spouse roles are swapped. +3. Couples: when both spouses are citizens, they have the same eligibility and + the same supplement. +4. Citizenship: a spouse who fails the SSI citizenship condition is never + eligible. + +YAML covers the source examples and their amounts. Python is needed here to +compare each person across role assignments in one simulation. +""" + +from itertools import product + +import numpy as np +import pytest + +from policyengine_us import Simulation + + +YEAR = 2026 +PERIOD = "2026-01" +STATES = ("CO", "TX", "NM") +ROLE_ORDERS = ("first_is_head", "second_is_head") +SECOND_CITIZENSHIP = ("CITIZEN", "UNDOCUMENTED") +FIRST_RESOURCES = (0, 4_000) +ANNUAL_PENSION = (0, 3_600) +GRID = tuple( + product(STATES, ROLE_ORDERS, SECOND_CITIZENSHIP, FIRST_RESOURCES, ANNUAL_PENSION) +) +PROGRAMS = { + "CO": ("co_state_supplement_eligible", "co_state_supplement"), + "TX": ("tx_ssi_state_supplement_eligible", "tx_ssi_state_supplement"), + "NM": ("nm_ssi_state_supplement_eligible", "nm_ssi_state_supplement"), +} +# Each state's program conditions other than SSI receipt. +STATE_INPUTS = { + # AND-CS: blind or disabled, ages 0-59. + "CO": dict( + ages=(50, 52), + is_disabled=True, + meets_ssi_disability_criteria=True, + ), + # SSI recipient in a Medicaid facility on the $30 reduced benefit. + "TX": dict( + ages=(72, 70), + is_in_medicaid_facility=True, + ssi_lives_in_medical_treatment_facility=True, + ssi_medicaid_pays_majority_of_care=True, + ), + # SSI recipient aged 18 or over in a licensed ARSCH home. + "NM": dict(ages=(68, 66), is_in_adult_residential_care=True), +} + + +def _situation(): + groups = ("tax_units", "marital_units", "spm_units", "families", "households") + situation = {"people": {}, **{group: {} for group in groups}} + for index, (state, order, citizenship, resources, pension) in enumerate(GRID): + conditions = dict(STATE_INPUTS[state]) + ages = conditions.pop("ages") + members = [f"couple{index}_first", f"couple{index}_second"] + head = 0 if order == "first_is_head" else 1 + for position, member in enumerate(members): + person = { + "age": {YEAR: ages[position]}, + "is_tax_unit_head": {YEAR: position == head}, + "is_tax_unit_spouse": {YEAR: position != head}, + "pension_income": {YEAR: pension}, + "ssi_countable_resources": {YEAR: resources if position == 0 else 0}, + "immigration_status_str": { + YEAR: citizenship if position == 1 else "CITIZEN" + }, + } + person.update({name: {YEAR: value} for name, value in conditions.items()}) + situation["people"][member] = person + for group in groups: + situation[group][f"{group}_{index}"] = {"members": members} + situation["households"][f"households_{index}"]["state_code"] = {YEAR: state} + return situation + + +@pytest.fixture(scope="module") +def results(): + simulation = Simulation(situation=_situation()) + ssi = simulation.calculate("ssi", PERIOD).reshape(len(GRID), 2) + eligible = np.zeros_like(ssi, dtype=bool) + supplement = np.zeros_like(ssi) + for index, (state, *_rest) in enumerate(GRID): + eligible_variable, amount_variable = PROGRAMS[state] + rows = slice(2 * index, 2 * index + 2) + eligible[index] = simulation.calculate(eligible_variable, PERIOD)[rows] + supplement[index] = simulation.calculate(amount_variable, PERIOD)[rows] + return ssi, eligible, supplement + + +def _case(state, order, citizenship, resources, pension): + return GRID.index((state, order, citizenship, resources, pension)) + + +def test_eligibility_is_ssi_receipt(results): + ssi, eligible, _ = results + np.testing.assert_array_equal(eligible, ssi > 0) + + +def test_supplement_does_not_depend_on_tax_unit_roles(results): + _, eligible, supplement = results + for state, citizenship, resources, pension in product( + STATES, SECOND_CITIZENSHIP, FIRST_RESOURCES, ANNUAL_PENSION + ): + first = _case(state, "first_is_head", citizenship, resources, pension) + second = _case(state, "second_is_head", citizenship, resources, pension) + np.testing.assert_array_equal(eligible[first], eligible[second]) + np.testing.assert_allclose(supplement[first], supplement[second]) + + +def test_citizen_spouses_are_treated_alike(results): + _, eligible, supplement = results + for index, (_state, _order, citizenship, _resources, _pension) in enumerate(GRID): + if citizenship == "CITIZEN": + assert eligible[index, 0] == eligible[index, 1] + assert supplement[index, 0] == pytest.approx(supplement[index, 1]) + + +def test_spouse_failing_citizenship_condition_is_never_eligible(results): + _, eligible, supplement = results + for index, (_state, _order, citizenship, _resources, _pension) in enumerate(GRID): + if citizenship == "UNDOCUMENTED": + assert not eligible[index, 1] + assert supplement[index, 1] == 0 + + +def test_grid_reaches_both_outcomes_for_each_state(results): + _, eligible, _ = results + for state in STATES: + rows = [index for index, case in enumerate(GRID) if case[0] == state] + assert eligible[rows].any() and not eligible[rows].all() From f2d650669e749016fb6d85aa40d5a8d327d16c69 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 12 Oct 2026 06:48:33 -0400 Subject: [PATCH 03/10] Count CO AND-CS spouses' own income; keep TX's $0-payment boundary Review round 1 (GPT-6.1 Sol): - Colorado: 9 CCR 2503-5 3.549(A) counts each AND-CS client's own income, with the $20 disregard split between spouses and no deeming between SSI-recipient spouses (3.549(F)). The model used ssi_countable_income, which halves a joint claim's pooled income. New co_state_supplement_countable_income does the per-client count where SSI's couple computation applies and keeps the federal amount elsewhere. - Texas: H-6000 keeps a facility resident eligible for SSI when countable income "does not exceed" the $30 reduced standard, and 42 USC 1382(a)(1)(A) agrees, so gate on SSI eligibility within the limit (a $0 payment included) plus take-up, or reported receipt. - One reference per document for the CO rule PDF. Also reverts the partner fixture edit: partner API contract tests change only after Ziming Hua approves (CLAUDE.md). Co-Authored-By: Claude Opus 5.5 --- ...plement-couple-spouse-eligibility.fixed.md | 2 +- ..._ssi_state_supplement_couple_invariants.py | 147 ++++++++++++------ .../state_supplement/co_state_supplement.yaml | 77 +++++++++ .../co_state_supplement_countable_income.yaml | 74 +++++++++ .../ssi_state_supplement/integration.yaml | 57 +++++++ .../tx_ssi_state_supplement_eligible.yaml | 59 +++++-- .../edge_cases/state/co/state_supplement.yaml | 12 +- .../state_supplement/co_state_supplement.py | 2 +- .../co_state_supplement_countable_income.py | 46 ++++++ .../co_state_supplement_eligible.py | 6 +- .../tx_ssi_state_supplement_eligible.py | 17 +- 11 files changed, 422 insertions(+), 77 deletions(-) create mode 100644 policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_countable_income.yaml create mode 100644 policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_countable_income.py diff --git a/changelog.d/ssi-supplement-couple-spouse-eligibility.fixed.md b/changelog.d/ssi-supplement-couple-spouse-eligibility.fixed.md index 602b88b4c35..82a22b68683 100644 --- a/changelog.d/ssi-supplement-couple-spouse-eligibility.fixed.md +++ b/changelog.d/ssi-supplement-couple-spouse-eligibility.fixed.md @@ -1 +1 @@ -Pay the Colorado AND-CS, Texas SSI facility and New Mexico residential care supplements to SSI recipients, so both members of an SSI couple qualify instead of only the one labelled the eligible individual. +Pay the Colorado AND-CS, Texas SSI facility and New Mexico residential care supplements to SSI recipients, so both members of an SSI couple qualify instead of only the one labelled the eligible individual. Colorado AND-CS now counts each spouse's own income (9 CCR 2503-5 3.549), and Texas keeps a facility resident whose countable income equals the $30 reduced standard. diff --git a/policyengine_us/tests/core/test_ssi_state_supplement_couple_invariants.py b/policyengine_us/tests/core/test_ssi_state_supplement_couple_invariants.py index c60f563ba96..afdf7772920 100644 --- a/policyengine_us/tests/core/test_ssi_state_supplement_couple_invariants.py +++ b/policyengine_us/tests/core/test_ssi_state_supplement_couple_invariants.py @@ -1,26 +1,34 @@ """Invariants for the CO, TX and NM SSI supplements over married couples. -Each state pays every SSI recipient on their own (9 CCR 2503-5 3.546-3.548; +Each state pays every SSI recipient on their own (9 CCR 2503-5 3.546-3.549; HHSC MEPD Handbook H-1500 and H-6000; 8.106.400.10(C) and 8.106.500.10 NMAC), so a spouse's supplement must not depend on which spouse holds the tax-unit head role. The grid crosses state, role assignment, the second spouse's -citizenship, the first spouse's resources and an equal pension for both -spouses: 48 married couples (96 people) in one vectorized simulation. +citizenship, the first spouse's resources and the two spouses' monthly +pensions (none, $300 each, or $400 and $200): 72 married couples (144 people) +in one vectorized simulation. Checked for every person in the grid: -1. Receipt: eligibility holds exactly when the person receives SSI (every - other program condition is met by construction, and no one reports SSI - receipt). +1. Receipt: anyone paid SSI is eligible, and no one is eligible without being + eligible for SSI with countable income not over the payment standard (no + one in the grid reports SSI receipt, and every other program condition is + met by construction). In Colorado and New Mexico eligibility is exactly a + positive SSI payment. 2. Role invariance: eligibility and the supplement are identical when the head and spouse roles are swapped. -3. Couples: when both spouses are citizens, they have the same eligibility and - the same supplement. +3. Couples: two citizen spouses with the same income have the same + eligibility and the same supplement. 4. Citizenship: a spouse who fails the SSI citizenship condition is never eligible. +5. Colorado couple total: when both spouses of a joint SSI claim receive + AND-CS, the two grants sum to twice the grant standard less the couple's + federal SSI and pooled countable income, as in SSA's Colorado couple + payment level (1,398 = 2 x 699 in January 2011), however 3.549(A) divides + their own income between them. YAML covers the source examples and their amounts. Python is needed here to -compare each person across role assignments in one simulation. +compare each person across role assignments and spouses in one simulation. """ from itertools import product @@ -37,16 +45,17 @@ ROLE_ORDERS = ("first_is_head", "second_is_head") SECOND_CITIZENSHIP = ("CITIZEN", "UNDOCUMENTED") FIRST_RESOURCES = (0, 4_000) -ANNUAL_PENSION = (0, 3_600) +# Monthly pensions of the first and second spouse. +PENSIONS = ((0, 0), (300, 300), (400, 200)) GRID = tuple( - product(STATES, ROLE_ORDERS, SECOND_CITIZENSHIP, FIRST_RESOURCES, ANNUAL_PENSION) + product(STATES, ROLE_ORDERS, SECOND_CITIZENSHIP, FIRST_RESOURCES, PENSIONS) ) PROGRAMS = { "CO": ("co_state_supplement_eligible", "co_state_supplement"), "TX": ("tx_ssi_state_supplement_eligible", "tx_ssi_state_supplement"), "NM": ("nm_ssi_state_supplement_eligible", "nm_ssi_state_supplement"), } -# Each state's program conditions other than SSI receipt. +# Each state's program conditions other than SSI. STATE_INPUTS = { # AND-CS: blind or disabled, ages 0-59. "CO": dict( @@ -69,7 +78,7 @@ def _situation(): groups = ("tax_units", "marital_units", "spm_units", "families", "households") situation = {"people": {}, **{group: {} for group in groups}} - for index, (state, order, citizenship, resources, pension) in enumerate(GRID): + for index, (state, order, citizenship, resources, pensions) in enumerate(GRID): conditions = dict(STATE_INPUTS[state]) ages = conditions.pop("ages") members = [f"couple{index}_first", f"couple{index}_second"] @@ -79,7 +88,7 @@ def _situation(): "age": {YEAR: ages[position]}, "is_tax_unit_head": {YEAR: position == head}, "is_tax_unit_spouse": {YEAR: position != head}, - "pension_income": {YEAR: pension}, + "pension_income": {YEAR: 12 * pensions[position]}, "ssi_countable_resources": {YEAR: resources if position == 0 else 0}, "immigration_status_str": { YEAR: citizenship if position == 1 else "CITIZEN" @@ -96,55 +105,91 @@ def _situation(): @pytest.fixture(scope="module") def results(): simulation = Simulation(situation=_situation()) - ssi = simulation.calculate("ssi", PERIOD).reshape(len(GRID), 2) - eligible = np.zeros_like(ssi, dtype=bool) - supplement = np.zeros_like(ssi) - for index, (state, *_rest) in enumerate(GRID): - eligible_variable, amount_variable = PROGRAMS[state] - rows = slice(2 * index, 2 * index + 2) - eligible[index] = simulation.calculate(eligible_variable, PERIOD)[rows] - supplement[index] = simulation.calculate(amount_variable, PERIOD)[rows] - return ssi, eligible, supplement + def couples(variable, period=PERIOD): + return simulation.calculate(variable, period).reshape(len(GRID), 2) -def _case(state, order, citizenship, resources, pension): - return GRID.index((state, order, citizenship, resources, pension)) - - -def test_eligibility_is_ssi_receipt(results): - ssi, eligible, _ = results - np.testing.assert_array_equal(eligible, ssi > 0) + eligible = np.zeros((len(GRID), 2), dtype=bool) + supplement = np.zeros((len(GRID), 2)) + for state, (eligible_variable, amount_variable) in PROGRAMS.items(): + rows = [index for index, case in enumerate(GRID) if case[0] == state] + eligible[rows] = couples(eligible_variable)[rows] + supplement[rows] = couples(amount_variable)[rows] + parameters = simulation.tax_benefit_system.parameters(PERIOD) + return dict( + grant_standard=parameters.gov.states.co.ssa.state_supplement.grant_standard, + ssi=couples("ssi"), + ssi_eligible=couples("is_ssi_eligible"), + uncapped_ssi=couples("uncapped_ssi"), + joint=couples("ssi_claim_is_joint", YEAR), + federal_countable=couples("ssi_countable_income", YEAR) / 12, + eligible=eligible, + supplement=supplement, + ) + + +def _rows(**fixed): + names = ("state", "order", "citizenship", "resources", "pensions") + return [ + index + for index, case in enumerate(GRID) + if all(case[names.index(name)] == value for name, value in fixed.items()) + ] + + +def test_eligibility_follows_ssi_eligibility(results): + paid = results["ssi"] > 0 + within_limit = results["ssi_eligible"] & (results["uncapped_ssi"] >= 0) + eligible = results["eligible"] + assert not (paid & ~eligible).any() + assert not (eligible & ~within_limit).any() + for state in ("CO", "NM"): + rows = _rows(state=state) + np.testing.assert_array_equal(eligible[rows], paid[rows]) def test_supplement_does_not_depend_on_tax_unit_roles(results): - _, eligible, supplement = results - for state, citizenship, resources, pension in product( - STATES, SECOND_CITIZENSHIP, FIRST_RESOURCES, ANNUAL_PENSION + for state, citizenship, resources, pensions in product( + STATES, SECOND_CITIZENSHIP, FIRST_RESOURCES, PENSIONS ): - first = _case(state, "first_is_head", citizenship, resources, pension) - second = _case(state, "second_is_head", citizenship, resources, pension) - np.testing.assert_array_equal(eligible[first], eligible[second]) - np.testing.assert_allclose(supplement[first], supplement[second]) + first, second = ( + GRID.index((state, order, citizenship, resources, pensions)) + for order in ROLE_ORDERS + ) + for name in ("eligible", "supplement"): + np.testing.assert_allclose(results[name][first], results[name][second]) -def test_citizen_spouses_are_treated_alike(results): - _, eligible, supplement = results - for index, (_state, _order, citizenship, _resources, _pension) in enumerate(GRID): - if citizenship == "CITIZEN": - assert eligible[index, 0] == eligible[index, 1] - assert supplement[index, 0] == pytest.approx(supplement[index, 1]) +def test_citizen_spouses_with_equal_income_are_treated_alike(results): + for index, (_, _, citizenship, _, pensions) in enumerate(GRID): + if citizenship == "CITIZEN" and pensions[0] == pensions[1]: + for name in ("eligible", "supplement"): + assert results[name][index, 0] == pytest.approx(results[name][index, 1]) def test_spouse_failing_citizenship_condition_is_never_eligible(results): - _, eligible, supplement = results - for index, (_state, _order, citizenship, _resources, _pension) in enumerate(GRID): - if citizenship == "UNDOCUMENTED": - assert not eligible[index, 1] - assert supplement[index, 1] == 0 + rows = _rows(citizenship="UNDOCUMENTED") + assert not results["eligible"][rows, 1].any() + assert not results["supplement"][rows, 1].any() + + +def test_colorado_couple_total_matches_pooled_computation(results): + rows = [ + index + for index in _rows(state="CO") + if results["joint"][index].all() and results["eligible"][index].all() + ] + assert rows + pooled = ( + 2 * results["grant_standard"] + - results["ssi"][rows].sum(axis=1) + - results["federal_countable"][rows].sum(axis=1) + ) + np.testing.assert_allclose(results["supplement"][rows].sum(axis=1), pooled) def test_grid_reaches_both_outcomes_for_each_state(results): - _, eligible, _ = results for state in STATES: - rows = [index for index, case in enumerate(GRID) if case[0] == state] - assert eligible[rows].any() and not eligible[rows].all() + rows = _rows(state=state) + assert results["eligible"][rows].any() + assert not results["eligible"][rows].all() diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement.yaml index 5a6a467da21..81412960d86 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement.yaml @@ -220,3 +220,80 @@ output: ssi: 0 co_state_supplement: 0 + +- name: Case 17, a disabled SSI couple with unequal pensions each count their own income. + # Pensions of $400 and $200 a month. Federal SSI pools them: 600 - 20 = 580 + # countable, so the couple payment is 1,491 - 580 = 911, or 455.50 each. + # 3.549(A)(2) counts each client's own income with the $20 disregard split + # between the spouses: head 400 - 10 = 390, spouse 200 - 10 = 190. Grants: + # head 994 - 455.50 - 390 = 148.50/month (1,782/year); spouse + # 994 - 455.50 - 190 = 348.50/month (4,182/year). The couple total is the + # same 497/month as with equal pensions. + absolute_error_margin: 0.1 + period: 2026 + input: + people: + head: + age: 50 + is_disabled: true + meets_ssi_disability_criteria: true + pension_income: 4_800 + is_tax_unit_head: true + spouse: + age: 52 + is_disabled: true + meets_ssi_disability_criteria: true + pension_income: 2_400 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + ssi: [5_466, 5_466] + co_state_supplement_countable_income: [4_680, 2_280] + co_state_supplement: [1_782, 4_182] + +- name: Case 18, a disabled SSI couple where only the head has earnings. + # The head earns $500/month and the spouse has no income. Federal SSI pools + # earnings: (500 - 20 - 65) / 2 = 207.50 countable, so the couple payment is + # 1,491 - 207.50 = 1,283.50, or 641.75 each. 3.549(A): the head's $10 share + # of the disregard goes to earned income (3.549(A)(2)(f)), then $65, then + # half: (500 - 10 - 65) / 2 = 212.50. Head grant 994 - 641.75 - 212.50 = + # 139.75/month (1,677/year); spouse 994 - 641.75 = 352.25/month + # (4,227/year). + absolute_error_margin: 0.1 + period: 2026 + input: + people: + head: + age: 50 + is_disabled: true + meets_ssi_disability_criteria: true + employment_income: 6_000 + is_tax_unit_head: true + spouse: + age: 52 + is_disabled: true + meets_ssi_disability_criteria: true + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + ssi: [7_701, 7_701] + co_state_supplement_countable_income: [2_550, 0] + co_state_supplement: [1_677, 4_227] diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_countable_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_countable_income.yaml new file mode 100644 index 00000000000..c5073147e9f --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_countable_income.yaml @@ -0,0 +1,74 @@ +- name: Case 1, a single client's countable income is their SSI countable income. + period: 2026 + input: + ssi_countable_income: 3_000 + state_code: CO + output: + co_state_supplement_countable_income: 3_000 + +- name: Case 2, spouses of a joint SSI claim each count their own income. + # 9 CCR 2503-5 3.549(A)(2): own unearned income less the client's half of + # the $20 disregard. Pensions of $400 and $200 a month give 390 and 190 + # a month, or 4,680 and 2,280 a year. Federal SSI would give each half of + # the pooled 580, or 290 a month. + period: 2026 + input: + people: + head: + age: 50 + is_disabled: true + meets_ssi_disability_criteria: true + pension_income: 4_800 + is_tax_unit_head: true + spouse: + age: 52 + is_disabled: true + meets_ssi_disability_criteria: true + pension_income: 2_400 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + ssi_countable_income: [3_480, 3_480] + co_state_supplement_countable_income: [4_680, 2_280] + +- name: Case 3, a spouse's unused share of the disregard goes to their earnings. + # The spouse has $5/month of interest and $200/month of earnings. Their $10 + # share of the disregard covers the $5 of unearned income and the other $5 + # goes to earnings (3.549(A)(2)(f)): (200 - 5 - 65) / 2 = 65 a month, or + # 780 a year. + period: 2026 + input: + people: + head: + age: 50 + is_disabled: true + meets_ssi_disability_criteria: true + is_tax_unit_head: true + spouse: + age: 52 + is_disabled: true + meets_ssi_disability_criteria: true + interest_income: 60 + employment_income: 2_400 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + co_state_supplement_countable_income: [0, 780] diff --git a/policyengine_us/tests/policy/baseline/gov/states/tx/hhsc/ssi_state_supplement/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/tx/hhsc/ssi_state_supplement/integration.yaml index 358ed96957b..86db34e9553 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/tx/hhsc/ssi_state_supplement/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/tx/hhsc/ssi_state_supplement/integration.yaml @@ -305,3 +305,60 @@ ssi: 0 tx_ssi_state_supplement_eligible: false tx_ssi_state_supplement: 0 + +- name: Scenario 11, facility resident whose countable income equals the $30 reduced standard. + # A $50/month pension less the $20 general exclusion is $30 of countable + # income, so the federal payment is $30 - $30 = $0. H-6000: the resident + # "remains eligible for SSI if their countable income does not exceed the + # SSI reduced standard payment amount", and HHSC pays the $45 supplement. + period: 2024 + input: + people: + person1: + age: 80 + pension_income: 600 + is_in_medicaid_facility: true + ssi_lives_in_medical_treatment_facility: true + ssi_medicaid_pays_majority_of_care: true + tax_units: + tax_unit: + members: [person1] + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + state_code: TX + output: + ssi: 0 + tx_ssi_state_supplement_eligible: true + # ($75 - $30) * 12 = $540/year + tx_ssi_state_supplement: 540 + +- name: Scenario 12, facility resident whose countable income exceeds the $30 reduced standard. + # A $51/month pension gives $31 of countable income, above the reduced + # standard, so the resident is not eligible for SSI or the supplement. + period: 2024 + input: + people: + person1: + age: 80 + pension_income: 612 + is_in_medicaid_facility: true + ssi_lives_in_medical_treatment_facility: true + ssi_medicaid_pays_majority_of_care: true + tax_units: + tax_unit: + members: [person1] + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + state_code: TX + output: + ssi: 0 + tx_ssi_state_supplement_eligible: false + tx_ssi_state_supplement: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/tx/hhsc/ssi_state_supplement/tx_ssi_state_supplement_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/tx/hhsc/ssi_state_supplement/tx_ssi_state_supplement_eligible.yaml index b57d90c2f64..b98ca5a2c0c 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/tx/hhsc/ssi_state_supplement/tx_ssi_state_supplement_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/tx/hhsc/ssi_state_supplement/tx_ssi_state_supplement_eligible.yaml @@ -1,14 +1,19 @@ # Unit tests for Texas SSI State Supplement eligibility -# Eligible if: SSI recipient AND residing in a Medicaid-funded facility -# AND in Texas +# Eligible if: eligible for SSI with countable income not over the payment +# standard (a $0 federal payment at the limit included) and takes up SSI, or +# reported to receive SSI; AND residing in a Medicaid-funded facility; AND in +# Texas # Reference: Texas Human Resources Code Section 32.024(w); HHSC MEPD Handbook # H-1500 ("SSI recipients who live in an institutional setting and receive the -# $30 reduced federal benefit receive a state supplement") +# $30 reduced federal benefit receive a state supplement") and H-6000 (an SSI +# recipient in a facility "remains eligible for SSI if their countable income +# does not exceed the SSI reduced standard payment amount") - name: Case 1, SSI recipient and in Medicaid facility in Texas. period: 2024 input: - ssi: 360 + is_ssi_eligible: true + uncapped_ssi: 360 is_in_medicaid_facility: true state_code: TX output: @@ -17,36 +22,68 @@ - name: Case 2, SSI recipient but not in Medicaid facility. period: 2024 input: - ssi: 360 + is_ssi_eligible: true + uncapped_ssi: 360 is_in_medicaid_facility: false state_code: TX output: tx_ssi_state_supplement_eligible: false -- name: Case 3, not SSI recipient but in Medicaid facility. +- name: Case 3, not SSI eligible but in Medicaid facility. period: 2024 input: - ssi: 0 + is_ssi_eligible: false is_in_medicaid_facility: true state_code: TX output: tx_ssi_state_supplement_eligible: false -- name: Case 4, neither SSI recipient nor in Medicaid facility. +- name: Case 4, neither SSI eligible nor in Medicaid facility. period: 2024 input: - ssi: 0 + is_ssi_eligible: false is_in_medicaid_facility: false state_code: TX output: tx_ssi_state_supplement_eligible: false -- name: Case 5, reported SSI receipt qualifies a facility resident with zero computed SSI. +- name: Case 5, reported SSI receipt qualifies a facility resident. period: 2024-01 input: - ssi: 0 + is_ssi_eligible: false receives_ssi: true is_in_medicaid_facility: true state_code: TX output: tx_ssi_state_supplement_eligible: true + +- name: Case 6, countable income equal to the reduced standard leaves a $0 federal payment and stays eligible. + period: 2024-01 + input: + is_ssi_eligible: true + uncapped_ssi: 0 + is_in_medicaid_facility: true + state_code: TX + output: + tx_ssi_state_supplement_eligible: true + +- name: Case 7, countable income above the reduced standard is not eligible. + period: 2024-01 + input: + is_ssi_eligible: true + uncapped_ssi: -1 + is_in_medicaid_facility: true + state_code: TX + output: + tx_ssi_state_supplement_eligible: false + +- name: Case 8, a facility resident who does not take up SSI is not eligible. + period: 2024-01 + input: + is_ssi_eligible: true + uncapped_ssi: 30 + takes_up_ssi_if_eligible: false + is_in_medicaid_facility: true + state_code: TX + output: + tx_ssi_state_supplement_eligible: false diff --git a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/state/co/state_supplement.yaml b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/state/co/state_supplement.yaml index 6ae5d8e189c..f5176cf680d 100644 --- a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/state/co/state_supplement.yaml +++ b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/state/co/state_supplement.yaml @@ -1,6 +1,6 @@ # YAML regression-test fixture (policyengine-us partner edge cases). # CO AND-CS state supplement (9 CCR 2503-5 Section 3.546), 2026 parameters: -# ages 0-59, blind/disabled SSI recipients; benefit = 994/month grant standard (2026) +# ages 0-59, blind/disabled SSI-eligible; benefit = 994/month grant standard (2026) # minus (SSI received + SSI-countable income). The 2026 single SSI FBR # (994/mo) exceeds 967, so singles on full SSI get 0; members of an SSI # couple (couple FBR 1,491/mo -> 745.50 each) get a positive supplement. @@ -74,14 +74,16 @@ co_family_affordability_credit: 0.0 # Each member of the SSI couple receives 1,491 / 2 = 745.50/mo SSI; # supplement = (994 - 745.50) x 12 = 248.50 x 12 = 2,982/yr. + # The model pays it only to the SSI "eligible individual" (the head): + # co_state_supplement_eligible checks is_ssi_eligible_individual, + # which is false for the designated "eligible spouse". co_state_supplement: 2_982.0 co_oap: 0.0 spouse: co_family_affordability_credit: 0.0 - # The tax-unit spouse also receives SSI, and 9 CCR 2503-5 3.548(A) - # calculates AND-CS for each client individually, so the spouse - # receives the same 2,982/yr. - co_state_supplement: 2_982.0 + # The tax-unit spouse is the SSI "eligible spouse", not an "eligible + # individual", so the current model pays them no AND-CS supplement. + co_state_supplement: 0.0 co_oap: 0.0 - name: co_state_supplement_single_disabled_fully_offset_by_ssi (co) period: 2026 diff --git a/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement.py b/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement.py index da55e8d65e7..dac180acbd4 100644 --- a/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement.py +++ b/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement.py @@ -9,7 +9,7 @@ class co_state_supplement(Variable): defined_for = "co_state_supplement_eligible" def formula(person, period, parameters): - income = person("ssi_countable_income", period) + income = person("co_state_supplement_countable_income", period) ssi = person("ssi", period) total_countable_income = ssi + income p = parameters(period).gov.states.co.ssa.state_supplement diff --git a/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_countable_income.py b/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_countable_income.py new file mode 100644 index 00000000000..edb2c489e02 --- /dev/null +++ b/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_countable_income.py @@ -0,0 +1,46 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.income._apply_ssi_exclusions import ( + _apply_ssi_exclusions, +) + + +class co_state_supplement_countable_income(Variable): + value_type = float + entity = Person + label = "Colorado State Supplement countable income" + unit = USD + definition_period = YEAR + # PDF pages 93-94 + reference = "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=93" + + def formula(person, period, parameters): + # 9 CCR 2503-5 3.549(A) counts each AND-CS client's own income. Where + # SSI's couple computation applies, ssi_countable_income instead gives + # each spouse half of the couple's pooled countable income. Both + # spouses then receive SSI, so 3.549(F) deems neither spouse's income + # to the other, and 3.549(A)(2)(c)(1) splits the $20 unearned income + # disregard "between the client and the spouse"; it is split equally + # here. Any part a spouse cannot use against unearned income goes to + # their earned income (3.549(A)(2)(f)), as _apply_ssi_exclusions does. + couple_computation = person("ssi_couple_computation_applies", period) + earned = max_( + person("ssi_earned_income", period) + - person("ssi_blind_or_disabled_working_student_exclusion", period), + 0, + ) + unearned = person("ssi_unearned_income", period) + person( + "ssi_in_kind_support_and_maintenance", period + ) + p = parameters(period).gov.ssa.ssi.income.exclusions + own_countable = _apply_ssi_exclusions( + earned, + unearned, + parameters, + period, + general_exclusion=p.general / 2, + ) + return where( + couple_computation, + own_countable, + person("ssi_countable_income", period), + ) diff --git a/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_eligible.py b/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_eligible.py index ee2c39b882b..24e9ea2c409 100644 --- a/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_eligible.py +++ b/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_eligible.py @@ -7,10 +7,8 @@ class co_state_supplement_eligible(Variable): label = "Colorado State Supplement Eligible" definition_period = MONTH defined_for = StateCode.CO - reference = ( - "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=91", - "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=92", - ) + # PDF pages 91-92 + reference = "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=91" def formula(person, period, parameters): # 9 CCR 2503-5 3.546 and 3.547(A), (D): AND-CS is for clients diff --git a/policyengine_us/variables/gov/states/tx/hhsc/ssi_state_supplement/tx_ssi_state_supplement_eligible.py b/policyengine_us/variables/gov/states/tx/hhsc/ssi_state_supplement/tx_ssi_state_supplement_eligible.py index 32a3838768d..1f6849e00f5 100644 --- a/policyengine_us/variables/gov/states/tx/hhsc/ssi_state_supplement/tx_ssi_state_supplement_eligible.py +++ b/policyengine_us/variables/gov/states/tx/hhsc/ssi_state_supplement/tx_ssi_state_supplement_eligible.py @@ -16,8 +16,17 @@ class tx_ssi_state_supplement_eligible(Variable): def formula(person, period, parameters): # MEPD Handbook H-1500: "SSI recipients who live in an institutional # setting and receive the $30 reduced federal benefit receive a state - # supplement." H-6000 gives a couple in a facility the $60 couple - # standard, so each spouse receives the reduced benefit and qualifies. - receives_ssi = (person("ssi", period) > 0) | person("receives_ssi", period) + # supplement." H-6000: such a person "remains eligible for SSI if + # their countable income does not exceed the SSI reduced standard + # payment amount", including income equal to it and a $0 federal + # payment (42 USC 1382(a)(1)(A): income "not more than" the rate). + # H-6000 gives a couple in a facility the $60 couple standard, so + # each spouse is assessed on their own half. + eligible_for_ssi = ( + person("is_ssi_eligible", period) + & person("takes_up_ssi_if_eligible", period.this_year) + & (person("uncapped_ssi", period) >= 0) + ) + receives_ssi = person("receives_ssi", period) in_facility = person("is_in_medicaid_facility", period) - return receives_ssi & in_facility + return (eligible_for_ssi | receives_ssi) & in_facility From 66ddf925ca64ce90a8350472d7f498698583e08d Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 12 Oct 2026 06:55:18 -0400 Subject: [PATCH 04/10] Follow 3.549(A)(2)(f) as written: no earnings spillover for SSI recipients 3.549(A)(2)(f) applies "the difference between the gross unearned income and the $20.00 deduction" to earnings only when the client's unearned income is under $20, and (A)(2)(a) counts unearned income "from all sources", SSI included. So an AND-CS client, who always receives SSI, gets no share of the disregard on earnings unless their gross unearned income is under $20. This matches Axiom's encoding of 3.549 (us-co:regulations/9-ccr-2503-5/3.549). co_state_supplement_countable_income becomes MONTH so it can read that month's SSI. Co-Authored-By: Claude Opus 5.5 --- ..._ssi_state_supplement_couple_invariants.py | 10 +++--- .../state_supplement/co_state_supplement.yaml | 15 +++++---- .../co_state_supplement_countable_income.yaml | 17 +++++----- .../co_state_supplement_countable_income.py | 32 +++++++++---------- 4 files changed, 39 insertions(+), 35 deletions(-) diff --git a/policyengine_us/tests/core/test_ssi_state_supplement_couple_invariants.py b/policyengine_us/tests/core/test_ssi_state_supplement_couple_invariants.py index afdf7772920..cdecc40f80c 100644 --- a/policyengine_us/tests/core/test_ssi_state_supplement_couple_invariants.py +++ b/policyengine_us/tests/core/test_ssi_state_supplement_couple_invariants.py @@ -22,10 +22,12 @@ 4. Citizenship: a spouse who fails the SSI citizenship condition is never eligible. 5. Colorado couple total: when both spouses of a joint SSI claim receive - AND-CS, the two grants sum to twice the grant standard less the couple's - federal SSI and pooled countable income, as in SSA's Colorado couple - payment level (1,398 = 2 x 699 in January 2011), however 3.549(A) divides - their own income between them. + AND-CS and have only unearned income, the two grants sum to twice the + grant standard less the couple's federal SSI and pooled countable income, + as in SSA's Colorado couple payment level (1,398 = 2 x 699 in January + 2011), however 3.549(A) divides their own income between them. (Earnings + break the identity: 3.549(A)(1) deducts $65 for each client, where SSI + deducts $65 once for the couple.) YAML covers the source examples and their amounts. Python is needed here to compare each person across role assignments and spouses in one simulation. diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement.yaml index 81412960d86..399832039ab 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement.yaml @@ -263,11 +263,12 @@ - name: Case 18, a disabled SSI couple where only the head has earnings. # The head earns $500/month and the spouse has no income. Federal SSI pools # earnings: (500 - 20 - 65) / 2 = 207.50 countable, so the couple payment is - # 1,491 - 207.50 = 1,283.50, or 641.75 each. 3.549(A): the head's $10 share - # of the disregard goes to earned income (3.549(A)(2)(f)), then $65, then - # half: (500 - 10 - 65) / 2 = 212.50. Head grant 994 - 641.75 - 212.50 = - # 139.75/month (1,677/year); spouse 994 - 641.75 = 352.25/month - # (4,227/year). + # 1,491 - 207.50 = 1,283.50, or 641.75 each. 3.549(A): the head's only + # unearned income is SSI, so no unearned disregard applies (3.549(A)(2)(c)), + # and their gross unearned income is not under $20, so none reaches earned + # income (3.549(A)(2)(f)); 3.549(A)(1) gives (500 - 65) / 2 = 217.50. Head + # grant 994 - 641.75 - 217.50 = 134.75/month (1,617/year); spouse + # 994 - 641.75 = 352.25/month (4,227/year). absolute_error_margin: 0.1 period: 2026 input: @@ -295,5 +296,5 @@ state_code: CO output: ssi: [7_701, 7_701] - co_state_supplement_countable_income: [2_550, 0] - co_state_supplement: [1_677, 4_227] + co_state_supplement_countable_income: [2_610, 0] + co_state_supplement: [1_617, 4_227] diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_countable_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_countable_income.yaml index c5073147e9f..774f71cc0cb 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_countable_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_countable_income.yaml @@ -1,4 +1,4 @@ -- name: Case 1, a single client's countable income is their SSI countable income. +- name: Case 1, outside an SSI couple computation, countable income is SSI countable income. period: 2026 input: ssi_countable_income: 3_000 @@ -7,8 +7,8 @@ co_state_supplement_countable_income: 3_000 - name: Case 2, spouses of a joint SSI claim each count their own income. - # 9 CCR 2503-5 3.549(A)(2): own unearned income less the client's half of - # the $20 disregard. Pensions of $400 and $200 a month give 390 and 190 + # 9 CCR 2503-5 3.549(A)(2): own unearned income other than SSI, less the + # client's half of the $20 disregard. Pensions of $400 and $200 a month give 390 and 190 # a month, or 4,680 and 2,280 a year. Federal SSI would give each half of # the pooled 580, or 290 a month. period: 2026 @@ -40,11 +40,12 @@ ssi_countable_income: [3_480, 3_480] co_state_supplement_countable_income: [4_680, 2_280] -- name: Case 3, a spouse's unused share of the disregard goes to their earnings. +- name: Case 3, an SSI recipient's earnings get no share of the unearned income disregard. # The spouse has $5/month of interest and $200/month of earnings. Their $10 - # share of the disregard covers the $5 of unearned income and the other $5 - # goes to earnings (3.549(A)(2)(f)): (200 - 5 - 65) / 2 = 65 a month, or - # 780 a year. + # share of the $20 disregard covers the $5 of interest (3.549(A)(2)(c)). Their + # gross unearned income, SSI included, is not under $20, so nothing reaches + # earned income (3.549(A)(2)(f)): (200 - 65) / 2 = 67.50 a month, or 810 a + # year. period: 2026 input: people: @@ -71,4 +72,4 @@ members: [head, spouse] state_code: CO output: - co_state_supplement_countable_income: [0, 780] + co_state_supplement_countable_income: [0, 810] diff --git a/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_countable_income.py b/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_countable_income.py index edb2c489e02..d3f6bf311a9 100644 --- a/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_countable_income.py +++ b/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_countable_income.py @@ -1,15 +1,12 @@ from policyengine_us.model_api import * -from policyengine_us.variables.gov.ssa.ssi.eligibility.income._apply_ssi_exclusions import ( - _apply_ssi_exclusions, -) class co_state_supplement_countable_income(Variable): value_type = float entity = Person - label = "Colorado State Supplement countable income" + label = "Colorado State Supplement countable income other than SSI" unit = USD - definition_period = YEAR + definition_period = MONTH # PDF pages 93-94 reference = "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=93" @@ -18,11 +15,9 @@ def formula(person, period, parameters): # SSI's couple computation applies, ssi_countable_income instead gives # each spouse half of the couple's pooled countable income. Both # spouses then receive SSI, so 3.549(F) deems neither spouse's income - # to the other, and 3.549(A)(2)(c)(1) splits the $20 unearned income - # disregard "between the client and the spouse"; it is split equally - # here. Any part a spouse cannot use against unearned income goes to - # their earned income (3.549(A)(2)(f)), as _apply_ssi_exclusions does. + # to the other, and the client's own income is counted as follows. couple_computation = person("ssi_couple_computation_applies", period) + p = parameters(period).gov.ssa.ssi.income.exclusions earned = max_( person("ssi_earned_income", period) - person("ssi_blind_or_disabled_working_student_exclusion", period), @@ -31,14 +26,19 @@ def formula(person, period, parameters): unearned = person("ssi_unearned_income", period) + person( "ssi_in_kind_support_and_maintenance", period ) - p = parameters(period).gov.ssa.ssi.income.exclusions - own_countable = _apply_ssi_exclusions( - earned, - unearned, - parameters, - period, - general_exclusion=p.general / 2, + ssi = person("ssi", period) + # 3.549(A)(2)(c): $20 is deducted from unearned income other than SSI, + # split between married spouses; a client whose only unearned income + # is SSI gets no unearned income disregard. + unearned_disregard = min_(p.general / 2, unearned) + # 3.549(A)(2)(f): only when the client's gross unearned income, SSI + # included, is under $20 does the difference reduce earned income. + earned_disregard = where(unearned > 0, max_(0, p.general - unearned - ssi), 0) + # 3.549(A)(1): deduct $65 from gross earned income and halve the rest. + countable_earned = max_(0, earned - p.earned - earned_disregard) * ( + 1 - p.earned_share ) + own_countable = unearned - unearned_disregard + countable_earned return where( couple_computation, own_countable, From 3d1da21317f9f0d80ca89304a3cf2734aa55a5d4 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 12 Oct 2026 07:14:13 -0400 Subject: [PATCH 05/10] Hold TX residents to the $30 reduced standard; halve a couple's ISM in CO Review round 2 (GPT-6.1 Sol): - Texas: testing uncapped_ssi admitted an SSI-abolished resident and a resident on the own-household rate whose income equalled it. Test H-6000's condition directly: countable income not over the $30 reduced standard, with SSI not abolished. - Colorado: a joint couple's in-kind support and maintenance is one presumed maximum value (ssi_pmv_amount), which SSI splits between the spouses; count half of it for each spouse, not all of it. Co-Authored-By: Claude Opus 5.5 --- .../state_supplement/co_state_supplement.yaml | 38 ++++++++++++++++++ .../ssi_state_supplement/integration.yaml | 27 +++++++++++++ .../tx_ssi_state_supplement_eligible.yaml | 39 ++++++++++++------- .../co_state_supplement_countable_income.py | 8 ++-- .../tx_ssi_state_supplement_eligible.py | 19 ++++++--- 5 files changed, 109 insertions(+), 22 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement.yaml index 399832039ab..dd50f2b0fc2 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement.yaml @@ -298,3 +298,41 @@ ssi: [7_701, 7_701] co_state_supplement_countable_income: [2_610, 0] co_state_supplement: [1_617, 4_227] + +- name: Case 19, a disabled SSI couple receiving outside shelter support. + # No cash income. In-kind support and maintenance for a couple is one + # presumed maximum value: 1,491 / 3 + 20 = 517/month (20 CFR 416.1140), so + # couple countable income is 517 - 20 = 497 and the couple payment is + # 1,491 - 497 = 994, or 497 each. Each spouse counts half the couple's + # in-kind support, 258.50, less their 10 share of the disregard: 248.50. + # Grant 994 - 497 - 248.50 = 248.50/month (2,982/year) each. + absolute_error_margin: 0.1 + period: 2026 + input: + people: + head: + age: 50 + is_disabled: true + meets_ssi_disability_criteria: true + ssi_receives_outside_shelter_support: true + is_tax_unit_head: true + spouse: + age: 52 + is_disabled: true + meets_ssi_disability_criteria: true + ssi_receives_outside_shelter_support: true + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + ssi: [5_964, 5_964] + co_state_supplement_countable_income: [2_982, 2_982] + co_state_supplement: [2_982, 2_982] diff --git a/policyengine_us/tests/policy/baseline/gov/states/tx/hhsc/ssi_state_supplement/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/tx/hhsc/ssi_state_supplement/integration.yaml index 86db34e9553..5bc7929b311 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/tx/hhsc/ssi_state_supplement/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/tx/hhsc/ssi_state_supplement/integration.yaml @@ -362,3 +362,30 @@ ssi: 0 tx_ssi_state_supplement_eligible: false tx_ssi_state_supplement: 0 + +- name: Scenario 13, facility resident outside the SSI facility living arrangement with income at the full federal rate. + # Only is_in_medicaid_facility is set, so SSI uses the $943 own-household + # rate. A $963/month pension gives $943 of countable income: SSI is $0, and + # the income is far above the $30 reduced standard that H-6000 applies to a + # facility resident, so there is no supplement. + period: 2024 + input: + people: + person1: + age: 80 + pension_income: 11_556 + is_in_medicaid_facility: true + tax_units: + tax_unit: + members: [person1] + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + state_code: TX + output: + ssi: 0 + tx_ssi_state_supplement_eligible: false + tx_ssi_state_supplement: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/tx/hhsc/ssi_state_supplement/tx_ssi_state_supplement_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/tx/hhsc/ssi_state_supplement/tx_ssi_state_supplement_eligible.yaml index b98ca5a2c0c..d5d5fb0042e 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/tx/hhsc/ssi_state_supplement/tx_ssi_state_supplement_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/tx/hhsc/ssi_state_supplement/tx_ssi_state_supplement_eligible.yaml @@ -1,5 +1,5 @@ # Unit tests for Texas SSI State Supplement eligibility -# Eligible if: eligible for SSI with countable income not over the payment +# Eligible if: eligible for SSI with countable income not over the $30 reduced # standard (a $0 federal payment at the limit included) and takes up SSI, or # reported to receive SSI; AND residing in a Medicaid-funded facility; AND in # Texas @@ -9,21 +9,21 @@ # recipient in a facility "remains eligible for SSI if their countable income # does not exceed the SSI reduced standard payment amount") -- name: Case 1, SSI recipient and in Medicaid facility in Texas. +- name: Case 1, SSI-eligible facility resident with no countable income. period: 2024 input: is_ssi_eligible: true - uncapped_ssi: 360 + ssi_countable_income: 0 is_in_medicaid_facility: true state_code: TX output: tx_ssi_state_supplement_eligible: true -- name: Case 2, SSI recipient but not in Medicaid facility. +- name: Case 2, SSI-eligible but not in Medicaid facility. period: 2024 input: is_ssi_eligible: true - uncapped_ssi: 360 + ssi_countable_income: 0 is_in_medicaid_facility: false state_code: TX output: @@ -57,33 +57,46 @@ output: tx_ssi_state_supplement_eligible: true -- name: Case 6, countable income equal to the reduced standard leaves a $0 federal payment and stays eligible. - period: 2024-01 +- name: Case 6, countable income equal to the $30 reduced standard stays eligible. + # $360/year = $30/month, so the federal payment is $0 (H-6000). + period: 2024 input: is_ssi_eligible: true - uncapped_ssi: 0 + ssi_countable_income: 360 is_in_medicaid_facility: true state_code: TX output: tx_ssi_state_supplement_eligible: true -- name: Case 7, countable income above the reduced standard is not eligible. - period: 2024-01 +- name: Case 7, countable income above the $30 reduced standard is not eligible. + # $372/year = $31/month. + period: 2024 input: is_ssi_eligible: true - uncapped_ssi: -1 + ssi_countable_income: 372 is_in_medicaid_facility: true state_code: TX output: tx_ssi_state_supplement_eligible: false - name: Case 8, a facility resident who does not take up SSI is not eligible. - period: 2024-01 + period: 2024 input: is_ssi_eligible: true - uncapped_ssi: 30 + ssi_countable_income: 0 takes_up_ssi_if_eligible: false is_in_medicaid_facility: true state_code: TX output: tx_ssi_state_supplement_eligible: false + +- name: Case 9, no supplement for SSI recipients once SSI is abolished. + period: 2024 + input: + gov.ssa.ssi.abolish_ssi: true + is_ssi_eligible: true + ssi_countable_income: 0 + is_in_medicaid_facility: true + state_code: TX + output: + tx_ssi_state_supplement_eligible: false diff --git a/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_countable_income.py b/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_countable_income.py index d3f6bf311a9..7c715bc5a79 100644 --- a/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_countable_income.py +++ b/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_countable_income.py @@ -23,9 +23,11 @@ def formula(person, period, parameters): - person("ssi_blind_or_disabled_working_student_exclusion", period), 0, ) - unearned = person("ssi_unearned_income", period) + person( - "ssi_in_kind_support_and_maintenance", period - ) + # A joint couple's in-kind support and maintenance is one presumed + # maximum value for the couple (ssi_pmv_amount), which SSI splits + # equally between the spouses. + ism_share = person("ssi_in_kind_support_and_maintenance", period) / 2 + unearned = person("ssi_unearned_income", period) + ism_share ssi = person("ssi", period) # 3.549(A)(2)(c): $20 is deducted from unearned income other than SSI, # split between married spouses; a client whose only unearned income diff --git a/policyengine_us/variables/gov/states/tx/hhsc/ssi_state_supplement/tx_ssi_state_supplement_eligible.py b/policyengine_us/variables/gov/states/tx/hhsc/ssi_state_supplement/tx_ssi_state_supplement_eligible.py index 1f6849e00f5..2dda8005e13 100644 --- a/policyengine_us/variables/gov/states/tx/hhsc/ssi_state_supplement/tx_ssi_state_supplement_eligible.py +++ b/policyengine_us/variables/gov/states/tx/hhsc/ssi_state_supplement/tx_ssi_state_supplement_eligible.py @@ -16,16 +16,23 @@ class tx_ssi_state_supplement_eligible(Variable): def formula(person, period, parameters): # MEPD Handbook H-1500: "SSI recipients who live in an institutional # setting and receive the $30 reduced federal benefit receive a state - # supplement." H-6000: such a person "remains eligible for SSI if + # supplement." H-6000: such a resident "remains eligible for SSI if # their countable income does not exceed the SSI reduced standard - # payment amount", including income equal to it and a $0 federal - # payment (42 USC 1382(a)(1)(A): income "not more than" the rate). - # H-6000 gives a couple in a facility the $60 couple standard, so - # each spouse is assessed on their own half. + # payment amount", so countable income equal to it, with a $0 federal + # payment, still qualifies. The test is against the reduced standard + # itself, since this program identifies residents through + # is_in_medicaid_facility rather than the SSI living arrangement. For + # a couple in a facility, SSI's couple computation gives each spouse + # half the couple's countable income, held to their $30 half of the + # $60 couple standard (H-6000). + p = parameters(period).gov.ssa.ssi + countable_income = person("ssi_countable_income", period) + within_reduced_standard = countable_income <= p.amount.institutional.individual eligible_for_ssi = ( person("is_ssi_eligible", period) & person("takes_up_ssi_if_eligible", period.this_year) - & (person("uncapped_ssi", period) >= 0) + & within_reduced_standard + & ~p.abolish_ssi ) receives_ssi = person("receives_ssi", period) in_facility = person("is_in_medicaid_facility", period) From e925852f98d214217c09a6ae8b5c4f7bb85aacd3 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 12 Oct 2026 14:22:16 -0400 Subject: [PATCH 06/10] Apply CO AND-CS payment floor and 3.549 income rules to all clients 9 CCR 2503-5 3.546(E) sets a $13 AND-CS payment floor from January 1, 2026, and 3.549(B), (H) raise a grant below it with a state supplementary payment. Under 3.520.781(A) a client whose total countable income equals or exceeds the grant standard is not eligible, so the floor tops up only positive grants. co_state_supplement_countable_income now applies 3.549 to every client, not only SSI couples: own income under 3.549(A), plus income deemed from a spouse (3.549(C)-(D)) or from a child's parents (3.549(E)), except from a spouse or parent on SSI, Old Age Pension or Medicaid with income within the SSI limit (3.549(F), dated March 1, 2020). Co-Authored-By: Claude Opus 5.5 --- .../co-and-cs-payment-floor-3549.fixed.md | 1 + .../deeming/assistance_exemption_applies.yaml | 18 ++ .../income/disregards/earned.yaml | 14 + .../income/disregards/earned_share.yaml | 13 + .../income/disregards/unearned.yaml | 14 + .../ssa/state_supplement/payment_floor.yaml | 23 ++ .../state_supplement/co_state_supplement.yaml | 259 +++++++++++++++++- .../co_state_supplement_countable_income.yaml | 48 +++- .../co_state_supplement_deemed_income.yaml | 246 +++++++++++++++++ .../co_state_supplement_deeming_exempt.yaml | 82 ++++++ ...state_supplement_own_countable_income.yaml | 162 +++++++++++ .../state_supplement/_and_cs_client_income.py | 39 +++ .../state_supplement/co_state_supplement.py | 19 +- .../co_state_supplement_countable_income.py | 46 +--- .../co_state_supplement_deemed_income.py | 62 +++++ .../co_state_supplement_deeming_exempt.py | 45 +++ ...o_state_supplement_own_countable_income.py | 29 ++ 17 files changed, 1063 insertions(+), 57 deletions(-) create mode 100644 changelog.d/co-and-cs-payment-floor-3549.fixed.md create mode 100644 policyengine_us/parameters/gov/states/co/ssa/state_supplement/income/deeming/assistance_exemption_applies.yaml create mode 100644 policyengine_us/parameters/gov/states/co/ssa/state_supplement/income/disregards/earned.yaml create mode 100644 policyengine_us/parameters/gov/states/co/ssa/state_supplement/income/disregards/earned_share.yaml create mode 100644 policyengine_us/parameters/gov/states/co/ssa/state_supplement/income/disregards/unearned.yaml create mode 100644 policyengine_us/parameters/gov/states/co/ssa/state_supplement/payment_floor.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_deemed_income.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_deeming_exempt.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_own_countable_income.yaml create mode 100644 policyengine_us/variables/gov/states/co/ssa/state_supplement/_and_cs_client_income.py create mode 100644 policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_deemed_income.py create mode 100644 policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_deeming_exempt.py create mode 100644 policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_own_countable_income.py diff --git a/changelog.d/co-and-cs-payment-floor-3549.fixed.md b/changelog.d/co-and-cs-payment-floor-3549.fixed.md new file mode 100644 index 00000000000..8e673dd8095 --- /dev/null +++ b/changelog.d/co-and-cs-payment-floor-3549.fixed.md @@ -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. diff --git a/policyengine_us/parameters/gov/states/co/ssa/state_supplement/income/deeming/assistance_exemption_applies.yaml b/policyengine_us/parameters/gov/states/co/ssa/state_supplement/income/deeming/assistance_exemption_applies.yaml new file mode 100644 index 00000000000..d14f6c4f4ad --- /dev/null +++ b/policyengine_us/parameters/gov/states/co/ssa/state_supplement/income/deeming/assistance_exemption_applies.yaml @@ -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 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, + # not the recipient's other income. + 2002-01-01: false + 2020-03-01: true + +metadata: + unit: bool + label: Colorado AND-CS deeming exemption for Adult Financial and Medicaid 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 diff --git a/policyengine_us/parameters/gov/states/co/ssa/state_supplement/income/disregards/earned.yaml b/policyengine_us/parameters/gov/states/co/ssa/state_supplement/income/disregards/earned.yaml new file mode 100644 index 00000000000..11d86ee6096 --- /dev/null +++ b/policyengine_us/parameters/gov/states/co/ssa/state_supplement/income/disregards/earned.yaml @@ -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 diff --git a/policyengine_us/parameters/gov/states/co/ssa/state_supplement/income/disregards/earned_share.yaml b/policyengine_us/parameters/gov/states/co/ssa/state_supplement/income/disregards/earned_share.yaml new file mode 100644 index 00000000000..b9546a5978c --- /dev/null +++ b/policyengine_us/parameters/gov/states/co/ssa/state_supplement/income/disregards/earned_share.yaml @@ -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 diff --git a/policyengine_us/parameters/gov/states/co/ssa/state_supplement/income/disregards/unearned.yaml b/policyengine_us/parameters/gov/states/co/ssa/state_supplement/income/disregards/unearned.yaml new file mode 100644 index 00000000000..635a0c1e629 --- /dev/null +++ b/policyengine_us/parameters/gov/states/co/ssa/state_supplement/income/disregards/unearned.yaml @@ -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 diff --git a/policyengine_us/parameters/gov/states/co/ssa/state_supplement/payment_floor.yaml b/policyengine_us/parameters/gov/states/co/ssa/state_supplement/payment_floor.yaml new file mode 100644 index 00000000000..9153a6d5058 --- /dev/null +++ b/policyengine_us/parameters/gov/states/co/ssa/state_supplement/payment_floor.yaml @@ -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 diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement.yaml index dd50f2b0fc2..4d448dc66f3 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement.yaml @@ -3,7 +3,7 @@ period: 2023 input: co_state_supplement_eligible: true - ssi_countable_income: 1_000 + co_state_supplement_countable_income: 1_000 ssi: 0 output: co_state_supplement: 9_968 @@ -13,7 +13,7 @@ period: 2023 input: co_state_supplement_eligible: true - ssi_countable_income: 0 + co_state_supplement_countable_income: 0 ssi: 2_000 output: co_state_supplement: 8_968 @@ -23,7 +23,7 @@ period: 2023 input: co_state_supplement_eligible: true - ssi_countable_income: 6_000 + co_state_supplement_countable_income: 6_000 ssi: 6_000 output: co_state_supplement: 0 @@ -33,7 +33,7 @@ period: 2023 input: co_state_supplement_eligible: true - ssi_countable_income: 0 + co_state_supplement_countable_income: 0 ssi: 10_968 output: co_state_supplement: 0 @@ -43,7 +43,7 @@ period: 2002 input: co_state_supplement_eligible: true - ssi_countable_income: 500 + co_state_supplement_countable_income: 500 ssi: 0 output: co_state_supplement: 6_484 @@ -53,7 +53,7 @@ period: 2005 input: co_state_supplement_eligible: true - ssi_countable_income: 0 + co_state_supplement_countable_income: 0 ssi: 6_000 output: co_state_supplement: 1_248 @@ -63,7 +63,7 @@ period: 2009 input: co_state_supplement_eligible: true - ssi_countable_income: 0 + co_state_supplement_countable_income: 0 ssi: 8_388 output: co_state_supplement: 0 @@ -73,7 +73,7 @@ period: 2012 input: co_state_supplement_eligible: true - ssi_countable_income: 1_000 + co_state_supplement_countable_income: 1_000 ssi: 0 output: co_state_supplement: 7_376 @@ -83,7 +83,7 @@ period: 2014 input: co_state_supplement_eligible: true - ssi_countable_income: 0 + co_state_supplement_countable_income: 0 ssi: 5_000 output: co_state_supplement: 3_652 @@ -93,7 +93,7 @@ period: 2023 input: co_state_supplement_eligible: false - ssi_countable_income: 0 + co_state_supplement_countable_income: 0 ssi: 0 output: co_state_supplement: 0 @@ -103,7 +103,7 @@ period: 2024 input: co_state_supplement_eligible: true - ssi_countable_income: 0 + co_state_supplement_countable_income: 0 ssi: 10_000 output: co_state_supplement: 1_316 @@ -113,7 +113,7 @@ period: 2025 input: co_state_supplement_eligible: true - ssi_countable_income: 2_000 + co_state_supplement_countable_income: 2_000 ssi: 5_000 output: co_state_supplement: 4_604 @@ -123,7 +123,7 @@ period: 2026 input: co_state_supplement_eligible: true - ssi_countable_income: 0 + co_state_supplement_countable_income: 0 ssi: 0 output: co_state_supplement: 11_928 @@ -336,3 +336,236 @@ ssi: [5_964, 5_964] co_state_supplement_countable_income: [2_982, 2_982] co_state_supplement: [2_982, 2_982] + +- name: Case 20, the 2026 payment floor raises a grant below $13. + # 9 CCR 2503-5 3.549(B): 994 - 985 = 9, raised to the $13 payment floor + # standard (3.546(E)) by a state supplementary payment. + period: 2026-01 + input: + co_state_supplement_eligible: true + co_state_supplement_countable_income: 0 + ssi: 985 + output: + co_state_supplement: 13 + +- name: Case 21, there was no payment floor in 2025. + # 967 - 958 = 9. Colorado met the pass-along requirement by total + # expenditures until the 2026 rule. + period: 2025-01 + input: + co_state_supplement_eligible: true + co_state_supplement_countable_income: 0 + ssi: 958 + output: + co_state_supplement: 9 + +- name: Case 22, a grant at the floor is unchanged. + period: 2026-01 + input: + co_state_supplement_eligible: true + co_state_supplement_countable_income: 0 + ssi: 981 + output: + co_state_supplement: 13 + +- name: Case 23, a grant of a fraction of a dollar is raised to the floor. + # 994 - 993.50 = 0.50: countable income is below the grant standard, so the + # client is eligible (3.520.781(A)) and the floor applies. + period: 2026-01 + input: + co_state_supplement_eligible: true + co_state_supplement_countable_income: 3.5 + ssi: 990 + output: + co_state_supplement: 13 + +- name: Case 24, a client whose countable income equals the grant standard gets no floor payment. + # 3.520.781(A): total countable income that "equals or exceeds" the grant + # standard makes the client ineligible, so no grant exists to raise. + period: 2026-01 + input: + co_state_supplement_eligible: true + co_state_supplement_countable_income: 200 + ssi: 794 + output: + co_state_supplement: 0 + +- name: Case 25, the floor applies to one member of an SSI couple. + # The head has a $487/month pension and the spouse has no income. Federal + # SSI: 487 - 20 = 467 couple countable income; couple payment + # 1,491 - 467 = 1,024, or 512 each (6,144/year). 3.549(A)(2): the head's own + # countable income is 487 - 10 = 477, so their grant is + # 994 - 512 - 477 = 5/month, raised to the $13 floor (156/year). The + # spouse's grant is 994 - 512 = 482/month (5,784/year). + period: 2026 + input: + people: + head: + age: 50 + is_disabled: true + meets_ssi_disability_criteria: true + pension_income: 5_844 + is_tax_unit_head: true + spouse: + age: 52 + is_disabled: true + meets_ssi_disability_criteria: true + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + ssi: [6_144, 6_144] + co_state_supplement_countable_income: [5_724, 0] + co_state_supplement: [156, 5_784] + +- name: Case 26, a couple member whose countable income reaches the grant standard gets nothing. + # A $497/month pension: couple payment 1,491 - 477 = 1,014, or 507 each. + # The head's total countable income is 507 + 487 = 994, equal to the grant + # standard, so they are not eligible (3.520.781(A)) and get no floor + # payment. The spouse's grant is 994 - 507 = 487/month (5,844/year). + period: 2026 + input: + people: + head: + age: 50 + is_disabled: true + meets_ssi_disability_criteria: true + pension_income: 5_964 + is_tax_unit_head: true + spouse: + age: 52 + is_disabled: true + meets_ssi_disability_criteria: true + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + ssi: [6_084, 6_084] + co_state_supplement: [0, 5_844] + +- name: Case 27, a single client with a disability benefit gets nothing. + # $300/month of SSDI: SSI 994 - 280 = 714. 3.549(A)(2) counts the same + # 280, so total countable income equals the $994 standard and the client is + # not eligible (3.520.781(A)); no floor payment is made. + period: 2026 + input: + people: + person: + age: 40 + is_disabled: true + meets_ssi_disability_criteria: true + social_security_disability: 3_600 + households: + household: + members: [person] + state_code: CO + output: + ssi: 8_568 + co_state_supplement: 0 + +- name: Case 28, a single SSI recipient in another person's household. + # SSI pays two-thirds of the $994 rate, 662.67/month (20 CFR 416.1131). + # With no countable income, the grant is 994 - 662.67 = 331.33/month, or + # 3,976/year. SSA's Colorado tables describe the supplement as increased to + # offset the reduced federal payment. + absolute_error_margin: 0.1 + period: 2026 + input: + people: + person: + age: 40 + is_disabled: true + meets_ssi_disability_criteria: true + ssi_lives_in_another_persons_household: true + ssi_receives_shelter_from_others_in_household: true + ssi_others_pay_all_meals: true + households: + household: + members: [person] + state_code: CO + output: + ssi: 7_952 + co_state_supplement: 3_976 + +- name: Case 29, a client whose working spouse is on Medicaid. + # The client has no income; the spouse is not disabled, earns $1,500/month + # and is enrolled in Medicaid. SSI deems (1,500 - 85) / 2 = 707.50 and pays + # 1,491 - 707.50 = 783.50/month (9,402/year). The spouse's SSI countable + # income, 707.50, is within the $994 individual rate, so 3.549(F) deems + # none of it to the client: grant 994 - 783.50 = 210.50/month + # (2,526/year). + period: 2026 + input: + people: + head: + age: 40 + is_disabled: true + meets_ssi_disability_criteria: true + is_tax_unit_head: true + spouse: + age: 42 + employment_income: 18_000 + medicaid_enrolled_for_ssi_state_supplement: true + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + ssi: [9_402, 0] + co_state_supplement_deemed_income: [0, 0] + co_state_supplement: [2_526, 0] + +- name: Case 30, a client whose working spouse is on no assistance program. + # Same couple as Case 29, but the spouse is not enrolled in Medicaid. + # 3.549(C) deems (1,500 - 65) / 2 = 717.50/month, so total countable income + # is 783.50 + 717.50 = 1,501, above the $994 standard. + period: 2026 + input: + people: + head: + age: 40 + is_disabled: true + meets_ssi_disability_criteria: true + is_tax_unit_head: true + spouse: + age: 42 + employment_income: 18_000 + medicaid_enrolled_for_ssi_state_supplement: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + ssi: [9_402, 0] + co_state_supplement_deemed_income: [8_610, 0] + co_state_supplement: [0, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_countable_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_countable_income.yaml index 774f71cc0cb..d2f3f1e6981 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_countable_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_countable_income.yaml @@ -1,10 +1,20 @@ -- name: Case 1, outside an SSI couple computation, countable income is SSI countable income. +- name: Case 1, a single client's countable income is their own income under 3.549(A). + # $300/month of SSDI less the $20 disregard: 280/month, or 3,360/year, with + # nothing deemed. period: 2026 input: - ssi_countable_income: 3_000 - state_code: CO + people: + person: + age: 40 + is_disabled: true + meets_ssi_disability_criteria: true + social_security_disability: 3_600 + households: + household: + members: [person] + state_code: CO output: - co_state_supplement_countable_income: 3_000 + co_state_supplement_countable_income: 3_360 - name: Case 2, spouses of a joint SSI claim each count their own income. # 9 CCR 2503-5 3.549(A)(2): own unearned income other than SSI, less the @@ -73,3 +83,33 @@ state_code: CO output: co_state_supplement_countable_income: [0, 810] + +- name: Case 4, a married client counts their own income plus income deemed from the spouse. + # 3.549(H): own 300 - 10 = 290 plus the spouse's pension 400 - 10 = 390 + # (see co_state_supplement_deemed_income Case 2): 680/month, or 8,160/year. + period: 2026 + input: + people: + head: + age: 40 + is_disabled: true + meets_ssi_disability_criteria: true + social_security_disability: 3_600 + is_tax_unit_head: true + spouse: + age: 45 + pension_income: 4_800 + takes_up_medicaid_if_eligible: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + co_state_supplement_countable_income: [8_160, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_deemed_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_deemed_income.yaml new file mode 100644 index 00000000000..9274c39e608 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_deemed_income.yaml @@ -0,0 +1,246 @@ +- name: Case 1, an ineligible spouse's earnings are deemed below SSI's deeming threshold. + # The client receives SSI only. The spouse is not disabled, earns + # $300/month and does not take up Medicaid. SSI deems nothing: 300 is under + # the $497 difference between the couple and individual rates (20 CFR + # 416.1163). 9 CCR 2503-5 3.549(C) has no threshold: + # (300 - 65) / 2 = 117.50/month, or 1,410/year. + period: 2026 + input: + people: + head: + age: 40 + is_disabled: true + meets_ssi_disability_criteria: true + is_tax_unit_head: true + spouse: + age: 42 + employment_income: 3_600 + takes_up_medicaid_if_eligible: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + ssi_income_deemed_from_ineligible_spouse: [0, 0] + ssi: [11_928, 0] + co_state_supplement_deeming_exempt: [true, false] + co_state_supplement_deemed_income: [1_410, 0] + +- name: Case 2, an ineligible spouse's unearned income is reduced by the rest of the couple's $20. + # The client has $300/month of SSDI and uses their $10 share of the $20 + # (3.549(A)(2)(c)(1)). The spouse's $400/month pension is reduced by the + # remaining $10 (3.549(D)(2)): 390/month, or 4,680/year. + period: 2026 + input: + people: + head: + age: 40 + is_disabled: true + meets_ssi_disability_criteria: true + social_security_disability: 3_600 + is_tax_unit_head: true + spouse: + age: 45 + pension_income: 4_800 + takes_up_medicaid_if_eligible: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + co_state_supplement_own_countable_income: [3_480, 0] + co_state_supplement_deemed_income: [4_680, 0] + co_state_supplement_countable_income: [8_160, 0] + +- name: Case 3, a spouse enrolled in Medicaid with income within the SSI limit has nothing deemed. + # Same couple as Case 2, but the spouse takes up Medicaid. Their SSI + # countable income, 400 - 20 = 380/month, is within the $994 individual + # rate, so 3.549(F) deems none of it. + period: 2026 + input: + people: + head: + age: 40 + is_disabled: true + meets_ssi_disability_criteria: true + social_security_disability: 3_600 + is_tax_unit_head: true + spouse: + age: 45 + pension_income: 4_800 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + medicaid_enrolled_for_ssi_state_supplement: [true, true] + co_state_supplement_deeming_exempt: [true, true] + co_state_supplement_deemed_income: [0, 0] + +- name: Case 4, the members of an SSI couple deem nothing to each other. + # Both spouses receive SSI, so 3.549(F) deems neither's income to the + # other; each counts their own pension (see co_state_supplement Case 17). + period: 2026 + input: + people: + head: + age: 50 + is_disabled: true + meets_ssi_disability_criteria: true + pension_income: 4_800 + is_tax_unit_head: true + spouse: + age: 52 + is_disabled: true + meets_ssi_disability_criteria: true + pension_income: 2_400 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + co_state_supplement_deemed_income: [0, 0] + +- name: Case 5, parents' income is deemed to a disabled child using the spouse calculation. + # A disabled child aged 10 receives SSI. One parent earns $2,000/month, the + # other has no income, and neither takes up Medicaid. 3.549(E) deems the + # parents' income "using the same calculations as a spouse": + # (2,000 - 65) / 2 = 967.50/month, or 11,610/year. SSI's parental + # allocation (the $1,491 couple rate) leaves nothing to deem: + # (2,000 - 85) / 2 = 957.50 < 1,491. + period: 2026 + input: + people: + parent1: + age: 40 + employment_income: 24_000 + own_children_in_household: 1 + takes_up_medicaid_if_eligible: false + is_tax_unit_head: true + parent2: + age: 38 + own_children_in_household: 1 + takes_up_medicaid_if_eligible: false + is_tax_unit_spouse: true + child: + age: 10 + is_disabled: true + meets_ssi_disability_criteria: true + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [parent1, parent2, child] + marital_units: + parents: + members: [parent1, parent2] + child: + members: [child] + households: + household: + members: [parent1, parent2, child] + state_code: CO + output: + ssi_unearned_income_deemed_from_ineligible_parent: [0, 0, 0] + ssi: [0, 0, 11_928] + co_state_supplement_deemed_income: [0, 0, 11_610] + +- name: Case 6, a parent enrolled in Medicaid within the SSI limit has nothing deemed. + # Same family as Case 5, but the earning parent takes up Medicaid. Their SSI + # countable income, (2,000 - 85) / 2 = 957.50/month, is within the $994 + # individual rate, so 3.549(F) deems none of it. + period: 2026 + input: + people: + parent1: + age: 40 + employment_income: 24_000 + own_children_in_household: 1 + medicaid_enrolled_for_ssi_state_supplement: true + is_tax_unit_head: true + parent2: + age: 38 + own_children_in_household: 1 + takes_up_medicaid_if_eligible: false + is_tax_unit_spouse: true + child: + age: 10 + is_disabled: true + meets_ssi_disability_criteria: true + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [parent1, parent2, child] + marital_units: + parents: + members: [parent1, parent2] + child: + members: [child] + households: + household: + members: [parent1, parent2, child] + state_code: CO + output: + co_state_supplement_deeming_exempt: [true, false, true] + co_state_supplement_deemed_income: [0, 0, 0] + +- name: Case 7, a parent's unearned income is reduced by the child's unused $20. + # A disabled child aged 10 with no income other than SSI; the only parent + # has a $500/month pension and does not take up Medicaid. The child uses + # none of the $20 (3.549(A)(2)(c)(2)), so the parent's pension is reduced by + # the full $20 (3.549(D)(2)): 480/month, or 5,760/year. + period: 2026 + input: + people: + parent: + age: 40 + pension_income: 6_000 + own_children_in_household: 1 + takes_up_medicaid_if_eligible: false + is_tax_unit_head: true + child: + age: 10 + is_disabled: true + meets_ssi_disability_criteria: true + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [parent, child] + marital_units: + parent: + members: [parent] + child: + members: [child] + households: + household: + members: [parent, child] + state_code: CO + output: + co_state_supplement_deemed_income: [0, 5_760] diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_deeming_exempt.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_deeming_exempt.yaml new file mode 100644 index 00000000000..685b2ba0561 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_deeming_exempt.yaml @@ -0,0 +1,82 @@ +- name: Case 1, an SSI recipient's income is not deemed. + # 9 CCR 2503-5 3.549(F). + period: 2026-01 + input: + ssi: 500 + state_code: CO + output: + co_state_supplement_deeming_exempt: true + +- name: Case 2, reported SSI receipt is enough. + period: 2026-01 + input: + ssi: 0 + receives_ssi: true + state_code: CO + output: + co_state_supplement_deeming_exempt: true + +- name: Case 3, a Medicaid enrollee with no income is exempt. + period: 2026-01 + input: + medicaid_enrolled_for_ssi_state_supplement: true + state_code: CO + output: + co_state_supplement_deeming_exempt: true + +- name: Case 4, an Old Age Pension recipient with no other income is exempt. + period: 2026-01 + input: + co_oap: 1_200 + state_code: CO + output: + co_state_supplement_deeming_exempt: true + +- name: Case 5, a Medicaid enrollee at the SSI limit is exempt. + # Earnings of $2,073/month: SSI countable income (2,073 - 20 - 65) / 2 = + # 994, no greater than the $994 individual benefit rate. + period: 2026-01 + input: + medicaid_enrolled_for_ssi_state_supplement: true + employment_income: 24_876 + state_code: CO + output: + co_state_supplement_deeming_exempt: true + +- name: Case 6, a Medicaid enrollee above the SSI limit is not exempt. + # Earnings of $2,074/month: (2,074 - 85) / 2 = 994.50 exceeds $994. + period: 2026-01 + input: + medicaid_enrolled_for_ssi_state_supplement: true + employment_income: 24_888 + state_code: CO + output: + co_state_supplement_deeming_exempt: false + +- name: Case 7, someone on none of the programs is not exempt. + period: 2026-01 + input: + medicaid_enrolled_for_ssi_state_supplement: false + co_oap: 0 + ssi: 0 + state_code: CO + output: + co_state_supplement_deeming_exempt: false + +- name: Case 8, before March 2020 a Medicaid enrollee's income was deemed. + # 3.545(F) (effective January 1, 2020) excluded only the income from those + # benefits; 3.549(F) took effect March 1, 2020. + period: 2020-02 + input: + medicaid_enrolled_for_ssi_state_supplement: true + state_code: CO + output: + co_state_supplement_deeming_exempt: false + +- name: Case 9, from March 2020 a Medicaid enrollee within the SSI limit is exempt. + period: 2020-03 + input: + medicaid_enrolled_for_ssi_state_supplement: true + state_code: CO + output: + co_state_supplement_deeming_exempt: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_own_countable_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_own_countable_income.yaml new file mode 100644 index 00000000000..f4add1af1c1 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_own_countable_income.yaml @@ -0,0 +1,162 @@ +- name: Case 1, a single client with a disability benefit. + # 9 CCR 2503-5 3.549(A)(2): $300/month of SSDI less the $20 disregard is + # 280/month, or 3,360/year. SSI counts the same 280. + period: 2026 + input: + people: + person: + age: 40 + is_disabled: true + meets_ssi_disability_criteria: true + social_security_disability: 3_600 + households: + household: + members: [person] + state_code: CO + output: + ssi_countable_income: 3_360 + co_state_supplement_own_countable_income: 3_360 + +- name: Case 2, a single SSI-only client with earnings gets no share of the unearned disregard. + # $500/month of earnings and no unearned income other than SSI. SSI applies + # the $20 general exclusion to earnings: (500 - 20 - 65) / 2 = 207.50. + # 3.549(A)(2)(c)(2) gives an SSI-only client no unearned disregard, and the + # client's gross unearned income, SSI included, is not under $20, so none + # reaches earnings (3.549(A)(2)(f)): (500 - 65) / 2 = 217.50/month, or + # 2,610/year. + period: 2026 + input: + people: + person: + age: 40 + is_disabled: true + meets_ssi_disability_criteria: true + employment_income: 6_000 + households: + household: + members: [person] + state_code: CO + output: + ssi_countable_income: 2_490 + co_state_supplement_own_countable_income: 2_610 + +- name: Case 3, the unused disregard does not reach earnings when SSI brings unearned income to $20. + # $5/month of interest and $200/month of earnings. The $20 disregard covers + # the $5 of interest (3.549(A)(2)(c)); gross unearned income including SSI + # is far above $20, so nothing reaches earnings (3.549(A)(2)(f)): + # (200 - 65) / 2 = 67.50/month, or 810/year. SSI moves the unused $15 to + # earnings: (200 - 65 - 15) / 2 = 60, or 720/year. + period: 2026 + input: + people: + person: + age: 40 + is_disabled: true + meets_ssi_disability_criteria: true + interest_income: 60 + employment_income: 2_400 + households: + household: + members: [person] + state_code: CO + output: + ssi_countable_income: 720 + co_state_supplement_own_countable_income: 810 + +- name: Case 4, the unused disregard reaches earnings when gross unearned income is under $20. + # $10/month of interest, $200/month of earnings and no SSI payment. The $20 + # disregard covers the $10 of interest and the other $10 reduces earnings + # (3.549(A)(2)(f)): (200 - 65 - 10) / 2 = 62.50/month. Axiom's companion + # test client_income_disregards_and_grant gives the same 62.50. The client + # reports SSI receipt, so is an AND-CS client. + period: 2026-01 + input: + people: + person: + age: 40 + is_ssi_disabled: true + receives_ssi: true + interest_income: 120 + employment_income: 2_400 + ssi: 0 + households: + household: + members: [person] + state_code: CO + output: + co_state_supplement_own_countable_income: 62.5 + +- name: Case 5, a married client gets half of the $20 disregard. + # The client has $300/month of SSDI; the spouse is not disabled and has no + # income. 3.549(A)(2)(c)(1) splits the $20 between the spouses: + # 300 - 10 = 290/month, or 3,480/year. SSI, with no income to deem from the + # spouse, applies the whole $20: 280/month. + period: 2026 + input: + people: + head: + age: 40 + is_disabled: true + meets_ssi_disability_criteria: true + social_security_disability: 3_600 + is_tax_unit_head: true + spouse: + age: 42 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + ssi_countable_income: [3_360, 0] + co_state_supplement_own_countable_income: [3_480, 0] + +- name: Case 6, a single client receiving outside shelter support counts the presumed maximum value. + # In-kind support and maintenance is SSI's presumed maximum value, + # 994 / 3 + 20 = 351.33/month (20 CFR 416.1140), which 3.520.79(B)(6) + # charges to AND-CS as well. Less the $20 disregard: 331.33/month, or + # 3,976/year, as for SSI. + absolute_error_margin: 0.1 + period: 2026 + input: + people: + person: + age: 40 + is_disabled: true + meets_ssi_disability_criteria: true + ssi_receives_outside_shelter_support: true + households: + household: + members: [person] + state_code: CO + output: + ssi_countable_income: 3_976 + co_state_supplement_own_countable_income: 3_976 + +- name: Case 7, a disabled student's earnings count in full. + # A disabled full-time student aged 19 earns $500/month. SSI excludes it + # under 20 CFR 416.1112(c)(3). 3.520.786 limits exempt income to the items + # it lists, which do not include that exclusion, so 3.549(A)(1) counts + # (500 - 65) / 2 = 217.50/month, or 2,610/year. + period: 2026 + input: + people: + person: + age: 19 + is_disabled: true + meets_ssi_disability_criteria: true + is_full_time_student: true + employment_income: 6_000 + households: + household: + members: [person] + state_code: CO + output: + ssi_countable_income: 0 + co_state_supplement_own_countable_income: 2_610 diff --git a/policyengine_us/variables/gov/states/co/ssa/state_supplement/_and_cs_client_income.py b/policyengine_us/variables/gov/states/co/ssa/state_supplement/_and_cs_client_income.py new file mode 100644 index 00000000000..7e7fd6bc2b7 --- /dev/null +++ b/policyengine_us/variables/gov/states/co/ssa/state_supplement/_and_cs_client_income.py @@ -0,0 +1,39 @@ +from policyengine_us.model_api import * + + +def and_cs_client_income(person, period, parameters): + """An AND-CS client's own monthly income and their use of the $20 disregard. + + 9 CCR 2503-5 3.549(A). Returns the client's gross earned income, gross + unearned income other than SSI, the part of the $20 unearned income + disregard applied to that unearned income, and the part of it that + 3.549(A)(2)(f) applies to earned income. + + Gross income uses SSI's earned and unearned income sources. 3.520.786 + limits exempt income to the items it lists, and SSI's student earned + income exclusion (20 CFR 416.1112(c)(3)) is not one of them, so it is + not deducted. + """ + p = parameters(period).gov.states.co.ssa.state_supplement.income.disregards + earned = person("ssi_earned_income", period) + # A joint couple's in-kind support and maintenance is one presumed + # maximum value for the couple (ssi_pmv_amount), which SSI splits + # equally between the spouses. 3.520.71(E) and 3.520.79(B)(6) charge an + # SSI recipient the same ISM that SSA charges. + ism = person("ssi_in_kind_support_and_maintenance", period) + couple_computation = person("ssi_couple_computation_applies", period) + ism = where(couple_computation, ism / 2, ism) + unearned = person("ssi_unearned_income", period) + ism + ssi = person("ssi", period) + # 3.549(A)(2)(c)(1): a married client's $20 disregard is split with the + # spouse. + married = person.marital_unit.nb_persons() == 2 + disregard = where(married, p.unearned / 2, p.unearned) + # 3.549(A)(2)(c)(2): a client whose only unearned income is SSI gets no + # unearned income disregard; min_ gives 0 when there is no other + # unearned income. + unearned_disregard = min_(disregard, unearned) + # 3.549(A)(2)(f): only when the client's gross unearned income, SSI + # included, is under $20 does the difference reduce earned income. + earned_disregard = where(unearned > 0, max_(0, p.unearned - unearned - ssi), 0) + return earned, unearned, unearned_disregard, earned_disregard diff --git a/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement.py b/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement.py index dac180acbd4..6ef4a4cf1a3 100644 --- a/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement.py +++ b/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement.py @@ -5,13 +5,28 @@ class co_state_supplement(Variable): value_type = float entity = Person label = "Colorado State Supplement" + unit = USD definition_period = MONTH defined_for = "co_state_supplement_eligible" + reference = ( + # PDF pages 64, 91-96 + "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=64", + ) def formula(person, period, parameters): income = person("co_state_supplement_countable_income", period) ssi = person("ssi", period) total_countable_income = ssi + income p = parameters(period).gov.states.co.ssa.state_supplement - grant_standard = p.grant_standard - return max_(0, grant_standard - total_countable_income) + # 9 CCR 2503-5 3.548(B), 3.549(B): the grant is the standard less + # total countable income, SSI included. + grant = max_(0, p.grant_standard - total_countable_income) + # 3.520.781(A): a client whose total countable income equals or + # exceeds the grant standard is not eligible. Since 2012 the standard + # has equaled the SSI benefit standard for an individual, so this + # also excludes those "receiving the full SSI benefit standard" + # (3.546). + # 3.546(C)(2), (E), 3.549(B): an eligible client's grant below the + # payment floor standard is raised to it by a state supplementary + # payment. + return where(grant > 0, max_(grant, p.payment_floor), 0) diff --git a/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_countable_income.py b/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_countable_income.py index 7c715bc5a79..c0574b902f2 100644 --- a/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_countable_income.py +++ b/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_countable_income.py @@ -7,42 +7,12 @@ class co_state_supplement_countable_income(Variable): label = "Colorado State Supplement countable income other than SSI" unit = USD definition_period = MONTH - # PDF pages 93-94 + # PDF pages 93-96 reference = "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=93" - - def formula(person, period, parameters): - # 9 CCR 2503-5 3.549(A) counts each AND-CS client's own income. Where - # SSI's couple computation applies, ssi_countable_income instead gives - # each spouse half of the couple's pooled countable income. Both - # spouses then receive SSI, so 3.549(F) deems neither spouse's income - # to the other, and the client's own income is counted as follows. - couple_computation = person("ssi_couple_computation_applies", period) - p = parameters(period).gov.ssa.ssi.income.exclusions - earned = max_( - person("ssi_earned_income", period) - - person("ssi_blind_or_disabled_working_student_exclusion", period), - 0, - ) - # A joint couple's in-kind support and maintenance is one presumed - # maximum value for the couple (ssi_pmv_amount), which SSI splits - # equally between the spouses. - ism_share = person("ssi_in_kind_support_and_maintenance", period) / 2 - unearned = person("ssi_unearned_income", period) + ism_share - ssi = person("ssi", period) - # 3.549(A)(2)(c): $20 is deducted from unearned income other than SSI, - # split between married spouses; a client whose only unearned income - # is SSI gets no unearned income disregard. - unearned_disregard = min_(p.general / 2, unearned) - # 3.549(A)(2)(f): only when the client's gross unearned income, SSI - # included, is under $20 does the difference reduce earned income. - earned_disregard = where(unearned > 0, max_(0, p.general - unearned - ssi), 0) - # 3.549(A)(1): deduct $65 from gross earned income and halve the rest. - countable_earned = max_(0, earned - p.earned - earned_disregard) * ( - 1 - p.earned_share - ) - own_countable = unearned - unearned_disregard + countable_earned - return where( - couple_computation, - own_countable, - person("ssi_countable_income", period), - ) + # 9 CCR 2503-5 3.549(H): the client's own countable income plus income + # deemed from the spouse or parents. Sponsor deeming (3.549(G)) and the + # institutionalized spouse's MMMNA (3.549(F)) are not modeled. + adds = [ + "co_state_supplement_own_countable_income", + "co_state_supplement_deemed_income", + ] diff --git a/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_deemed_income.py b/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_deemed_income.py new file mode 100644 index 00000000000..adeb1fb2f22 --- /dev/null +++ b/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_deemed_income.py @@ -0,0 +1,62 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.co.ssa.state_supplement._and_cs_client_income import ( + and_cs_client_income, +) + + +class co_state_supplement_deemed_income(Variable): + value_type = float + entity = Person + label = "Colorado State Supplement income deemed from a spouse or parents" + unit = USD + definition_period = MONTH + defined_for = "co_state_supplement_eligible" + # PDF pages 94-95 + reference = "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=94" + + def formula(person, period, parameters): + p = parameters(period).gov.states.co.ssa.state_supplement.income.disregards + # 3.549(D)(2): the spouse's unearned income is reduced by the part of + # the couple's combined $20 disregard the client did not use. + earned, _, unearned_disregard, earned_disregard = and_cs_client_income( + person, period, parameters + ) + used_on_earnings = min_(earned_disregard, max_(0, earned - p.earned)) + remaining_disregard = max_( + 0, p.unearned - unearned_disregard - used_on_earnings + ) + + # 3.549(F): no income is deemed from a spouse or parent who receives + # Adult Financial, SSI or Medicaid with income within the SSI limit. + deemable = ~person("co_state_supplement_deeming_exempt", period) + gross_earned = person("ssi_earned_income", period) * deemable + gross_unearned = person("ssi_unearned_income", period) * deemable + + def deem(earned_income, unearned_income): + # 3.549(C): deduct $65 from gross earned income and halve the rest. + # 3.549(D): deduct the remaining unearned income disregard. + # Unlike SSI (20 CFR 416.1163), there is no deeming threshold and + # no allocation for ineligible children. + return max_(0, earned_income - p.earned) * p.earned_share + max_( + 0, unearned_income - remaining_disregard + ) + + # 3.549(C)-(D): the client's spouse. + marital_unit = person.marital_unit + spouse_deemed = deem( + marital_unit.sum(gross_earned) - gross_earned, + marital_unit.sum(gross_unearned) - gross_unearned, + ) + + # 3.549(E): for an unemancipated child under 18, "the parents' income + # shall be deemed using the same calculations as a spouse". The + # parents' income is deemed in full to each child who is a client. + year = period.this_year + child = person("is_child", year) + parent = person("is_parent", year) & ~child + tax_unit = person.tax_unit + parents_deemed = child * deem( + tax_unit.sum(gross_earned * parent), + tax_unit.sum(gross_unearned * parent), + ) + return spouse_deemed + parents_deemed diff --git a/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_deeming_exempt.py b/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_deeming_exempt.py new file mode 100644 index 00000000000..d26e988c44a --- /dev/null +++ b/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_deeming_exempt.py @@ -0,0 +1,45 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.income._apply_ssi_exclusions import ( + _apply_ssi_exclusions, +) + + +class co_state_supplement_deeming_exempt(Variable): + value_type = bool + entity = Person + label = "Colorado State Supplement spouse or parent income not deemed" + definition_period = MONTH + # PDF page 95 + reference = "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=95" + + def formula(person, period, parameters): + # 9 CCR 2503-5 3.549(F): a spouse or parent who receives Adult + # Financial grant payments, SSI or Medicaid "and has income no greater + # than the SSI limit" has no income deemed to the AND-CS client. + # SSA has found an SSI recipient's income within the SSI limit (for a + # member of a couple, the couple's), so SSI receipt alone qualifies. + # Each spouse of an SSI couple is then a client on their own income, + # as 3.548(A) requires. + receives_ssi = (person("ssi", period) > 0) | person("receives_ssi", period) + p = parameters(period).gov.states.co.ssa.state_supplement.income.deeming + if not p.assistance_exemption_applies: + # Before March 2020, 3.545(F) excluded only the income from those + # benefits, which the deemed income sources do not include. + return receives_ssi + # Old Age Pension is the Adult Financial program modeled for someone + # who is not on SSI; AND-CS requires SSI. + year = period.this_year + adult_financial = person("co_oap", year) > 0 + medicaid = person("medicaid_enrolled_for_ssi_state_supplement", year) + # The rule does not define the SSI limit. We read it as SSI's income + # test for an individual: countable income, after SSI's exclusions, + # no greater than the individual federal benefit rate. + countable = _apply_ssi_exclusions( + person("ssi_earned_income", year), + person("ssi_unearned_income", year), + parameters, + year, + ) + fbr = parameters(period).gov.ssa.ssi.amount.individual + within_ssi_limit = countable <= fbr * MONTHS_IN_YEAR + return receives_ssi | ((adult_financial | medicaid) & within_ssi_limit) diff --git a/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_own_countable_income.py b/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_own_countable_income.py new file mode 100644 index 00000000000..f35360bbdd0 --- /dev/null +++ b/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_own_countable_income.py @@ -0,0 +1,29 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.co.ssa.state_supplement._and_cs_client_income import ( + and_cs_client_income, +) + + +class co_state_supplement_own_countable_income(Variable): + value_type = float + entity = Person + label = "Colorado State Supplement countable income of the client other than SSI" + unit = USD + definition_period = MONTH + defined_for = "co_state_supplement_eligible" + # PDF pages 93-94 + reference = "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=93" + + def formula(person, period, parameters): + # 9 CCR 2503-5 3.549(A) counts each AND-CS client's own income, + # whether they are single, married or a member of an SSI couple. + # The SSI that 3.549(A)(2)(d) adds back is added in co_state_supplement. + p = parameters(period).gov.states.co.ssa.state_supplement.income.disregards + earned, unearned, unearned_disregard, earned_disregard = and_cs_client_income( + person, period, parameters + ) + # 3.549(A)(1): deduct $65 from gross earned income and halve the rest. + countable_earned = ( + max_(0, earned - p.earned - earned_disregard) * p.earned_share + ) + return unearned - unearned_disregard + countable_earned From 96f5c6e2d4c9e8113cb566a561a6833779a46999 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 12 Oct 2026 14:47:46 -0400 Subject: [PATCH 07/10] Add AND-CS invariant tests; cover CO in the SSP Medicaid order test test_co_and_cs_invariants.py checks, over Hypothesis-drawn single and married clients in one simulation per example: the payment rule and its bounds, that 3.549(A) never counts less than SSI for a single client (and the same where SSI's and Colorado's disregards coincide), that the 3.549(F) Medicaid exemption only lowers countable income, monotonicity in a non-exempt spouse's earnings, and invariance to tax-unit roles. AND-CS now reads medicaid_enrolled_for_ssi_state_supplement for the 3.549(F) exemption, so Colorado joins the SSP/SNAP/Medicaid calculation order test. Co-Authored-By: Claude Opus 5.5 --- .../tests/core/test_co_and_cs_invariants.py | 225 ++++++++++++++++++ ...si_state_supplement_medicaid_dependency.py | 9 +- .../co_state_supplement_deemed_income.yaml | 37 +++ 3 files changed, 269 insertions(+), 2 deletions(-) create mode 100644 policyengine_us/tests/core/test_co_and_cs_invariants.py diff --git a/policyengine_us/tests/core/test_co_and_cs_invariants.py b/policyengine_us/tests/core/test_co_and_cs_invariants.py new file mode 100644 index 00000000000..edf5967dd77 --- /dev/null +++ b/policyengine_us/tests/core/test_co_and_cs_invariants.py @@ -0,0 +1,225 @@ +"""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 receiving SSI, 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. 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 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: more earnings for a non-exempt spouse never lower the + client's countable income or raise the supplement. +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. + "medicaid_enrolled_for_ssi_state_supplement": {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"]}, + "medicaid_enrolled_for_ssi_state_supplement": { + 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. + if ssi[client] > 0: + assert supplement[client] == 0, c + assert countable[client] >= results["ssi_countable_income"][client] - ( + 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( + results["ssi_countable_income"][client], abs=TOLERANCE + ), c + continue + a, b, c_, d = (f"client_{x}{i}" for x in ARRANGEMENTS) + # 3. The Medicaid exemption only lowers countable income. + assert countable[a] <= countable[b] + TOLERANCE, c + assert supplement[a] >= supplement[b] - TOLERANCE, c + # 4. More non-exempt spousal earnings never lower countable income. + 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]) + + +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) diff --git a/policyengine_us/tests/core/test_ssi_state_supplement_medicaid_dependency.py b/policyengine_us/tests/core/test_ssi_state_supplement_medicaid_dependency.py index a64c80a17a7..3cee861915b 100644 --- a/policyengine_us/tests/core/test_ssi_state_supplement_medicaid_dependency.py +++ b/policyengine_us/tests/core/test_ssi_state_supplement_medicaid_dependency.py @@ -36,7 +36,10 @@ def situation(year, state="IN"): @pytest.mark.parametrize("year", [2027, 2030, 2035]) -@pytest.mark.parametrize("state,benefit", [("IN", "in_ssp"), ("KS", "ks_sspp")]) +@pytest.mark.parametrize( + "state,benefit", + [("IN", "in_ssp"), ("KS", "ks_sspp"), ("CO", "co_state_supplement")], +) def test_ssp_snap_medicaid_calculation_order(year, state, benefit): """YAML cannot express repeated calculations in different orders.""" variables = (benefit, "snap", "medicaid_enrolled") @@ -54,7 +57,9 @@ def test_ssp_snap_medicaid_calculation_order(year, state, benefit): assert results[0]["snap"][1] > 0 # Kansas does not cover this childless adult through Medicaid expansion; # SNAP pass-through does not replace Medicaid's category requirement. - assert results[0]["medicaid_enrolled"].tolist() == [True, state == "IN"] + # Colorado AND-CS reads Medicaid enrollment for its deeming exemption + # (9 CCR 2503-5 3.549(F)). + assert results[0]["medicaid_enrolled"].tolist() == [True, state != "KS"] @pytest.mark.parametrize( diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_deemed_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_deemed_income.yaml index 9274c39e608..fe6e8a32623 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_deemed_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_deemed_income.yaml @@ -244,3 +244,40 @@ state_code: CO output: co_state_supplement_deemed_income: [0, 5_760] + +- name: Case 8, the couple's combined $20 is not offered again after the client applies it to earnings. + # The married client reports SSI receipt with no payment this month and has + # $10 of interest and $200 of earnings. Their $10 share covers the interest + # (3.549(A)(2)(c)(1)); gross unearned income is under $20, so the other $10 + # reduces their earnings (3.549(A)(2)(f)): (200 - 65 - 10) / 2 = 62.50. + # That leaves none of the couple's combined $20 for the spouse's $50 + # pension (3.549(D)(2)), so all $50 is deemed. + period: 2026-01 + input: + people: + head: + age: 40 + is_ssi_disabled: true + receives_ssi: true + ssi: 0 + interest_income: 120 + employment_income: 2_400 + is_tax_unit_head: true + spouse: + age: 45 + pension_income: 600 + medicaid_enrolled_for_ssi_state_supplement: false + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + co_state_supplement_own_countable_income: [62.5, 0] + co_state_supplement_deemed_income: [50, 0] From 87281e2751cb6b3480ab406d4165828008b21639 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 12 Oct 2026 20:43:28 -0400 Subject: [PATCH 08/10] Address review round 1: Medicaid order, parents, pre-2020 deeming, float floor - 3.549(F) Medicaid prong: read co_state_supplement_medicaid_enrolled, computed in a private branch with January's AND-CS set to zero. It keeps the SNAP route to Medicaid's community engagement exemption (SNAP counts AND-CS as income) and gives the same answer whatever is calculated first. medicaid_enrolled_for_ssi_state_supplement drops that route and answers from the cache when Medicaid was calculated first. - 3.549(E): deem from the parents the child's parent ids name, whatever their age; without ids, the tax unit's unlinked parents other than the child. A grandparent named only as the mother's parent no longer counts, and a 17-year-old parent now does. - Before March 2020, 3.545(F) excluded only the benefit income itself, so no spouse or parent is exempt from deeming, SSI recipients included. - The payment floor tests eligibility on the grant in cents: a single client whose 3.549(A) income equals SSI's sits exactly at the standard, and single precision left a grant of $0.00006 that the floor raised to $13 (found by the property test; YAML Case 32). - Property tests compare countable income only for clients who receive SSI, as 3.546 requires; regression test for the review's examples. Co-Authored-By: Claude Opus 5.5 --- .../deeming/assistance_exemption_applies.yaml | 8 +- .../tests/core/test_co_and_cs_invariants.py | 87 ++++++++++++------ ...si_state_supplement_medicaid_dependency.py | 58 ++++++++++++ .../state_supplement/co_state_supplement.yaml | 65 +++++++++++++- .../co_state_supplement_deemed_income.yaml | 88 ++++++++++++++++++- .../co_state_supplement_deeming_exempt.yaml | 22 +++-- .../state_supplement/co_state_supplement.py | 8 +- .../co_state_supplement_deemed_income.py | 41 +++++++-- .../co_state_supplement_deeming_exempt.py | 14 +-- .../co_state_supplement_medicaid_enrolled.py | 47 ++++++++++ 10 files changed, 383 insertions(+), 55 deletions(-) create mode 100644 policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_medicaid_enrolled.py diff --git a/policyengine_us/parameters/gov/states/co/ssa/state_supplement/income/deeming/assistance_exemption_applies.yaml b/policyengine_us/parameters/gov/states/co/ssa/state_supplement/income/deeming/assistance_exemption_applies.yaml index d14f6c4f4ad..272b404961d 100644 --- a/policyengine_us/parameters/gov/states/co/ssa/state_supplement/income/deeming/assistance_exemption_applies.yaml +++ b/policyengine_us/parameters/gov/states/co/ssa/state_supplement/income/deeming/assistance_exemption_applies.yaml @@ -1,14 +1,14 @@ -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 or Medicaid with income no greater than the SSI limit, if this is true. +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, - # not the recipient's other income. + # 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 Adult Financial and Medicaid recipients + 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 diff --git a/policyengine_us/tests/core/test_co_and_cs_invariants.py b/policyengine_us/tests/core/test_co_and_cs_invariants.py index edf5967dd77..bd6a9f6db66 100644 --- a/policyengine_us/tests/core/test_co_and_cs_invariants.py +++ b/policyengine_us/tests/core/test_co_and_cs_invariants.py @@ -7,20 +7,22 @@ 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 receiving SSI, 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. For 2026: +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 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 + 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 @@ -72,7 +74,7 @@ def build_situation(cases): "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. - "medicaid_enrolled_for_ssi_state_supplement": {YEAR: True}, + "co_state_supplement_medicaid_enrolled": {YEAR: True}, } members = [client] if c["married"]: @@ -81,9 +83,7 @@ def build_situation(cases): "age": {YEAR: c["spouse_age"]}, "employment_income": {YEAR: c["spouse_earnings"] + extra}, "pension_income": {YEAR: c["spouse_pension"]}, - "medicaid_enrolled_for_ssi_state_supplement": { - YEAR: arrangement == "A" - }, + "co_state_supplement_medicaid_enrolled": {YEAR: arrangement == "A"}, } members = [spouse, client] if arrangement == "D" else [client, spouse] head, partner = members @@ -139,23 +139,26 @@ def assert_properties(cases): client = f"client_A{i}" if not c["married"]: # 2. Singles. - if ssi[client] > 0: - assert supplement[client] == 0, c - assert countable[client] >= results["ssi_countable_income"][client] - ( - 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( - results["ssi_countable_income"][client], abs=TOLERANCE - ), c + 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. - assert countable[a] <= countable[b] + TOLERANCE, c + 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. - assert countable[c_] >= countable[b] - TOLERANCE, c + # 4. More non-exempt spousal earnings never lower countable income, + # for a client who stays one. + 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 @@ -190,6 +193,38 @@ def test_worked_example(): 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]) + + try: import hypothesis import hypothesis.strategies as st diff --git a/policyengine_us/tests/core/test_ssi_state_supplement_medicaid_dependency.py b/policyengine_us/tests/core/test_ssi_state_supplement_medicaid_dependency.py index 3cee861915b..d11859aacde 100644 --- a/policyengine_us/tests/core/test_ssi_state_supplement_medicaid_dependency.py +++ b/policyengine_us/tests/core/test_ssi_state_supplement_medicaid_dependency.py @@ -62,6 +62,64 @@ def test_ssp_snap_medicaid_calculation_order(year, state, benefit): assert results[0]["medicaid_enrolled"].tolist() == [True, state != "KS"] +def co_couple(year, snap_input): + """A disabled AND-CS client and a spouse with an $800/month pension whose + Medicaid community engagement exemption is SNAP receipt.""" + unit = {"members": ["client", "spouse"]} + spm_unit = dict(unit) + if snap_input: + spm_unit["snap"] = {year: 1_200} + return { + "people": { + "client": { + "age": {year: 45}, + "is_disabled": {year: True}, + "meets_ssi_disability_criteria": {year: True}, + }, + "spouse": { + "age": {year: 40}, + "pension_income": {year: 9_600}, + # Subject to SNAP's work requirement, so SNAP receipt passes + # through to Medicaid's community engagement requirement. + "is_snap_workfare_participant": {year: True}, + }, + }, + "tax_units": {"unit": unit}, + "marital_units": {"unit": unit}, + "families": {"unit": unit}, + "spm_units": {"unit": spm_unit}, + "households": {"unit": {**unit, "state_code": {year: "CO"}}}, + } + + +@pytest.mark.parametrize("snap_input", [False, True]) +def test_co_and_cs_spouse_medicaid_through_snap(snap_input): + """9 CCR 2503-5 3.549(F) deems nothing from a spouse enrolled in Medicaid + with income within the SSI limit. Here enrollment rests on SNAP receipt + (42 CFR 435.554(c)(7)), and SNAP counts AND-CS as income. The exemption, + and so the supplement, must not depend on which of AND-CS, SNAP and + Medicaid is calculated first.""" + year = 2030 + variables = ("co_state_supplement", "snap", "medicaid_enrolled") + results = [] + for order in (variables, variables[::-1]): + simulation = Simulation(situation=co_couple(year, snap_input)) + result = {name: simulation.calculate(name, year) for name in order} + result["exempt"] = simulation.calculate( + "co_state_supplement_deeming_exempt", f"{year}-01" + ) + results.append(result) + assert not any( + "co_state_supplement_medicaid" in name for name in simulation.branches + ) + for name in results[0]: + np.testing.assert_array_equal(results[0][name], results[1][name]) + assert results[0]["medicaid_enrolled"].tolist() == [True, True] + assert results[0]["exempt"].tolist() == [True, True] + # SSI deems the spouse's pension; AND-CS does not, so the client is paid. + assert results[0]["co_state_supplement"][0] > 0 + + @pytest.mark.parametrize( "variable", ["medicaid_enrolled", "is_medicaid_eligible", "takes_up_medicaid_if_eligible"], diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement.yaml index 4d448dc66f3..35c11d8a0ef 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement.yaml @@ -521,7 +521,7 @@ spouse: age: 42 employment_income: 18_000 - medicaid_enrolled_for_ssi_state_supplement: true + co_state_supplement_medicaid_enrolled: true is_tax_unit_spouse: true tax_units: tax_unit: @@ -553,7 +553,7 @@ spouse: age: 42 employment_income: 18_000 - medicaid_enrolled_for_ssi_state_supplement: false + co_state_supplement_medicaid_enrolled: false is_tax_unit_spouse: true tax_units: tax_unit: @@ -569,3 +569,64 @@ ssi: [9_402, 0] co_state_supplement_deemed_income: [8_610, 0] co_state_supplement: [0, 0] + +- name: Case 31, in 2019 the members of an SSI couple deemed each other's pensions. + # The spouse has a $100/month pension. Federal SSI: couple countable income + # 100 - 20 = 80; couple payment 1,157 - 80 = 1,077, or 538.50 each. Before + # March 2020, 3.545(F) excluded only the SSI itself, so 3.545(C) deems the + # spouse's pension to the head, less the $20 the head (SSI only) did not + # use: 100 - 20 = 80. Head grant 771 - 538.50 - 80 = 152.50/month + # (1,830/year). The spouse counts 100 - 10 = 90 of their own and nothing + # from the head: 771 - 538.50 - 90 = 142.50/month (1,710/year). + period: 2019 + input: + people: + head: + age: 50 + is_disabled: true + meets_ssi_disability_criteria: true + is_tax_unit_head: true + spouse: + age: 52 + is_disabled: true + meets_ssi_disability_criteria: true + pension_income: 1_200 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: CO + output: + ssi: [6_462, 6_462] + co_state_supplement_deemed_income: [960, 0] + co_state_supplement: [1_830, 1_710] + +- name: Case 32, a single client whose income sits exactly at the standard gets no floor payment. + # A $7,174/year pension (597.83/month) and outside shelter support, counted + # at SSI's presumed maximum value of 351.33 (994 / 3 + 20). SSI counts + # 597.83 + 351.33 - 20 = 929.17 and pays 994 - 929.17 = 64.83. 3.549(A) + # counts the same 929.17, so total countable income is exactly the $994 + # standard and the client is not eligible (3.520.781(A)). Found by the + # property test: single precision left a grant of $0.00006, which the floor + # raised to $13. + period: 2026 + input: + people: + person: + age: 18 + is_disabled: true + meets_ssi_disability_criteria: true + pension_income: 7_174 + ssi_receives_outside_shelter_support: true + households: + household: + members: [person] + state_code: CO + output: + co_state_supplement: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_deemed_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_deemed_income.yaml index fe6e8a32623..cb8b0a17ddb 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_deemed_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_deemed_income.yaml @@ -94,7 +94,7 @@ members: [head, spouse] state_code: CO output: - medicaid_enrolled_for_ssi_state_supplement: [true, true] + co_state_supplement_medicaid_enrolled: [true, true] co_state_supplement_deeming_exempt: [true, true] co_state_supplement_deemed_income: [0, 0] @@ -183,7 +183,7 @@ age: 40 employment_income: 24_000 own_children_in_household: 1 - medicaid_enrolled_for_ssi_state_supplement: true + co_state_supplement_medicaid_enrolled: true is_tax_unit_head: true parent2: age: 38 @@ -266,7 +266,7 @@ spouse: age: 45 pension_income: 600 - medicaid_enrolled_for_ssi_state_supplement: false + co_state_supplement_medicaid_enrolled: false is_tax_unit_spouse: true tax_units: tax_unit: @@ -281,3 +281,85 @@ output: co_state_supplement_own_countable_income: [62.5, 0] co_state_supplement_deemed_income: [50, 0] + +- name: Case 9, a grandparent's income is not deemed to a grandchild. + # The infant's parent id names the mother, who has no income; the + # grandfather (named as the mother's parent) earns $865/month. 3.549(E) + # deems "the child's parents" income, so nothing is deemed: grant + # 994 - 500 = 494/month, or 5,928/year. + period: 2026 + input: + people: + grandfather: + age: 50 + person_id: 1 + employment_income: 10_380 + takes_up_medicaid_if_eligible: false + is_tax_unit_head: true + mother: + age: 20 + person_id: 2 + parent_1_id: 1 + takes_up_medicaid_if_eligible: false + is_tax_unit_dependent: true + infant: + age: 1 + person_id: 3 + parent_1_id: 2 + is_disabled: true + meets_ssi_disability_criteria: true + ssi: 6_000 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [grandfather, mother, infant] + marital_units: + grandfather: + members: [grandfather] + mother: + members: [mother] + infant: + members: [infant] + households: + household: + members: [grandfather, mother, infant] + state_code: CO + output: + co_state_supplement_deemed_income: [0, 0, 0] + co_state_supplement: [0, 0, 5_928] + +- name: Case 10, a parent under 18 has income deemed to their child. + # A 17-year-old earns $865/month and does not take up Medicaid; their + # disabled infant receives $500/month of SSI. 3.549(E)'s age limit applies + # to the client, not the parent: (865 - 65) / 2 = 400/month deemed, or + # 4,800/year; grant 994 - 500 - 400 = 94/month, or 1,128/year. + period: 2026 + input: + people: + parent: + age: 17 + employment_income: 10_380 + own_children_in_household: 1 + takes_up_medicaid_if_eligible: false + is_tax_unit_head: true + infant: + age: 1 + is_disabled: true + meets_ssi_disability_criteria: true + ssi: 6_000 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [parent, infant] + marital_units: + parent: + members: [parent] + infant: + members: [infant] + households: + household: + members: [parent, infant] + state_code: CO + output: + co_state_supplement_deemed_income: [0, 4_800] + co_state_supplement: [0, 1_128] diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_deeming_exempt.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_deeming_exempt.yaml index 685b2ba0561..217653629a3 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_deeming_exempt.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/ssa/state_supplement/co_state_supplement_deeming_exempt.yaml @@ -19,7 +19,7 @@ - name: Case 3, a Medicaid enrollee with no income is exempt. period: 2026-01 input: - medicaid_enrolled_for_ssi_state_supplement: true + co_state_supplement_medicaid_enrolled: true state_code: CO output: co_state_supplement_deeming_exempt: true @@ -37,7 +37,7 @@ # 994, no greater than the $994 individual benefit rate. period: 2026-01 input: - medicaid_enrolled_for_ssi_state_supplement: true + co_state_supplement_medicaid_enrolled: true employment_income: 24_876 state_code: CO output: @@ -47,7 +47,7 @@ # Earnings of $2,074/month: (2,074 - 85) / 2 = 994.50 exceeds $994. period: 2026-01 input: - medicaid_enrolled_for_ssi_state_supplement: true + co_state_supplement_medicaid_enrolled: true employment_income: 24_888 state_code: CO output: @@ -56,7 +56,7 @@ - name: Case 7, someone on none of the programs is not exempt. period: 2026-01 input: - medicaid_enrolled_for_ssi_state_supplement: false + co_state_supplement_medicaid_enrolled: false co_oap: 0 ssi: 0 state_code: CO @@ -68,7 +68,7 @@ # benefits; 3.549(F) took effect March 1, 2020. period: 2020-02 input: - medicaid_enrolled_for_ssi_state_supplement: true + co_state_supplement_medicaid_enrolled: true state_code: CO output: co_state_supplement_deeming_exempt: false @@ -76,7 +76,17 @@ - name: Case 9, from March 2020 a Medicaid enrollee within the SSI limit is exempt. period: 2020-03 input: - medicaid_enrolled_for_ssi_state_supplement: true + co_state_supplement_medicaid_enrolled: true state_code: CO output: co_state_supplement_deeming_exempt: true + +- name: Case 10, before March 2020 an SSI recipient's other income was deemed. + # 3.545(F) excluded only the SSI itself, which is not a deemed income + # source. + period: 2020-02 + input: + ssi: 500 + state_code: CO + output: + co_state_supplement_deeming_exempt: false diff --git a/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement.py b/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement.py index 6ef4a4cf1a3..beb6452a0f1 100644 --- a/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement.py +++ b/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement.py @@ -25,8 +25,12 @@ def formula(person, period, parameters): # exceeds the grant standard is not eligible. Since 2012 the standard # has equaled the SSI benefit standard for an individual, so this # also excludes those "receiving the full SSI benefit standard" - # (3.546). + # (3.546). A single client whose 3.549(A) income equals SSI's + # countable income has SSI plus that income exactly at the standard, + # so the test is on the grant in cents: single-precision arithmetic + # must not turn a $0 grant into a positive one. + eligible = np.round(grant, 2) > 0 # 3.546(C)(2), (E), 3.549(B): an eligible client's grant below the # payment floor standard is raised to it by a state supplementary # payment. - return where(grant > 0, max_(grant, p.payment_floor), 0) + return where(eligible, max_(grant, p.payment_floor), 0) diff --git a/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_deemed_income.py b/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_deemed_income.py index adeb1fb2f22..610f1da6d35 100644 --- a/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_deemed_income.py +++ b/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_deemed_income.py @@ -2,6 +2,11 @@ from policyengine_us.variables.gov.states.co.ssa.state_supplement._and_cs_client_income import ( and_cs_client_income, ) +from policyengine_us.variables.household.demographic.person._parent_links import ( + co_resident_parent_indices, + has_parent_ids, + unlinked_parent, +) class co_state_supplement_deemed_income(Variable): @@ -50,13 +55,37 @@ def deem(earned_income, unearned_income): # 3.549(E): for an unemancipated child under 18, "the parents' income # shall be deemed using the same calculations as a spouse". The - # parents' income is deemed in full to each child who is a client. + # parents' combined income is deemed in full to each child who is a + # client. The parents are those the child's parent ids name in the + # household, whatever their age; without ids, the tax unit's other + # members who are parents of a child no id names, as the parent-link + # contract requires (this cannot tell a grandparent from a parent). year = period.this_year - child = person("is_child", year) - parent = person("is_parent", year) & ~child + first, second = co_resident_parent_indices(person, year) + + def from_named_parents(values): + return sum( + np.where(rows >= 0, values[np.maximum(rows, 0)], 0) + for rows in (first, second) + ) + + parent = unlinked_parent(person, year) tax_unit = person.tax_unit - parents_deemed = child * deem( - tax_unit.sum(gross_earned * parent), - tax_unit.sum(gross_unearned * parent), + + def from_tax_unit_parents(values): + return tax_unit.sum(values * parent) - values * parent + + linked = has_parent_ids(person, year) + parents_deemed = person("is_child", year) * deem( + where( + linked, + from_named_parents(gross_earned), + from_tax_unit_parents(gross_earned), + ), + where( + linked, + from_named_parents(gross_unearned), + from_tax_unit_parents(gross_unearned), + ), ) return spouse_deemed + parents_deemed diff --git a/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_deeming_exempt.py b/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_deeming_exempt.py index d26e988c44a..42d16dde285 100644 --- a/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_deeming_exempt.py +++ b/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_deeming_exempt.py @@ -13,6 +13,13 @@ class co_state_supplement_deeming_exempt(Variable): reference = "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=95" def formula(person, period, parameters): + p = parameters(period).gov.states.co.ssa.state_supplement.income.deeming + if not p.assistance_exemption_applies: + # Before March 2020, 3.545(F) excluded only "the income from those + # benefits" (public assistance, SSI or medical assistance), which + # the deemed income sources do not include. The recipient's other + # income was deemed. + return np.zeros(person.count, dtype=bool) # 9 CCR 2503-5 3.549(F): a spouse or parent who receives Adult # Financial grant payments, SSI or Medicaid "and has income no greater # than the SSI limit" has no income deemed to the AND-CS client. @@ -21,16 +28,11 @@ def formula(person, period, parameters): # Each spouse of an SSI couple is then a client on their own income, # as 3.548(A) requires. receives_ssi = (person("ssi", period) > 0) | person("receives_ssi", period) - p = parameters(period).gov.states.co.ssa.state_supplement.income.deeming - if not p.assistance_exemption_applies: - # Before March 2020, 3.545(F) excluded only the income from those - # benefits, which the deemed income sources do not include. - return receives_ssi # Old Age Pension is the Adult Financial program modeled for someone # who is not on SSI; AND-CS requires SSI. year = period.this_year adult_financial = person("co_oap", year) > 0 - medicaid = person("medicaid_enrolled_for_ssi_state_supplement", year) + medicaid = person("co_state_supplement_medicaid_enrolled", year) # The rule does not define the SSI limit. We read it as SSI's income # test for an individual: countable income, after SSI's exclusions, # no greater than the individual federal benefit rate. diff --git a/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_medicaid_enrolled.py b/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_medicaid_enrolled.py new file mode 100644 index 00000000000..39d74804d27 --- /dev/null +++ b/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_medicaid_enrolled.py @@ -0,0 +1,47 @@ +from policyengine_us.model_api import * + + +class co_state_supplement_medicaid_enrolled(Variable): + value_type = bool + entity = Person + label = "Medicaid enrollment for the Colorado State Supplement deeming exemption" + definition_period = YEAR + # PDF page 95 + reference = "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=9+CCR+2503-5&ruleVersionId=12428#page=95" + documentation = ( + "Medicaid enrollment of an AND-CS client's spouse or parent, for the " + "9 CCR 2503-5 3.549(F) deeming exemption. Under the Medicaid community " + "engagement requirement, enrollment can turn on membership of a " + "household receiving SNAP (42 CFR 435.554(c)(7)), and SNAP counts " + "AND-CS as income, so enrollment is computed with that year's January " + "AND-CS, the month the requirement reads SNAP, set to zero." + ) + + def formula(person, period, parameters): + p = parameters(period).gov.hhs.medicaid.eligibility.work_requirements + if not p.applies: + return person("medicaid_enrolled", period) + # A private branch keeps the SNAP route to enrollment and gives the + # same result whichever of SNAP, Medicaid and AND-CS is calculated + # first. get_override_branch drops inherited values when the parent + # already holds January's AND-CS, so no enrollment computed with it + # carries over. + simulation = person.simulation + branch_name = f"{simulation.branch_name}_co_state_supplement_medicaid_{period}" + try: + branch = get_override_branch( + simulation, + branch_name, + period, + { + "co_state_supplement": ( + period.first_month, + np.zeros(person.count), + ) + }, + ) + return branch.calculate("medicaid_enrolled", period) + finally: + # A branch clones cached arrays; retaining it would duplicate a + # substantial part of a full-population simulation in memory. + simulation.branches.pop(branch_name, None) From 5d5e92a9d083d01039d7af1cea55ac8127de6839 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 12 Oct 2026 20:46:23 -0400 Subject: [PATCH 09/10] Override the AND-CS Medicaid variable itself in its branch co_state_supplement_medicaid_enrolled now computes medicaid_enrolled in a branch where it is itself false for everyone, rather than one where January's AND-CS is zero. AND-CS in the branch deems every Medicaid spouse's or parent's income, so it is no higher than its final value and SNAP is no lower: one step from the final fixed point, from the conservative side. The parent never holds this variable before computing it, so get_override_branch always shares the parent's cache. This matches co_oap_medicaid_enrolled in #10176, so the two can merge into one Adult Financial variable. Co-Authored-By: Claude Opus 5.5 --- .../co_state_supplement_medicaid_enrolled.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_medicaid_enrolled.py b/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_medicaid_enrolled.py index 39d74804d27..0a7952db0f1 100644 --- a/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_medicaid_enrolled.py +++ b/policyengine_us/variables/gov/states/co/ssa/state_supplement/co_state_supplement_medicaid_enrolled.py @@ -13,19 +13,21 @@ class co_state_supplement_medicaid_enrolled(Variable): "9 CCR 2503-5 3.549(F) deeming exemption. Under the Medicaid community " "engagement requirement, enrollment can turn on membership of a " "household receiving SNAP (42 CFR 435.554(c)(7)), and SNAP counts " - "AND-CS as income, so enrollment is computed with that year's January " - "AND-CS, the month the requirement reads SNAP, set to zero." + "AND-CS as income. Enrollment is therefore computed in a private " + "branch where this variable is false for everyone, so AND-CS deems " + "every Medicaid spouse's or parent's income and is no higher than " + "its final value, and SNAP is no lower." ) def formula(person, period, parameters): p = parameters(period).gov.hhs.medicaid.eligibility.work_requirements if not p.applies: return person("medicaid_enrolled", period) - # A private branch keeps the SNAP route to enrollment and gives the - # same result whichever of SNAP, Medicaid and AND-CS is calculated - # first. get_override_branch drops inherited values when the parent - # already holds January's AND-CS, so no enrollment computed with it - # carries over. + # The branch keeps the SNAP route to enrollment, and AND-CS inside it + # reads this variable as an input, so nothing recurses. The parent + # cannot hold this variable before computing it, so the branch + # always shares the parent's cache, and the result does not depend + # on whether SNAP, Medicaid or AND-CS is calculated first. simulation = person.simulation branch_name = f"{simulation.branch_name}_co_state_supplement_medicaid_{period}" try: @@ -34,9 +36,8 @@ def formula(person, period, parameters): branch_name, period, { - "co_state_supplement": ( - period.first_month, - np.zeros(person.count), + "co_state_supplement_medicaid_enrolled": np.zeros( + person.count, dtype=bool ) }, ) From aaa00d545d6cdb56ac9e218f13de9fed2c27c8c3 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 12 Oct 2026 20:55:38 -0400 Subject: [PATCH 10/10] Scope the spousal-earnings invariant to clients without in-kind support 3.520.79(B)(6) charges an AND-CS client the ISM SSA charges, and SSA's presumed maximum value halves once spousal deeming starts. Spousal earnings that cross SSI's deeming threshold therefore lower the client's ISM by about as much as the deemed earnings rise, and countable income can fall by cents. Found by Hypothesis in the ISM session (#10183); pinned by test_ism_basis_switch_at_ssi_deeming_threshold. Co-Authored-By: Claude Opus 5.5 --- .../tests/core/test_co_and_cs_invariants.py | 43 ++++++++++++++++--- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/policyengine_us/tests/core/test_co_and_cs_invariants.py b/policyengine_us/tests/core/test_co_and_cs_invariants.py index bd6a9f6db66..db181d60570 100644 --- a/policyengine_us/tests/core/test_co_and_cs_invariants.py +++ b/policyengine_us/tests/core/test_co_and_cs_invariants.py @@ -27,8 +27,12 @@ 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: more earnings for a non-exempt spouse never lower the - client's countable income or 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. @@ -156,10 +160,11 @@ def assert_properties(cases): 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. - if eligible[b] and eligible[c_]: - assert countable[c_] >= countable[b] - TOLERANCE, c - assert supplement[c_] <= supplement[b] + TOLERANCE, c + # 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 @@ -225,6 +230,32 @@ def test_clients_who_leave_ssi(): 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