From 0b69127ff73004cf193bb56b4e6bc053af3cb291 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 13 Nov 2026 20:15:55 -0500 Subject: [PATCH] Correct Ohio Works First initial income boundaries --- changelog.d/history-oh-owf-fpg-2017.fixed.md | 1 + .../gov/states/oh/odjfs/owf/oh_owf_fpg.yaml | 98 ++++++++++++++++++- .../owf/oh_owf_initial_income_eligible.yaml | 89 +++++++++++++++-- .../oh_owf_initial_income_eligible.py | 10 +- .../gov/states/oh/odjfs/owf/oh_owf_fpg.py | 20 +++- 5 files changed, 202 insertions(+), 16 deletions(-) create mode 100644 changelog.d/history-oh-owf-fpg-2017.fixed.md diff --git a/changelog.d/history-oh-owf-fpg-2017.fixed.md b/changelog.d/history-oh-owf-fpg-2017.fixed.md new file mode 100644 index 00000000000..3f7899d636a --- /dev/null +++ b/changelog.d/history-oh-owf-fpg-2017.fixed.md @@ -0,0 +1 @@ +Correct Ohio Works First initial-income eligibility to include the poverty-guideline boundary and use the exact 2016 household-size table at the existing July cutover. diff --git a/policyengine_us/tests/policy/baseline/gov/states/oh/odjfs/owf/oh_owf_fpg.yaml b/policyengine_us/tests/policy/baseline/gov/states/oh/odjfs/owf/oh_owf_fpg.yaml index 51a49fca576..ab88236370b 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/oh/odjfs/owf/oh_owf_fpg.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/oh/odjfs/owf/oh_owf_fpg.yaml @@ -2,9 +2,7 @@ # For 2024-01 (January): month < 7, uses July 1, 2023 FPL # 2023 FPL (contiguous US): first_person = $14,580, additional_person = $5,140 # -# NOTE: Cannot test July+ periods because PolicyEngine test system only supports -# 2024-01 (first month) or 2024 (whole year) formats. The July 1 effective date -# logic in the variable is correct, but tests must use January periods. +# Non-January controls below explicitly key YEAR-defined inputs by year. - name: Case 1, January 2024 one person uses 2023 FPL. period: 2024-01 @@ -71,3 +69,97 @@ # 2024 FPL: 15,060 + 5,380 * 2 = 25,820 # Monthly: 25,820 / 12 = 2,151.67 oh_owf_fpg: 2_151.67 + +- name: Case 7, Source-bound 2017 January literal guideline rows and extensions. + period: 2017-01 + absolute_error_margin: 0.01 + input: + spm_unit_size: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + state_code: [OH, OH, OH, OH, OH, OH, OH, OH, OH, OH] + output: + # HHS 81 FR 4036: eight literal 2016 annual rows; add $4,160 only after 8. + # ORC 5107.10(D)(2), effective 2013: January 2017 still uses 2016 guidelines. + oh_owf_fpg: [990, 1_335, 1_680, 2_025, 2_370, 2_715, 3_060.833333, 3_407.5, 3_754.166667, 4_100.833333] + +- name: Case 8, June 2017 uses the exact prior guideline with current household size. + period: 2017-06 + absolute_error_margin: 0.01 + input: + spm_unit_size: + 2016: [1, 1, 1, 1] + 2017: [1, 3, 8, 9] + state_code: + 2017: [OH, OH, OH, OH] + output: + # The July date selects the guideline, not prior-year household membership. + oh_owf_fpg: [990, 1_680, 3_407.5, 3_754.166667] + +- name: Case 9, July 2017 leaves the literal 2016 table and preserves monthly arithmetic. + period: 2017-07 + absolute_error_margin: 0.01 + input: + spm_unit_size: + 2017: [3, 8, 9] + state_code: + 2017: [OH, OH, OH] + output: + # Existing 2017 linear parameters: ($12,060 + $4,180 x (size - 1)) / 12. + # This is a compatibility control, not a new legal audit of the 2017 table. + oh_owf_fpg: [1_701.666667, 3_443.333333, 3_791.666667] + +- name: Case 10, June 2016 preserves the prior guideline arithmetic. + period: 2016-06 + absolute_error_margin: 0.01 + input: + spm_unit_size: + 2016: [3, 8] + state_code: + 2016: [OH, OH] + output: + # Existing 2015 parameters; preservation rather than fresh certification. + oh_owf_fpg: [1_674.166667, 3_407.5] + +- name: Case 11, July 2016 starts the literal guideline window. + period: 2016-07 + absolute_error_margin: 0.01 + input: + spm_unit_size: + 2016: 3 + state_code: + 2016: OH + output: + # The statutory July adoption follows the HHS January 2016 notice. + oh_owf_fpg: 1_680 + +- name: Case 12, invalid household sizes preserve the prior monthly fallback. + period: 2017-01 + absolute_error_margin: 0.01 + input: + # Component compatibility inputs, not demographic household claims. + spm_unit_size: [0, -1, 3] + state_code: [OH, OH, OH] + output: + oh_owf_fpg: [643.333333, 296.666667, 1_680] + +- name: Case 13, mixed geography placeholders preserve the prior monthly fallback. + period: 2017-01 + absolute_error_margin: 0.01 + input: + # Deliberate component geography overrides; no Ohio/territory policy claim. + spm_unit_size: [3, 3, 3, 3] + state_code: [OH, OH, OH, OH] + state_group_str: [CONTIGUOUS_US, GU, PR, VI] + output: + # Territories retain existing zero placeholders, not published HHS amounts. + oh_owf_fpg: [1_680, 0, 0, 0] + +- name: Case 14, annual 2017 FPG component aggregates both July-selected schedules. + period: 2017 + absolute_error_margin: 0.01 + input: + spm_unit_size: 3 + state_code: OH + output: + # Six months x $1,680 plus six x ($20,420 / 12) = $20,290. + # FPG component only; no annual cash or complete eligibility claim. + oh_owf_fpg: 20_290 diff --git a/policyengine_us/tests/policy/baseline/gov/states/oh/odjfs/owf/oh_owf_initial_income_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/oh/odjfs/owf/oh_owf_initial_income_eligible.yaml index 56aba0973f3..a0300d4175b 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/oh/odjfs/owf/oh_owf_initial_income_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/oh/odjfs/owf/oh_owf_initial_income_eligible.yaml @@ -1,12 +1,13 @@ # Edge case tests for oh_owf_initial_income_eligible -# Tests initial income eligibility: gross income < (FPG * 50%) +# Tests initial income eligibility: gross income <= (FPG * 50%) # # Formula inputs: # - tanf_gross_earned_income + tanf_gross_unearned_income = gross income # - oh_owf_fpg (monthly federal poverty guideline) # - income_limit_rate = 0.5 (50% of FPL) # -# Uses strict less than (<), so income at threshold is NOT eligible +# ORC 5107.10(D)(1)(a)-(b) allows equality at this initial gross-income gate. +# The separate countable-income/payment-standard comparison remains strict. - name: Case 1, zero income is eligible. period: 2025-01 @@ -42,17 +43,16 @@ # 850 < 851.67 = true oh_owf_initial_income_eligible: true -- name: Case 4, income exactly at 50 percent FPL threshold is not eligible. +- name: Case 4, income exactly at 50 percent FPL threshold is eligible. period: 2025-01 - absolute_error_margin: 0.01 input: tanf_gross_earned_income: 851.665 tanf_gross_unearned_income: 0 oh_owf_fpg: 1_703.33 state_code: OH output: - # 851.665 is NOT < 851.665 (strict less than) - oh_owf_initial_income_eligible: false + # 851.665 equals the supplied component FPG times 50%; equality is allowed. + oh_owf_initial_income_eligible: true - name: Case 5, income one cent above threshold is not eligible. period: 2025-01 @@ -78,7 +78,6 @@ - name: Case 7, single person household threshold test. period: 2025-01 - absolute_error_margin: 0.01 input: tanf_gross_earned_income: 627.5 tanf_gross_unearned_income: 0 @@ -86,8 +85,8 @@ state_code: OH output: # Threshold = 1255 * 0.5 = 627.5 - # 627.5 is NOT < 627.5 (strict less than) - oh_owf_initial_income_eligible: false + # 627.5 equals the supplied component FPG times 50%; equality is allowed. + oh_owf_initial_income_eligible: true - name: Case 8, single person just below threshold is eligible. period: 2025-01 @@ -158,3 +157,75 @@ # Threshold = 3496.67 * 0.5 = 1748.335 # 1748.34 is NOT < 1748.335 (strict less than) oh_owf_initial_income_eligible: false + +- name: Case 14, Source-bound 2017 isolated inclusive initial-gross component. + period: 2017-01 + input: + # Component fixture deliberately supplies the source FPG to isolate <=. + tanf_gross_earned_income: 840 + tanf_gross_unearned_income: 0 + oh_owf_fpg: 1_680 + state_code: OH + output: + # ORC 5107.10(D)(1)(a)-(b): adjusted gross income not exceeding 50% passes. + oh_owf_initial_income_eligible: true + +- name: Case 15, Source-bound 2017 actual wages of 841 test initial-gross boundary. + period: 2017-01 + input: + people: + person1: + age: 35 + employment_income_before_lsr: + 2017: 10_092 + person2: + age: 8 + is_tax_unit_dependent: true + person3: + age: 5 + is_tax_unit_dependent: true + spm_units: + spm_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: OH + output: + # First-time applicant; constant actual wages $841/month, no required gross + # exclusions. This checks only the initial financial gate, not cash/enrollment. + # Source FPG $20,160/year / 12 = $1,680; half is exactly $840. + oh_owf_initial_income_eligible: false + oh_owf_fpg: 1_680 + tanf_gross_earned_income: [841, 0, 0] + tanf_gross_unearned_income: [0, 0, 0] + +- name: Case 16, Source-bound 2017 actual wages of 840 test initial-gross boundary. + period: 2017-01 + input: + people: + person1: + age: 35 + employment_income_before_lsr: + 2017: 10_080 + person2: + age: 8 + is_tax_unit_dependent: true + person3: + age: 5 + is_tax_unit_dependent: true + spm_units: + spm_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: OH + output: + # First-time applicant; constant actual wages $840/month, no required gross + # exclusions. This checks only the initial financial gate, not cash/enrollment. + # Source FPG $20,160/year / 12 = $1,680; half is exactly $840. + oh_owf_initial_income_eligible: true + oh_owf_fpg: 1_680 + tanf_gross_earned_income: [840, 0, 0] + tanf_gross_unearned_income: [0, 0, 0] diff --git a/policyengine_us/variables/gov/states/oh/odjfs/owf/eligibility/oh_owf_initial_income_eligible.py b/policyengine_us/variables/gov/states/oh/odjfs/owf/eligibility/oh_owf_initial_income_eligible.py index ffd05b16b6a..7b54720d7fe 100644 --- a/policyengine_us/variables/gov/states/oh/odjfs/owf/eligibility/oh_owf_initial_income_eligible.py +++ b/policyengine_us/variables/gov/states/oh/odjfs/owf/eligibility/oh_owf_initial_income_eligible.py @@ -8,6 +8,10 @@ class oh_owf_initial_income_eligible(Variable): definition_period = MONTH defined_for = StateCode.OH reference = ( + "https://codes.ohio.gov/ohio-revised-code/section-5107.10/9-29-2013", + "https://codes.ohio.gov/ohio-revised-code/section-5107.10/9-29-2017", + "https://codes.ohio.gov/ohio-revised-code/section-5107.10/9-30-2021", + "https://codes.ohio.gov/ohio-revised-code/section-5107.10", "https://codes.ohio.gov/ohio-administrative-code/rule-5101:1-23-20", "https://dam.assets.ohio.gov/image/upload/jfs.ohio.gov/OWF/tanf/2024%20TANF%20State%20Plan%20Combined.pdf#page=4", ) @@ -25,6 +29,6 @@ def formula(spm_unit, period, parameters): p = parameters(period).gov.states.oh.odjfs.owf.initial_eligibility income_limit = fpg * p.income_limit_rate - # Initial eligibility test: gross income must be below limit - # Per ORC 5107.10(D)(1) and ODJFS Payment Standards Table - return gross_income < income_limit + # ORC 5107.10(D)(1)(a)-(b) permits initial gross income at the limit. + # The separate countable-income/payment-standard test remains strict. + return gross_income <= income_limit diff --git a/policyengine_us/variables/gov/states/oh/odjfs/owf/oh_owf_fpg.py b/policyengine_us/variables/gov/states/oh/odjfs/owf/oh_owf_fpg.py index c9ea7c605d7..bb55abe59de 100644 --- a/policyengine_us/variables/gov/states/oh/odjfs/owf/oh_owf_fpg.py +++ b/policyengine_us/variables/gov/states/oh/odjfs/owf/oh_owf_fpg.py @@ -1,4 +1,5 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.hhs.tax_unit_fpg import fpg class oh_owf_fpg(Variable): @@ -11,6 +12,8 @@ class oh_owf_fpg(Variable): reference = ( "https://dam.assets.ohio.gov/image/upload/jfs.ohio.gov/OWF/tanf/2024%20TANF%20State%20Plan%20Combined.pdf#page=4", "https://codes.ohio.gov/ohio-revised-code/section-5107.10", # (D)(2) + "https://codes.ohio.gov/ohio-revised-code/section-5107.10/9-29-2013", + "https://www.govinfo.gov/content/pkg/FR-2016-01-25/pdf/2016-01450.pdf#page=1", ) def formula(spm_unit, period, parameters): @@ -29,4 +32,19 @@ def formula(spm_unit, period, parameters): p_fpg = parameters(instant_str).gov.hhs.fpg p1 = p_fpg.first_person[state_group] / MONTHS_IN_YEAR pn = p_fpg.additional_person[state_group] / MONTHS_IN_YEAR - return p1 + pn * (n - 1) + monthly_fpg = p1 + pn * (n - 1) + table = parameters(instant_str).gov.hhs.fpg_table + if not table.in_effect: + return monthly_fpg + + # Preserve the prior monthly operation order outside the source domain. + valid_size = ( + np.isfinite(n) & (n >= table.min_household_size) & (n == np.floor(n)) + ) + has_table = np.isin(state_group, list(table.amount)) + annual_fpg = fpg(n, state_group, instant_str, parameters) + return where( + valid_size & has_table, + annual_fpg / MONTHS_IN_YEAR, + monthly_fpg, + )