Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/history-oh-owf-fpg-2017.fixed.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand All @@ -78,16 +78,15 @@

- 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
oh_owf_fpg: 1_255 # Single person monthly FPG (15060/12)
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
Expand Down Expand Up @@ -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]
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)
Expand All @@ -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
20 changes: 19 additions & 1 deletion policyengine_us/variables/gov/states/oh/odjfs/owf/oh_owf_fpg.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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):
Expand All @@ -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,
)