From e925852f98d214217c09a6ae8b5c4f7bb85aacd3 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 12 Oct 2026 14:22:16 -0400 Subject: [PATCH 1/5] 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 2/5] 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 3/5] 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 4/5] 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 5/5] 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