From 35209302a6a74a0de14ce7321a0a237df6be0043 Mon Sep 17 00:00:00 2001 From: Ziming Date: Tue, 5 May 2026 11:38:03 -0400 Subject: [PATCH 1/3] Initialize Montana SSP branch From 12ddbef27964002847abcb2be5ba87547b7d93d3 Mon Sep 17 00:00:00 2001 From: Ziming Date: Tue, 5 May 2026 11:38:20 -0400 Subject: [PATCH 2/3] Add changelog fragment for Montana SSP --- changelog.d/mt-ssp.added.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/mt-ssp.added.md diff --git a/changelog.d/mt-ssp.added.md b/changelog.d/mt-ssp.added.md new file mode 100644 index 00000000000..b548e325ee7 --- /dev/null +++ b/changelog.d/mt-ssp.added.md @@ -0,0 +1 @@ +Implemented Montana State Supplementary Payment (SSP). From a371d6a47e354b66a93b53f87e1b233202e5cd65 Mon Sep 17 00:00:00 2001 From: Ziming Date: Tue, 5 May 2026 12:29:48 -0400 Subject: [PATCH 3/3] Implement Montana State Supplementation (SSP) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #8232 Implements Montana's Optional State Supplementation under ARM 37.43.101–104 and MCA 52-1-104. Covers aged/blind/disabled SSI recipients in qualifying living arrangements (personal care / group home / community home, foster care, transitional living). Rates frozen since 1989. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../household/household_state_benefits.yaml | 4 + .../states/mt/dphhs/ssp/amount/couple.yaml | 25 ++ .../mt/dphhs/ssp/amount/individual.yaml | 25 ++ policyengine_us/programs.yaml | 6 + .../gov/states/mt/dphhs/ssp/edge_cases.yaml | 179 ++++++++ .../gov/states/mt/dphhs/ssp/integration.yaml | 386 ++++++++++++++++++ .../dphhs/ssp/mt_ssp_couple_per_spouse.yaml | 144 +++++++ .../states/mt/dphhs/ssp/mt_ssp_eligible.yaml | 76 ++++ .../mt/dphhs/ssp/mt_ssp_individual.yaml | 80 ++++ .../gov/states/mt/dphhs/ssp/mt_ssp.py | 34 ++ .../mt/dphhs/ssp/mt_ssp_couple_per_spouse.py | 27 ++ .../states/mt/dphhs/ssp/mt_ssp_eligible.py | 25 ++ .../states/mt/dphhs/ssp/mt_ssp_individual.py | 19 + .../mt/dphhs/ssp/mt_ssp_payment_category.py | 25 ++ .../income/spm_unit/spm_unit_benefits.py | 1 + 15 files changed, 1056 insertions(+) create mode 100644 policyengine_us/parameters/gov/states/mt/dphhs/ssp/amount/couple.yaml create mode 100644 policyengine_us/parameters/gov/states/mt/dphhs/ssp/amount/individual.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ssp/edge_cases.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ssp/integration.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ssp/mt_ssp_couple_per_spouse.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ssp/mt_ssp_eligible.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ssp/mt_ssp_individual.yaml create mode 100644 policyengine_us/variables/gov/states/mt/dphhs/ssp/mt_ssp.py create mode 100644 policyengine_us/variables/gov/states/mt/dphhs/ssp/mt_ssp_couple_per_spouse.py create mode 100644 policyengine_us/variables/gov/states/mt/dphhs/ssp/mt_ssp_eligible.py create mode 100644 policyengine_us/variables/gov/states/mt/dphhs/ssp/mt_ssp_individual.py create mode 100644 policyengine_us/variables/gov/states/mt/dphhs/ssp/mt_ssp_payment_category.py diff --git a/policyengine_us/parameters/gov/household/household_state_benefits.yaml b/policyengine_us/parameters/gov/household/household_state_benefits.yaml index b74241a36b5..32dd5fe0a6a 100644 --- a/policyengine_us/parameters/gov/household/household_state_benefits.yaml +++ b/policyengine_us/parameters/gov/household/household_state_benefits.yaml @@ -16,6 +16,8 @@ values: - ma_state_supplement # Michigan benefits - mi_ssp + # Montana benefits + - mt_ssp # Colorado benefits - co_state_supplement - co_oap @@ -83,6 +85,8 @@ values: - ma_state_supplement # Michigan benefits - mi_ssp + # Montana benefits + - mt_ssp # Colorado benefits - co_state_supplement - co_oap diff --git a/policyengine_us/parameters/gov/states/mt/dphhs/ssp/amount/couple.yaml b/policyengine_us/parameters/gov/states/mt/dphhs/ssp/amount/couple.yaml new file mode 100644 index 00000000000..eda21356125 --- /dev/null +++ b/policyengine_us/parameters/gov/states/mt/dphhs/ssp/amount/couple.yaml @@ -0,0 +1,25 @@ +description: Montana provides this amount as the couple state supplement payment under the State Supplementation program. + +metadata: + unit: currency-USD + period: month + label: Montana SSP couple payment amount + breakdown: + - mt_ssp_payment_category + reference: + - title: SSA POMS SI DEN01415.010, Montana State Supplementary Payments + href: https://secure.ssa.gov/poms.nsf/lnx/0501415010DEN + - title: SSA State Assistance Programs for SSI Recipients (January 2011), Montana, Table 1 + href: https://www.ssa.gov/policy/docs/progdesc/ssi_st_asst/2011/mt.html + +# Couple totals (not per-spouse). ARM 37.43.104 lists individual rates +# only; couple rates are published only in SSA POMS and the SSA 2011 +# report (couple = 2x individual + $5). Frozen since January 1989. +ASSISTED_LIVING_OR_GROUP_OR_COMMUNITY_HOME: + 1989-01-01: 193 +FOSTER_CARE: + 1989-01-01: 110.50 +TRANSITIONAL_LIVING: + 1989-01-01: 57 +NONE: + 1989-01-01: 0 diff --git a/policyengine_us/parameters/gov/states/mt/dphhs/ssp/amount/individual.yaml b/policyengine_us/parameters/gov/states/mt/dphhs/ssp/amount/individual.yaml new file mode 100644 index 00000000000..0ea74104232 --- /dev/null +++ b/policyengine_us/parameters/gov/states/mt/dphhs/ssp/amount/individual.yaml @@ -0,0 +1,25 @@ +description: Montana provides this amount as the individual state supplement payment under the State Supplementation program. + +metadata: + unit: currency-USD + period: month + label: Montana SSP individual payment amount + breakdown: + - mt_ssp_payment_category + reference: + - title: ARM 37.43.104(1), Payment Standards + href: https://www.law.cornell.edu/regulations/montana/ARM-37-43-104 + - title: SSA POMS SI DEN01415.010, Montana State Supplementary Payments + href: https://secure.ssa.gov/poms.nsf/lnx/0501415010DEN + - title: SSA State Assistance Programs for SSI Recipients (January 2011), Montana, Table 1 + href: https://www.ssa.gov/policy/docs/progdesc/ssi_st_asst/2011/mt.html + +# Rates frozen since January 1989 per SSA POMS SI DEN01415.010. +ASSISTED_LIVING_OR_GROUP_OR_COMMUNITY_HOME: + 1989-01-01: 94 +FOSTER_CARE: + 1989-01-01: 52.75 +TRANSITIONAL_LIVING: + 1989-01-01: 26 +NONE: + 1989-01-01: 0 diff --git a/policyengine_us/programs.yaml b/policyengine_us/programs.yaml index 76b5377b5f3..2078ef03fec 100644 --- a/policyengine_us/programs.yaml +++ b/policyengine_us/programs.yaml @@ -707,6 +707,12 @@ programs: full_name: Michigan State Supplementary Payment variable: mi_ssp parameter_prefix: gov.states.mi.mdhhs.ssp + - state: MT + status: complete + name: Montana SSP + full_name: Montana State Supplementation + variable: mt_ssp + parameter_prefix: gov.states.mt.dphhs.ssp - state: ME status: complete name: Maine SSP diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ssp/edge_cases.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ssp/edge_cases.yaml new file mode 100644 index 00000000000..b5ae45614b4 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ssp/edge_cases.yaml @@ -0,0 +1,179 @@ +- name: Case 1, uncapped_ssi exactly $0 (boundary, strict greater-than) is ineligible. + absolute_error_margin: 0.01 + period: 2024-01 + input: + people: + person1: + # Boundary: uncapped_ssi == 0 fails the strict (uncapped_ssi > 0) gate. + uncapped_ssi: 0 + mt_ssp_payment_category: ASSISTED_LIVING_OR_GROUP_OR_COMMUNITY_HOME + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + state_code: MT + output: + mt_ssp_eligible: false + mt_ssp: 0 + +- name: Case 2, uncapped_ssi exactly $0.01 (smallest positive value) is eligible. + absolute_error_margin: 0.01 + period: 2024-01 + input: + people: + person1: + # Annual $0.01 -> monthly ~$0.000833 in the MONTH formula; still > 0 + # so eligibility passes. Spillover deduction = max_(0, -uncapped_ssi) + # = 0 since uncapped_ssi is positive, so full individual rate applies. + uncapped_ssi: 0.01 + mt_ssp_payment_category: TRANSITIONAL_LIVING + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + state_code: MT + output: + # K tier individual = $26 - $0 spillover = $26. + mt_ssp_eligible: true + mt_ssp: 26 + +- name: Case 3, federal SSI payment is $0 but uncapped_ssi positive (income just below FBR) is eligible. + absolute_error_margin: 0.01 + period: 2024-01 + input: + people: + person1: + # Group 2 path: categorically eligible, federal SSI takeup zeros the + # actual payment, uncapped_ssi remains positive (income just below the + # federal SSI cutoff). Distinct from Case 10 of integration.yaml: this + # case explicitly verifies the (ssi == 0, uncapped_ssi > 0) boundary. + is_ssi_eligible: true + ssi: 0 + uncapped_ssi: 12 + mt_ssp_payment_category: ASSISTED_LIVING_OR_GROUP_OR_COMMUNITY_HOME + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + state_code: MT + output: + # uncapped_ssi annual $12 -> monthly $1; spillover = max_(0, -1) = 0; + # G/H/I tier = $94 - $0 = $94. + mt_ssp_eligible: true + mt_ssp: 94 + +- name: Case 4, single ineligible person (NONE category) returns zero benefit. + absolute_error_margin: 0.01 + period: 2024-01 + input: + people: + person1: + is_ssi_eligible: true + mt_ssp_payment_category: NONE + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + state_code: MT + output: + # NONE category fails the qualifying-arrangement gate. + mt_ssp_eligible: false + mt_ssp: 0 + +- name: Case 5, three eligible persons in same SPM unit (parent, adult child, grandparent) sums correctly. + absolute_error_margin: 0.01 + period: 2024-01 + input: + people: + person1: + # Aged grandparent in personal care facility. + age: 80 + is_ssi_eligible: true + is_ssi_aged_blind_disabled: true + mt_ssp_payment_category: ASSISTED_LIVING_OR_GROUP_OR_COMMUNITY_HOME + person2: + # Disabled parent in foster care. + age: 50 + is_ssi_eligible: true + is_ssi_aged_blind_disabled: true + mt_ssp_payment_category: FOSTER_CARE + person3: + # Disabled adult child in transitional living. + age: 28 + is_ssi_eligible: true + is_ssi_aged_blind_disabled: true + mt_ssp_payment_category: TRANSITIONAL_LIVING + marital_units: + marital_unit1: + members: [person1] + marital_unit2: + members: [person2] + marital_unit3: + members: [person3] + spm_units: + spm_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: MT + output: + # Three separate marital units -> each gets individual rate. + # person1: G/H/I = $94; person2: J = $52.75; person3: K = $26. + # SPM total = $94 + $52.75 + $26 = $172.75. + mt_ssp_eligible: [true, true, true] + mt_ssp: 172.75 + +- name: Case 6, January period for a person who would move state mid-year still uses MT inputs. + absolute_error_margin: 0.01 + period: 2024-01 + input: + people: + person1: + # Period format restriction: only 2024-01 or 2024 are supported, so we + # cannot model a mid-year move directly. This case documents that for + # the January snapshot, the state_code: MT input controls the result. + is_ssi_eligible: true + mt_ssp_payment_category: FOSTER_CARE + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + state_code: MT + output: + # January snapshot with MT state code -> J tier individual = $52.75. + mt_ssp_eligible: true + mt_ssp: 52.75 + +- name: Case 7, high earnings make person SSI-ineligible (uncapped_ssi=0) so MT SSP is zero. + absolute_error_margin: 0.01 + period: 2024-01 + input: + people: + person1: + # High employment income -> not SSI-eligible at all -> uncapped_ssi=0 + # (uncapped_ssi has defined_for="is_ssi_eligible"). The boundary case + # demonstrates that when SSI eligibility itself fails, MT SSP correctly + # returns zero rather than inflating from the spillover deduction. + employment_income: 60_000 + mt_ssp_payment_category: ASSISTED_LIVING_OR_GROUP_OR_COMMUNITY_HOME + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + state_code: MT + output: + mt_ssp_eligible: false + mt_ssp: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ssp/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ssp/integration.yaml new file mode 100644 index 00000000000..5a81d50f129 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ssp/integration.yaml @@ -0,0 +1,386 @@ +- name: Case 1, single eligible person in personal care facility receives $94. + absolute_error_margin: 0.01 + period: 2024-01 + input: + people: + person1: + is_ssi_eligible: true + mt_ssp_payment_category: ASSISTED_LIVING_OR_GROUP_OR_COMMUNITY_HOME + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + state_code: MT + output: + # Individual rate G/H/I tier = $94 - $0 income offset = $94. + mt_ssp_eligible: true + mt_ssp: 94 + +- name: Case 2, single eligible person in foster care receives $52.75. + absolute_error_margin: 0.01 + period: 2024-01 + input: + people: + person1: + is_ssi_eligible: true + mt_ssp_payment_category: FOSTER_CARE + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + state_code: MT + output: + # Individual rate J tier = $52.75 - $0 income offset = $52.75. + mt_ssp_eligible: true + mt_ssp: 52.75 + +- name: Case 3, single eligible person in transitional living receives $26. + absolute_error_margin: 0.01 + period: 2024-01 + input: + people: + person1: + is_ssi_eligible: true + mt_ssp_payment_category: TRANSITIONAL_LIVING + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + state_code: MT + output: + # Individual rate K tier = $26 - $0 income offset = $26. + mt_ssp_eligible: true + mt_ssp: 26 + +- name: Case 4, joint couple both in ASSISTED_LIVING get $193 total ($96.50 each). + absolute_error_margin: 0.01 + period: 2024-01 + input: + people: + person1: + # Couple split federal SSI: $11,316 individual FBR -> use joint + # total $1,415/mo = $16,980/yr -> half is $8,490 each. + ssi: 8_490 + is_ssi_aged_blind_disabled: true + is_tax_unit_head_or_spouse: true + ssi_claim_is_joint: true + mt_ssp_payment_category: ASSISTED_LIVING_OR_GROUP_OR_COMMUNITY_HOME + person2: + ssi: 8_490 + is_ssi_aged_blind_disabled: true + is_tax_unit_head_or_spouse: true + ssi_claim_is_joint: true + mt_ssp_payment_category: ASSISTED_LIVING_OR_GROUP_OR_COMMUNITY_HOME + marital_units: + marital_unit: + members: [person1, person2] + tax_units: + tax_unit: + members: [person1, person2] + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # Couple gate met -> each spouse $193 / 2 = $96.50; SPM total $193. + mt_ssp_eligible: [true, true] + mt_ssp: 193 + +- name: Case 5, joint couple in different categories fall back to individual rates. + absolute_error_margin: 0.01 + period: 2024-01 + input: + people: + person1: + ssi: 8_490 + is_ssi_aged_blind_disabled: true + is_tax_unit_head_or_spouse: true + ssi_claim_is_joint: true + mt_ssp_payment_category: ASSISTED_LIVING_OR_GROUP_OR_COMMUNITY_HOME + person2: + ssi: 8_490 + is_ssi_aged_blind_disabled: true + is_tax_unit_head_or_spouse: true + ssi_claim_is_joint: true + mt_ssp_payment_category: FOSTER_CARE + marital_units: + marital_unit: + members: [person1, person2] + tax_units: + tax_unit: + members: [person1, person2] + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # Asymmetric categories -> couple gate fails. + # person1: G/H/I individual = $94; person2: J individual = $52.75. + # SPM total = $94 + $52.75 = $146.75. + mt_ssp_eligible: [true, true] + mt_ssp: 146.75 + +- name: Case 6, joint couple with only one eligible falls back to individual rate. + absolute_error_margin: 0.01 + period: 2024-01 + input: + people: + person1: + is_ssi_eligible: true + is_tax_unit_head_or_spouse: true + ssi_claim_is_joint: true + mt_ssp_payment_category: FOSTER_CARE + person2: + # Spouse not aged/blind/disabled -> not SSI-eligible. + is_ssi_eligible: false + is_tax_unit_head_or_spouse: true + ssi_claim_is_joint: true + mt_ssp_payment_category: FOSTER_CARE + marital_units: + marital_unit: + members: [person1, person2] + tax_units: + tax_unit: + members: [person1, person2] + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # Only person1 is MT-SSP-eligible -> couple gate fails. + # person1 gets J individual = $52.75; person2 gets $0. + mt_ssp_eligible: [true, false] + mt_ssp: 52.75 + +- name: Case 7, NONE living arrangement gets zero supplement. + absolute_error_margin: 0.01 + period: 2024-01 + input: + people: + person1: + is_ssi_eligible: true + mt_ssp_payment_category: NONE + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + state_code: MT + output: + # NONE category fails the qualifying-arrangement gate. + mt_ssp_eligible: false + mt_ssp: 0 + +- name: Case 8, non-Montana resident gets zero supplement. + absolute_error_margin: 0.01 + period: 2024-01 + input: + people: + person1: + is_ssi_eligible: true + mt_ssp_payment_category: ASSISTED_LIVING_OR_GROUP_OR_COMMUNITY_HOME + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + # defined_for=StateCode.MT zeros mt_ssp outside Montana. + state_code: WY + output: + mt_ssp: 0 + +- name: Case 9, disabled child SSI recipient in foster care receives $52.75 (no age gate). + absolute_error_margin: 0.01 + period: 2024-01 + input: + people: + person1: + age: 10 + is_disabled: true + # Disabled minor receives federal SSI at 2024 individual FBR + # ($943/mo = $11,316/yr). + ssi: 11_316 + mt_ssp_payment_category: FOSTER_CARE + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + state_code: MT + output: + # Children covered (309 of 968 recipients per SSA 2011 report); + # individual J tier = $52.75. + mt_ssp_eligible: true + mt_ssp: 52.75 + +- name: Case 10, Group 2 person with income just above FBR (ssi=0, uncapped_ssi>0). + absolute_error_margin: 0.01 + period: 2024-01 + input: + people: + person1: + is_ssi_eligible: true + # Categorically eligible but model takeup zeros the actual ssi + # payment. uncapped_ssi remains positive (Group 2 path). + ssi: 0 + mt_ssp_payment_category: FOSTER_CARE + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + state_code: MT + output: + # Group 2 (ARM 37.43.102(2)(a)): uncapped_ssi > 0 with ssi == 0 + # still passes mt_ssp_eligible. Individual J = $52.75. + mt_ssp_eligible: true + mt_ssp: 52.75 + +- name: Case 11, eligible recipient at full SSI receives full supplement (no spillover). + absolute_error_margin: 0.01 + period: 2024-01 + input: + people: + person1: + is_ssi_eligible: true + # 2024 individual FBR = $11,316/yr -> uncapped_ssi positive, + # spillover deduction = max_(0, -uncapped_ssi) = 0. + ssi: 11_316 + mt_ssp_payment_category: ASSISTED_LIVING_OR_GROUP_OR_COMMUNITY_HOME + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + state_code: MT + output: + # Full G/H/I individual rate $94 with no spillover reduction. + mt_ssp_eligible: true + mt_ssp: 94 + +- name: Case 12, historical 2010 individual in foster care still receives $52.75 (rates frozen since 1989). + absolute_error_margin: 0.01 + period: 2010-01 + input: + people: + person1: + is_ssi_eligible: true + # 2010 federal SSI individual FBR = $674/mo = $8,088/yr. + ssi: 8_088 + mt_ssp_payment_category: FOSTER_CARE + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + state_code: MT + output: + # Rates frozen at 1989 levels per ARM 37.43.104; J tier still $52.75. + mt_ssp_eligible: true + mt_ssp: 52.75 + +- name: Case 13, single person with ssi_claim_is_joint=true cannot reach couple rate (marital_unit size 1). + absolute_error_margin: 0.01 + period: 2024-01 + input: + people: + person1: + is_ssi_eligible: true + # Degenerate: single person with stale joint flag. + ssi_claim_is_joint: true + mt_ssp_payment_category: ASSISTED_LIVING_OR_GROUP_OR_COMMUNITY_HOME + marital_units: + marital_unit: + members: [person1] + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + state_code: MT + output: + # marital_unit.nb_persons() == 1 means couple gate is unreachable; + # falls back to individual rate $94. + mt_ssp_eligible: true + mt_ssp: 94 + +- name: Case 14, large business loss does not inflate the supplement. + absolute_error_margin: 0.01 + period: 2024-01 + input: + people: + person1: + is_ssi_eligible: true + # Large business loss; max_(0, -uncapped_ssi) clamps the spillover + # at 0 so a negative reduction cannot inflate the supplement. + self_employment_income: -50_000 + mt_ssp_payment_category: TRANSITIONAL_LIVING + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + state_code: MT + output: + # Negative income does NOT inflate the supplement; max_(0, -uncapped_ssi) + # clamps spillover at 0. K tier individual rate = $26. + mt_ssp_eligible: true + mt_ssp: 26 + +- name: Case 15, multi-person SPM unit with parent + adult child both eligible sums correctly. + absolute_error_margin: 0.01 + period: 2024-01 + input: + people: + person1: + # Aged parent in personal care facility. + age: 70 + is_ssi_eligible: true + is_ssi_aged_blind_disabled: true + mt_ssp_payment_category: ASSISTED_LIVING_OR_GROUP_OR_COMMUNITY_HOME + person2: + # Adult disabled child in transitional living. + age: 30 + is_ssi_eligible: true + is_ssi_aged_blind_disabled: true + mt_ssp_payment_category: TRANSITIONAL_LIVING + marital_units: + marital_unit1: + members: [person1] + marital_unit2: + members: [person2] + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # Two separate marital units -> each gets individual rate. + # person1: G/H/I = $94; person2: K = $26. SPM total = $120. + mt_ssp_eligible: [true, true] + mt_ssp: 120 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ssp/mt_ssp_couple_per_spouse.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ssp/mt_ssp_couple_per_spouse.yaml new file mode 100644 index 00000000000..8187f46104b --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ssp/mt_ssp_couple_per_spouse.yaml @@ -0,0 +1,144 @@ +- name: Case 1, ASSISTED_LIVING couple gate met -> each spouse gets $96.50 ($193 / 2). + absolute_error_margin: 0.01 + period: 2024-01 + input: + people: + person1: + is_ssi_eligible: true + ssi_claim_is_joint: true + mt_ssp_payment_category: ASSISTED_LIVING_OR_GROUP_OR_COMMUNITY_HOME + person2: + is_ssi_eligible: true + ssi_claim_is_joint: true + mt_ssp_payment_category: ASSISTED_LIVING_OR_GROUP_OR_COMMUNITY_HOME + marital_units: + marital_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # Couple total $193 / 2 = $96.50 per spouse. + mt_ssp_couple_per_spouse: [96.50, 96.50] + +- name: Case 2, FOSTER_CARE couple gate met -> each spouse gets $55.25 ($110.50 / 2). + absolute_error_margin: 0.01 + period: 2024-01 + input: + people: + person1: + is_ssi_eligible: true + ssi_claim_is_joint: true + mt_ssp_payment_category: FOSTER_CARE + person2: + is_ssi_eligible: true + ssi_claim_is_joint: true + mt_ssp_payment_category: FOSTER_CARE + marital_units: + marital_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # Couple total $110.50 / 2 = $55.25 per spouse. + mt_ssp_couple_per_spouse: [55.25, 55.25] + +- name: Case 3, TRANSITIONAL_LIVING couple gate met -> each spouse gets $28.50 ($57 / 2). + absolute_error_margin: 0.01 + period: 2024-01 + input: + people: + person1: + is_ssi_eligible: true + ssi_claim_is_joint: true + mt_ssp_payment_category: TRANSITIONAL_LIVING + person2: + is_ssi_eligible: true + ssi_claim_is_joint: true + mt_ssp_payment_category: TRANSITIONAL_LIVING + marital_units: + marital_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # Couple total $57 / 2 = $28.50 per spouse. + mt_ssp_couple_per_spouse: [28.50, 28.50] + +- name: Case 4, mismatched payment categories breaks the couple gate. + absolute_error_margin: 0.01 + period: 2024-01 + input: + people: + person1: + is_ssi_eligible: true + ssi_claim_is_joint: true + mt_ssp_payment_category: ASSISTED_LIVING_OR_GROUP_OR_COMMUNITY_HOME + person2: + is_ssi_eligible: true + ssi_claim_is_joint: true + mt_ssp_payment_category: FOSTER_CARE + marital_units: + marital_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # Different categories -> gate fails; caller falls back to individual. + mt_ssp_couple_per_spouse: [0, 0] + +- name: Case 5, no joint claim breaks the couple gate. + absolute_error_margin: 0.01 + period: 2024-01 + input: + people: + person1: + is_ssi_eligible: true + ssi_claim_is_joint: false + mt_ssp_payment_category: ASSISTED_LIVING_OR_GROUP_OR_COMMUNITY_HOME + person2: + is_ssi_eligible: true + ssi_claim_is_joint: false + mt_ssp_payment_category: ASSISTED_LIVING_OR_GROUP_OR_COMMUNITY_HOME + marital_units: + marital_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # No joint claim -> couple gate fails. + mt_ssp_couple_per_spouse: [0, 0] + +- name: Case 6, only one spouse eligible breaks the couple gate. + absolute_error_margin: 0.01 + period: 2024-01 + input: + people: + person1: + is_ssi_eligible: true + ssi_claim_is_joint: true + mt_ssp_payment_category: ASSISTED_LIVING_OR_GROUP_OR_COMMUNITY_HOME + person2: + # Spouse has too much income to be SSI-eligible. + employment_income: 60_000 + ssi_claim_is_joint: true + mt_ssp_payment_category: ASSISTED_LIVING_OR_GROUP_OR_COMMUNITY_HOME + marital_units: + marital_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: MT + output: + # Only person1 is mt_ssp_eligible -> couple gate fails. + mt_ssp_couple_per_spouse: [0, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ssp/mt_ssp_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ssp/mt_ssp_eligible.yaml new file mode 100644 index 00000000000..055af536a77 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ssp/mt_ssp_eligible.yaml @@ -0,0 +1,76 @@ +- name: Case 1, Montana resident with positive uncapped SSI in qualifying arrangement is eligible. + period: 2024-01 + input: + people: + person1: + is_ssi_eligible: true + mt_ssp_payment_category: ASSISTED_LIVING_OR_GROUP_OR_COMMUNITY_HOME + households: + household: + members: [person1] + state_code: MT + output: + mt_ssp_eligible: true + +- name: Case 2, Montana resident in NONE arrangement is ineligible. + period: 2024-01 + input: + people: + person1: + is_ssi_eligible: true + mt_ssp_payment_category: NONE + households: + household: + members: [person1] + state_code: MT + output: + # Living arrangement is not on the qualifying ARM 37.43.103 list. + mt_ssp_eligible: false + +- name: Case 3, Montana resident with no SSI eligibility (income too high) is ineligible. + period: 2024-01 + input: + people: + person1: + # Earnings too high to be SSI-eligible -> uncapped_ssi = 0. + employment_income: 60_000 + mt_ssp_payment_category: ASSISTED_LIVING_OR_GROUP_OR_COMMUNITY_HOME + households: + household: + members: [person1] + state_code: MT + output: + mt_ssp_eligible: false + +- name: Case 4, non-Montana resident in qualifying arrangement is ineligible. + period: 2024-01 + input: + people: + person1: + is_ssi_eligible: true + mt_ssp_payment_category: FOSTER_CARE + households: + household: + members: [person1] + state_code: WY + output: + # defined_for filter on the eligibility variable returns false outside MT. + mt_ssp_eligible: false + +- name: Case 5, Group 2 SSI-categorically-eligible person with income above FBR (ssi=0 but uncapped_ssi>0) is eligible. + period: 2024-01 + input: + people: + person1: + is_ssi_eligible: true + # Categorically eligible but no payment due to takeup; ARM + # 37.43.102(2)(a) covers Group 2 (eligible-except-for-income). + ssi: 0 + mt_ssp_payment_category: TRANSITIONAL_LIVING + households: + household: + members: [person1] + state_code: MT + output: + # uncapped_ssi > 0 gate covers Group 2 (income just below FBR). + mt_ssp_eligible: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ssp/mt_ssp_individual.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ssp/mt_ssp_individual.yaml new file mode 100644 index 00000000000..1bdceaa157a --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/dphhs/ssp/mt_ssp_individual.yaml @@ -0,0 +1,80 @@ +- name: Case 1, ASSISTED_LIVING_OR_GROUP_OR_COMMUNITY_HOME individual rate is $94. + absolute_error_margin: 0.01 + period: 2024-01 + input: + people: + person1: + is_ssi_eligible: true + mt_ssp_payment_category: ASSISTED_LIVING_OR_GROUP_OR_COMMUNITY_HOME + households: + household: + members: [person1] + state_code: MT + output: + # ARM 37.43.104(1): G/H/I tier individual rate frozen at $94 since 1989. + mt_ssp_individual: 94 + +- name: Case 2, FOSTER_CARE individual rate is $52.75. + absolute_error_margin: 0.01 + period: 2024-01 + input: + people: + person1: + is_ssi_eligible: true + mt_ssp_payment_category: FOSTER_CARE + households: + household: + members: [person1] + state_code: MT + output: + # ARM 37.43.104(1): J tier individual rate frozen at $52.75 since 1989. + mt_ssp_individual: 52.75 + +- name: Case 3, TRANSITIONAL_LIVING individual rate is $26. + absolute_error_margin: 0.01 + period: 2024-01 + input: + people: + person1: + is_ssi_eligible: true + mt_ssp_payment_category: TRANSITIONAL_LIVING + households: + household: + members: [person1] + state_code: MT + output: + # ARM 37.43.104(1): K tier individual rate frozen at $26 since 1989. + mt_ssp_individual: 26 + +- name: Case 4, NONE category returns zero individual rate. + absolute_error_margin: 0.01 + period: 2024-01 + input: + people: + person1: + is_ssi_eligible: true + mt_ssp_payment_category: NONE + households: + household: + members: [person1] + state_code: MT + output: + # NONE tier maps to $0 in payment/individual.yaml. + mt_ssp_individual: 0 + +- name: Case 5, ineligible person (income too high) gets zero individual rate. + absolute_error_margin: 0.01 + period: 2024-01 + input: + people: + person1: + # Earnings too high to be SSI-eligible. + employment_income: 60_000 + mt_ssp_payment_category: ASSISTED_LIVING_OR_GROUP_OR_COMMUNITY_HOME + households: + household: + members: [person1] + state_code: MT + output: + # Eligibility gate fails -> individual amount is $0. + mt_ssp_individual: 0 diff --git a/policyengine_us/variables/gov/states/mt/dphhs/ssp/mt_ssp.py b/policyengine_us/variables/gov/states/mt/dphhs/ssp/mt_ssp.py new file mode 100644 index 00000000000..bd0b18b1de5 --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/dphhs/ssp/mt_ssp.py @@ -0,0 +1,34 @@ +from policyengine_us.model_api import * + + +class mt_ssp(Variable): + value_type = float + entity = SPMUnit + label = "Montana State Supplementation" + unit = USD + definition_period = MONTH + defined_for = StateCode.MT + reference = ( + "https://www.law.cornell.edu/regulations/montana/ARM-37-43-101", + "https://www.law.cornell.edu/regulations/montana/ARM-37-43-102", + "https://www.law.cornell.edu/regulations/montana/ARM-37-43-103", + "https://www.law.cornell.edu/regulations/montana/ARM-37-43-104", + "https://secure.ssa.gov/poms.nsf/lnx/0501415010DEN", + "https://www.ssa.gov/policy/docs/progdesc/ssi_st_asst/2011/mt.html", + "https://leg.mt.gov/bills/mca/title_0520/chapter_0010/part_0010/section_0040/0520-0010-0010-0040.html", + ) + + def formula(spm_unit, period, parameters): + person = spm_unit.members + eligible = person("mt_ssp_eligible", period) + couple_per_spouse = person("mt_ssp_couple_per_spouse", period) + individual = person("mt_ssp_individual", period) + couple_active = couple_per_spouse > 0 + base_amount = where(couple_active, couple_per_spouse, individual) + # Residual countable income (federal SSI exhausted) spills onto + # the state supplement; uncapped_ssi is YEAR-defined so the + # framework auto-divides to monthly when accessed with period. + uncapped_ssi = person("uncapped_ssi", period) + reduction = max_(0, -uncapped_ssi) + per_person = max_(0, base_amount - reduction) * eligible + return spm_unit.sum(per_person) diff --git a/policyengine_us/variables/gov/states/mt/dphhs/ssp/mt_ssp_couple_per_spouse.py b/policyengine_us/variables/gov/states/mt/dphhs/ssp/mt_ssp_couple_per_spouse.py new file mode 100644 index 00000000000..7143b2b0e2f --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/dphhs/ssp/mt_ssp_couple_per_spouse.py @@ -0,0 +1,27 @@ +from policyengine_us.model_api import * + + +class mt_ssp_couple_per_spouse(Variable): + value_type = float + entity = Person + label = "Montana SSP couple payment amount per spouse" + unit = USD + definition_period = MONTH + defined_for = "mt_ssp_eligible" + reference = ( + "https://secure.ssa.gov/poms.nsf/lnx/0501415010DEN", + "https://www.ssa.gov/policy/docs/progdesc/ssi_st_asst/2011/mt.html", + ) + + def formula(person, period, parameters): + joint_claim = person("ssi_claim_is_joint", period.this_year) + eligible = person("mt_ssp_eligible", period) + both_eligible = person.marital_unit.sum(eligible) == 2 + is_couple = person.marital_unit.nb_persons() == 2 + category = person("mt_ssp_payment_category", period) + shared_category = person.marital_unit.max(category) == person.marital_unit.min( + category + ) + couple_gate = joint_claim & both_eligible & is_couple & shared_category + p = parameters(period).gov.states.mt.dphhs.ssp + return where(couple_gate, p.amount.couple[category] / 2, 0) diff --git a/policyengine_us/variables/gov/states/mt/dphhs/ssp/mt_ssp_eligible.py b/policyengine_us/variables/gov/states/mt/dphhs/ssp/mt_ssp_eligible.py new file mode 100644 index 00000000000..c52e9507fcc --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/dphhs/ssp/mt_ssp_eligible.py @@ -0,0 +1,25 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.mt.dphhs.ssp.mt_ssp_payment_category import ( + MTSSPPaymentCategory, +) + + +class mt_ssp_eligible(Variable): + value_type = bool + entity = Person + label = "Montana SSP eligible" + definition_period = MONTH + defined_for = StateCode.MT + reference = ( + "https://www.law.cornell.edu/regulations/montana/ARM-37-43-102", + "https://www.law.cornell.edu/regulations/montana/ARM-37-43-103", + ) + + def formula(person, period, parameters): + # ARM 37.43.102(2)(a) covers Group 2 (eligible-but-for-income), + # so we gate on uncapped_ssi > 0 to admit recipients whose + # countable income has zeroed the federal SSI payment. + uncapped_ssi = person("uncapped_ssi", period) + category = person("mt_ssp_payment_category", period) + in_qualifying_arrangement = category != MTSSPPaymentCategory.NONE + return (uncapped_ssi > 0) & in_qualifying_arrangement diff --git a/policyengine_us/variables/gov/states/mt/dphhs/ssp/mt_ssp_individual.py b/policyengine_us/variables/gov/states/mt/dphhs/ssp/mt_ssp_individual.py new file mode 100644 index 00000000000..a04f940949b --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/dphhs/ssp/mt_ssp_individual.py @@ -0,0 +1,19 @@ +from policyengine_us.model_api import * + + +class mt_ssp_individual(Variable): + value_type = float + entity = Person + label = "Montana SSP individual payment amount" + unit = USD + definition_period = MONTH + defined_for = "mt_ssp_eligible" + reference = ( + "https://www.law.cornell.edu/regulations/montana/ARM-37-43-104", + "https://secure.ssa.gov/poms.nsf/lnx/0501415010DEN", + ) + + def formula(person, period, parameters): + category = person("mt_ssp_payment_category", period) + p = parameters(period).gov.states.mt.dphhs.ssp + return p.amount.individual[category] diff --git a/policyengine_us/variables/gov/states/mt/dphhs/ssp/mt_ssp_payment_category.py b/policyengine_us/variables/gov/states/mt/dphhs/ssp/mt_ssp_payment_category.py new file mode 100644 index 00000000000..e795fac6798 --- /dev/null +++ b/policyengine_us/variables/gov/states/mt/dphhs/ssp/mt_ssp_payment_category.py @@ -0,0 +1,25 @@ +from policyengine_us.model_api import * + + +class MTSSPPaymentCategory(Enum): + ASSISTED_LIVING_OR_GROUP_OR_COMMUNITY_HOME = ( + "Personal care, group home, or community home" + ) + FOSTER_CARE = "Foster care home" + TRANSITIONAL_LIVING = "Transitional living services" + NONE = "None" + + +class mt_ssp_payment_category(Variable): + value_type = Enum + entity = Person + label = "Montana SSP payment category" + definition_period = MONTH + defined_for = StateCode.MT + possible_values = MTSSPPaymentCategory + default_value = MTSSPPaymentCategory.NONE + reference = ( + "https://www.law.cornell.edu/regulations/montana/ARM-37-43-103", + "https://www.law.cornell.edu/regulations/montana/ARM-37-43-104", + "https://secure.ssa.gov/poms.nsf/lnx/0501415010DEN", + ) diff --git a/policyengine_us/variables/household/income/spm_unit/spm_unit_benefits.py b/policyengine_us/variables/household/income/spm_unit/spm_unit_benefits.py index 4d6704d8346..daeb70f2fd0 100644 --- a/policyengine_us/variables/household/income/spm_unit/spm_unit_benefits.py +++ b/policyengine_us/variables/household/income/spm_unit/spm_unit_benefits.py @@ -29,6 +29,7 @@ def formula(spm_unit, period, parameters): "wa_ssp", # Washington benefits "mi_ssp", # Michigan benefits "me_ssp", # Maine benefits + "mt_ssp", # Montana benefits # California programs. "ca_cvrp", # California Clean Vehicle Rebate Project. # Colorado programs.