diff --git a/changelog_entry.yaml b/changelog_entry.yaml index e69de29bb2d..65926855c36 100644 --- a/changelog_entry.yaml +++ b/changelog_entry.yaml @@ -0,0 +1,4 @@ +- bump: minor + changes: + added: + - Iowa Family Investment Program (FIP), the state's TANF cash assistance program. diff --git a/policyengine_us/parameters/gov/states/ia/dhs/fip/income/earned_income_deduction.yaml b/policyengine_us/parameters/gov/states/ia/dhs/fip/income/earned_income_deduction.yaml new file mode 100644 index 00000000000..172343734d7 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/dhs/fip/income/earned_income_deduction.yaml @@ -0,0 +1,12 @@ +description: Iowa deducts this percentage of the gross nonexempt earned income under the Family Investment Program. + +values: + 1997-01-01: 0.2 + +metadata: + unit: /1 + period: month + label: Iowa FIP earned income deduction rate + reference: + - title: Iowa Administrative Code 441-41.27(239B) - Income, 41.27(2)(a) + href: https://www.legis.iowa.gov/docs/iac/chapter/01-07-2026.441.41.pdf#page=21 diff --git a/policyengine_us/parameters/gov/states/ia/dhs/fip/income/gross_income_limit_percent.yaml b/policyengine_us/parameters/gov/states/ia/dhs/fip/income/gross_income_limit_percent.yaml new file mode 100644 index 00000000000..781963b08dd --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/dhs/fip/income/gross_income_limit_percent.yaml @@ -0,0 +1,12 @@ +description: Iowa sets the gross income limit for the first income eligibility test as this percentage of the standard of need under the Family Investment Program. + +values: + 1997-01-01: 1.85 + +metadata: + unit: /1 + period: month + label: Iowa FIP gross income limit percent of standard of need + reference: + - title: Iowa Administrative Code 441-41.27(239B) - Income, (2) + href: https://www.legis.iowa.gov/docs/iac/chapter/01-07-2026.441.41.pdf#page=20 diff --git a/policyengine_us/parameters/gov/states/ia/dhs/fip/income/work_incentive_disregard.yaml b/policyengine_us/parameters/gov/states/ia/dhs/fip/income/work_incentive_disregard.yaml new file mode 100644 index 00000000000..d47abbb3595 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/dhs/fip/income/work_incentive_disregard.yaml @@ -0,0 +1,15 @@ +description: Iowa deducts this rate of work incentive disregard after the earned income deduction under the Family Investment Program. + +values: + 1997-01-01: 0.5 + 2007-08-01: 0.58 + +metadata: + unit: /1 + period: month + label: Iowa FIP work incentive disregard rate + reference: + - title: Iowa Administrative Code 441-41.27(239B) - Income, 41.27(2)(c) + href: https://www.legis.iowa.gov/docs/iac/chapter/01-07-2026.441.41.pdf#page=21 + - title: Iowa TANF State Plan 2025, Section (3) + href: https://hhs.iowa.gov/media/17445/download?inline#page=7 diff --git a/policyengine_us/parameters/gov/states/ia/dhs/fip/max_unit_size.yaml b/policyengine_us/parameters/gov/states/ia/dhs/fip/max_unit_size.yaml new file mode 100644 index 00000000000..dfaf8d43f48 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/dhs/fip/max_unit_size.yaml @@ -0,0 +1,14 @@ +description: Iowa sets this maximum unit size for determining standard of need and payment standards under the Family Investment Program. + +values: + 1997-01-01: 10 + +metadata: + unit: person + period: year + label: Iowa FIP maximum unit size for standards lookup + reference: + - title: Iowa Administrative Code 441-41.28(239B) - Need Standards, 41.28(2) + href: https://www.legis.iowa.gov/docs/iac/chapter/01-07-2026.441.41.pdf#page=31 + - title: Iowa TANF State Plan 2025, Section (3) + href: https://hhs.iowa.gov/media/17445/download?inline#page=7 diff --git a/policyengine_us/parameters/gov/states/ia/dhs/fip/need_standard/additional.yaml b/policyengine_us/parameters/gov/states/ia/dhs/fip/need_standard/additional.yaml new file mode 100644 index 00000000000..ac6231c785e --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/dhs/fip/need_standard/additional.yaml @@ -0,0 +1,12 @@ +description: Iowa sets this value as the additional standard of need amount per person for families larger than the max unit size under the Family Investment Program. + +metadata: + unit: currency-USD + period: month + label: Iowa FIP additional standard of need per person + reference: + - title: Iowa Administrative Code 441-41.28(239B) - Need Standards, 41.28(2) + href: https://www.legis.iowa.gov/docs/iac/chapter/01-07-2026.441.41.pdf#page=31 + +values: + 1997-01-01: 173 diff --git a/policyengine_us/parameters/gov/states/ia/dhs/fip/need_standard/main.yaml b/policyengine_us/parameters/gov/states/ia/dhs/fip/need_standard/main.yaml new file mode 100644 index 00000000000..422eb6c8f89 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/dhs/fip/need_standard/main.yaml @@ -0,0 +1,34 @@ +description: Iowa sets these values as the standard of need amount, or the schedule of living costs, by family size under the Family Investment Program. + +1: + 1997-01-01: 365 +2: + 1997-01-01: 719 +3: + 1997-01-01: 849 +4: + 1997-01-01: 986 +5: + 1997-01-01: 1_092 +6: + 1997-01-01: 1_216 +7: + 1997-01-01: 1_335 +8: + 1997-01-01: 1_457 +9: + 1997-01-01: 1_576 +10: + 1997-01-01: 1_724 + +metadata: + unit: currency-USD + period: month + breakdown: + - range(1, 11) + breakdown_labels: + - Assistance unit size + label: Iowa FIP standard of need + reference: + - title: Iowa Administrative Code 441-41.28(239B) - Need Standards, 41.28(2) + href: https://www.legis.iowa.gov/docs/iac/chapter/01-07-2026.441.41.pdf#page=31 diff --git a/policyengine_us/parameters/gov/states/ia/dhs/fip/payment_standard/additional.yaml b/policyengine_us/parameters/gov/states/ia/dhs/fip/payment_standard/additional.yaml new file mode 100644 index 00000000000..5d606aafc02 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/dhs/fip/payment_standard/additional.yaml @@ -0,0 +1,14 @@ +description: Iowa sets this value as the additional payment standard amount per person for families larger than the max unit size under the Family Investment Program. + +values: + 1997-01-01: 87 + +metadata: + unit: currency-USD + period: month + label: Iowa FIP additional payment standard per person + reference: + - title: Iowa Administrative Code 441-41.28(239B) - Need Standards, 41.28(2) + href: https://www.legis.iowa.gov/docs/iac/chapter/01-07-2026.441.41.pdf#page=31 + - title: Iowa TANF State Plan 2025, Section (3) + href: https://hhs.iowa.gov/media/17445/download?inline#page=7 diff --git a/policyengine_us/parameters/gov/states/ia/dhs/fip/payment_standard/main.yaml b/policyengine_us/parameters/gov/states/ia/dhs/fip/payment_standard/main.yaml new file mode 100644 index 00000000000..9064794ebab --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/dhs/fip/payment_standard/main.yaml @@ -0,0 +1,36 @@ +description: Iowa sets these values as the payment standard, or the schedule of basic needs, by family size under the Family Investment Program. + +1: + 1997-01-01: 183 +2: + 1997-01-01: 361 +3: + 1997-01-01: 426 +4: + 1997-01-01: 495 +5: + 1997-01-01: 548 +6: + 1997-01-01: 610 +7: + 1997-01-01: 670 +8: + 1997-01-01: 731 +9: + 1997-01-01: 791 +10: + 1997-01-01: 865 + +metadata: + unit: currency-USD + period: month + breakdown: + - range(1, 11) + breakdown_labels: + - Assistance unit size + label: Iowa FIP payment standard + reference: + - title: Iowa Administrative Code 441-41.28(239B) - Need Standards, 41.28(2) + href: https://www.legis.iowa.gov/docs/iac/chapter/01-07-2026.441.41.pdf#page=31 + - title: Iowa TANF State Plan 2025, Section (3) + href: https://hhs.iowa.gov/media/17445/download?inline#page=7 diff --git a/policyengine_us/parameters/gov/states/ia/dhs/fip/resources/applicant_limit.yaml b/policyengine_us/parameters/gov/states/ia/dhs/fip/resources/applicant_limit.yaml new file mode 100644 index 00000000000..e0804c5005c --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/dhs/fip/resources/applicant_limit.yaml @@ -0,0 +1,14 @@ +description: Iowa limits resources to this amount under the Family Investment Program (FIP) for applicant families. + +values: + 1997-01-01: 2_000 + +metadata: + unit: currency-USD + period: month + label: Iowa FIP applicant resource limit + reference: + - title: Iowa Administrative Code 441-41.26(239B) - Resources, 41.26(1)(e) + href: https://www.legis.iowa.gov/docs/iac/rule/441.41.26.pdf#page=1 + - title: Iowa TANF State Plan 2025, Section (3) + href: https://hhs.iowa.gov/media/17445/download?inline#page=7 diff --git a/policyengine_us/parameters/gov/states/ia/dhs/fip/resources/recipient_limit.yaml b/policyengine_us/parameters/gov/states/ia/dhs/fip/resources/recipient_limit.yaml new file mode 100644 index 00000000000..2f6bd2b4278 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/dhs/fip/resources/recipient_limit.yaml @@ -0,0 +1,14 @@ +description: Iowa limits resources to this amount under the Family Investment Program for recipient families (received FIP in prior month). + +values: + 1997-01-01: 5_000 + +metadata: + unit: currency-USD + period: month + label: Iowa FIP recipient resource limit + reference: + - title: Iowa Administrative Code 441-41.26(239B) - Resources, 41.26(1)(e) + href: https://www.legis.iowa.gov/docs/iac/rule/441.41.26.pdf#page=1 + - title: Iowa TANF State Plan 2025, Section (3) + href: https://hhs.iowa.gov/media/17445/download?inline#page=7 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/dhs/fip/ia_fip.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/dhs/fip/ia_fip.yaml new file mode 100644 index 00000000000..39ac1dbe34f --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/dhs/fip/ia_fip.yaml @@ -0,0 +1,379 @@ +# Iowa TANF (Family Investment Program) benefit tests +# Per 441 IAC 41.27(239B) and 41.28(239B) + +# Unit tests - isolate core benefit calculation logic + +- name: Unit test - eligible household receives benefit + period: 2025 + input: + ia_fip_payment_standard: 5_112 + ia_fip_countable_income: 3_225.60 + ia_fip_eligible: true + state_code: IA + output: + ia_fip: 1_886.40 + +- name: Unit test - countable income exceeds payment standard + period: 2025 + input: + ia_fip_payment_standard: 5_112 + ia_fip_countable_income: 6_000 + ia_fip_eligible: true + state_code: IA + output: + ia_fip: 0 + +- name: Unit test - ineligible household receives no benefit + period: 2025 + input: + ia_fip_payment_standard: 5_112 + ia_fip_countable_income: 3_000 + ia_fip_eligible: false + state_code: IA + output: + ia_fip: 0 + +# Integration tests - full calculation with realistic household structures + +- name: Integration test - family of 3 with $800/month earnings + # Example from 441 IAC 41.27(239B) documentation + # Family of 3 with one employed parent earning $800/month gross + # Calculations (annual): + # - Gross Income: $9,600 + # - 20% earned income deduction: $9,600 * 0.20 = $1,920 + # - Net after 20%: $9,600 - $1,920 = $7,680 + # - 58% work incentive: $7,680 * 0.58 = $4,454.40 + # - Countable income: $7,680 - $4,454.40 = $3,225.60 + # - Payment standard for 3: $426/month * 12 = $5,112/year + # - Benefit: $5,112 - $3,225.60 = $1,886.40/year + period: 2025 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + employment_income_before_lsr: 9_600 + is_tax_unit_head_or_spouse: true + child1: + age: 5 + employment_income_before_lsr: 0 + is_tax_unit_dependent: true + child2: + age: 3 + employment_income_before_lsr: 0 + is_tax_unit_dependent: true + spm_units: + spm_unit: + members: [parent, child1, child2] + tax_units: + tax_unit: + members: [parent, child1, child2] + households: + household: + members: [parent, child1, child2] + state_code: IA + output: + ia_fip_standard_of_need: 10_188 + ia_fip_payment_standard: 5_112 + ia_fip_gross_income: 9_600 + ia_fip_countable_income: 3_225.60 + ia_fip_income_eligible: true + ia_fip_eligible: true + ia_fip: 1_886.40 + +- name: Integration test - family exceeds gross income limit + # Family of 2 with $2,000/month earnings + # Standard of need for 2: $719/month * 12 = $8,628/year + # Gross income limit: $8,628 * 1.85 = $15,961.80/year + # Gross income: $24,000/year > $15,961.80 - FAIL Test 1 + period: 2025 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + employment_income_before_lsr: 24_000 + is_tax_unit_head_or_spouse: true + child: + age: 5 + employment_income_before_lsr: 0 + is_tax_unit_dependent: true + spm_units: + spm_unit: + members: [parent, child] + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: IA + output: + ia_fip_standard_of_need: 8_628 + ia_fip_gross_income: 24_000 + ia_fip_income_eligible: false + ia_fip_eligible: false + ia_fip: 0 + +- name: Integration test - zero income household receives max benefit + # Family of 2 with zero income + # Payment standard for 2: $361/month * 12 = $4,332/year + # Countable income: $0 + # Benefit: $4,332 - $0 = $4,332/year + period: 2025 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + employment_income_before_lsr: 0 + is_tax_unit_head_or_spouse: true + child: + age: 5 + employment_income_before_lsr: 0 + is_tax_unit_dependent: true + spm_units: + spm_unit: + members: [parent, child] + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: IA + output: + ia_fip_standard_of_need: 8_628 + ia_fip_payment_standard: 4_332 + ia_fip_gross_income: 0 + ia_fip_countable_income: 0 + ia_fip_income_eligible: true + ia_fip_eligible: true + ia_fip: 4_332 + +- name: Integration test - unearned income only (no deductions) + # Family of 2 with $400/month unearned income + # Unearned income does not get the 20% deduction or 58% work incentive + # Standard of need for 2: $719/month * 12 = $8,628/year + # Payment standard for 2: $361/month * 12 = $4,332/year + # Gross income: $4,800/year + # Gross income limit: $8,628 * 1.85 = $15,961.80 > $4,800 - PASS Test 1 + # Net income: $4,800 < $8,628 - PASS Test 2 + # Countable income: $4,800 (no earned income deductions apply) + # Countable income $4,800 > payment standard $4,332 - FAIL Test 3 + period: 2025 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + employment_income_before_lsr: 0 + social_security: 4_800 + is_tax_unit_head_or_spouse: true + child: + age: 5 + employment_income_before_lsr: 0 + is_tax_unit_dependent: true + spm_units: + spm_unit: + members: [parent, child] + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: IA + output: + ia_fip_standard_of_need: 8_628 + ia_fip_payment_standard: 4_332 + ia_fip_gross_income: 4_800 + ia_fip_countable_income: 4_800 + ia_fip_income_eligible: false + ia_fip_eligible: false + ia_fip: 0 + +- name: Integration test - mixed earned and unearned income + # Family of 3 with $500/month earned and $200/month unearned + # Standard of need for 3: $849/month * 12 = $10,188/year + # Payment standard for 3: $426/month * 12 = $5,112/year + # Gross earned: $6,000/year + # Gross unearned: $2,400/year + # Gross income: $8,400/year + # Gross income limit: $10,188 * 1.85 = $18,847.80 > $8,400 - PASS Test 1 + # 20% deduction: $6,000 * 0.20 = $1,200 + # Net income: $8,400 - $1,200 = $7,200 < $10,188 - PASS Test 2 + # Remaining earned: $6,000 - $1,200 = $4,800 + # 58% work incentive: $4,800 * 0.58 = $2,784 + # Countable earned: $4,800 - $2,784 = $2,016 + # Countable income: $2,016 + $2,400 = $4,416 < $5,112 - PASS Test 3 + # Benefit: $5,112 - $4,416 = $696/year + period: 2025 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + employment_income_before_lsr: 6_000 + social_security: 2_400 + is_tax_unit_head_or_spouse: true + child1: + age: 5 + employment_income_before_lsr: 0 + is_tax_unit_dependent: true + child2: + age: 3 + employment_income_before_lsr: 0 + is_tax_unit_dependent: true + spm_units: + spm_unit: + members: [parent, child1, child2] + tax_units: + tax_unit: + members: [parent, child1, child2] + households: + household: + members: [parent, child1, child2] + state_code: IA + output: + ia_fip_standard_of_need: 10_188 + ia_fip_payment_standard: 5_112 + ia_fip_gross_income: 8_400 + ia_fip_countable_income: 4_416 + ia_fip_income_eligible: true + ia_fip_eligible: true + ia_fip: 696 + +- name: Integration test - single parent household + # Single parent with 1 child earning $600/month + # Standard of need for 2: $719/month * 12 = $8,628/year + # Payment standard for 2: $361/month * 12 = $4,332/year + # Gross earned: $7,200/year + # Gross income limit: $8,628 * 1.85 = $15,961.80 > $7,200 - PASS Test 1 + # 20% deduction: $7,200 * 0.20 = $1,440 + # Net income: $7,200 - $1,440 = $5,760 < $8,628 - PASS Test 2 + # Remaining earned: $7,200 - $1,440 = $5,760 + # 58% work incentive: $5,760 * 0.58 = $3,340.80 + # Countable income: $5,760 - $3,340.80 = $2,419.20 < $4,332 - PASS Test 3 + # Benefit: $4,332 - $2,419.20 = $1,912.80/year + period: 2025 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 25 + employment_income_before_lsr: 7_200 + is_tax_unit_head_or_spouse: true + child: + age: 2 + employment_income_before_lsr: 0 + is_tax_unit_dependent: true + spm_units: + spm_unit: + members: [parent, child] + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: IA + output: + ia_fip_standard_of_need: 8_628 + ia_fip_payment_standard: 4_332 + ia_fip_countable_income: 2_419.20 + ia_fip_income_eligible: true + ia_fip_eligible: true + ia_fip: 1_912.80 + +- name: Integration test - large family (over 10 members) + # Family of 11 members (at max cap 10 + 1 additional) + # Standard of need for 10: $1,724/month + # Additional per person: varies, let's check the additional parameter + # Payment standard for 10: $865/month + period: 2025 + absolute_error_margin: 0.1 + input: + people: + parent1: + age: 35 + employment_income_before_lsr: 0 + is_tax_unit_head_or_spouse: true + parent2: + age: 33 + employment_income_before_lsr: 0 + is_tax_unit_head_or_spouse: true + child1: + age: 16 + employment_income_before_lsr: 0 + is_tax_unit_dependent: true + child2: + age: 14 + employment_income_before_lsr: 0 + is_tax_unit_dependent: true + child3: + age: 12 + employment_income_before_lsr: 0 + is_tax_unit_dependent: true + child4: + age: 10 + employment_income_before_lsr: 0 + is_tax_unit_dependent: true + child5: + age: 8 + employment_income_before_lsr: 0 + is_tax_unit_dependent: true + child6: + age: 6 + employment_income_before_lsr: 0 + is_tax_unit_dependent: true + child7: + age: 4 + employment_income_before_lsr: 0 + is_tax_unit_dependent: true + child8: + age: 2 + employment_income_before_lsr: 0 + is_tax_unit_dependent: true + child9: + age: 1 + employment_income_before_lsr: 0 + is_tax_unit_dependent: true + spm_units: + spm_unit: + members: [parent1, parent2, child1, child2, child3, child4, child5, child6, child7, child8, child9] + tax_units: + tax_unit: + members: [parent1, parent2, child1, child2, child3, child4, child5, child6, child7, child8, child9] + households: + household: + members: [parent1, parent2, child1, child2, child3, child4, child5, child6, child7, child8, child9] + state_code: IA + output: + ia_fip_income_eligible: true + ia_fip_eligible: true + +- name: Integration test - non-Iowa state gets zero benefit + period: 2025 + input: + people: + parent: + age: 30 + employment_income_before_lsr: 0 + is_tax_unit_head_or_spouse: true + child: + age: 5 + employment_income_before_lsr: 0 + is_tax_unit_dependent: true + spm_units: + spm_unit: + members: [parent, child] + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: IL + output: + ia_fip: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/dhs/fip/ia_fip_countable_earned_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/dhs/fip/ia_fip_countable_earned_income.yaml new file mode 100644 index 00000000000..6858d14e142 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/dhs/fip/ia_fip_countable_earned_income.yaml @@ -0,0 +1,60 @@ +- name: Case 1, no earned income. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + state_code: IA + output: + ia_fip_countable_earned_income: 0 + +- name: Case 2, earned income with deductions. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income_before_lsr: 12_000 # $1,000/month + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + state_code: IA + output: + # Gross earned: $12,000/year = $1,000/month + # After 20% deduction: $1,000 * 0.8 = $800 + # After 58% work incentive disregard: $800 * 0.42 = $336 + # Annual: $336 * 12 = $4,032 + ia_fip_countable_earned_income: 4_032 + +- name: Case 3, higher earned income. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income_before_lsr: 24_000 # $2,000/month + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + state_code: IA + output: + # Gross earned: $24,000/year = $2,000/month + # After 20% deduction: $2,000 * 0.8 = $1,600 + # After 58% work incentive disregard: $1,600 * 0.42 = $672 + # Annual: $672 * 12 = $8,064 + ia_fip_countable_earned_income: 8_064 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/dhs/fip/ia_fip_countable_income_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/dhs/fip/ia_fip_countable_income_eligible.yaml new file mode 100644 index 00000000000..782a3da680e --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/dhs/fip/ia_fip_countable_income_eligible.yaml @@ -0,0 +1,149 @@ +# Test 3: Countable income < payment standard +# Countable income applies both 20% deduction AND 58% work incentive disregard + +- name: Case 1, no income. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: IA + output: + # Countable: $0 < $361 payment standard + ia_fip_countable_income_eligible: true + +- name: Case 2, moderate earned income passes. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income_before_lsr: 9_600 # $800/month + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: IA + output: + # Earned: $800/month + # After 20% deduction: $800 * 0.8 = $640 + # After 58% disregard: $640 * 0.42 = $268.80 + # Countable: $268.80 < $361 payment standard + ia_fip_countable_income_eligible: true + +- name: Case 3, higher earned income fails. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income_before_lsr: 12_000 # $1,000/month + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: IA + output: + # Earned: $1,000/month + # After 20% deduction: $1,000 * 0.8 = $800 + # After 58% disregard: $800 * 0.42 = $336 + # Plus unearned: $0 + # Countable: $336 < $361 payment standard + ia_fip_countable_income_eligible: true + +- name: Case 4, unearned income no disregard. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + social_security: 4_800 # $400/month unearned (SSI not counted in TANF) + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: IA + output: + # Unearned has no deductions + # Countable: $400 >= $361 payment standard + ia_fip_countable_income_eligible: false + +- name: Case 5, mixed income with disregard on earned only. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income_before_lsr: 6_000 # $500/month earned + social_security: 1_200 # $100/month unearned + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: IA + output: + # Earned: $500/month + # After 20% deduction: $500 * 0.8 = $400 + # After 58% disregard: $400 * 0.42 = $168 + # Plus unearned: $100 + # Countable: $168 + $100 = $268 < $361 + ia_fip_countable_income_eligible: true + +- name: Case 6, two-parent family with higher payment standard. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 35 + employment_income_before_lsr: 12_000 # $1,000/month + person2: + age: 32 + employment_income_before_lsr: 6_000 # $500/month + person3: + age: 8 + person4: + age: 5 + spm_units: + spm_unit: + members: [person1, person2, person3, person4] + households: + household: + members: [person1, person2, person3, person4] + state_code: IA + output: + # Family of 4: payment standard = $495 + # Total earned: $1,500/month + # After 20% deduction: $1,500 * 0.8 = $1,200 + # After 58% disregard: $1,200 * 0.42 = $504 + # Countable: $504 >= $495 payment standard + ia_fip_countable_income_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/dhs/fip/ia_fip_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/dhs/fip/ia_fip_eligible.yaml new file mode 100644 index 00000000000..69b65288868 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/dhs/fip/ia_fip_eligible.yaml @@ -0,0 +1,214 @@ +# Unit tests for Iowa FIP eligibility +# Requires: demographic eligibility (minor child), immigration eligibility, +# resource eligibility, and income eligibility +# Reference: https://www.legis.iowa.gov/docs/iac/chapter/01-07-2026.441.41.pdf + +- name: Case 1, eligible family with minor child. + period: 2025 + input: + people: + person1: + age: 30 + employment_income_before_lsr: 0 + is_tax_unit_head: true + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_assets: 1_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: IA + output: + ia_fip_eligible: true + +- name: Case 2, ineligible due to no minor child. + period: 2025 + input: + people: + person1: + age: 30 + employment_income_before_lsr: 0 + is_tax_unit_head: true + spm_units: + spm_unit: + members: [person1] + spm_unit_assets: 1_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: IA + output: + # Single adult without minor child fails demographic eligibility + ia_fip_eligible: false + +- name: Case 3, ineligible due to high income. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income_before_lsr: 24_000 + is_tax_unit_head: true + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_assets: 1_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: IA + output: + # Gross income $24,000 > 185% of standard of need ($8,628 * 1.85 = $15,961.80) + # Fails Test 1 (gross income test) + ia_fip_eligible: false + +- name: Case 4, ineligible due to high assets (applicant). + period: 2025 + input: + people: + person1: + age: 30 + employment_income_before_lsr: 0 + is_tax_unit_head: true + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_assets: 3_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: IA + output: + # $3,000 > $2,000 applicant resource limit + ia_fip_eligible: false + +- name: Case 5, recipient eligible with higher assets. + period: 2025 + input: + people: + person1: + age: 30 + employment_income_before_lsr: 0 + is_tax_unit_head: true + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_assets: 4_000 + is_tanf_enrolled: true + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: IA + output: + # Recipients have $5,000 limit, $4,000 < $5,000 + ia_fip_eligible: true + +- name: Case 6, recipient ineligible with assets over limit. + period: 2025 + input: + people: + person1: + age: 30 + employment_income_before_lsr: 0 + is_tax_unit_head: true + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_assets: 6_000 + is_tanf_enrolled: true + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: IA + output: + # Recipients have $5,000 limit, $6,000 > $5,000 + ia_fip_eligible: false + +- name: Case 7, eligible pregnant woman. + period: 2025 + input: + people: + person1: + age: 25 + employment_income_before_lsr: 0 + is_tax_unit_head: true + is_pregnant: true + spm_units: + spm_unit: + members: [person1] + spm_unit_assets: 500 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: IA + output: + # Pregnant woman is demographically eligible + ia_fip_eligible: true + +- name: Case 8, eligible family with moderate income. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income_before_lsr: 6_000 + is_tax_unit_head: true + person2: + age: 5 + person3: + age: 3 + spm_units: + spm_unit: + members: [person1, person2, person3] + spm_unit_assets: 1_500 + tax_units: + tax_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: IA + output: + # Family of 3 with $6,000/year earned income + # Standard of need: $849 * 12 = $10,188 + # Gross income limit: $10,188 * 1.85 = $18,847.80 > $6,000 - PASS Test 1 + # 20% deduction: $6,000 * 0.20 = $1,200 + # Net income: $6,000 - $1,200 = $4,800 < $10,188 - PASS Test 2 + # Remaining earned: $4,800 + # 58% work incentive: $4,800 * 0.58 = $2,784 + # Countable income: $4,800 - $2,784 = $2,016 < $5,112 - PASS Test 3 + ia_fip_eligible: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/dhs/fip/ia_fip_gross_income_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/dhs/fip/ia_fip_gross_income_eligible.yaml new file mode 100644 index 00000000000..04d31a25056 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/dhs/fip/ia_fip_gross_income_eligible.yaml @@ -0,0 +1,112 @@ +# Test 1: Gross income <= 185% of standard of need +# Family of 2: Standard of need = $719/month, 185% = $1,330.15/month + +- name: Case 1, no income single parent with child. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: IA + output: + ia_fip_gross_income_eligible: true + +- name: Case 2, earned income below 185% limit. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income_before_lsr: 12_000 # $1,000/month < $1,330.15 + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: IA + output: + # Family of 2: 185% of $719 = $1,330.15/month + # Gross income: $1,000/month < $1,330.15 + ia_fip_gross_income_eligible: true + +- name: Case 3, earned income above 185% limit. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income_before_lsr: 24_000 # $2,000/month > $1,330.15 + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: IA + output: + # Gross income: $2,000/month > $1,330.15 + ia_fip_gross_income_eligible: false + +- name: Case 4, mixed earned and unearned income at threshold. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income_before_lsr: 6_000 # $500/month earned + social_security: 9_600 # $800/month unearned + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: IA + output: + # Gross income: $500 + $800 = $1,300/month < $1,330.15 + ia_fip_gross_income_eligible: true + +- name: Case 5, two-parent family with two children. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 35 + employment_income_before_lsr: 18_000 # $1,500/month + person2: + age: 32 + person3: + age: 8 + person4: + age: 5 + spm_units: + spm_unit: + members: [person1, person2, person3, person4] + households: + household: + members: [person1, person2, person3, person4] + state_code: IA + output: + # Family of 4: 185% of $986 = $1,824.10/month + # Gross income: $1,500/month < $1,824.10 + ia_fip_gross_income_eligible: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/dhs/fip/ia_fip_income_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/dhs/fip/ia_fip_income_eligible.yaml new file mode 100644 index 00000000000..2f3854915f4 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/dhs/fip/ia_fip_income_eligible.yaml @@ -0,0 +1,351 @@ +# Unit tests for Iowa FIP income eligibility +# Three-step income test: +# 1. Gross income <= 185% of standard of need +# 2. Net income (after 20% earned deduction) < standard of need +# 3. Countable income (after 58% work incentive) < payment standard +# Reference: https://www.legis.iowa.gov/docs/iac/chapter/01-07-2026.441.41.pdf + +- name: Case 1, zero income eligible. + period: 2025 + input: + people: + person1: + age: 30 + employment_income_before_lsr: 0 + is_tax_unit_head: true + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: IA + output: + # Zero income passes all three tests + ia_fip_income_eligible: true + +- name: Case 2, low earned income eligible. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income_before_lsr: 7_200 + is_tax_unit_head: true + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: IA + output: + # Family of 2, $7,200/year earned income ($600/month) + # Standard of need: $8,628/year + # Payment standard: $4,332/year + # Test 1: $7,200 <= $8,628 * 1.85 = $15,961.80 - PASS + # Test 2: $7,200 - ($7,200 * 0.20) = $5,760 < $8,628 - PASS + # Test 3: $5,760 - ($5,760 * 0.58) = $2,419.20 < $4,332 - PASS + ia_fip_income_eligible: true + +- name: Case 3, income exceeds gross income limit (Test 1 fails). + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income_before_lsr: 24_000 + is_tax_unit_head: true + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: IA + output: + # Family of 2, $24,000/year gross income ($2,000/month) + # Standard of need: $8,628/year + # Gross income limit: $8,628 * 1.85 = $15,961.80 + # Test 1: $24,000 > $15,961.80 - FAIL + ia_fip_income_eligible: false + +- name: Case 4, unearned income exceeds payment standard (Test 3 fails). + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income_before_lsr: 0 + social_security: 4_800 + is_tax_unit_head: true + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: IA + output: + # Family of 2, $4,800/year unearned income ($400/month) + # Standard of need: $8,628/year + # Payment standard: $4,332/year + # Test 1: $4,800 <= $15,961.80 - PASS + # Test 2: $4,800 < $8,628 - PASS + # Test 3: $4,800 >= $4,332 - FAIL (no deductions for unearned income) + ia_fip_income_eligible: false + +- name: Case 5, larger family with mixed income eligible. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income_before_lsr: 6_000 + social_security: 2_400 + is_tax_unit_head: true + person2: + age: 5 + person3: + age: 3 + spm_units: + spm_unit: + members: [person1, person2, person3] + tax_units: + tax_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: IA + output: + # Family of 3, $6,000 earned + $2,400 unearned = $8,400 gross + # Standard of need: $10,188/year + # Payment standard: $5,112/year + # Test 1: $8,400 <= $10,188 * 1.85 = $18,847.80 - PASS + # Test 2: $8,400 - ($6,000 * 0.20) = $7,200 < $10,188 - PASS + # Remaining earned: $6,000 - $1,200 = $4,800 + # Work incentive: $4,800 * 0.58 = $2,784 + # Countable earned: $4,800 - $2,784 = $2,016 + # Test 3: $2,016 + $2,400 = $4,416 < $5,112 - PASS + ia_fip_income_eligible: true + +- name: Case 6, high mixed income fails Test 3. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income_before_lsr: 9_600 + social_security: 3_600 + is_tax_unit_head: true + person2: + age: 5 + person3: + age: 3 + spm_units: + spm_unit: + members: [person1, person2, person3] + tax_units: + tax_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: IA + output: + # Family of 3, $9,600 earned + $3,600 unearned = $13,200 gross + # Standard of need: $10,188/year + # Payment standard: $5,112/year + # Test 1: $13,200 <= $18,847.80 - PASS + # Test 2: $13,200 - ($9,600 * 0.20) = $11,280 >= $10,188 - FAIL + ia_fip_income_eligible: false + +- name: Case 7, single person household eligible. + period: 2025 + input: + people: + person1: + age: 25 + employment_income_before_lsr: 0 + is_tax_unit_head: true + is_pregnant: true + spm_units: + spm_unit: + members: [person1] + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: IA + output: + # Single pregnant person with zero income + # Standard of need: $365 * 12 = $4,380/year + # Payment standard: $183 * 12 = $2,196/year + # All tests pass with zero income + ia_fip_income_eligible: true + +- name: Case 8, family of 4 at income threshold. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 35 + employment_income_before_lsr: 10_000 + is_tax_unit_head: true + person2: + age: 33 + employment_income_before_lsr: 2_000 + is_tax_unit_spouse: true + person3: + age: 10 + person4: + age: 7 + spm_units: + spm_unit: + members: [person1, person2, person3, person4] + tax_units: + tax_unit: + members: [person1, person2, person3, person4] + households: + household: + members: [person1, person2, person3, person4] + state_code: IA + output: + # Family of 4, $12,000/year gross earned income + # Standard of need: $986 * 12 = $11,832/year + # Payment standard: $495 * 12 = $5,940/year + # Test 1: $12,000 <= $11,832 * 1.85 = $21,889.20 - PASS + # Test 2: $12,000 - ($12,000 * 0.20) = $9,600 < $11,832 - PASS + # Test 3: $9,600 - ($9,600 * 0.58) = $4,032 < $5,940 - PASS + ia_fip_income_eligible: true + +# Continuing eligibility tests per IAC 441-41.27(239B) +# Initial eligibility uses 3 tests; continuing eligibility uses only 2 +# (skips the net income < standard of need test) + +- name: Case 9, initial applicant fails net income test (not enrolled). + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income_before_lsr: 15_600 + is_tax_unit_head: true + person2: + age: 5 + person3: + age: 3 + spm_units: + spm_unit: + members: [person1, person2, person3] + is_tanf_enrolled: false + tax_units: + tax_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: IA + output: + # Family of 3, $15,600/year earned income ($1,300/month) + # Standard of need: $10,188/year + # Payment standard: $5,112/year + # Test 1: $15,600 <= $10,188 * 1.85 = $18,847.80 - PASS + # Test 2: $15,600 - ($15,600 * 0.20) = $12,480 >= $10,188 - FAIL + # Test 3: $15,600 * 0.80 * 0.42 = $5,241.60 >= $5,112 - FAIL + # Initial eligibility requires all 3 tests - FAIL + ia_fip_income_eligible: false + +- name: Case 10, continuing recipient passes with same income (enrolled). + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income_before_lsr: 13_200 + is_tax_unit_head: true + person2: + age: 5 + person3: + age: 3 + spm_units: + spm_unit: + members: [person1, person2, person3] + is_tanf_enrolled: true + tax_units: + tax_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: IA + output: + # Family of 3, $13,200/year earned income ($1,100/month) + # Standard of need: $10,188/year + # Payment standard: $5,112/year + # Test 1: $13,200 <= $18,847.80 - PASS + # Test 2: $13,200 * 0.80 = $10,560 >= $10,188 - FAIL (but skipped for continuing) + # Test 3: $13,200 * 0.80 * 0.42 = $4,435.20 < $5,112 - PASS + # Continuing eligibility only requires tests 1 and 3 - PASS + ia_fip_income_eligible: true + +- name: Case 11, same income ineligible as initial applicant. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income_before_lsr: 13_200 + is_tax_unit_head: true + person2: + age: 5 + person3: + age: 3 + spm_units: + spm_unit: + members: [person1, person2, person3] + is_tanf_enrolled: false + tax_units: + tax_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: IA + output: + # Same income as Case 10, but as initial applicant + # Test 2: $10,560 >= $10,188 - FAIL + # Initial eligibility requires all 3 tests - FAIL + ia_fip_income_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/dhs/fip/ia_fip_net_income_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/dhs/fip/ia_fip_net_income_eligible.yaml new file mode 100644 index 00000000000..3d1202d2f85 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/dhs/fip/ia_fip_net_income_eligible.yaml @@ -0,0 +1,144 @@ +# Test 2: Net income < standard of need (applicants only) +# Net income = gross income - 20% earned income deduction +# Does NOT include 58% work incentive disregard + +- name: Case 1, no income. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: IA + output: + # Net income: $0 < $719 + ia_fip_net_income_eligible: true + +- name: Case 2, earned income with 20% deduction passes. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income_before_lsr: 9_600 # $800/month + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: IA + output: + # Gross: $800/month + # 20% deduction: $800 * 0.20 = $160 + # Net: $800 - $160 = $640 < $719 standard of need + ia_fip_net_income_eligible: true + +- name: Case 3, earned income with 20% deduction fails. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income_before_lsr: 12_000 # $1,000/month + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: IA + output: + # Gross: $1,000/month + # 20% deduction: $1,000 * 0.20 = $200 + # Net: $1,000 - $200 = $800 >= $719 standard of need + ia_fip_net_income_eligible: false + +- name: Case 4, unearned income only no deduction. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + social_security: 8_400 # $700/month unearned + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: IA + output: + # Gross: $700/month (unearned) + # 20% deduction only applies to earned: $0 + # Net: $700 - $0 = $700 < $719 + ia_fip_net_income_eligible: true + +- name: Case 5, mixed income deduction only on earned. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income_before_lsr: 6_000 # $500/month earned + social_security: 3_600 # $300/month unearned + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: IA + output: + # Gross: $500 + $300 = $800/month + # 20% deduction on earned only: $500 * 0.20 = $100 + # Net: $800 - $100 = $700 < $719 + ia_fip_net_income_eligible: true + +- name: Case 6, larger family higher threshold. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 35 + employment_income_before_lsr: 12_000 # $1,000/month + person2: + age: 32 + person3: + age: 10 + person4: + age: 7 + spm_units: + spm_unit: + members: [person1, person2, person3, person4] + households: + household: + members: [person1, person2, person3, person4] + state_code: IA + output: + # Family of 4: standard of need = $986 + # Gross: $1,000/month + # 20% deduction: $1,000 * 0.20 = $200 + # Net: $1,000 - $200 = $800 < $986 + ia_fip_net_income_eligible: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/dhs/fip/ia_fip_payment_standard.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/dhs/fip/ia_fip_payment_standard.yaml new file mode 100644 index 00000000000..733b22b3a78 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/dhs/fip/ia_fip_payment_standard.yaml @@ -0,0 +1,196 @@ +# Unit tests for Iowa FIP payment standard +# The payment standard is the schedule of basic needs by family size +# Reference: https://www.legis.iowa.gov/docs/iac/chapter/01-07-2026.441.41.pdf#page=31 + +- name: Case 1, family of 1. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 25 + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + state_code: IA + output: + # Payment standard for 1: $183/month * 12 = $2,196/year + ia_fip_payment_standard: 2_196 + +- name: Case 2, family of 2. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: IA + output: + # Payment standard for 2: $361/month * 12 = $4,332/year + ia_fip_payment_standard: 4_332 + +- name: Case 3, family of 3. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + person2: + age: 5 + person3: + age: 3 + spm_units: + spm_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: IA + output: + # Payment standard for 3: $426/month * 12 = $5,112/year + ia_fip_payment_standard: 5_112 + +- name: Case 4, family of 4. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 35 + person2: + age: 33 + person3: + age: 10 + person4: + age: 8 + spm_units: + spm_unit: + members: [person1, person2, person3, person4] + households: + household: + members: [person1, person2, person3, person4] + state_code: IA + output: + # Payment standard for 4: $495/month * 12 = $5,940/year + ia_fip_payment_standard: 5_940 + +- name: Case 5, family of 5. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 35 + person2: + age: 33 + person3: + age: 12 + person4: + age: 10 + person5: + age: 8 + spm_units: + spm_unit: + members: [person1, person2, person3, person4, person5] + households: + household: + members: [person1, person2, person3, person4, person5] + state_code: IA + output: + # Payment standard for 5: $548/month * 12 = $6,576/year + ia_fip_payment_standard: 6_576 + +- name: Case 6, family of 6. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 35 + person2: + age: 33 + person3: + age: 14 + person4: + age: 12 + person5: + age: 10 + person6: + age: 8 + spm_units: + spm_unit: + members: [person1, person2, person3, person4, person5, person6] + households: + household: + members: [person1, person2, person3, person4, person5, person6] + state_code: IA + output: + # Payment standard for 6: $610/month * 12 = $7,320/year + ia_fip_payment_standard: 7_320 + +- name: Case 7, family of 10 (max size in table). + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 40 + person2: + age: 38 + person3: + age: 17 + person4: + age: 15 + person5: + age: 13 + person6: + age: 11 + person7: + age: 9 + person8: + age: 7 + person9: + age: 5 + person10: + age: 3 + spm_units: + spm_unit: + members: [person1, person2, person3, person4, person5, person6, person7, person8, person9, person10] + households: + household: + members: [person1, person2, person3, person4, person5, person6, person7, person8, person9, person10] + state_code: IA + output: + # Payment standard for 10: $865/month * 12 = $10,380/year + ia_fip_payment_standard: 10_380 + +- name: Case 8, non-Iowa state gets zero. + period: 2025 + input: + people: + person1: + age: 30 + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: IL + output: + # defined_for StateCode.IA, other states return 0 + ia_fip_payment_standard: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/dhs/fip/ia_fip_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/dhs/fip/ia_fip_resources_eligible.yaml new file mode 100644 index 00000000000..409080b416d --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/dhs/fip/ia_fip_resources_eligible.yaml @@ -0,0 +1,99 @@ +- name: Case 1, applicant with resources below limit. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + spm_units: + spm_unit: + members: [person1] + spm_unit_assets: 1_500 + is_tanf_enrolled: false + households: + household: + members: [person1] + state_code: IA + output: + # Applicant limit is $2,000 + ia_fip_resources_eligible: true + +- name: Case 2, applicant with resources above limit. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + spm_units: + spm_unit: + members: [person1] + spm_unit_assets: 2_500 + is_tanf_enrolled: false + households: + household: + members: [person1] + state_code: IA + output: + # Applicant limit is $2,000 + ia_fip_resources_eligible: false + +- name: Case 3, recipient with resources below higher limit. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + spm_units: + spm_unit: + members: [person1] + spm_unit_assets: 4_000 + is_tanf_enrolled: true + households: + household: + members: [person1] + state_code: IA + output: + # Recipient limit is $5,000 + ia_fip_resources_eligible: true + +- name: Case 4, recipient with resources above higher limit. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + spm_units: + spm_unit: + members: [person1] + spm_unit_assets: 6_000 + is_tanf_enrolled: true + households: + household: + members: [person1] + state_code: IA + output: + # Recipient limit is $5,000 + ia_fip_resources_eligible: false + +- name: Case 5, applicant at exact limit. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + spm_units: + spm_unit: + members: [person1] + spm_unit_assets: 2_000 + is_tanf_enrolled: false + households: + household: + members: [person1] + state_code: IA + output: + # At limit should be eligible (<=) + ia_fip_resources_eligible: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/dhs/fip/ia_fip_standard_of_need.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/dhs/fip/ia_fip_standard_of_need.yaml new file mode 100644 index 00000000000..1f4ae9c277b --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/dhs/fip/ia_fip_standard_of_need.yaml @@ -0,0 +1,197 @@ +# Unit tests for Iowa FIP standard of need +# The standard of need is the schedule of living costs by family size +# Used for gross income and net income eligibility tests +# Reference: https://www.legis.iowa.gov/docs/iac/chapter/01-07-2026.441.41.pdf#page=31 + +- name: Case 1, family of 1. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 25 + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + state_code: IA + output: + # Standard of need for 1: $365/month * 12 = $4,380/year + ia_fip_standard_of_need: 4_380 + +- name: Case 2, family of 2. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: IA + output: + # Standard of need for 2: $719/month * 12 = $8,628/year + ia_fip_standard_of_need: 8_628 + +- name: Case 3, family of 3. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + person2: + age: 5 + person3: + age: 3 + spm_units: + spm_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: IA + output: + # Standard of need for 3: $849/month * 12 = $10,188/year + ia_fip_standard_of_need: 10_188 + +- name: Case 4, family of 4. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 35 + person2: + age: 33 + person3: + age: 10 + person4: + age: 8 + spm_units: + spm_unit: + members: [person1, person2, person3, person4] + households: + household: + members: [person1, person2, person3, person4] + state_code: IA + output: + # Standard of need for 4: $986/month * 12 = $11,832/year + ia_fip_standard_of_need: 11_832 + +- name: Case 5, family of 5. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 35 + person2: + age: 33 + person3: + age: 12 + person4: + age: 10 + person5: + age: 8 + spm_units: + spm_unit: + members: [person1, person2, person3, person4, person5] + households: + household: + members: [person1, person2, person3, person4, person5] + state_code: IA + output: + # Standard of need for 5: $1,092/month * 12 = $13,104/year + ia_fip_standard_of_need: 13_104 + +- name: Case 6, family of 6. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 35 + person2: + age: 33 + person3: + age: 14 + person4: + age: 12 + person5: + age: 10 + person6: + age: 8 + spm_units: + spm_unit: + members: [person1, person2, person3, person4, person5, person6] + households: + household: + members: [person1, person2, person3, person4, person5, person6] + state_code: IA + output: + # Standard of need for 6: $1,216/month * 12 = $14,592/year + ia_fip_standard_of_need: 14_592 + +- name: Case 7, family of 10 (max size in table). + period: 2025 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 40 + person2: + age: 38 + person3: + age: 17 + person4: + age: 15 + person5: + age: 13 + person6: + age: 11 + person7: + age: 9 + person8: + age: 7 + person9: + age: 5 + person10: + age: 3 + spm_units: + spm_unit: + members: [person1, person2, person3, person4, person5, person6, person7, person8, person9, person10] + households: + household: + members: [person1, person2, person3, person4, person5, person6, person7, person8, person9, person10] + state_code: IA + output: + # Standard of need for 10: $1,724/month * 12 = $20,688/year + ia_fip_standard_of_need: 20_688 + +- name: Case 8, non-Iowa state gets zero. + period: 2025 + input: + people: + person1: + age: 30 + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: IL + output: + # defined_for StateCode.IA, other states return 0 + ia_fip_standard_of_need: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/dhs/fip/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/dhs/fip/integration.yaml new file mode 100644 index 00000000000..af7de78f1ec --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/dhs/fip/integration.yaml @@ -0,0 +1,299 @@ +# Integration tests for Iowa FIP (Family Investment Program) - full pipeline testing +# Tests realistic family scenarios with complete eligibility and benefit calculations +# Reference: https://www.legis.iowa.gov/docs/iac/chapter/01-07-2026.441.41.pdf +# +# Key Iowa FIP rules: +# - 20% earned income deduction +# - 58% work incentive disregard (applied after 20% deduction) +# - Gross income limit: 185% of standard of need +# - Net income test (applicants only): net income < standard of need +# - Payment standard test: countable income < payment standard +# - Payment standard ~ 50.18% of standard of need +# - Resource limits: $2,000 applicants, $5,000 recipients + +- name: Scenario 1, single parent with 2 children, no income - full benefit. + period: 2025-01 + absolute_error_margin: 1 + input: + people: + parent: + age: 30 + employment_income_before_lsr: 0 + child1: + age: 5 + child2: + age: 3 + spm_units: + spm_unit: + members: [parent, child1, child2] + tax_units: + tax_unit: + members: [parent, child1, child2] + households: + household: + members: [parent, child1, child2] + state_code: IA + output: + # Family of 3 + # Standard of need: $849, Payment standard: $426 + ia_fip_standard_of_need: 849 + ia_fip_payment_standard: 426 + # No income - all tests pass + ia_fip_gross_income: 0 + ia_fip_countable_income: 0 + ia_fip_income_eligible: true + ia_fip_eligible: true + # Benefit: $426 - $0 = $426 + ia_fip: 426 + +- name: Scenario 2, single parent with 1 child, $800/month earnings. + period: 2025-01 + absolute_error_margin: 1 + input: + people: + parent: + age: 28 + employment_income_before_lsr: 9_600 # $800/month * 12 + child1: + age: 4 + spm_units: + spm_unit: + members: [parent, child1] + tax_units: + tax_unit: + members: [parent, child1] + households: + household: + members: [parent, child1] + state_code: IA + output: + # Family of 2 + # Standard of need: $719, Payment standard: $361 + ia_fip_standard_of_need: 719 + ia_fip_payment_standard: 361 + # Gross income: $800 + ia_fip_gross_income: 800 + # Test 1: $800 <= $719 * 1.85 = $1,330.15 -> pass + # Test 2: $800 - ($800 * 0.20) = $640 < $719 -> pass + # Test 3: 20% deduction: $800 * 0.20 = $160 + # Remaining: $640, 58% disregard: $640 * 0.58 = $371.20 + # Countable earned: $640 - $371.20 = $268.80 + ia_fip_countable_earned_income: 269 + ia_fip_countable_income: 269 + # $269 < $361 -> pass + ia_fip_income_eligible: true + ia_fip_eligible: true + # Benefit: $361 - $269 = $92 + ia_fip: 92 + +- name: Scenario 3, family of 4 with mixed income. + period: 2025-01 + absolute_error_margin: 1 + input: + people: + parent: + age: 35 + employment_income_before_lsr: 7_200 # $600/month * 12 + social_security: 2_400 # $200/month * 12 + child1: + age: 10 + child2: + age: 7 + child3: + age: 3 + spm_units: + spm_unit: + members: [parent, child1, child2, child3] + tax_units: + tax_unit: + members: [parent, child1, child2, child3] + households: + household: + members: [parent, child1, child2, child3] + state_code: IA + output: + # Family of 4 + # Standard of need: $986, Payment standard: $495 + ia_fip_standard_of_need: 986 + ia_fip_payment_standard: 495 + # Gross income: $600 + $200 = $800 + ia_fip_gross_income: 800 + # Test 1: $800 <= $986 * 1.85 = $1,824.10 -> pass + # Test 2: $800 - ($600 * 0.20) = $800 - $120 = $680 < $986 -> pass + # Test 3: 20% deduction on earned: $600 * 0.20 = $120 + # Remaining earned: $480, 58% disregard: $480 * 0.58 = $278.40 + # Countable earned: $480 - $278.40 = $201.60 + # Countable income: $201.60 + $200 = $401.60 + ia_fip_countable_earned_income: 202 + ia_fip_countable_income: 402 + # $402 < $495 -> pass + ia_fip_income_eligible: true + ia_fip_eligible: true + # Benefit: $495 - $402 = $93 + ia_fip: 93 + +- name: Scenario 4, income exceeds payment standard - ineligible. + period: 2025-01 + absolute_error_margin: 1 + input: + people: + parent: + age: 32 + employment_income_before_lsr: 18_000 # $1,500/month * 12 + child1: + age: 6 + spm_units: + spm_unit: + members: [parent, child1] + tax_units: + tax_unit: + members: [parent, child1] + households: + household: + members: [parent, child1] + state_code: IA + output: + # Family of 2 + # Standard of need: $719, Payment standard: $361 + ia_fip_standard_of_need: 719 + ia_fip_payment_standard: 361 + # Gross income: $1,500 + # Test 1: $1,500 > $719 * 1.85 = $1,330.15 -> FAIL + ia_fip_gross_income: 1_500 + ia_fip_income_eligible: false + ia_fip_eligible: false + ia_fip: 0 + +- name: Scenario 5, passes gross test but fails net income test (applicant). + period: 2025-01 + absolute_error_margin: 1 + input: + people: + parent: + age: 30 + employment_income_before_lsr: 1_200 # $100/month * 12 + social_security: 7_200 # $600/month * 12 + child1: + age: 8 + spm_units: + spm_unit: + members: [parent, child1] + tax_units: + tax_unit: + members: [parent, child1] + households: + household: + members: [parent, child1] + state_code: IA + output: + # Family of 2 + # Standard of need: $719, Payment standard: $361 + ia_fip_standard_of_need: 719 + ia_fip_payment_standard: 361 + # Gross income: $100 + $600 = $700 + ia_fip_gross_income: 700 + # Test 1: $700 <= $1,330.15 -> pass + # Test 2 (applicant): net = $700 - ($100 * 0.20) = $680 + # $680 < $719 -> pass + # Test 3: earned after deductions: $100 * 0.80 = $80 + # 58% disregard: $80 * 0.58 = $46.40 + # Countable earned: $80 - $46.40 = $33.60 + # Countable: $33.60 + $600 = $633.60 + ia_fip_countable_income: 634 + # $634 > $361 -> FAIL payment standard test + ia_fip_income_eligible: false + ia_fip_eligible: false + ia_fip: 0 + +- name: Scenario 6, single parent, no children - ineligible (no TANF demographics). + period: 2025-01 + input: + people: + person1: + age: 25 + employment_income_before_lsr: 0 + spm_units: + spm_unit: + members: [person1] + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: IA + output: + ia_fip_eligible: false + ia_fip: 0 + +- name: Scenario 7, non-Iowa state returns zero. + period: 2025-01 + input: + people: + parent: + age: 30 + employment_income_before_lsr: 0 + child1: + age: 5 + spm_units: + spm_unit: + members: [parent, child1] + tax_units: + tax_unit: + members: [parent, child1] + households: + household: + members: [parent, child1] + state_code: IL + output: + ia_fip_eligible: false + ia_fip: 0 + +- name: Scenario 8, large family of 6, low income - eligible. + period: 2025-01 + absolute_error_margin: 1 + input: + people: + parent1: + age: 35 + employment_income_before_lsr: 6_000 # $500/month * 12 + parent2: + age: 33 + employment_income_before_lsr: 0 + child1: + age: 12 + child2: + age: 9 + child3: + age: 6 + child4: + age: 2 + spm_units: + spm_unit: + members: [parent1, parent2, child1, child2, child3, child4] + tax_units: + tax_unit: + members: [parent1, parent2, child1, child2, child3, child4] + households: + household: + members: [parent1, parent2, child1, child2, child3, child4] + state_code: IA + output: + # Family of 6 + # Standard of need: $1,216, Payment standard: $610 + ia_fip_standard_of_need: 1_216 + ia_fip_payment_standard: 610 + # Gross: $500 + ia_fip_gross_income: 500 + # Test 1: $500 <= $1,216 * 1.85 = $2,249.60 -> pass + # Test 2: $500 - ($500 * 0.20) = $400 < $1,216 -> pass + # Test 3: 20% deduction: $500 * 0.20 = $100 + # Remaining: $400, 58% disregard: $400 * 0.58 = $232 + # Countable earned: $400 - $232 = $168 + ia_fip_countable_earned_income: 168 + ia_fip_countable_income: 168 + # $168 < $610 -> pass + ia_fip_income_eligible: true + ia_fip_eligible: true + # Benefit: $610 - $168 = $442 + ia_fip: 442 diff --git a/policyengine_us/variables/gov/states/ia/dhs/fip/eligibility/ia_fip_countable_income_eligible.py b/policyengine_us/variables/gov/states/ia/dhs/fip/eligibility/ia_fip_countable_income_eligible.py new file mode 100644 index 00000000000..de2a29ffbaa --- /dev/null +++ b/policyengine_us/variables/gov/states/ia/dhs/fip/eligibility/ia_fip_countable_income_eligible.py @@ -0,0 +1,17 @@ +from policyengine_us.model_api import * + + +class ia_fip_countable_income_eligible(Variable): + value_type = bool + entity = SPMUnit + label = "Iowa FIP countable income eligible" + definition_period = MONTH + defined_for = StateCode.IA + reference = "https://www.legis.iowa.gov/docs/iac/chapter/01-07-2026.441.41.pdf#page=20" + + def formula(spm_unit, period, parameters): + # Test 3: Countable income < payment standard + # Countable income applies both 20% deduction AND 58% work incentive disregard + countable_income = spm_unit("ia_fip_countable_income", period) + payment_standard = spm_unit("ia_fip_payment_standard", period) + return countable_income < payment_standard diff --git a/policyengine_us/variables/gov/states/ia/dhs/fip/eligibility/ia_fip_eligible.py b/policyengine_us/variables/gov/states/ia/dhs/fip/eligibility/ia_fip_eligible.py new file mode 100644 index 00000000000..c66b0c4a647 --- /dev/null +++ b/policyengine_us/variables/gov/states/ia/dhs/fip/eligibility/ia_fip_eligible.py @@ -0,0 +1,30 @@ +from policyengine_us.model_api import * + + +class ia_fip_eligible(Variable): + value_type = bool + entity = SPMUnit + label = "Iowa FIP eligible" + definition_period = MONTH + defined_for = StateCode.IA + reference = ( + "https://www.legis.iowa.gov/docs/iac/chapter/01-07-2026.441.41.pdf" + ) + + def formula(spm_unit, period, parameters): + # Must have at least one eligible child (uses federal demographic rules) + has_children = spm_unit("is_demographic_tanf_eligible", period) + # Must meet immigration status eligibility (citizen or qualified alien) + # Per Iowa legal code 441—41.23(239B), 41.23(5) + immigration_eligible = ( + add(spm_unit, period, ["is_citizen_or_legal_immigrant"]) > 0 + ) + resources_eligible = spm_unit("ia_fip_resources_eligible", period) + income_eligible = spm_unit("ia_fip_income_eligible", period) + + return ( + has_children + & immigration_eligible + & resources_eligible + & income_eligible + ) diff --git a/policyengine_us/variables/gov/states/ia/dhs/fip/eligibility/ia_fip_gross_income_eligible.py b/policyengine_us/variables/gov/states/ia/dhs/fip/eligibility/ia_fip_gross_income_eligible.py new file mode 100644 index 00000000000..7de595a60eb --- /dev/null +++ b/policyengine_us/variables/gov/states/ia/dhs/fip/eligibility/ia_fip_gross_income_eligible.py @@ -0,0 +1,17 @@ +from policyengine_us.model_api import * + + +class ia_fip_gross_income_eligible(Variable): + value_type = bool + entity = SPMUnit + label = "Iowa FIP gross income eligible" + definition_period = MONTH + defined_for = StateCode.IA + reference = "https://www.legis.iowa.gov/docs/iac/chapter/01-07-2026.441.41.pdf#page=20" + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.ia.dhs.fip.income + gross_income = spm_unit("ia_fip_gross_income", period) + standard_of_need = spm_unit("ia_fip_standard_of_need", period) + gross_income_limit = standard_of_need * p.gross_income_limit_percent + return gross_income <= gross_income_limit diff --git a/policyengine_us/variables/gov/states/ia/dhs/fip/eligibility/ia_fip_income_eligible.py b/policyengine_us/variables/gov/states/ia/dhs/fip/eligibility/ia_fip_income_eligible.py new file mode 100644 index 00000000000..0e2cf7f48eb --- /dev/null +++ b/policyengine_us/variables/gov/states/ia/dhs/fip/eligibility/ia_fip_income_eligible.py @@ -0,0 +1,46 @@ +from policyengine_us.model_api import * + + +class ia_fip_income_eligible(Variable): + value_type = bool + entity = SPMUnit + label = "Iowa FIP income eligible" + definition_period = MONTH + defined_for = StateCode.IA + reference = ( + "https://www.legis.iowa.gov/docs/iac/chapter/01-07-2026.441.41.pdf#page=19", + "https://www.law.cornell.edu/regulations/iowa/Iowa-Code-r-441-41.27", + ) + + def formula(spm_unit, period, parameters): + # Iowa FIP has different income tests for initial vs continuing eligibility + # per IAC 441-41.27(239B): + # + # Initial eligibility (3-step process): + # 1. Gross income <= 185% of standard of need + # 2. Net income (after 20% earned income deduction) < standard of need + # 3. Countable income (after 58% work incentive disregard) < payment standard + # + # Continuing eligibility (2-step process): + # 1. Gross income <= 185% of standard of need + # 2. Countable income (after 58% work incentive disregard) < payment standard + # + # The work incentive disregard does NOT apply to the net income test (step 2) + # for initial eligibility. Continuing recipients skip step 2 entirely. + + is_enrolled = spm_unit("is_tanf_enrolled", period) + gross_income_eligible = spm_unit( + "ia_fip_gross_income_eligible", period + ) + net_income_eligible = spm_unit("ia_fip_net_income_eligible", period) + countable_income_eligible = spm_unit( + "ia_fip_countable_income_eligible", period + ) + + return where( + is_enrolled, + gross_income_eligible & countable_income_eligible, + gross_income_eligible + & net_income_eligible + & countable_income_eligible, + ) diff --git a/policyengine_us/variables/gov/states/ia/dhs/fip/eligibility/ia_fip_net_income_eligible.py b/policyengine_us/variables/gov/states/ia/dhs/fip/eligibility/ia_fip_net_income_eligible.py new file mode 100644 index 00000000000..d9cd54ffcce --- /dev/null +++ b/policyengine_us/variables/gov/states/ia/dhs/fip/eligibility/ia_fip_net_income_eligible.py @@ -0,0 +1,23 @@ +from policyengine_us.model_api import * + + +class ia_fip_net_income_eligible(Variable): + value_type = bool + entity = SPMUnit + label = "Iowa FIP net income eligible" + definition_period = MONTH + defined_for = StateCode.IA + reference = "https://www.legis.iowa.gov/docs/iac/chapter/01-07-2026.441.41.pdf#page=20" + + def formula(spm_unit, period, parameters): + # Test 2 for initial eligibility only + # Net income = gross income - 20% earned income deduction + # Must be < standard of need + # Note: Does NOT include the 58% work incentive disregard + p = parameters(period).gov.states.ia.dhs.fip.income + gross_income = spm_unit("ia_fip_gross_income", period) + gross_earned = add(spm_unit, period, ["tanf_gross_earned_income"]) + earned_income_deduction = gross_earned * p.earned_income_deduction + net_income = gross_income - earned_income_deduction + standard_of_need = spm_unit("ia_fip_standard_of_need", period) + return net_income < standard_of_need diff --git a/policyengine_us/variables/gov/states/ia/dhs/fip/eligibility/ia_fip_resources_eligible.py b/policyengine_us/variables/gov/states/ia/dhs/fip/eligibility/ia_fip_resources_eligible.py new file mode 100644 index 00000000000..0c71bdd2128 --- /dev/null +++ b/policyengine_us/variables/gov/states/ia/dhs/fip/eligibility/ia_fip_resources_eligible.py @@ -0,0 +1,21 @@ +from policyengine_us.model_api import * + + +class ia_fip_resources_eligible(Variable): + value_type = bool + entity = SPMUnit + label = "Iowa FIP resources eligible" + definition_period = MONTH + defined_for = StateCode.IA + reference = "https://www.legis.iowa.gov/docs/iac/chapter/01-07-2026.441.41.pdf#page=16" + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.ia.dhs.fip.resources + current_recipient = spm_unit("is_tanf_enrolled", period) + limit = where( + current_recipient, + p.recipient_limit, + p.applicant_limit, + ) + resources = spm_unit("spm_unit_assets", period.this_year) + return resources <= limit diff --git a/policyengine_us/variables/gov/states/ia/dhs/fip/ia_fip.py b/policyengine_us/variables/gov/states/ia/dhs/fip/ia_fip.py new file mode 100644 index 00000000000..79fb334cbf5 --- /dev/null +++ b/policyengine_us/variables/gov/states/ia/dhs/fip/ia_fip.py @@ -0,0 +1,16 @@ +from policyengine_us.model_api import * + + +class ia_fip(Variable): + value_type = float + entity = SPMUnit + label = "Iowa Family Investment Program (FIP)" + unit = USD + definition_period = MONTH + defined_for = "ia_fip_eligible" + reference = "https://www.legis.iowa.gov/docs/iac/chapter/01-07-2026.441.41.pdf#page=20" + + def formula(spm_unit, period, parameters): + payment_standard = spm_unit("ia_fip_payment_standard", period) + countable_income = spm_unit("ia_fip_countable_income", period) + return max_(payment_standard - countable_income, 0) diff --git a/policyengine_us/variables/gov/states/ia/dhs/fip/ia_fip_payment_standard.py b/policyengine_us/variables/gov/states/ia/dhs/fip/ia_fip_payment_standard.py new file mode 100644 index 00000000000..ecde0f97809 --- /dev/null +++ b/policyengine_us/variables/gov/states/ia/dhs/fip/ia_fip_payment_standard.py @@ -0,0 +1,21 @@ +from policyengine_us.model_api import * + + +class ia_fip_payment_standard(Variable): + value_type = float + entity = SPMUnit + label = "Iowa FIP payment standard" + unit = USD + definition_period = MONTH + defined_for = StateCode.IA + reference = "https://www.legis.iowa.gov/docs/iac/chapter/01-07-2026.441.41.pdf#page=31" + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.ia.dhs.fip + people = spm_unit("spm_unit_size", period.this_year) + capped_people = min_(people, p.max_unit_size).astype(int) + additional_people = people - capped_people + + base = p.payment_standard.main[capped_people] + additional_amount = p.payment_standard.additional * additional_people + return base + additional_amount diff --git a/policyengine_us/variables/gov/states/ia/dhs/fip/ia_fip_standard_of_need.py b/policyengine_us/variables/gov/states/ia/dhs/fip/ia_fip_standard_of_need.py new file mode 100644 index 00000000000..0c729672485 --- /dev/null +++ b/policyengine_us/variables/gov/states/ia/dhs/fip/ia_fip_standard_of_need.py @@ -0,0 +1,21 @@ +from policyengine_us.model_api import * + + +class ia_fip_standard_of_need(Variable): + value_type = float + entity = SPMUnit + label = "Iowa FIP standard of need" + unit = USD + definition_period = MONTH + defined_for = StateCode.IA + reference = "https://www.legis.iowa.gov/docs/iac/chapter/01-07-2026.441.41.pdf#page=31" + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.ia.dhs.fip + people = spm_unit("spm_unit_size", period.this_year) + capped_people = min_(people, p.max_unit_size).astype(int) + additional_people = people - capped_people + + base = p.need_standard.main[capped_people] + additional_amount = p.need_standard.additional * additional_people + return base + additional_amount diff --git a/policyengine_us/variables/gov/states/ia/dhs/fip/income/ia_fip_countable_earned_income.py b/policyengine_us/variables/gov/states/ia/dhs/fip/income/ia_fip_countable_earned_income.py new file mode 100644 index 00000000000..a06366d69b1 --- /dev/null +++ b/policyengine_us/variables/gov/states/ia/dhs/fip/income/ia_fip_countable_earned_income.py @@ -0,0 +1,19 @@ +from policyengine_us.model_api import * + + +class ia_fip_countable_earned_income(Variable): + value_type = float + entity = SPMUnit + label = "Iowa FIP countable earned income" + unit = USD + definition_period = MONTH + defined_for = StateCode.IA + reference = "https://www.legis.iowa.gov/docs/iac/chapter/01-07-2026.441.41.pdf#page=19" + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.ia.dhs.fip.income + gross_earned = add(spm_unit, period, ["tanf_gross_earned_income"]) + earned_income_after_deduction = gross_earned * ( + 1 - p.earned_income_deduction + ) + return earned_income_after_deduction * (1 - p.work_incentive_disregard) diff --git a/policyengine_us/variables/gov/states/ia/dhs/fip/income/ia_fip_countable_income.py b/policyengine_us/variables/gov/states/ia/dhs/fip/income/ia_fip_countable_income.py new file mode 100644 index 00000000000..9568b461cc4 --- /dev/null +++ b/policyengine_us/variables/gov/states/ia/dhs/fip/income/ia_fip_countable_income.py @@ -0,0 +1,13 @@ +from policyengine_us.model_api import * + + +class ia_fip_countable_income(Variable): + value_type = float + entity = SPMUnit + label = "Iowa FIP countable income" + unit = USD + definition_period = MONTH + defined_for = StateCode.IA + reference = "https://www.legis.iowa.gov/docs/iac/chapter/01-07-2026.441.41.pdf#page=19" + + adds = ["ia_fip_countable_earned_income", "tanf_gross_unearned_income"] diff --git a/policyengine_us/variables/gov/states/ia/dhs/fip/income/ia_fip_gross_income.py b/policyengine_us/variables/gov/states/ia/dhs/fip/income/ia_fip_gross_income.py new file mode 100644 index 00000000000..6f80b5d61fb --- /dev/null +++ b/policyengine_us/variables/gov/states/ia/dhs/fip/income/ia_fip_gross_income.py @@ -0,0 +1,18 @@ +from policyengine_us.model_api import * + + +class ia_fip_gross_income(Variable): + value_type = float + entity = SPMUnit + label = "Iowa FIP gross income" + unit = USD + definition_period = MONTH + defined_for = StateCode.IA + reference = ( + "https://www.legis.iowa.gov/docs/iac/chapter/01-07-2026.441.41.pdf" + ) + + adds = [ + "tanf_gross_earned_income", + "tanf_gross_unearned_income", + ]