diff --git a/changelog.d/oh-ssp-2026.added.md b/changelog.d/oh-ssp-2026.added.md new file mode 100644 index 00000000000..dfd805223e8 --- /dev/null +++ b/changelog.d/oh-ssp-2026.added.md @@ -0,0 +1 @@ +Add Ohio Residential State Supplement for 2026. diff --git a/policyengine_us/parameters/gov/household/household_state_benefits.yaml b/policyengine_us/parameters/gov/household/household_state_benefits.yaml index 235a7a5d63a..f420150d6f9 100644 --- a/policyengine_us/parameters/gov/household/household_state_benefits.yaml +++ b/policyengine_us/parameters/gov/household/household_state_benefits.yaml @@ -74,6 +74,8 @@ values: - nm_ssi_state_supplement # North Carolina benefits - nc_sa + # Ohio benefits + - oh_rss # Pennsylvania benefits - pa_ssp # Rhode Island benefits @@ -181,6 +183,8 @@ values: - nj_ssp # New Mexico benefits - nm_ssi_state_supplement + # Ohio benefits + - oh_rss # Pennsylvania benefits - pa_ssp # Rhode Island benefits @@ -299,6 +303,8 @@ values: - nj_ssp # New Mexico benefits - nm_ssi_state_supplement + # Ohio benefits + - oh_rss # Pennsylvania benefits - pa_ssp # Rhode Island benefits @@ -418,6 +424,8 @@ values: - nj_ssp # New Mexico benefits - nm_ssi_state_supplement + # Ohio benefits + - oh_rss # Pennsylvania benefits - pa_ssp # Rhode Island benefits diff --git a/policyengine_us/parameters/gov/states/oh/dbh/rss/allowable_fee.yaml b/policyengine_us/parameters/gov/states/oh/dbh/rss/allowable_fee.yaml new file mode 100644 index 00000000000..1265bb544cb --- /dev/null +++ b/policyengine_us/parameters/gov/states/oh/dbh/rss/allowable_fee.yaml @@ -0,0 +1,13 @@ +description: Ohio sets this amount as the monthly allowable fee under the Residential State Supplement program. +values: + 2023-07-01: 1_600 + +metadata: + unit: currency-USD + period: month + label: Ohio RSS allowable facility fee + reference: + - title: Ohio Administrative Code 5122-36-05(B) + href: https://codes.ohio.gov/ohio-administrative-code/rule-5122-36-05 + - title: Ohio Medicaid Eligibility Procedure Letter 175 + href: https://dam.assets.ohio.gov/image/upload/medicaid.ohio.gov/About%20Us/PoliciesGuidelines/MEPL/MEPL-175.pdf#page=1 diff --git a/policyengine_us/parameters/gov/states/oh/dbh/rss/minimum_age.yaml b/policyengine_us/parameters/gov/states/oh/dbh/rss/minimum_age.yaml new file mode 100644 index 00000000000..4eac9f45c7c --- /dev/null +++ b/policyengine_us/parameters/gov/states/oh/dbh/rss/minimum_age.yaml @@ -0,0 +1,13 @@ +description: Ohio sets this age as the minimum age under the Residential State Supplement program. +values: + 2017-07-15: 18 + +metadata: + unit: year + period: year + label: Ohio RSS minimum age + reference: + - title: Ohio Administrative Code 5122-36-02(A)(1) + href: https://codes.ohio.gov/ohio-administrative-code/rule-5122-36-02 + - title: Ohio Administrative Code 5122-36-02(A), effective July 15, 2017 + href: https://codes.ohio.gov/assets/laws/administrative-code/authenticated/5122/0/36/5122-36-02_20170715.pdf#page=1 diff --git a/policyengine_us/parameters/gov/states/oh/dbh/rss/personal_needs_allowance.yaml b/policyengine_us/parameters/gov/states/oh/dbh/rss/personal_needs_allowance.yaml new file mode 100644 index 00000000000..2f6cdf821f0 --- /dev/null +++ b/policyengine_us/parameters/gov/states/oh/dbh/rss/personal_needs_allowance.yaml @@ -0,0 +1,13 @@ +description: Ohio provides this amount as the monthly personal needs allowance under the Residential State Supplement program. +values: + 2023-07-01: 200 + +metadata: + unit: currency-USD + period: month + label: Ohio RSS personal needs allowance + reference: + - title: Ohio Administrative Code 5122-36-05(C) + href: https://codes.ohio.gov/ohio-administrative-code/rule-5122-36-05 + - title: Ohio Medicaid Eligibility Procedure Letter 175 + href: https://dam.assets.ohio.gov/image/upload/medicaid.ohio.gov/About%20Us/PoliciesGuidelines/MEPL/MEPL-175.pdf#page=1 diff --git a/policyengine_us/parameters/gov/usda/snap/income/sources/unearned.yaml b/policyengine_us/parameters/gov/usda/snap/income/sources/unearned.yaml index e8d950512b9..b35bc903647 100644 --- a/policyengine_us/parameters/gov/usda/snap/income/sources/unearned.yaml +++ b/policyengine_us/parameters/gov/usda/snap/income/sources/unearned.yaml @@ -52,6 +52,7 @@ values: - nm_ssi_state_supplement - nv_oss - ny_ssp + - oh_rss - ok_ssp - pa_ssp - ri_ssp diff --git a/policyengine_us/programs.yaml b/policyengine_us/programs.yaml index 43fdd714b80..3c935941616 100644 --- a/policyengine_us/programs.yaml +++ b/policyengine_us/programs.yaml @@ -1215,6 +1215,18 @@ programs: notes: Models the 2026 community supplement for individuals, couples and individuals with an ineligible spouse. Essential spouses, persons in a TANF case and grandfathered institutional payments are not modeled. + - state: OH + status: partial + name: Ohio RSS + full_name: Ohio Residential State Supplement + variable: oh_rss + parameter_prefix: gov.states.oh.dbh.rss + verified_years: "2026" + notes: >- + Models 2026 payments for approved residential placements, including + first-month proration. Care certification is an input, so + microsimulation pays no RSS; PASS and specialized work-expense + exclusions are not modeled. - state: NY status: partial name: New York SSP diff --git a/policyengine_us/tests/policy/baseline/gov/states/oh/dbh/rss/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/oh/dbh/rss/integration.yaml new file mode 100644 index 00000000000..5dd0297e5b4 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/oh/dbh/rss/integration.yaml @@ -0,0 +1,458 @@ +- name: Case 1, both spouses in approved care. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 65 + oh_rss_certified: true + medicaid_enrolled: true + person2: + age: 65 + oh_rss_certified: true + medicaid_enrolled: true + marital_units: + couple: + members: [person1, person2] + tax_units: + unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: OH + output: + oh_rss_eligible: [true, true] + # SSI couple rate 1,491 / 2 = 745.50 each. + ssi: [745.5, 745.5] + # RSS pools the couple's SSI: 1,491 / 2 = 745.50 each. + oh_rss_countable_income: [745.5, 745.5] + # (2 x 1,800 - 1,491) / 2 = 1,054.50 each. + oh_rss: [1_054.5, 1_054.5] + +- name: Case 2, one spouse in approved care, both in the same RSS home. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 65 + oh_rss_certified: true + medicaid_enrolled: true + person2: + age: 65 + medicaid_enrolled: true + marital_units: + couple: + members: [person1, person2] + tax_units: + unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: OH + output: + oh_rss_eligible: [true, false] + # Both spouses are aged, so SSI pays the couple rate: 1,491 / 2 = 745.50. + ssi: [745.5, 745.5] + # 5160:1-5-01(E)(3) uses the individual standard: own SSI 745.50 only. + oh_rss_countable_income: [745.5, 745.5] + # 1,800 - 745.50 = 1,054.50; person2 has no level of care. + oh_rss: [1_054.5, 0] + +- name: Case 3, ineligible spouse income is not deemed. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 65 + oh_rss_certified: true + medicaid_enrolled: true + person2: + age: 40 + medicaid_enrolled: false + # BLS OEWS May 2025, Ohio all-occupations median annual wage. + employment_income: 49_380 + marital_units: + couple: + members: [person1, person2] + tax_units: + unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: OH + output: + # SSI deems the spouse's wages: 49,380 / 12 = 4,115; + # (4,115 - 20 - 65) / 2 = 2,015. + ssi_income_deemed_from_ineligible_spouse: [2_015, 0] + # Couple rate under deeming: 1,491 - 2,015 < 0. + ssi: [0, 0] + oh_rss_eligible: [true, false] + # RSS counts only person1's own income (0) plus SSI (0). Person2 is not + # an RSS candidate; person2's own wages compute to (4,115 - 85) / 2 = 2,015. + oh_rss_countable_income: [0, 2_015] + # 1,600 fee + 200 allowance - 0 = 1,800. + oh_rss: [1_800, 0] + +- name: Case 4, combined resource boundary. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 65 + oh_rss_certified: true + medicaid_enrolled: true + ssi_countable_resources: 2_000 + person2: + age: 65 + oh_rss_certified: true + medicaid_enrolled: true + ssi_countable_resources: 1_000 + marital_units: + couple: + members: [person1, person2] + tax_units: + unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: OH + output: + # 2,000 + 1,000 = 3,000 <= 3,000 couple limit; SSI 1,491 / 2 = 745.50. + ssi: [745.5, 745.5] + # (2 x 1,800 - 1,491) / 2 = 1,054.50 each. + oh_rss: [1_054.5, 1_054.5] + +- name: Case 5, combined excess resources. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 65 + oh_rss_certified: true + medicaid_enrolled: true + ssi_countable_resources: 2_000 + person2: + age: 65 + oh_rss_certified: true + medicaid_enrolled: true + ssi_countable_resources: 1_001 + marital_units: + couple: + members: [person1, person2] + tax_units: + unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: OH + output: + # 2,000 + 1,001 = 3,001 > 3,000 couple limit for both SSI and RSS. + ssi: [0, 0] + oh_rss: [0, 0] + +- name: Case 6, annual benefits and SNAP income. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: OH + age: 65 + oh_rss_certified: true + medicaid_enrolled: true + takes_up_snap_if_eligible: false + output: + # 994 x 12 = 11,928. + ssi: 11_928 + # (1,800 - 994) x 12 = 9,672. + oh_rss: 9_672 + household_state_benefits: 9_672 + # SSI 11,928 + RSS 9,672 = 21,600. + snap_unearned_income: 21_600 + # SSI 11,928 + RSS 9,672 = 21,600, with SNAP take-up off. + spm_unit_benefits: 21_600 + # HUD income excludes RSS (see oh_rss): SSI 994 * 12 = 11_928. + hud_unearned_income: 11_928 + +- name: Case 7, partial month daily amount rounded upward. + period: 2026-04 + absolute_error_margin: 0.01 + input: + state_code: + 2026: OH + age: + 2026: 65 + oh_rss_certified: true + medicaid_enrolled: + 2026: true + oh_rss_partial_month_days: 15 + output: + # 1,800 - SSI 994 = 806; 806 / 30 days = 26.8667, rounded up to 26.87; + # 26.87 x 15 days = 403.05. + oh_rss: 403.05 + +- name: Case 8, couple income pooled with one general exclusion. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + oh_rss_certified: true + medicaid_enrolled: true + # SSA Monthly Statistical Snapshot, August 2026, Table 2: average + # retired-worker benefit $2,087.52; 2,087.52 x 12 = 25,050.24. + social_security_retirement: 25_050.24 + person2: + age: 70 + oh_rss_certified: true + medicaid_enrolled: true + # Same table: average benefit for a retired worker's spouse $986.70; + # 986.70 x 12 = 11,840.40. + social_security_dependents: 11_840.4 + marital_units: + couple: + members: [person1, person2] + tax_units: + unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: OH + output: + oh_rss_eligible: [true, true] + # SSI couple computation: (2,087.52 + 986.70 - 20) / 2 = 1,527.11 each. + ssi_countable_income: [1_527.11, 1_527.11] + # 1,491 / 2 = 745.50 - 1,527.11 < 0. + ssi: [0, 0] + # RSS pools the couple's income: (3,074.22 - 20) / 2 = 1,527.11 each (not + # 2,067.52 and 966.70 with a separate exclusion per spouse); SSI adds 0. + oh_rss_countable_income: [1_527.11, 1_527.11] + # 1,600 fee + 200 allowance - 1,527.11 = 272.89 each. + oh_rss: [272.89, 272.89] + +- name: Case 9, partial month with a whole-cent daily amount. + period: 2026-04 + absolute_error_margin: 0.01 + input: + state_code: + 2026: OH + age: + 2026: 65 + oh_rss_certified: true + medicaid_enrolled: + 2026: true + # Set directly so the daily amount is a whole cent. + oh_rss_countable_income: 1_558.5 + oh_rss_partial_month_days: 15 + output: + oh_rss_eligible: true + # 1,800 - 1,558.50 = 241.50; 241.50 / 30 days = 8.05 exactly (no + # float round-up to 8.06); 8.05 x 15 days = 120.75. + oh_rss: 120.75 + +- name: Case 10, partial-month days entered for one spouse leave the other spouse's month full. + period: 2026-04 + absolute_error_margin: 0.01 + input: + people: + person1: + age: + 2026: 65 + oh_rss_certified: true + medicaid_enrolled: + 2026: true + # Moved into the RSS home on April 16: April 16 through 30. + oh_rss_partial_month_days: 15 + person2: + age: + 2026: 65 + oh_rss_certified: true + medicaid_enrolled: + 2026: true + marital_units: + couple: + members: [person1, person2] + tax_units: + unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: + 2026: OH + output: + oh_rss_partial_month_days: [15, 0] + oh_rss_eligible: [true, true] + # SSI couple rate 1,491 / 2 = 745.50 each. + ssi: [745.5, 745.5] + oh_rss_countable_income: [745.5, 745.5] + # (2 x 1,800 - 1,491) / 2 = 1,054.50; person1: 1,054.50 / 30 days = + # 35.15 x 15 days = 527.25; person2 was in the home all month. + oh_rss: [527.25, 1_054.5] + +- name: Case 11, partial-month days entered for one month leave the next month full. + period: 2026 + absolute_error_margin: 0.01 + input: + state_code: OH + age: 65 + medicaid_enrolled: true + oh_rss_certified: + 2026-03: false + 2026-04: true + 2026-05: true + oh_rss_partial_month_days: + 2026-04: 15 + output: + oh_rss_partial_month_days: + 2026-04: 15 + 2026-05: 0 + # March: no placement yet. April: 806 / 30 days = 26.8667, rounded up to + # 26.87; 26.87 x 15 days = 403.05. May is a full month: 1,800 - 994 = 806. + oh_rss: + 2026-03: 0 + 2026-04: 403.05 + 2026-05: 806 + +- name: Case 12, spouses in different RSS homes entered as separate marital units. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 65 + oh_rss_certified: true + medicaid_enrolled: true + person2: + age: 65 + oh_rss_certified: true + medicaid_enrolled: true + marital_units: + m1: + members: [person1] + m2: + members: [person2] + tax_units: + unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: OH + output: + # Each spouse is budgeted as an individual: SSI 994 each. + ssi: [994, 994] + oh_rss_eligible: [true, true] + oh_rss_countable_income: [994, 994] + # 1,800 - 994 = 806 each. + oh_rss: [806, 806] + +# NOTE: 5160:1-5-01(E)(3) applies the individual standard when only one spouse +# has a level of care, so the model tests person1's own resources against +# $2,000. 5160:1-3-05.1(B)(9)(b) and 5160:1-3-05.20(E)(1)-(2) would instead +# combine the couple's 3,100 against $3,000 and pay 0. +- name: Case 13, one eligible spouse tested on own resources. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 65 + oh_rss_certified: true + medicaid_enrolled: true + ssi_countable_resources: 1_500 + person2: + age: 65 + medicaid_enrolled: true + ssi_countable_resources: 1_600 + marital_units: + couple: + members: [person1, person2] + tax_units: + unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: OH + output: + oh_rss_eligible: [true, false] + # SSI combines the couple's resources: 1,500 + 1,600 = 3,100 > 3,000. + ssi: [0, 0] + oh_rss_countable_income: [0, 0] + # Person1's own 1,500 <= 2,000; 1,600 + 200 - 0 = 1,800. + oh_rss: [1_800, 0] + +- name: Case 14, one eligible spouse with Medicaid computed by the model and combined resources above the couple limit. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 65 + oh_rss_certified: true + ssi_countable_resources: 1_500 + person2: + age: 65 + ssi_countable_resources: 1_600 + marital_units: + couple: + members: [person1, person2] + tax_units: + unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: OH + output: + # Case 13 without the Medicaid input. Medicaid and SSI combine the + # couple's resources (5160:1-3-05.20(E)(1)): 3,100 > 3,000, so person1 + # meets neither Medicaid path. + ssi: [0, 0] + oh_rss_eligible: [false, false] + oh_rss: [0, 0] + +- name: Case 15, one eligible spouse compared with the RSS income standard. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 60 + oh_rss_certified: true + meets_ssi_disability_criteria: true + # $1,200 a month in Social Security disability benefits. + social_security_disability: 14_400 + person2: + age: 58 + marital_units: + couple: + members: [person1, person2] + tax_units: + unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: OH + output: + # Person2 is not aged, blind or disabled and has no income to deem: + # 1,200 - 20 = 1,180 > 994, so no SSI. + ssi: [0, 0] + # Person1 meets SSI's other criteria, so the Medicaid condition is met + # without an input. + oh_rss_eligible: [true, false] + oh_rss_countable_income: [1_180, 0] + # 5160:1-5-01(E)(3) points to the $994 individual standard; the model uses + # the RSS standard: 1,800 - 1,180 = 620. + oh_rss: [620, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/oh/dbh/rss/oh_rss.yaml b/policyengine_us/tests/policy/baseline/gov/states/oh/dbh/rss/oh_rss.yaml new file mode 100644 index 00000000000..1cd83c0deeb --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/oh/dbh/rss/oh_rss.yaml @@ -0,0 +1,349 @@ +- name: Case 1, full SSI. + period: 2026-01 + absolute_error_margin: 0.01 + input: + state_code: OH + age: 65 + oh_rss_certified: true + medicaid_enrolled: true + output: + # No other income: SSI pays the 994 federal benefit rate. + ssi: 994 + oh_rss_countable_income: 994 + # 1,600 fee + 200 allowance - SSI 994 = 806. + oh_rss: 806 + +- name: Case 2, non SSI Social Security. + period: 2026-01 + absolute_error_margin: 0.01 + input: + state_code: OH + age: 65 + oh_rss_certified: true + medicaid_enrolled: true + # SSA Monthly Statistical Snapshot, August 2026, Table 2: average + # disabled-worker benefit $1,635.90; 1,635.90 x 12 = 19,630.80. + social_security_disability: 19_630.8 + output: + # 1,635.90 - 20 = 1,615.90 > 994, so no SSI. + ssi: 0 + oh_rss_countable_income: 1_615.9 + # 1,800 - 1,615.90 = 184.10. + oh_rss: 184.1 + +- name: Case 3, countable income exactly at the income standard. + period: 2026-01 + absolute_error_margin: 0.01 + input: + state_code: OH + age: 65 + oh_rss_certified: true + medicaid_enrolled: true + oh_rss_countable_income: 1_800 + output: + oh_rss_eligible: true + # 5160:1-5-01(E)(4)(b): income equal to the 1,800 standard is ineligible. + oh_rss: 0 + +- name: Case 4, countable income one dollar below the income standard. + period: 2026-01 + absolute_error_margin: 0.01 + input: + state_code: OH + age: 65 + oh_rss_certified: true + medicaid_enrolled: true + oh_rss_countable_income: 1_799 + output: + # 1,800 - 1,799 = 1. + oh_rss: 1 + +- name: Case 5, income above threshold. + period: 2026-01 + absolute_error_margin: 0.01 + input: + state_code: OH + age: 65 + oh_rss_certified: true + medicaid_enrolled: true + # SSA Monthly Statistical Snapshot, August 2026, Table 2: average + # retired-worker benefit $2,087.52; 2,087.52 x 12 = 25,050.24. + social_security_retirement: 25_050.24 + output: + # 2,087.52 - 20 = 2,067.52 > 994, so no SSI. + ssi: 0 + oh_rss_countable_income: 2_067.52 + # 2,067.52 > 1,800 standard. + oh_rss: 0 + +- name: Case 6, earned income exclusion. + period: 2026-01 + absolute_error_margin: 0.01 + input: + state_code: OH + age: 65 + oh_rss_certified: true + medicaid_enrolled: true + # Ohio minimum wage $11.00 (US DOL state minimum wage table) x 5 hours + # x 52 weeks = 2,860. + employment_income: 2_860 + output: + # 2,860 / 12 = 238.33; (238.33 - 20 - 65) / 2 = 76.67. + ssi_countable_income: 76.67 + # 994 - 76.67 = 917.33. + ssi: 917.33 + # Same exclusions: 76.67 + SSI 917.33 = 994. + oh_rss_countable_income: 994 + # 1,800 - 994 = 806. + oh_rss: 806 + +- name: Case 7, no care certification. + period: 2026-01 + absolute_error_margin: 0.01 + input: + state_code: OH + age: 65 + oh_rss_certified: false + medicaid_enrolled: true + output: + oh_rss: 0 + +- name: Case 8, no Medicaid. + period: 2026-01 + absolute_error_margin: 0.01 + input: + state_code: OH + age: 65 + oh_rss_certified: true + medicaid_enrolled: false + output: + oh_rss: 0 + +- name: Case 9, age seventeen. + period: 2026-01 + absolute_error_margin: 0.01 + input: + state_code: OH + age: 17 + oh_rss_certified: true + medicaid_enrolled: true + meets_ssi_disability_criteria: true + output: + # Below the minimum age of 18 (5122-36-02(A)(1)). + oh_rss: 0 + +- name: Case 10, age eighteen disabled. + period: 2026-01 + absolute_error_margin: 0.01 + input: + state_code: OH + age: 18 + oh_rss_certified: true + medicaid_enrolled: true + meets_ssi_disability_criteria: true + output: + # 1,600 + 200 - SSI 994 = 806. + oh_rss: 806 + +- name: Case 11, not aged blind or disabled. + period: 2026-01 + absolute_error_margin: 0.01 + input: + state_code: OH + age: 40 + oh_rss_certified: true + medicaid_enrolled: true + output: + oh_rss: 0 + +- name: Case 12, resource boundary. + period: 2026-01 + absolute_error_margin: 0.01 + input: + state_code: OH + age: 65 + oh_rss_certified: true + medicaid_enrolled: true + ssi_countable_resources: 2_000 + output: + # 2,000 <= 2,000 individual limit; 1,800 - SSI 994 = 806. + oh_rss: 806 + +- name: Case 13, excess resources. + period: 2026-01 + absolute_error_margin: 0.01 + input: + state_code: OH + age: 65 + oh_rss_certified: true + medicaid_enrolled: true + ssi_countable_resources: 2_001 + output: + # 2,001 > 2,000 individual limit. + oh_rss: 0 + +- name: Case 14, out of state. + period: 2026-01 + absolute_error_margin: 0.01 + input: + state_code: IN + age: 65 + oh_rss_certified: true + medicaid_enrolled: true + output: + oh_rss: 0 + +- name: Case 15, partial first month sixteen days. + period: 2026-01 + absolute_error_margin: 0.01 + input: + state_code: OH + age: 65 + oh_rss_certified: true + medicaid_enrolled: true + oh_rss_partial_month_days: 16 + output: + # 806 / 31 days = 26.00 a day; 26.00 x 16 days = 416. + oh_rss: 416 + +- name: Case 16, zero partial-month days pays the full month. + period: 2026-01 + absolute_error_margin: 0.01 + input: + state_code: OH + age: 65 + oh_rss_certified: true + medicaid_enrolled: true + oh_rss_partial_month_days: 0 + output: + # 0 means a full month: 1,600 + 200 - SSI 994 = 806. + oh_rss: 806 + +- name: Case 17, interest and dividends excluded from RSS income. + period: 2026-01 + absolute_error_margin: 0.01 + input: + state_code: OH + age: 55 + oh_rss_certified: true + medicaid_enrolled: true + meets_ssi_disability_criteria: true + # SSA Monthly Statistical Snapshot, August 2026: average disabled-worker + # benefit $1,635.90; $1,636 x 12 = 19,632. + social_security_disability: 19_632 + # $1,200 in savings at 4% and $400 in a dividend fund at 3%. + bank_account_assets: 1_200 + taxable_interest_income: 48 + stock_assets: 400 + qualified_dividend_income: 12 + output: + oh_rss_eligible: true + # PolicyEngine's federal SSI counts interest and dividends (#10079): + # 1,636 + 48 / 12 + 12 / 12 = 1,641. + ssi_unearned_income: 1_641 + # 1,641 - 20 = 1,621. + ssi_countable_income: 1_621 + # 994 - 1,621 < 0. + ssi: 0 + # 5160:1-3-03.2(D)(2)(v): 1,641 - 4 - 1 = 1,636; 1,636 - 20 = 1,616; SSI 0. + oh_rss_countable_income: 1_616 + # Resources 1,200 + 400 = 1,600 <= 2,000; 1,800 - 1,616 = 184. + oh_rss: 184 + +- name: Case 18, disabled working student earnings excluded. + period: 2026-01 + absolute_error_margin: 0.01 + input: + state_code: OH + age: 20 + oh_rss_certified: true + medicaid_enrolled: true + meets_ssi_disability_criteria: true + is_full_time_college_student: true + # Ohio minimum wage $11.00 (US DOL state minimum wage table) x 15 hours + # x 52 weeks = 8,580. + employment_income: 8_580 + # SSA Monthly Statistical Snapshot, August 2026: average benefit for + # children of deceased workers $1,181.07; $1,181 x 12 = 14,172. + social_security_survivors: 14_172 + output: + oh_rss_eligible: true + # 8,580 / 12 = 715 < 2,410 monthly limit; 8,580 a year < 9,730 cap. + ssi_blind_or_disabled_working_student_exclusion: 715 + # Earned 715 - 715 = 0; unearned 1,181 - 20 = 1,161. + ssi_countable_income: 1_161 + # 994 - 1,161 < 0. + ssi: 0 + # Same exclusion: 0 earned + (1,181 - 20) unearned + SSI 0 = 1,161. + oh_rss_countable_income: 1_161 + # 1,800 - 1,161 = 639. Without the exclusion, (715 - 65) / 2 = 325 more + # would count and pay 314. + oh_rss: 639 + +- name: Case 19, Medicaid enrollment computed by the model. + period: 2026-01 + absolute_error_margin: 0.01 + input: + state_code: OH + age: 65 + oh_rss_certified: true + output: + # An aged SSI recipient qualifies for Ohio Medicaid with no input. + medicaid_enrolled: true + oh_rss_eligible: true + ssi: 994 + # 1,600 + 200 - SSI 994 = 806. + oh_rss: 806 + +# 5122-36-05 sets the fee and allowance with no cost-of-living adjustment, so +# the 2027 SSI increase lowers RSS by the same amount. +- name: Case 20, fixed RSS income standard with the 2027 SSI rate. + period: 2027-01 + absolute_error_margin: 0.01 + input: + state_code: OH + age: 65 + oh_rss_certified: true + medicaid_enrolled: true + output: + # 2027 SSI federal benefit rate. + ssi: 1_029 + # 1,600 + 200 - SSI 1,029 = 771. + oh_rss: 771 + +# 42 CFR 435.232 (SPA 15-015, Attachment 2.2-A): Ohio covers people who receive +# only a state supplement and would be eligible for SSI except for income. +- name: Case 21, non SSI Social Security with Medicaid computed by the model. + period: 2026-01 + absolute_error_margin: 0.01 + input: + state_code: OH + age: 65 + oh_rss_certified: true + # Case 2's average disabled-worker benefit, with no Medicaid input. + social_security_disability: 19_630.8 + output: + # 1,635.90 - 20 = 1,615.90 > 994, so no SSI; SSI's other criteria are met. + ssi: 0 + oh_rss_eligible: true + # 1,800 - 1,615.90 = 184.10. + oh_rss: 184.1 + +- name: Case 22, partial first month with a whole-cent daily amount from monthly income. + period: 2026-01 + absolute_error_margin: 0.01 + input: + state_code: OH + age: 65 + oh_rss_certified: true + medicaid_enrolled: true + # 17,249.52 / 12 = 1,437.46 a month, a payment that divides into whole + # cents a day. + social_security_retirement: 17_249.52 + oh_rss_partial_month_days: 16 + output: + # 1,437.46 - 20 = 1,417.46 > 994, so no SSI. + oh_rss_countable_income: 1_417.46 + # 1,800 - 1,417.46 = 382.54; 382.54 / 31 = 12.34 a day exactly; + # 12.34 x 16 days = 197.44. + oh_rss: 197.44 diff --git a/policyengine_us/variables/gov/hhs/medicaid/medicaid_enrolled_for_ssi_state_supplement.py b/policyengine_us/variables/gov/hhs/medicaid/medicaid_enrolled_for_ssi_state_supplement.py index d02f1625c4e..aadcfc81799 100644 --- a/policyengine_us/variables/gov/hhs/medicaid/medicaid_enrolled_for_ssi_state_supplement.py +++ b/policyengine_us/variables/gov/hhs/medicaid/medicaid_enrolled_for_ssi_state_supplement.py @@ -14,7 +14,7 @@ class medicaid_enrolled_for_ssi_state_supplement(Variable): ) documentation = ( "Medicaid enrollment condition for SSI/aged/blind/disabled state " - "supplement candidates only (Indiana SAPN/RCAP and Kansas SSPP). " + "supplement candidates only (state supplements that require Medicaid enrollment). " "SSI recipients and aged, blind, or disabled RCAP candidates do not " "need the SNAP/TANF community-engagement exclusion. Other consumers " "must use medicaid_enrolled, which retains that exclusion." diff --git a/policyengine_us/variables/gov/states/oh/dbh/rss/oh_rss.py b/policyengine_us/variables/gov/states/oh/dbh/rss/oh_rss.py new file mode 100644 index 00000000000..ff1cde2f8e8 --- /dev/null +++ b/policyengine_us/variables/gov/states/oh/dbh/rss/oh_rss.py @@ -0,0 +1,63 @@ +from policyengine_us.model_api import * + + +class oh_rss(Variable): + value_type = float + entity = Person + label = "Ohio Residential State Supplement" + definition_period = MONTH + unit = USD + defined_for = "oh_rss_eligible" + reference = ( + "https://codes.ohio.gov/ohio-revised-code/section-5119.41", + "https://codes.ohio.gov/ohio-administrative-code/rule-5160:1-5-01", + "https://codes.ohio.gov/ohio-administrative-code/rule-5160:1-3-05.1", + ) + # NOTE: Not in the HUD income list. RSS is paid to the person (5122-36-04(A)(7)), + # but recipients live in licensed residential care (5122-36-02(A)(5)), and HUD + # income would make the Medicaid condition circular via rent tax deductions. + # NOTE: ORC 5119.41(A) describes RSS as supplementing "the amounts received by + # aged, blind, or disabled adults as supplemental security income payments ... + # or as social security benefits or social security disability insurance + # benefits". Under 5119.41(B), eligibility is set by the rules, and neither + # 5160:1-5-01(C) nor 5122-36-02(A) requires receipt of SSI or Social Security. + # Ohio's Medicaid conditions of eligibility (5160:1-2-10(B)) also do not + # require applying for SSI. The model follows the rules, so it pays eligible + # people who receive neither benefit and does not require SSI take-up. + + def formula(person, period, parameters): + p = parameters(period).gov.states.oh.dbh.rss + # Ohio prints the same $2,000 individual and $3,000 couple resource + # limits (5160:1-3-05.1(B)(9)), so the SSI parameters are used and an + # SSI reform carries over to RSS. + limits = parameters(period).gov.ssa.ssi.eligibility.resources.limit + eligible = person("oh_rss_eligible", period) + couple = person.marital_unit.sum(eligible) == 2 + # NOTE: When only one spouse qualifies, 5160:1-5-01(E)(3) applies the + # individual standard, so the model tests own resources against + # $2,000. Ohio's Medicaid resource rules, 5160:1-3-05.1(B)(9)(b) and + # 5160:1-3-05.20(E)(1)-(2), would instead combine the couple's resources + # against $3,000. The model follows the RSS rule. The reading decides the + # result only when medicaid_enrolled is entered: computed Medicaid and + # the SSI resource test both combine the couple's resources, so a + # couple above $3,000 fails the Medicaid condition. + resources = person("ssi_countable_resources", period.this_year) + resources = where(couple, person.marital_unit.sum(resources), resources) + limit = where(couple, limits.couple, limits.individual) + payment = max_( + p.allowable_fee + + p.personal_needs_allowance + - person("oh_rss_countable_income", period), + 0, + ) + days = person("oh_rss_partial_month_days", period) + prorate = (days > 0) & (days < period.days) + # A first month that starts after the first day is prorated: the daily + # amount and the prorated total are each rounded up to the next whole + # cent (5160:1-5-01(G)(5)-(6)). Countable income is stored in float32, + # so the daily amount allows a thousandth of a cent before rounding up. + daily_payment = ( + np.ceil(payment.astype(np.float64) / period.days * 100 - 1e-3) / 100 + ) + partial = np.ceil(daily_payment * days * 100 - 1e-8) / 100 + return (resources <= limit) * where(prorate, partial, payment) diff --git a/policyengine_us/variables/gov/states/oh/dbh/rss/oh_rss_certified.py b/policyengine_us/variables/gov/states/oh/dbh/rss/oh_rss_certified.py new file mode 100644 index 00000000000..df6a592a89d --- /dev/null +++ b/policyengine_us/variables/gov/states/oh/dbh/rss/oh_rss_certified.py @@ -0,0 +1,16 @@ +from policyengine_us.model_api import * + + +class oh_rss_certified(Variable): + value_type = bool + entity = Person + label = "Ohio RSS certified care placement" + definition_period = MONTH + documentation = "Ohio has approved the current RSS placement and protective level of care, including the facility, supervision, waiver, PACE, hospice and provider-relationship conditions in rule 5122-36-02. Includes a state-approved temporary institutional continuation under 5160:1-5-01(H). Spouses are budgeted together only when they live in the same RSS living arrangement. Enter spouses who live apart as separate marital units from the month after they stop living together; marital units cannot change between months of one calculation." + reference = ( + "https://codes.ohio.gov/ohio-administrative-code/rule-5122-36-02", + "https://codes.ohio.gov/ohio-administrative-code/rule-5160:1-5-01", + "https://codes.ohio.gov/ohio-administrative-code/rule-5160:1-3-05.20", + ) + # NOTE: No microdata input sets this, so oh_rss is 0 in microsimulation and + # reaches household calculations only. diff --git a/policyengine_us/variables/gov/states/oh/dbh/rss/oh_rss_countable_income.py b/policyengine_us/variables/gov/states/oh/dbh/rss/oh_rss_countable_income.py new file mode 100644 index 00000000000..8988542e5be --- /dev/null +++ b/policyengine_us/variables/gov/states/oh/dbh/rss/oh_rss_countable_income.py @@ -0,0 +1,70 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.ssa.ssi.eligibility.income._apply_ssi_exclusions import ( + _apply_ssi_exclusions, +) + + +class oh_rss_countable_income(Variable): + value_type = float + entity = Person + label = "Ohio RSS countable income" + definition_period = MONTH + unit = USD + defined_for = StateCode.OH + reference = ( + "https://codes.ohio.gov/ohio-administrative-code/rule-5160:1-5-01", + "https://codes.ohio.gov/ohio-administrative-code/rule-5160:1-3-03.2", + ) + + def formula(person, period, parameters): + # RSS does not deem income from an ineligible spouse or parent. + eligible = person("oh_rss_eligible", period) + couple = person.marital_unit.sum(eligible) == 2 + earned = max_( + person("ssi_earned_income", period) + - person("ssi_blind_or_disabled_working_student_exclusion", period), + 0, + ) + # Interest and dividends are excluded under 5160:1-3-03.2(D)(2)(v). + unearned = max_( + person("ssi_unearned_income", period) + - person("interest_income", period) + - person("dividend_income", period), + 0, + ) + earned = where(couple, person.marital_unit.sum(earned), earned) + unearned = where(couple, person.marital_unit.sum(unearned), unearned) + # NOTE: PolicyEngine's federal SSI still counts interest and dividends + # (#10079), so for an SSI recipient with that income the SSI counted + # here is understated and RSS is overstated by the same amount. + # NOTE: SSI's joint-claim test reads only aged, blind or disabled + # status, so a spouse who declines SSI is still budgeted jointly + # (#10138). + federal_ssi = person("ssi", period) + federal_ssi = where(couple, person.marital_unit.sum(federal_ssi), federal_ssi) + # Ohio applies the same $20 general, $65 earned and one-half earned + # income exclusions (5160:1-3-03.2(D)(2)(l), (E)(5), (E)(7)), so the SSI + # exclusion parameters are used and an SSI reform carries over to RSS. + # They apply once per couple. SSI is needs-based income and does not + # receive the general $20 disregard (D)(2)(l). + countable = ( + _apply_ssi_exclusions( + earned * MONTHS_IN_YEAR, unearned * MONTHS_IN_YEAR, parameters, period + ) + / MONTHS_IN_YEAR + + federal_ssi + ) + # NOTE: 5160:1-5-01(E)(2) determines a couple's eligibility + # "collectively, utilizing the appropriate couple income standard as + # defined in rule 5160:1-3-03.5", and 5160:1-3-03.5(B)(2) defines that + # standard as "the current supplemental security income (SSI) benefit + # rate for a couple". Read literally, a couple whose only income is SSI + # would be at that standard and ineligible under (E)(4)(b). Ohio + # publishes only the RSS income standard of 5160:1-5-01(B)(2), so the + # model compares pooled countable income with twice the RSS income + # standard, which pays each spouse the RSS income standard less half + # the pooled income. When only one spouse qualifies, (E)(3) uses "the + # appropriate individual income standard as defined in rule + # 5160:1-3-03.5", the SSI individual rate. The model compares that + # spouse's own countable income with the RSS income standard instead. + return where(couple, countable / 2, countable) diff --git a/policyengine_us/variables/gov/states/oh/dbh/rss/oh_rss_eligible.py b/policyengine_us/variables/gov/states/oh/dbh/rss/oh_rss_eligible.py new file mode 100644 index 00000000000..c8e8d5d658f --- /dev/null +++ b/policyengine_us/variables/gov/states/oh/dbh/rss/oh_rss_eligible.py @@ -0,0 +1,41 @@ +from policyengine_us.model_api import * + + +class oh_rss_eligible(Variable): + value_type = bool + entity = Person + label = "Meets Ohio RSS categorical and placement requirements" + definition_period = MONTH + defined_for = StateCode.OH + documentation = "Medicaid enrollment comes from medicaid_enrolled. A candidate who meets SSI's criteria other than income but whose income is too high for SSI meets the Medicaid requirement through Ohio's group for people who receive only a state supplement, even when medicaid_enrolled is entered as false. If you enter medicaid_enrolled, enter it for every person in the simulation; an entry for one person sets everyone else to false." + reference = ( + "https://codes.ohio.gov/ohio-administrative-code/rule-5160:1-5-01", + "https://codes.ohio.gov/ohio-administrative-code/rule-5122-36-02", + # PDF pages 17-18 + "https://www.medicaid.gov/State-resource-center/Medicaid-State-Plan-Amendments/Downloads/OH/OH-15-015.pdf#page=17", + ) + + def formula(person, period, parameters): + p = parameters(period).gov.states.oh.dbh.rss + # 5160:1-5-01(C)(1)(a) requires eligibility for medical assistance. + # PolicyEngine's Ohio aged, blind or disabled Medicaid income limit is + # the SSI benefit rate (5160:1-3-03.5(B)(3)). Ohio's state plan also + # covers, under 42 CFR 435.232, people who receive only a state + # supplement and "would be eligible for SSI except for the level of + # their income", including aged, blind and disabled individuals in + # group living arrangements (SPA 15-015, Attachment 2.2-A, pages + # 15-16). RSS pays the difference between countable income and its + # income standard in cash, so a candidate who meets SSI's other + # criteria but whose income is too high for SSI meets the condition. + medicaid = person( + "medicaid_enrolled_for_ssi_state_supplement", period.this_year + ) + state_supplement_only = person("is_ssi_eligible", period) & ( + person("uncapped_ssi", period) <= 0 + ) + return ( + person("oh_rss_certified", period) + & (person("age", period.this_year) >= p.minimum_age) + & person("is_ssi_aged_blind_disabled", period.this_year) + & (medicaid | state_supplement_only) + ) diff --git a/policyengine_us/variables/gov/states/oh/dbh/rss/oh_rss_partial_month_days.py b/policyengine_us/variables/gov/states/oh/dbh/rss/oh_rss_partial_month_days.py new file mode 100644 index 00000000000..8e6c6bbeeee --- /dev/null +++ b/policyengine_us/variables/gov/states/oh/dbh/rss/oh_rss_partial_month_days.py @@ -0,0 +1,16 @@ +from policyengine_us.model_api import * + + +class oh_rss_partial_month_days(Variable): + value_type = int + entity = Person + label = "Ohio RSS days of placement in a partial first month" + definition_period = MONTH + defined_for = StateCode.OH + default_value = 0 + documentation = "For a first month in which the person moves into the RSS living arrangement after the first day, the days from the move-in day through the last day of the month. Leave at 0 for a full month." + reference = "https://codes.ohio.gov/ohio-administrative-code/rule-5160:1-5-01" + + def formula(person, period, parameters): + # A formula stops a one-month input from carrying into later months. + return 0 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 4613c4da26a..e2e80eef120 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 @@ -28,6 +28,7 @@ def formula(spm_unit, period, parameters): "nc_sa", # North Carolina benefits "mt_ssp", # Montana benefits "ok_ssp", # Oklahoma benefits + "oh_rss", # Ohio benefits "ny_ssp", # New York benefits "ri_ssp", # Rhode Island benefits "sd_ssp", # South Dakota benefits