From 6f6d6c428e7898901d9fd297a97f1bb6db8597f8 Mon Sep 17 00:00:00 2001 From: Ziming Date: Mon, 8 Jun 2026 21:34:07 -0400 Subject: [PATCH 1/7] Scaffold Iowa CCAP branch with changelog fragment Closes #8611 Co-Authored-By: Claude Opus 4.8 (1M context) --- changelog.d/ia-ccap.added.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/ia-ccap.added.md diff --git a/changelog.d/ia-ccap.added.md b/changelog.d/ia-ccap.added.md new file mode 100644 index 00000000000..3101011fda4 --- /dev/null +++ b/changelog.d/ia-ccap.added.md @@ -0,0 +1 @@ +Add Iowa Child Care Assistance (CCAP) program. From 663bcfeea61c45783668b3c6776fefc0505a143a Mon Sep 17 00:00:00 2001 From: Ziming Date: Mon, 8 Jun 2026 23:27:32 -0400 Subject: [PATCH 2/7] Implement Iowa Child Care Assistance (CCA / CCAP) (ref #8611) Full 3-tier CCDF child care subsidy: CCA (initial <=160%/200% FPL or 85% MFI), CCA Plus (ongoing <=225%), CCA Exit (ongoing <=250%/275%, state-funded). Benefit = min(provider charge, max rate ceiling) - copay per half-day unit, across a 5-provider x 3-age x 4-quality x Basic/SN rate matrix, with both copay mechanisms (sliding flat unit fee and CCA Exit %-of-cost). 28 params, 22 variables, 135 tests. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../hhs/ccdf/child_care_subsidy_programs.yaml | 1 + .../activity_requirements/weekly_hours.yaml | 11 + .../weekly_hours_special_needs.yaml | 11 + .../ia/hhs/cca/age_group/age_group.yaml | 24 + .../states/ia/hhs/cca/copay/exit/fee_pct.yaml | 21 + .../copay/exit/income_thresholds_basic.yaml | 71 ++ .../exit/income_thresholds_special_needs.yaml | 71 ++ .../copay/sliding_fee/income_thresholds.yaml | 407 ++++++++++ .../hhs/cca/copay/sliding_fee/unit_fee.yaml | 127 ++++ .../ia/hhs/cca/eligibility/asset_limit.yaml | 11 + .../hhs/cca/eligibility/child_age_limit.yaml | 11 + .../eligibility/special_needs_age_limit.yaml | 11 + .../countable_income/earned_sources.yaml | 17 + .../countable_income/unearned_sources.yaml | 33 + .../hhs/cca/income/fpl_rate/exit_basic.yaml | 11 + .../income/fpl_rate/exit_special_needs.yaml | 11 + .../cca/income/fpl_rate/initial_basic.yaml | 11 + .../fpl_rate/initial_special_needs.yaml | 11 + .../hhs/cca/income/fpl_rate/plus_basic.yaml | 11 + .../ia/hhs/cca/income/minor_earnings_age.yaml | 11 + .../ia/hhs/cca/income/minor_student_age.yaml | 11 + .../states/ia/hhs/cca/income/smi_rate.yaml | 11 + .../ia/hhs/cca/payment/hours_per_unit.yaml | 11 + .../hhs/cca/payment/in_home_min_children.yaml | 11 + .../ia/hhs/cca/payment/in_home_rate.yaml | 13 + .../rates/child_care_home_not_registered.yaml | 23 + .../cca/payment/rates/child_dev_home_ab.yaml | 64 ++ .../cca/payment/rates/child_dev_home_c.yaml | 64 ++ .../cca/payment/rates/licensed_center.yaml | 64 ++ policyengine_us/programs.yaml | 8 +- .../states/ia/hhs/cca/copay/ia_cca_copay.yaml | 354 +++++++++ .../hhs/cca/copay/ia_cca_exit_fee_level.yaml | 171 +++++ .../ia/hhs/cca/copay/ia_cca_in_exit_tier.yaml | 130 ++++ .../cca/copay/ia_cca_sliding_fee_level.yaml | 186 +++++ .../eligibility/ia_cca_activity_eligible.yaml | 248 +++++++ .../eligibility/ia_cca_asset_eligible.yaml | 59 ++ .../hhs/cca/eligibility/ia_cca_eligible.yaml | 220 ++++++ .../eligibility/ia_cca_eligible_child.yaml | 226 ++++++ .../eligibility/ia_cca_income_eligible.yaml | 697 ++++++++++++++++++ .../eligibility/ia_cca_income_exception.yaml | 85 +++ .../states/ia/hhs/cca/ia_cca_age_group.yaml | 148 ++++ .../ia/hhs/cca/ia_cca_children_in_care.yaml | 90 +++ .../ia/hhs/cca/ia_cca_countable_income.yaml | 139 ++++ .../states/ia/hhs/cca/ia_cca_max_rate.yaml | 319 ++++++++ .../ia/hhs/cca/ia_cca_monthly_units.yaml | 68 ++ .../ia/hhs/cca/ia_cca_provider_type.yaml | 46 ++ .../ia/hhs/cca/ia_cca_quality_rating.yaml | 42 ++ .../gov/states/ia/hhs/cca/ia_cca_smi.yaml | 29 + .../ia/hhs/cca/ia_child_care_subsidies.yaml | 43 ++ .../gov/states/ia/hhs/cca/integration.yaml | 571 ++++++++++++++ .../states/ia/hhs/cca/copay/ia_cca_copay.py | 60 ++ .../ia/hhs/cca/copay/ia_cca_exit_fee_level.py | 31 + .../ia/hhs/cca/copay/ia_cca_in_exit_tier.py | 21 + .../hhs/cca/copay/ia_cca_sliding_fee_level.py | 62 ++ .../eligibility/ia_cca_activity_eligible.py | 42 ++ .../cca/eligibility/ia_cca_asset_eligible.py | 19 + .../ia/hhs/cca/eligibility/ia_cca_eligible.py | 22 + .../cca/eligibility/ia_cca_eligible_child.py | 34 + .../ia_cca_has_special_needs_child.py | 16 + .../cca/eligibility/ia_cca_income_eligible.py | 38 + .../eligibility/ia_cca_income_exception.py | 25 + .../variables/gov/states/ia/hhs/cca/ia_cca.py | 29 + .../gov/states/ia/hhs/cca/ia_cca_age_group.py | 39 + .../ia/hhs/cca/ia_cca_children_in_care.py | 18 + .../ia/hhs/cca/ia_cca_countable_income.py | 28 + .../gov/states/ia/hhs/cca/ia_cca_enrolled.py | 16 + .../gov/states/ia/hhs/cca/ia_cca_max_rate.py | 50 ++ .../states/ia/hhs/cca/ia_cca_monthly_units.py | 21 + .../states/ia/hhs/cca/ia_cca_provider_type.py | 20 + .../ia/hhs/cca/ia_cca_quality_rating.py | 19 + .../gov/states/ia/hhs/cca/ia_cca_smi.py | 27 + .../ia/hhs/cca/ia_child_care_subsidies.py | 11 + 72 files changed, 5691 insertions(+), 1 deletion(-) create mode 100644 policyengine_us/parameters/gov/states/ia/hhs/cca/activity_requirements/weekly_hours.yaml create mode 100644 policyengine_us/parameters/gov/states/ia/hhs/cca/activity_requirements/weekly_hours_special_needs.yaml create mode 100644 policyengine_us/parameters/gov/states/ia/hhs/cca/age_group/age_group.yaml create mode 100644 policyengine_us/parameters/gov/states/ia/hhs/cca/copay/exit/fee_pct.yaml create mode 100644 policyengine_us/parameters/gov/states/ia/hhs/cca/copay/exit/income_thresholds_basic.yaml create mode 100644 policyengine_us/parameters/gov/states/ia/hhs/cca/copay/exit/income_thresholds_special_needs.yaml create mode 100644 policyengine_us/parameters/gov/states/ia/hhs/cca/copay/sliding_fee/income_thresholds.yaml create mode 100644 policyengine_us/parameters/gov/states/ia/hhs/cca/copay/sliding_fee/unit_fee.yaml create mode 100644 policyengine_us/parameters/gov/states/ia/hhs/cca/eligibility/asset_limit.yaml create mode 100644 policyengine_us/parameters/gov/states/ia/hhs/cca/eligibility/child_age_limit.yaml create mode 100644 policyengine_us/parameters/gov/states/ia/hhs/cca/eligibility/special_needs_age_limit.yaml create mode 100644 policyengine_us/parameters/gov/states/ia/hhs/cca/income/countable_income/earned_sources.yaml create mode 100644 policyengine_us/parameters/gov/states/ia/hhs/cca/income/countable_income/unearned_sources.yaml create mode 100644 policyengine_us/parameters/gov/states/ia/hhs/cca/income/fpl_rate/exit_basic.yaml create mode 100644 policyengine_us/parameters/gov/states/ia/hhs/cca/income/fpl_rate/exit_special_needs.yaml create mode 100644 policyengine_us/parameters/gov/states/ia/hhs/cca/income/fpl_rate/initial_basic.yaml create mode 100644 policyengine_us/parameters/gov/states/ia/hhs/cca/income/fpl_rate/initial_special_needs.yaml create mode 100644 policyengine_us/parameters/gov/states/ia/hhs/cca/income/fpl_rate/plus_basic.yaml create mode 100644 policyengine_us/parameters/gov/states/ia/hhs/cca/income/minor_earnings_age.yaml create mode 100644 policyengine_us/parameters/gov/states/ia/hhs/cca/income/minor_student_age.yaml create mode 100644 policyengine_us/parameters/gov/states/ia/hhs/cca/income/smi_rate.yaml create mode 100644 policyengine_us/parameters/gov/states/ia/hhs/cca/payment/hours_per_unit.yaml create mode 100644 policyengine_us/parameters/gov/states/ia/hhs/cca/payment/in_home_min_children.yaml create mode 100644 policyengine_us/parameters/gov/states/ia/hhs/cca/payment/in_home_rate.yaml create mode 100644 policyengine_us/parameters/gov/states/ia/hhs/cca/payment/rates/child_care_home_not_registered.yaml create mode 100644 policyengine_us/parameters/gov/states/ia/hhs/cca/payment/rates/child_dev_home_ab.yaml create mode 100644 policyengine_us/parameters/gov/states/ia/hhs/cca/payment/rates/child_dev_home_c.yaml create mode 100644 policyengine_us/parameters/gov/states/ia/hhs/cca/payment/rates/licensed_center.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_copay.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_exit_fee_level.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_in_exit_tier.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_sliding_fee_level.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_activity_eligible.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_asset_eligible.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_eligible.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_eligible_child.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_income_eligible.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_income_exception.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_age_group.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_children_in_care.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_countable_income.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_max_rate.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_monthly_units.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_provider_type.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_quality_rating.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_smi.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_child_care_subsidies.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/integration.yaml create mode 100644 policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_copay.py create mode 100644 policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_exit_fee_level.py create mode 100644 policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_in_exit_tier.py create mode 100644 policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_sliding_fee_level.py create mode 100644 policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_activity_eligible.py create mode 100644 policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_asset_eligible.py create mode 100644 policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_eligible.py create mode 100644 policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_eligible_child.py create mode 100644 policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_has_special_needs_child.py create mode 100644 policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_income_eligible.py create mode 100644 policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_income_exception.py create mode 100644 policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca.py create mode 100644 policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_age_group.py create mode 100644 policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_children_in_care.py create mode 100644 policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_countable_income.py create mode 100644 policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_enrolled.py create mode 100644 policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_max_rate.py create mode 100644 policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_monthly_units.py create mode 100644 policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_provider_type.py create mode 100644 policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_quality_rating.py create mode 100644 policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_smi.py create mode 100644 policyengine_us/variables/gov/states/ia/hhs/cca/ia_child_care_subsidies.py diff --git a/policyengine_us/parameters/gov/hhs/ccdf/child_care_subsidy_programs.yaml b/policyengine_us/parameters/gov/hhs/ccdf/child_care_subsidy_programs.yaml index 598ca5cd7a0..be21600c464 100644 --- a/policyengine_us/parameters/gov/hhs/ccdf/child_care_subsidy_programs.yaml +++ b/policyengine_us/parameters/gov/hhs/ccdf/child_care_subsidy_programs.yaml @@ -8,6 +8,7 @@ values: - co_child_care_subsidies # Colorado Child Care Assistance Program - ct_child_care_subsidies # Connecticut Care 4 Kids - de_child_care_subsidies # Delaware Purchase of Care + - ia_child_care_subsidies # Iowa Child Care Assistance - ma_child_care_subsidies # Massachusetts Child Care Financial Assistance - me_child_care_subsidies # Maine Child Care Affordability Program - ne_child_care_subsidies # Nebraska Child Care Subsidy diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/activity_requirements/weekly_hours.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/activity_requirements/weekly_hours.yaml new file mode 100644 index 00000000000..8f68bff468f --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/activity_requirements/weekly_hours.yaml @@ -0,0 +1,11 @@ +description: Iowa requires each parent to average at least this many hours per week in an approved activity to qualify for Child Care Assistance. +values: + 2024-07-01: 32 + +metadata: + unit: hour + period: week + label: Iowa CCA weekly activity hours requirement + reference: + - title: IAC 441-170.2(2)"b"(2) — Need for service (employment 32 hours per week) + href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=6 diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/activity_requirements/weekly_hours_special_needs.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/activity_requirements/weekly_hours_special_needs.yaml new file mode 100644 index 00000000000..7be54a574c9 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/activity_requirements/weekly_hours_special_needs.yaml @@ -0,0 +1,11 @@ +description: Iowa requires each parent to average at least this many hours per week in an approved activity to qualify for Child Care Assistance when the family includes a child with special needs. +values: + 2024-07-01: 28 + +metadata: + unit: hour + period: week + label: Iowa CCA weekly activity hours requirement (special-needs family) + reference: + - title: IAC 441-170.2(2)"b"(2) — Need for service (28 hours per week if special-needs child) + href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=6 diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/age_group/age_group.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/age_group/age_group.yaml new file mode 100644 index 00000000000..0ce05ec837b --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/age_group/age_group.yaml @@ -0,0 +1,24 @@ +description: Iowa assigns a child to a child care rate age group based on their age in years under the Child Care Assistance program. Infant and toddler is two weeks to three years; preschool is three years to school age; school age is a child attending school. Iowa defines the preschool-to-school-age boundary by school attendance rather than a numeric age, so we treat school age as starting at age 5 (kindergarten entry). +brackets: + - threshold: + 2024-07-01: 0 + amount: + 2024-07-01: 0 # INFANT_TODDLER + - threshold: + 2024-07-01: 3 + amount: + 2024-07-01: 1 # PRESCHOOL + - threshold: + 2024-07-01: 5 + amount: + 2024-07-01: 2 # SCHOOL_AGE + +metadata: + type: single_amount + threshold_unit: year + amount_unit: /1 + period: year + label: Iowa CCA child care age group by age + reference: + - title: IAC 441-170.4(7) — Definitions (infant and toddler; preschool; school age) + href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=15 diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/copay/exit/fee_pct.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/copay/exit/fee_pct.yaml new file mode 100644 index 00000000000..42ef6b5d2a8 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/copay/exit/fee_pct.yaml @@ -0,0 +1,21 @@ +description: Iowa charges this CCA Exit copay as a share of the cost of care for each child under the Child Care Assistance program, by fee level. The same percentages apply to basic care and special-needs care. +metadata: + period: year + unit: /1 + label: Iowa CCA Exit copay percentage of cost of care by fee level + breakdown: + - [A, B, C, D] + breakdown_labels: + - Fee level + reference: + - title: Iowa HHS CCA Exit Program Family Fee Chart (effective July 1, 2024) + href: https://hhs.iowa.gov/media/12445/download?inline=#page=2 + +A: + 2024-07-01: 0.33 +B: + 2024-07-01: 0.45 +C: + 2024-07-01: 0.60 +D: + 2024-07-01: 0.60 diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/copay/exit/income_thresholds_basic.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/copay/exit/income_thresholds_basic.yaml new file mode 100644 index 00000000000..356af035c2e --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/copay/exit/income_thresholds_basic.yaml @@ -0,0 +1,71 @@ +description: Iowa assigns a CCA Exit family using basic care to a fee level (A through D) under the Child Care Assistance program based on monthly income and family size. A family is at the first level whose monthly-income threshold for its family size is at or above the family's monthly income. +metadata: + period: month + unit: currency-USD + label: Iowa CCA Exit basic-care fee level income thresholds by family size + breakdown: + - [A, B, C, D] + - range(1, 14) + breakdown_labels: + - Fee level + - Family size + reference: + - title: Iowa HHS CCA Exit Program Family Fee Chart, Basic Care (effective July 1, 2024) + href: https://hhs.iowa.gov/media/12445/download?inline=#page=2 + +A: + 1: {2024-07-01: 2824} + 2: {2024-07-01: 3834} + 3: {2024-07-01: 4842} + 4: {2024-07-01: 5850} + 5: {2024-07-01: 6860} + 6: {2024-07-01: 7868} + 7: {2024-07-01: 8876} + 8: {2024-07-01: 9887} + 9: {2024-07-01: 10438} + 10: {2024-07-01: 10660} + 11: {2024-07-01: 10883} + 12: {2024-07-01: 11106} + 13: {2024-07-01: 11329} +B: + 1: {2024-07-01: 2949} + 2: {2024-07-01: 4004} + 3: {2024-07-01: 5057} + 4: {2024-07-01: 6110} + 5: {2024-07-01: 7165} + 6: {2024-07-01: 8218} + 7: {2024-07-01: 9271} + 8: {2024-07-01: 10326} + 9: {2024-07-01: 11379} + 10: {2024-07-01: 12432} + 11: {2024-07-01: 13487} + 12: {2024-07-01: 14539} + 13: {2024-07-01: 15592} +C: + 1: {2024-07-01: 3075} + 2: {2024-07-01: 4175} + 3: {2024-07-01: 5272} + 4: {2024-07-01: 6370} + 5: {2024-07-01: 7470} + 6: {2024-07-01: 8568} + 7: {2024-07-01: 9665} + 8: {2024-07-01: 10765} + 9: {2024-07-01: 11863} + 10: {2024-07-01: 12961} + 11: {2024-07-01: 14061} + 12: {2024-07-01: 15158} + 13: {2024-07-01: 16256} +D: + 1: {2024-07-01: 3138} + 2: {2024-07-01: 4260} + 3: {2024-07-01: 5380} + 4: {2024-07-01: 6500} + 5: {2024-07-01: 7623} + 6: {2024-07-01: 8743} + 7: {2024-07-01: 9863} + 8: {2024-07-01: 10985} + 9: {2024-07-01: 12105} + 10: {2024-07-01: 13225} + 11: {2024-07-01: 14348} + 12: {2024-07-01: 15468} + 13: {2024-07-01: 16588} diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/copay/exit/income_thresholds_special_needs.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/copay/exit/income_thresholds_special_needs.yaml new file mode 100644 index 00000000000..ed2ae9dd756 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/copay/exit/income_thresholds_special_needs.yaml @@ -0,0 +1,71 @@ +description: Iowa assigns a CCA Exit family using special-needs care to a fee level (A through D) under the Child Care Assistance program based on monthly income and family size. A family is at the first level whose monthly-income threshold for its family size is at or above the family's monthly income. +metadata: + period: month + unit: currency-USD + label: Iowa CCA Exit special-needs-care fee level income thresholds by family size + breakdown: + - [A, B, C, D] + - range(1, 14) + breakdown_labels: + - Fee level + - Family size + reference: + - title: Iowa HHS CCA Exit Program Family Fee Chart, Special-Needs Care (effective July 1, 2024) + href: https://hhs.iowa.gov/media/12445/download?inline=#page=2 + +A: + 1: {2024-07-01: 2824} + 2: {2024-07-01: 3834} + 3: {2024-07-01: 4842} + 4: {2024-07-01: 5850} + 5: {2024-07-01: 6860} + 6: {2024-07-01: 7868} + 7: {2024-07-01: 8876} + 8: {2024-07-01: 9887} + 9: {2024-07-01: 10438} + 10: {2024-07-01: 10660} + 11: {2024-07-01: 10883} + 12: {2024-07-01: 11106} + 13: {2024-07-01: 11329} +B: + 1: {2024-07-01: 3075} + 2: {2024-07-01: 4175} + 3: {2024-07-01: 5272} + 4: {2024-07-01: 6370} + 5: {2024-07-01: 7470} + 6: {2024-07-01: 8568} + 7: {2024-07-01: 9665} + 8: {2024-07-01: 10765} + 9: {2024-07-01: 11863} + 10: {2024-07-01: 12961} + 11: {2024-07-01: 14061} + 12: {2024-07-01: 15158} + 13: {2024-07-01: 16256} +C: + 1: {2024-07-01: 3326} + 2: {2024-07-01: 4516} + 3: {2024-07-01: 5703} + 4: {2024-07-01: 6890} + 5: {2024-07-01: 8080} + 6: {2024-07-01: 9267} + 7: {2024-07-01: 10454} + 8: {2024-07-01: 11644} + 9: {2024-07-01: 12831} + 10: {2024-07-01: 14019} + 11: {2024-07-01: 15208} + 12: {2024-07-01: 16396} + 13: {2024-07-01: 17583} +D: + 1: {2024-07-01: 3451} + 2: {2024-07-01: 4686} + 3: {2024-07-01: 5918} + 4: {2024-07-01: 7150} + 5: {2024-07-01: 8385} + 6: {2024-07-01: 9617} + 7: {2024-07-01: 10849} + 8: {2024-07-01: 12084} + 9: {2024-07-01: 13316} + 10: {2024-07-01: 14548} + 11: {2024-07-01: 15782} + 12: {2024-07-01: 17014} + 13: {2024-07-01: 18246} diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/copay/sliding_fee/income_thresholds.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/copay/sliding_fee/income_thresholds.yaml new file mode 100644 index 00000000000..0bcaa3167a3 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/copay/sliding_fee/income_thresholds.yaml @@ -0,0 +1,407 @@ +description: Iowa assigns a CCA and CCA Plus family to a sliding-fee level (A through BB) under the Child Care Assistance program based on monthly income and family size. A family is at the first level whose monthly-income threshold for its family size is at or above the family's monthly income. +metadata: + period: month + unit: currency-USD + label: Iowa CCA and CCA Plus sliding-fee level income thresholds by family size + breakdown: + - [A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, AA, BB] + - range(1, 14) + breakdown_labels: + - Fee level + - Family size + reference: + - title: Iowa HHS CCA and CCA Plus Family Fee Chart (effective July 1, 2024) + href: https://hhs.iowa.gov/media/12445/download?inline=#page=1 + +A: + 1: {2024-07-01: 1192} + 2: {2024-07-01: 1619} + 3: {2024-07-01: 2044} + 4: {2024-07-01: 2470} + 5: {2024-07-01: 2897} + 6: {2024-07-01: 3322} + 7: {2024-07-01: 3748} + 8: {2024-07-01: 4174} + 9: {2024-07-01: 4600} + 10: {2024-07-01: 5026} + 11: {2024-07-01: 5452} + 12: {2024-07-01: 5878} + 13: {2024-07-01: 6303} +B: + 1: {2024-07-01: 1255} + 2: {2024-07-01: 1704} + 3: {2024-07-01: 2152} + 4: {2024-07-01: 2600} + 5: {2024-07-01: 3049} + 6: {2024-07-01: 3497} + 7: {2024-07-01: 3945} + 8: {2024-07-01: 4394} + 9: {2024-07-01: 4842} + 10: {2024-07-01: 5290} + 11: {2024-07-01: 5739} + 12: {2024-07-01: 6187} + 13: {2024-07-01: 6635} +C: + 1: {2024-07-01: 1290} + 2: {2024-07-01: 1752} + 3: {2024-07-01: 2212} + 4: {2024-07-01: 2673} + 5: {2024-07-01: 3134} + 6: {2024-07-01: 3595} + 7: {2024-07-01: 4055} + 8: {2024-07-01: 4517} + 9: {2024-07-01: 4978} + 10: {2024-07-01: 5438} + 11: {2024-07-01: 5900} + 12: {2024-07-01: 6360} + 13: {2024-07-01: 6821} +D: + 1: {2024-07-01: 1325} + 2: {2024-07-01: 1799} + 3: {2024-07-01: 2273} + 4: {2024-07-01: 2746} + 5: {2024-07-01: 3220} + 6: {2024-07-01: 3693} + 7: {2024-07-01: 4166} + 8: {2024-07-01: 4640} + 9: {2024-07-01: 5113} + 10: {2024-07-01: 5586} + 11: {2024-07-01: 6060} + 12: {2024-07-01: 6533} + 13: {2024-07-01: 7007} +E: + 1: {2024-07-01: 1362} + 2: {2024-07-01: 1850} + 3: {2024-07-01: 2336} + 4: {2024-07-01: 2822} + 5: {2024-07-01: 3310} + 6: {2024-07-01: 3796} + 7: {2024-07-01: 4283} + 8: {2024-07-01: 4770} + 9: {2024-07-01: 5256} + 10: {2024-07-01: 5743} + 11: {2024-07-01: 6230} + 12: {2024-07-01: 6716} + 13: {2024-07-01: 7203} +F: + 1: {2024-07-01: 1399} + 2: {2024-07-01: 1900} + 3: {2024-07-01: 2400} + 4: {2024-07-01: 2899} + 5: {2024-07-01: 3400} + 6: {2024-07-01: 3900} + 7: {2024-07-01: 4399} + 8: {2024-07-01: 4900} + 9: {2024-07-01: 5399} + 10: {2024-07-01: 5899} + 11: {2024-07-01: 6400} + 12: {2024-07-01: 6899} + 13: {2024-07-01: 7399} +G: + 1: {2024-07-01: 1439} + 2: {2024-07-01: 1953} + 3: {2024-07-01: 2467} + 4: {2024-07-01: 2981} + 5: {2024-07-01: 3495} + 6: {2024-07-01: 4009} + 7: {2024-07-01: 4522} + 8: {2024-07-01: 5037} + 9: {2024-07-01: 5551} + 10: {2024-07-01: 6064} + 11: {2024-07-01: 6579} + 12: {2024-07-01: 7093} + 13: {2024-07-01: 7606} +H: + 1: {2024-07-01: 1478} + 2: {2024-07-01: 2007} + 3: {2024-07-01: 2534} + 4: {2024-07-01: 3062} + 5: {2024-07-01: 3590} + 6: {2024-07-01: 4118} + 7: {2024-07-01: 4646} + 8: {2024-07-01: 5174} + 9: {2024-07-01: 5702} + 10: {2024-07-01: 6229} + 11: {2024-07-01: 6758} + 12: {2024-07-01: 7286} + 13: {2024-07-01: 7813} +I: + 1: {2024-07-01: 1519} + 2: {2024-07-01: 2063} + 3: {2024-07-01: 2605} + 4: {2024-07-01: 3147} + 5: {2024-07-01: 3691} + 6: {2024-07-01: 4233} + 7: {2024-07-01: 4776} + 8: {2024-07-01: 5319} + 9: {2024-07-01: 5862} + 10: {2024-07-01: 6404} + 11: {2024-07-01: 6947} + 12: {2024-07-01: 7490} + 13: {2024-07-01: 8032} +J: + 1: {2024-07-01: 1561} + 2: {2024-07-01: 2119} + 3: {2024-07-01: 2676} + 4: {2024-07-01: 3233} + 5: {2024-07-01: 3792} + 6: {2024-07-01: 4349} + 7: {2024-07-01: 4906} + 8: {2024-07-01: 5464} + 9: {2024-07-01: 6021} + 10: {2024-07-01: 6578} + 11: {2024-07-01: 7137} + 12: {2024-07-01: 7694} + 13: {2024-07-01: 8251} +K: + 1: {2024-07-01: 1604} + 2: {2024-07-01: 2178} + 3: {2024-07-01: 2751} + 4: {2024-07-01: 3324} + 5: {2024-07-01: 3898} + 6: {2024-07-01: 4470} + 7: {2024-07-01: 5043} + 8: {2024-07-01: 5617} + 9: {2024-07-01: 6190} + 10: {2024-07-01: 6762} + 11: {2024-07-01: 7336} + 12: {2024-07-01: 7909} + 13: {2024-07-01: 8482} +L: + 1: {2024-07-01: 1648} + 2: {2024-07-01: 2238} + 3: {2024-07-01: 2826} + 4: {2024-07-01: 3414} + 5: {2024-07-01: 4004} + 6: {2024-07-01: 4592} + 7: {2024-07-01: 5180} + 8: {2024-07-01: 5770} + 9: {2024-07-01: 6358} + 10: {2024-07-01: 6947} + 11: {2024-07-01: 7536} + 12: {2024-07-01: 8125} + 13: {2024-07-01: 8713} +M: + 1: {2024-07-01: 1694} + 2: {2024-07-01: 2300} + 3: {2024-07-01: 2905} + 4: {2024-07-01: 3510} + 5: {2024-07-01: 4116} + 6: {2024-07-01: 4721} + 7: {2024-07-01: 5325} + 8: {2024-07-01: 5932} + 9: {2024-07-01: 6536} + 10: {2024-07-01: 7141} + 11: {2024-07-01: 7747} + 12: {2024-07-01: 8352} + 13: {2024-07-01: 8957} +N: + 1: {2024-07-01: 1740} + 2: {2024-07-01: 2363} + 3: {2024-07-01: 2984} + 4: {2024-07-01: 3605} + 5: {2024-07-01: 4228} + 6: {2024-07-01: 4849} + 7: {2024-07-01: 5471} + 8: {2024-07-01: 6093} + 9: {2024-07-01: 6714} + 10: {2024-07-01: 7336} + 11: {2024-07-01: 7958} + 12: {2024-07-01: 8580} + 13: {2024-07-01: 9201} +O: + 1: {2024-07-01: 1789} + 2: {2024-07-01: 2429} + 3: {2024-07-01: 3068} + 4: {2024-07-01: 3706} + 5: {2024-07-01: 4346} + 6: {2024-07-01: 4985} + 7: {2024-07-01: 5624} + 8: {2024-07-01: 6264} + 9: {2024-07-01: 6902} + 10: {2024-07-01: 7541} + 11: {2024-07-01: 8181} + 12: {2024-07-01: 8820} + 13: {2024-07-01: 9458} +P: + 1: {2024-07-01: 1838} + 2: {2024-07-01: 2495} + 3: {2024-07-01: 3151} + 4: {2024-07-01: 3807} + 5: {2024-07-01: 4465} + 6: {2024-07-01: 5121} + 7: {2024-07-01: 5777} + 8: {2024-07-01: 6434} + 9: {2024-07-01: 7090} + 10: {2024-07-01: 7746} + 11: {2024-07-01: 8404} + 12: {2024-07-01: 9060} + 13: {2024-07-01: 9716} +Q: + 1: {2024-07-01: 1889} + 2: {2024-07-01: 2565} + 3: {2024-07-01: 3240} + 4: {2024-07-01: 3914} + 5: {2024-07-01: 4590} + 6: {2024-07-01: 5264} + 7: {2024-07-01: 5939} + 8: {2024-07-01: 6615} + 9: {2024-07-01: 7289} + 10: {2024-07-01: 7963} + 11: {2024-07-01: 8639} + 12: {2024-07-01: 9314} + 13: {2024-07-01: 9988} +R: + 1: {2024-07-01: 1941} + 2: {2024-07-01: 2635} + 3: {2024-07-01: 3328} + 4: {2024-07-01: 4021} + 5: {2024-07-01: 4715} + 6: {2024-07-01: 5408} + 7: {2024-07-01: 6100} + 8: {2024-07-01: 6795} + 9: {2024-07-01: 7487} + 10: {2024-07-01: 8180} + 11: {2024-07-01: 8875} + 12: {2024-07-01: 9567} + 13: {2024-07-01: 10260} +S: + 1: {2024-07-01: 1995} + 2: {2024-07-01: 2709} + 3: {2024-07-01: 3421} + 4: {2024-07-01: 4133} + 5: {2024-07-01: 4847} + 6: {2024-07-01: 5559} + 7: {2024-07-01: 6271} + 8: {2024-07-01: 6985} + 9: {2024-07-01: 7697} + 10: {2024-07-01: 8409} + 11: {2024-07-01: 9123} + 12: {2024-07-01: 9835} + 13: {2024-07-01: 10547} +T: + 1: {2024-07-01: 2049} + 2: {2024-07-01: 2783} + 3: {2024-07-01: 3514} + 4: {2024-07-01: 4246} + 5: {2024-07-01: 4979} + 6: {2024-07-01: 5710} + 7: {2024-07-01: 6442} + 8: {2024-07-01: 7175} + 9: {2024-07-01: 7907} + 10: {2024-07-01: 8638} + 11: {2024-07-01: 9372} + 12: {2024-07-01: 10103} + 13: {2024-07-01: 10835} +U: + 1: {2024-07-01: 2107} + 2: {2024-07-01: 2860} + 3: {2024-07-01: 3613} + 4: {2024-07-01: 4365} + 5: {2024-07-01: 5118} + 6: {2024-07-01: 5870} + 7: {2024-07-01: 6622} + 8: {2024-07-01: 7376} + 9: {2024-07-01: 8128} + 10: {2024-07-01: 8880} + 11: {2024-07-01: 9634} + 12: {2024-07-01: 10386} + 13: {2024-07-01: 11138} +V: + 1: {2024-07-01: 2164} + 2: {2024-07-01: 2938} + 3: {2024-07-01: 3711} + 4: {2024-07-01: 4483} + 5: {2024-07-01: 5258} + 6: {2024-07-01: 6030} + 7: {2024-07-01: 6803} + 8: {2024-07-01: 7577} + 9: {2024-07-01: 8350} + 10: {2024-07-01: 9122} + 11: {2024-07-01: 9896} + 12: {2024-07-01: 10669} + 13: {2024-07-01: 11441} +W: + 1: {2024-07-01: 2225} + 2: {2024-07-01: 3021} + 3: {2024-07-01: 3815} + 4: {2024-07-01: 4609} + 5: {2024-07-01: 5405} + 6: {2024-07-01: 6199} + 7: {2024-07-01: 6993} + 8: {2024-07-01: 7789} + 9: {2024-07-01: 8583} + 10: {2024-07-01: 9378} + 11: {2024-07-01: 10173} + 12: {2024-07-01: 10968} + 13: {2024-07-01: 11762} +X: + 1: {2024-07-01: 2285} + 2: {2024-07-01: 3103} + 3: {2024-07-01: 3919} + 4: {2024-07-01: 4735} + 5: {2024-07-01: 5552} + 6: {2024-07-01: 6368} + 7: {2024-07-01: 7184} + 8: {2024-07-01: 8001} + 9: {2024-07-01: 8817} + 10: {2024-07-01: 9633} + 11: {2024-07-01: 10451} + 12: {2024-07-01: 11266} + 13: {2024-07-01: 12082} +Y: + 1: {2024-07-01: 2349} + 2: {2024-07-01: 3190} + 3: {2024-07-01: 4028} + 4: {2024-07-01: 4867} + 5: {2024-07-01: 5708} + 6: {2024-07-01: 6546} + 7: {2024-07-01: 7385} + 8: {2024-07-01: 8225} + 9: {2024-07-01: 9064} + 10: {2024-07-01: 9903} + 11: {2024-07-01: 10743} + 12: {2024-07-01: 11582} + 13: {2024-07-01: 12420} +Z: + 1: {2024-07-01: 2413} + 2: {2024-07-01: 3277} + 3: {2024-07-01: 4138} + 4: {2024-07-01: 5000} + 5: {2024-07-01: 5863} + 6: {2024-07-01: 6725} + 7: {2024-07-01: 7586} + 8: {2024-07-01: 8449} + 9: {2024-07-01: 9311} + 10: {2024-07-01: 10172} + 11: {2024-07-01: 11036} + 12: {2024-07-01: 11897} + 13: {2024-07-01: 12759} +AA: + 1: {2024-07-01: 2481} + 2: {2024-07-01: 3368} + 3: {2024-07-01: 4254} + 4: {2024-07-01: 5140} + 5: {2024-07-01: 6027} + 6: {2024-07-01: 6913} + 7: {2024-07-01: 7798} + 8: {2024-07-01: 8686} + 9: {2024-07-01: 9572} + 10: {2024-07-01: 10457} + 11: {2024-07-01: 11345} + 12: {2024-07-01: 12230} + 13: {2024-07-01: 13116} +BB: + 1: {2024-07-01: 4000} + 2: {2024-07-01: 5000} + 3: {2024-07-01: 6000} + 4: {2024-07-01: 7000} + 5: {2024-07-01: 8000} + 6: {2024-07-01: 9000} + 7: {2024-07-01: 9000} + 8: {2024-07-01: 9000} + 9: {2024-07-01: 9500} + 10: {2024-07-01: 9500} + 11: {2024-07-01: 10500} + 12: {2024-07-01: 11000} + 13: {2024-07-01: 12000} diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/copay/sliding_fee/unit_fee.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/copay/sliding_fee/unit_fee.yaml new file mode 100644 index 00000000000..d818ca22a95 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/copay/sliding_fee/unit_fee.yaml @@ -0,0 +1,127 @@ +description: Iowa charges this CCA and CCA Plus copay per half-day unit under the Child Care Assistance program, by sliding-fee level and the number of children in care (one, two, or three or more). The fee is assessed on only one child, the child receiving the most units of service. +metadata: + period: year + unit: currency-USD + label: Iowa CCA and CCA Plus per-unit copay by fee level and children in care + breakdown: + - [A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, AA, BB] + - range(1, 4) + breakdown_labels: + - Fee level + - Children in care (3 means three or more) + reference: + - title: Iowa HHS CCA and CCA Plus Family Fee Chart (effective July 1, 2024) + href: https://hhs.iowa.gov/media/12445/download?inline=#page=1 + +A: + 1: {2024-07-01: 0.00} + 2: {2024-07-01: 0.00} + 3: {2024-07-01: 0.00} +B: + 1: {2024-07-01: 0.20} + 2: {2024-07-01: 0.45} + 3: {2024-07-01: 0.70} +C: + 1: {2024-07-01: 0.45} + 2: {2024-07-01: 0.70} + 3: {2024-07-01: 0.95} +D: + 1: {2024-07-01: 0.70} + 2: {2024-07-01: 0.95} + 3: {2024-07-01: 1.20} +E: + 1: {2024-07-01: 0.95} + 2: {2024-07-01: 1.20} + 3: {2024-07-01: 1.45} +F: + 1: {2024-07-01: 1.20} + 2: {2024-07-01: 1.45} + 3: {2024-07-01: 1.70} +G: + 1: {2024-07-01: 1.45} + 2: {2024-07-01: 1.70} + 3: {2024-07-01: 1.95} +H: + 1: {2024-07-01: 1.70} + 2: {2024-07-01: 1.95} + 3: {2024-07-01: 2.20} +I: + 1: {2024-07-01: 1.95} + 2: {2024-07-01: 2.20} + 3: {2024-07-01: 2.45} +J: + 1: {2024-07-01: 2.20} + 2: {2024-07-01: 2.45} + 3: {2024-07-01: 2.70} +K: + 1: {2024-07-01: 2.45} + 2: {2024-07-01: 2.70} + 3: {2024-07-01: 2.95} +L: + 1: {2024-07-01: 2.70} + 2: {2024-07-01: 2.95} + 3: {2024-07-01: 3.20} +M: + 1: {2024-07-01: 2.95} + 2: {2024-07-01: 3.20} + 3: {2024-07-01: 3.45} +N: + 1: {2024-07-01: 3.20} + 2: {2024-07-01: 3.45} + 3: {2024-07-01: 3.70} +O: + 1: {2024-07-01: 3.45} + 2: {2024-07-01: 3.70} + 3: {2024-07-01: 3.95} +P: + 1: {2024-07-01: 3.70} + 2: {2024-07-01: 3.95} + 3: {2024-07-01: 4.20} +Q: + 1: {2024-07-01: 3.95} + 2: {2024-07-01: 4.20} + 3: {2024-07-01: 4.45} +R: + 1: {2024-07-01: 4.20} + 2: {2024-07-01: 4.45} + 3: {2024-07-01: 4.70} +S: + 1: {2024-07-01: 4.45} + 2: {2024-07-01: 4.70} + 3: {2024-07-01: 4.95} +T: + 1: {2024-07-01: 4.70} + 2: {2024-07-01: 4.95} + 3: {2024-07-01: 5.20} +U: + 1: {2024-07-01: 4.95} + 2: {2024-07-01: 5.20} + 3: {2024-07-01: 5.45} +V: + 1: {2024-07-01: 5.20} + 2: {2024-07-01: 5.45} + 3: {2024-07-01: 5.70} +W: + 1: {2024-07-01: 5.45} + 2: {2024-07-01: 5.70} + 3: {2024-07-01: 5.95} +X: + 1: {2024-07-01: 5.70} + 2: {2024-07-01: 5.95} + 3: {2024-07-01: 6.20} +Y: + 1: {2024-07-01: 5.95} + 2: {2024-07-01: 6.20} + 3: {2024-07-01: 6.45} +Z: + 1: {2024-07-01: 6.20} + 2: {2024-07-01: 6.45} + 3: {2024-07-01: 6.70} +AA: + 1: {2024-07-01: 6.45} + 2: {2024-07-01: 6.70} + 3: {2024-07-01: 6.95} +BB: + 1: {2024-07-01: 6.70} + 2: {2024-07-01: 6.95} + 3: {2024-07-01: 7.20} diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/eligibility/asset_limit.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/eligibility/asset_limit.yaml new file mode 100644 index 00000000000..e1d1df9ecf0 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/eligibility/asset_limit.yaml @@ -0,0 +1,11 @@ +description: Iowa limits Child Care Assistance to families whose resources do not exceed this amount. +values: + 2024-07-01: 1_000_000 + +metadata: + unit: currency-USD + period: year + label: Iowa CCA resource limit + reference: + - title: IAC 441-170.2(1)"g" — Resource limit ($1 million) + href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=5 diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/eligibility/child_age_limit.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/eligibility/child_age_limit.yaml new file mode 100644 index 00000000000..338b1cadb5d --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/eligibility/child_age_limit.yaml @@ -0,0 +1,11 @@ +description: Iowa provides Child Care Assistance for a child under this age. +values: + 2024-07-01: 13 + +metadata: + unit: year + period: year + label: Iowa CCA child age limit + reference: + - title: IAC 441-170.2(2)"a" — Child age (up to age 13) + href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=5 diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/eligibility/special_needs_age_limit.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/eligibility/special_needs_age_limit.yaml new file mode 100644 index 00000000000..480f23bd60a --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/eligibility/special_needs_age_limit.yaml @@ -0,0 +1,11 @@ +description: Iowa provides Child Care Assistance for a child with special needs who is under this age. +values: + 2024-07-01: 19 + +metadata: + unit: year + period: year + label: Iowa CCA special-needs child age limit + reference: + - title: IAC 441-170.2(2)"a" — Child age (up to age 19 for a child with special needs) + href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=5 diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/income/countable_income/earned_sources.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/countable_income/earned_sources.yaml new file mode 100644 index 00000000000..fc41ae07a9a --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/countable_income/earned_sources.yaml @@ -0,0 +1,17 @@ +description: Iowa counts these earned income sources under the Child Care Assistance program. Iowa excludes the earnings of a child age 14 or younger and the earnings of a child 18 or younger who is a full-time student, so these sources are summed only for persons who are not excluded minors. +values: + 2024-07-01: + - employment_income + - self_employment_income + - sstb_self_employment_income + - farm_operations_income + +metadata: + unit: list + period: year + label: Iowa CCA countable earned income sources + reference: + - title: IAC 441-170.2(1)"c" — Income counted (wages, self-employment net profit) + href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=3 + - title: IAC 441-170.2(1)"d" — Income excluded (child age 14 or under; student 18 or under) + href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=3 diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/income/countable_income/unearned_sources.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/countable_income/unearned_sources.yaml new file mode 100644 index 00000000000..a1bcddad40b --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/countable_income/unearned_sources.yaml @@ -0,0 +1,33 @@ +description: Iowa counts these unearned income sources under the Child Care Assistance program. +values: + 2024-07-01: + - social_security # includes retirement, survivors, and disability + - pension_income # pensions and annuities + - dividend_income + - interest_income + - tax_exempt_interest_income + - estate_income # estate and trust income + - rental_income # net rental and royalty income + - unemployment_compensation + - workers_compensation + - alimony_income + - child_support_received + - veterans_benefits # veterans pensions + - gambling_winnings # casino profits + - railroad_benefits # railroad retirement + - strike_benefits # strike pay + +metadata: + unit: list + period: year + label: Iowa CCA countable unearned income sources + # Iowa counts public assistance generally, but the model omits TANF/FIP cash + # dollars here to avoid the CCAP-to-TANF circular dependency (FIP recipients + # qualify through the income-exception path instead). Iowa excludes SSI except + # in specified FIP-linked cases; we don't track that linkage at the moment, so + # SSI is conservatively left out of countable income. AmeriCorps living + # allowance, permanent disability insurance, and royalty income have no + # separate PolicyEngine variable at the moment and are therefore not counted. + reference: + - title: IAC 441-170.2(1)"c" — Income counted (Census median-income sources) + href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=3 diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/income/fpl_rate/exit_basic.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/fpl_rate/exit_basic.yaml new file mode 100644 index 00000000000..236a73cf068 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/fpl_rate/exit_basic.yaml @@ -0,0 +1,11 @@ +description: Iowa keeps already-enrolled families eligible for CCA Exit basic care at annual redetermination if their income is at or below this share of the federal poverty level. +values: + 2024-07-01: 2.5 + +metadata: + unit: /1 + period: year + label: Iowa CCA Exit ongoing basic income limit (share of FPL) + reference: + - title: IAC 441-170.2(1)"a"(3) — Income (ongoing CCA Exit basic 250% FPL) + href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=3 diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/income/fpl_rate/exit_special_needs.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/fpl_rate/exit_special_needs.yaml new file mode 100644 index 00000000000..a64595271d5 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/fpl_rate/exit_special_needs.yaml @@ -0,0 +1,11 @@ +description: Iowa keeps already-enrolled families eligible for CCA Exit special-needs care at annual redetermination if their income is at or below this share of the federal poverty level. +values: + 2024-07-01: 2.75 + +metadata: + unit: /1 + period: year + label: Iowa CCA Exit ongoing special-needs income limit (share of FPL) + reference: + - title: IAC 441-170.2(1)"a"(3) — Income (ongoing CCA Exit special-needs 275% FPL) + href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=3 diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/income/fpl_rate/initial_basic.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/fpl_rate/initial_basic.yaml new file mode 100644 index 00000000000..57e240ed1c1 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/fpl_rate/initial_basic.yaml @@ -0,0 +1,11 @@ +description: Iowa limits initial Child Care Assistance eligibility for basic care to families with income at or below this share of the federal poverty level. +values: + 2024-07-01: 1.6 + +metadata: + unit: /1 + period: year + label: Iowa CCA initial basic income limit (share of FPL) + reference: + - title: IAC 441-170.2(1)"a"(1) — Income (initial, basic care 160% FPL) + href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=3 diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/income/fpl_rate/initial_special_needs.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/fpl_rate/initial_special_needs.yaml new file mode 100644 index 00000000000..0d4d9aaac1f --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/fpl_rate/initial_special_needs.yaml @@ -0,0 +1,11 @@ +description: Iowa limits initial Child Care Assistance eligibility for special-needs care to families with income at or below this share of the federal poverty level. +values: + 2024-07-01: 2 + +metadata: + unit: /1 + period: year + label: Iowa CCA initial special-needs income limit (share of FPL) + reference: + - title: IAC 441-170.2(1)"a"(1) — Income (initial, special-needs care 200% FPL) + href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=3 diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/income/fpl_rate/plus_basic.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/fpl_rate/plus_basic.yaml new file mode 100644 index 00000000000..8d0e8e9687a --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/fpl_rate/plus_basic.yaml @@ -0,0 +1,11 @@ +description: Iowa keeps already-enrolled families eligible for CCA Plus at annual redetermination if their income is at or below this share of the federal poverty level. +values: + 2024-07-01: 2.25 + +metadata: + unit: /1 + period: year + label: Iowa CCA Plus ongoing income limit (share of FPL) + reference: + - title: IAC 441-170.2(1)"a"(2) — Income (ongoing CCA Plus 225% FPL) + href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=3 diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/income/minor_earnings_age.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/minor_earnings_age.yaml new file mode 100644 index 00000000000..4a74f904fd3 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/minor_earnings_age.yaml @@ -0,0 +1,11 @@ +description: Iowa excludes the earnings of a child this age or younger from countable income under the Child Care Assistance program. +values: + 2024-07-01: 14 + +metadata: + unit: year + period: year + label: Iowa CCA excluded-earnings child age + reference: + - title: IAC 441-170.2(1)"d" — Income excluded (earnings of a child age 14 or younger) + href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=3 diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/income/minor_student_age.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/minor_student_age.yaml new file mode 100644 index 00000000000..c3850de9f03 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/minor_student_age.yaml @@ -0,0 +1,11 @@ +description: Iowa excludes the earnings of a full-time student this age or younger from countable income under the Child Care Assistance program. +values: + 2024-07-01: 18 + +metadata: + unit: year + period: year + label: Iowa CCA excluded-earnings student age + reference: + - title: IAC 441-170.2(1)"d" — Income excluded (earnings of a student 18 or younger) + href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=3 diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/income/smi_rate.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/smi_rate.yaml new file mode 100644 index 00000000000..2551b4a777d --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/smi_rate.yaml @@ -0,0 +1,11 @@ +description: Iowa caps initial Child Care Assistance income at this share of the state median family income when that cap is lower than the federal poverty level standard. +values: + 2024-07-01: 0.85 + +metadata: + unit: /1 + period: year + label: Iowa CCA median family income cap (share of MFI) + reference: + - title: IAC 441-170.2(1)"a"(1) — Income (85% of Iowa median family income) + href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=3 diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/payment/hours_per_unit.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/payment/hours_per_unit.yaml new file mode 100644 index 00000000000..82cc2790456 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/payment/hours_per_unit.yaml @@ -0,0 +1,11 @@ +description: Iowa defines a Child Care Assistance unit of service as a half day of up to this many hours of service per 24-hour period. +values: + 2024-07-01: 5 + +metadata: + unit: hour + period: year + label: Iowa CCA hours per unit of service + reference: + - title: IAC 441-170.1 — Definitions (unit of service, half day, up to 5 hours) + href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=3 diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/payment/in_home_min_children.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/payment/in_home_min_children.yaml new file mode 100644 index 00000000000..b10e9f7b178 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/payment/in_home_min_children.yaml @@ -0,0 +1,11 @@ +description: Iowa pays for in-home child care under the Child Care Assistance program only when at least this many children in the family need care. +values: + 2024-07-01: 3 + +metadata: + unit: int + period: year + label: Iowa CCA in-home minimum children needing care + reference: + - title: IAC 441-170.4(8)"d" — In-home care (three or more children require care) + href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=15 diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/payment/in_home_rate.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/payment/in_home_rate.yaml new file mode 100644 index 00000000000..d089636c78a --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/payment/in_home_rate.yaml @@ -0,0 +1,13 @@ +description: Iowa pays this maximum half-day-unit rate for in-home child care (Table 5) under the Child Care Assistance program. The rule sets the rate at the minimum wage amount (Iowa minimum wage of $7.25 per hour times the 5-hour unit equals $36.25), the same for basic and special-needs care across all age groups. +values: + 2024-07-01: 36.25 + +metadata: + unit: currency-USD + period: year + label: Iowa CCA in-home half-day-unit rate + reference: + - title: IAC 441-170.4(8)"d" — In-home care rate (minimum wage amount; paid only with 3 or more children needing care) + href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=15 + - title: Iowa HHS CCA Maximum Rates, Table 5, In-Home ($36.25 per half-day unit, effective July 1, 2024) + href: https://iowaccrr.org/resources/files/BGP/149%20CCA%20Rates.pdf#page=2 diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/payment/rates/child_care_home_not_registered.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/payment/rates/child_care_home_not_registered.yaml new file mode 100644 index 00000000000..a022f6e0c20 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/payment/rates/child_care_home_not_registered.yaml @@ -0,0 +1,23 @@ +description: Iowa provides these maximum half-day-unit rates for a nonregistered child care home (Table 4) under the Child Care Assistance program. This provider type has no quality rating dimension. +metadata: + period: year + unit: currency-USD + label: Iowa CCA nonregistered child care home half-day-unit rates + breakdown: + - ia_cca_age_group + reference: + - title: IAC 441-170.4(7) — Table 4, Child Care Home (not registered) (effective July 1, 2024) + href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=15 + +INFANT_TODDLER: + 2024-07-01: 16.63 +INFANT_TODDLER_SN: + 2024-07-01: 24.95 +PRESCHOOL: + 2024-07-01: 15.00 +PRESCHOOL_SN: + 2024-07-01: 22.50 +SCHOOL_AGE: + 2024-07-01: 15.00 +SCHOOL_AGE_SN: + 2024-07-01: 22.50 diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/payment/rates/child_dev_home_ab.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/payment/rates/child_dev_home_ab.yaml new file mode 100644 index 00000000000..ad51456266b --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/payment/rates/child_dev_home_ab.yaml @@ -0,0 +1,64 @@ +description: Iowa provides these maximum half-day-unit rates for child development homes A and B (Table 2) under the Child Care Assistance program. +metadata: + period: year + unit: currency-USD + label: Iowa CCA child development home A/B half-day-unit rates + breakdown: + - ia_cca_quality_rating + - ia_cca_age_group + reference: + - title: IAC 441-170.4(7) — Table 2, Child Development Home A/B (effective July 1, 2024) + href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=14 + +NO_RATING: + INFANT_TODDLER: + 2024-07-01: 16.63 + INFANT_TODDLER_SN: + 2024-07-01: 24.95 + PRESCHOOL: + 2024-07-01: 15.00 + PRESCHOOL_SN: + 2024-07-01: 22.50 + SCHOOL_AGE: + 2024-07-01: 15.00 + SCHOOL_AGE_SN: + 2024-07-01: 22.50 +RATING_1_OR_2: + INFANT_TODDLER: + 2024-07-01: 16.63 + INFANT_TODDLER_SN: + 2024-07-01: 24.95 + PRESCHOOL: + 2024-07-01: 15.50 + PRESCHOOL_SN: + 2024-07-01: 23.25 + SCHOOL_AGE: + 2024-07-01: 15.25 + SCHOOL_AGE_SN: + 2024-07-01: 22.88 +RATING_3_OR_4: + INFANT_TODDLER: + 2024-07-01: 16.63 + INFANT_TODDLER_SN: + 2024-07-01: 24.95 + PRESCHOOL: + 2024-07-01: 16.00 + PRESCHOOL_SN: + 2024-07-01: 24.00 + SCHOOL_AGE: + 2024-07-01: 15.50 + SCHOOL_AGE_SN: + 2024-07-01: 23.25 +RATING_5: + INFANT_TODDLER: + 2024-07-01: 17.50 + INFANT_TODDLER_SN: + 2024-07-01: 26.25 + PRESCHOOL: + 2024-07-01: 17.00 + PRESCHOOL_SN: + 2024-07-01: 25.50 + SCHOOL_AGE: + 2024-07-01: 15.75 + SCHOOL_AGE_SN: + 2024-07-01: 23.63 diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/payment/rates/child_dev_home_c.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/payment/rates/child_dev_home_c.yaml new file mode 100644 index 00000000000..f46a3acd911 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/payment/rates/child_dev_home_c.yaml @@ -0,0 +1,64 @@ +description: Iowa provides these maximum half-day-unit rates for child development home C (Table 3) under the Child Care Assistance program. +metadata: + period: year + unit: currency-USD + label: Iowa CCA child development home C half-day-unit rates + breakdown: + - ia_cca_quality_rating + - ia_cca_age_group + reference: + - title: IAC 441-170.4(7) — Table 3, Child Development Home C (effective July 1, 2024) + href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=14 + +NO_RATING: + INFANT_TODDLER: + 2024-07-01: 18.00 + INFANT_TODDLER_SN: + 2024-07-01: 27.00 + PRESCHOOL: + 2024-07-01: 16.28 + PRESCHOOL_SN: + 2024-07-01: 24.42 + SCHOOL_AGE: + 2024-07-01: 15.00 + SCHOOL_AGE_SN: + 2024-07-01: 22.50 +RATING_1_OR_2: + INFANT_TODDLER: + 2024-07-01: 18.00 + INFANT_TODDLER_SN: + 2024-07-01: 27.00 + PRESCHOOL: + 2024-07-01: 17.00 + PRESCHOOL_SN: + 2024-07-01: 25.50 + SCHOOL_AGE: + 2024-07-01: 15.75 + SCHOOL_AGE_SN: + 2024-07-01: 23.63 +RATING_3_OR_4: + INFANT_TODDLER: + 2024-07-01: 18.00 + INFANT_TODDLER_SN: + 2024-07-01: 27.00 + PRESCHOOL: + 2024-07-01: 17.50 + PRESCHOOL_SN: + 2024-07-01: 26.25 + SCHOOL_AGE: + 2024-07-01: 16.44 + SCHOOL_AGE_SN: + 2024-07-01: 24.66 +RATING_5: + INFANT_TODDLER: + 2024-07-01: 18.50 + INFANT_TODDLER_SN: + 2024-07-01: 27.75 + PRESCHOOL: + 2024-07-01: 18.50 + PRESCHOOL_SN: + 2024-07-01: 27.75 + SCHOOL_AGE: + 2024-07-01: 17.50 + SCHOOL_AGE_SN: + 2024-07-01: 26.25 diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/payment/rates/licensed_center.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/payment/rates/licensed_center.yaml new file mode 100644 index 00000000000..286978590a4 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/payment/rates/licensed_center.yaml @@ -0,0 +1,64 @@ +description: Iowa provides these maximum half-day-unit rates for licensed child care centers (Table 1) under the Child Care Assistance program. +metadata: + period: year + unit: currency-USD + label: Iowa CCA licensed center half-day-unit rates + breakdown: + - ia_cca_quality_rating + - ia_cca_age_group + reference: + - title: IAC 441-170.4(7) — Table 1, Licensed Center (effective July 1, 2024) + href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=14 + +NO_RATING: + INFANT_TODDLER: + 2024-07-01: 25.66 + INFANT_TODDLER_SN: + 2024-07-01: 51.94 + PRESCHOOL: + 2024-07-01: 19.50 + PRESCHOOL_SN: + 2024-07-01: 30.43 + SCHOOL_AGE: + 2024-07-01: 16.50 + SCHOOL_AGE_SN: + 2024-07-01: 30.34 +RATING_1_OR_2: + INFANT_TODDLER: + 2024-07-01: 25.66 + INFANT_TODDLER_SN: + 2024-07-01: 51.94 + PRESCHOOL: + 2024-07-01: 20.25 + PRESCHOOL_SN: + 2024-07-01: 30.43 + SCHOOL_AGE: + 2024-07-01: 17.10 + SCHOOL_AGE_SN: + 2024-07-01: 30.34 +RATING_3_OR_4: + INFANT_TODDLER: + 2024-07-01: 25.66 + INFANT_TODDLER_SN: + 2024-07-01: 51.94 + PRESCHOOL: + 2024-07-01: 21.50 + PRESCHOOL_SN: + 2024-07-01: 30.43 + SCHOOL_AGE: + 2024-07-01: 17.50 + SCHOOL_AGE_SN: + 2024-07-01: 30.34 +RATING_5: + INFANT_TODDLER: + 2024-07-01: 27.00 + INFANT_TODDLER_SN: + 2024-07-01: 51.94 + PRESCHOOL: + 2024-07-01: 22.97 + PRESCHOOL_SN: + 2024-07-01: 30.43 + SCHOOL_AGE: + 2024-07-01: 18.50 + SCHOOL_AGE_SN: + 2024-07-01: 30.34 diff --git a/policyengine_us/programs.yaml b/policyengine_us/programs.yaml index ade1f4b04ab..6910166a1a8 100644 --- a/policyengine_us/programs.yaml +++ b/policyengine_us/programs.yaml @@ -460,7 +460,7 @@ programs: category: Benefits agency: HHS status: partial - coverage: AK, AR, AL, CA, CO, CT, DE, DC, IL, MA, MD, ME, NC, NH, NJ, PA, RI, SC, TX, VA, VT, WA, WV + coverage: AK, AR, AL, CA, CO, CT, DE, DC, IA, IL, MA, MD, ME, NC, NH, NJ, PA, RI, SC, TX, VA, VT, WA, WV state_implementations: - state: AK status: complete @@ -515,6 +515,12 @@ programs: name: Maine CCAP full_name: Maine Child Care Affordability Program variable: me_ccap + - state: IA + status: complete + name: Iowa CCA + full_name: Iowa Child Care Assistance + variable: ia_cca + parameter_prefix: gov.states.ia.hhs.cca - state: NH status: complete name: New Hampshire CCAP diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_copay.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_copay.yaml new file mode 100644 index 00000000000..58949225323 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_copay.yaml @@ -0,0 +1,354 @@ +# ia_cca_copay: SPMUnit, USD, MONTH (REQ-028 / REQ-029 / REQ-030). +# Two mechanisms dispatched by ia_cca_in_exit_tier (enrolled & income > 225% FPL): +# A. CCA / CCA Plus sliding unit fee = unit_fee[level][children_col] * focal_units, +# where focal_units = the most monthly units of any in-care eligible child +# and children_col = clip(children in care, 1, 3). +# B. CCA Exit = exit_pct[level] * sum of childcare expenses for children in care. +# Families eligible without regard to income (FIP/protective/foster) pay no fee. +# monthly_units(hpw) = hpw * (52 / 12) / 5; monthly_units(40 hrs/wk) = 34.6667. +# 2025 FPG: family of 3 = 26_650 (225% monthly = 4_996.88); family of 4 = 32_150. +# Sliding-fee monthly income thresholds (family 3): A 2_044, B 2_152, C 2_212. +# Sliding-fee monthly income thresholds (family 4): B 2_600, C 2_673. +# pre_subsidy_childcare_expenses is YEAR-defined, so an annual value is divided to +# a monthly figure inside the MONTH copay formula (e.g. 12_000/yr -> 1_000/mo). + +# ===== Mechanism A: CCA / CCA Plus sliding unit fee ===== + +- name: Sliding fee is zero at Level A. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 18_000 + child: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 40 + tax_units: + tax_unit: + members: [parent, child] + spm_units: + spm_unit: + members: [parent, child] + ia_cca_enrolled: false + households: + household: + members: [parent, child] + state_code: IA + output: + # Family of 2; income 1_500/mo <= Level A threshold (1_619 for size 2) -> fee 0 + ia_cca_copay: 0 + +- name: Sliding fee at Level B with one child in care. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 25_200 + coparent: + age: 30 + child: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 40 + tax_units: + tax_unit: + members: [parent, coparent, child] + spm_units: + spm_unit: + members: [parent, coparent, child] + ia_cca_enrolled: false + households: + household: + members: [parent, coparent, child] + state_code: IA + output: + # Family of 3; income 2_100/mo: > A (2_044), <= B (2_152) -> Level B (index 1). + # 1 child in care -> column 1 -> unit fee 0.20. + # focal_units = monthly_units(40) = 34.6667; copay = 0.20 * 34.6667 = 6.9333 + ia_cca_copay: 6.93 + +- name: Sliding fee at Level B with two children in care uses the two-child column. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 25_200 + child1: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 40 + child2: + age: 8 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 40 + tax_units: + tax_unit: + members: [parent, child1, child2] + spm_units: + spm_unit: + members: [parent, child1, child2] + ia_cca_enrolled: false + households: + household: + members: [parent, child1, child2] + state_code: IA + output: + # Family of 3; income 2_100/mo -> Level B. 2 children in care -> column 2 -> 0.45. + # focal_units = monthly_units(40) = 34.6667; copay = 0.45 * 34.6667 = 15.60. + # Fee is still assessed on only one child (the most units), via focal_units. + ia_cca_copay: 15.60 + +- name: Sliding fee at Level C with three children in care uses the three-or-more column. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 31_800 + child1: + age: 1 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 40 + child2: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 40 + child3: + age: 8 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 40 + tax_units: + tax_unit: + members: [parent, child1, child2, child3] + spm_units: + spm_unit: + members: [parent, child1, child2, child3] + ia_cca_enrolled: false + households: + household: + members: [parent, child1, child2, child3] + state_code: IA + output: + # Family of 4; income 2_650/mo: > B (2_600), <= C (2_673) -> Level C (index 2). + # 3 children in care -> column 3 (three or more) -> 0.95. + # focal_units = monthly_units(40) = 34.6667; copay = 0.95 * 34.6667 = 32.9333 + ia_cca_copay: 32.93 + +# ===== Mechanism B: CCA Exit percentage of cost of care ===== +# Note: CCA Exit Level A (33%) is unreachable in practice — entering the exit tier +# requires income above 225% FPL, which always exceeds the exit Level A income +# threshold, so the lowest reachable exit level is B (45%). + +- name: CCA Exit fee at Level B is 45 percent of the cost of care. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 60_000 + coparent: + age: 30 + child: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 40 + pre_subsidy_childcare_expenses: 12_000 + tax_units: + tax_unit: + members: [parent, coparent, child] + spm_units: + spm_unit: + members: [parent, coparent, child] + ia_cca_enrolled: true + households: + household: + members: [parent, coparent, child] + state_code: IA + output: + # Family of 3; income 5_000/mo > 225% FPL (4_996.88) -> exit tier. + # Exit basic: > A (4_842), <= B (5_057) -> Level B -> 45%. + # Expense 12_000/yr -> 1_000/mo; copay = 0.45 * 1_000 = 450 + ia_cca_copay: 450.00 + +- name: CCA Exit fee at Level C is 60 percent across all children in care. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 61_200 + child1: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 40 + pre_subsidy_childcare_expenses: 12_000 + child2: + age: 8 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 40 + pre_subsidy_childcare_expenses: 9_600 + tax_units: + tax_unit: + members: [parent, child1, child2] + spm_units: + spm_unit: + members: [parent, child1, child2] + ia_cca_enrolled: true + households: + household: + members: [parent, child1, child2] + state_code: IA + output: + # Family of 3; income 5_100/mo > 225% FPL (4_996.88) -> exit tier. + # Exit basic: > A (4_842), > B (5_057), <= C (5_272) -> Level C -> 60%. + # The exit fee applies to EACH child in care, summed. Expenses 12_000/yr and + # 9_600/yr -> 1_000/mo and 800/mo; copay = 0.60 * (1_000 + 800) = 1_080 + ia_cca_copay: 1_080.00 + +- name: CCA Exit special-needs fee uses the special-needs income thresholds. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 64_800 + coparent: + age: 30 + child: + age: 4 + is_disabled: true + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 40 + pre_subsidy_childcare_expenses: 12_000 + tax_units: + tax_unit: + members: [parent, coparent, child] + spm_units: + spm_unit: + members: [parent, coparent, child] + ia_cca_enrolled: true + households: + household: + members: [parent, coparent, child] + state_code: IA + output: + # Family of 3 special-needs; income 5_400/mo > 225% FPL (4_996.88) -> exit tier. + # Exit special-needs thresholds (family 3): A 4_842, B 5_272, C 5_703. + # 5_400: > A, > B (5_272), <= C (5_703) -> Level C -> 60%. + # Expense 12_000/yr -> 1_000/mo; copay = 0.60 * 1_000 = 600 + ia_cca_copay: 600.00 + +# ===== Copay exemption for the no-income-test population ===== + +- name: Foster family pays no copay despite income in the fee range. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 25_200 + child: + age: 4 + is_in_foster_care: true + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 40 + tax_units: + tax_unit: + members: [parent, child] + spm_units: + spm_unit: + members: [parent, child] + ia_cca_enrolled: false + households: + household: + members: [parent, child] + state_code: IA + output: + # Foster child -> income exception -> copay forced to 0 regardless of level + ia_cca_copay: 0 + +- name: FIP-enrolled family pays no copay. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 25_200 + child: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 40 + tax_units: + tax_unit: + members: [parent, child] + spm_units: + spm_unit: + members: [parent, child] + ia_cca_enrolled: false + is_tanf_enrolled: true + households: + household: + members: [parent, child] + state_code: IA + output: + # FIP/TANF recipient -> income exception -> copay 0 + ia_cca_copay: 0 + +# ===== Edge cases ===== + +- name: No children in care produces a zero copay. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 25_200 + is_tax_unit_head_or_spouse: true + child: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 0 + tax_units: + tax_unit: + members: [parent, child] + spm_units: + spm_unit: + members: [parent, child] + ia_cca_enrolled: false + households: + household: + members: [parent, child] + state_code: IA + output: + # Income lands at a non-zero sliding-fee level, but with no child in care the + # focal_units max is 0, so the per-unit fee multiplies to 0 (degenerate; the + # copay must not crash or produce a stray fee with nobody in care) + ia_cca_copay: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_exit_fee_level.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_exit_fee_level.yaml new file mode 100644 index 00000000000..4c375aefb64 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_exit_fee_level.yaml @@ -0,0 +1,171 @@ +# ia_cca_exit_fee_level: SPMUnit, int index 0-3, MONTH (REQ-029). +# Selects the CCA Exit fee level (A=0 .. D=3) from monthly income and family size, +# using the basic thresholds for a basic-care family and the special-needs +# thresholds when the family includes a special-needs child. +# Family-of-3 exit basic thresholds: A 4_842, B 5_057, C 5_272, D 5_380. +# Family-of-3 exit special-needs thresholds: A 4_842, B 5_272, C 5_703, D 5_918. + +- name: Low exit income lands at Level A index 0. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + employment_income: 57_600 + coparent: + age: 30 + child: + age: 4 + spm_units: + spm_unit: + members: [parent, coparent, child] + households: + household: + members: [parent, coparent, child] + state_code: IA + output: + # Family of 3 basic; income 4_800/mo <= A (4_842) -> index 0 + # (This income alone is not in the exit tier; the index is still defined.) + ia_cca_exit_fee_level: 0 + +- name: Exit income between A and B is Level B index 1. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + employment_income: 60_000 + coparent: + age: 30 + child: + age: 4 + spm_units: + spm_unit: + members: [parent, coparent, child] + households: + household: + members: [parent, coparent, child] + state_code: IA + output: + # Income 5_000/mo: > A (4_842), <= B (5_057) -> index 1 + ia_cca_exit_fee_level: 1 + +- name: Exit income between C and D is Level D index 3. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + employment_income: 64_200 + coparent: + age: 30 + child: + age: 4 + spm_units: + spm_unit: + members: [parent, coparent, child] + households: + household: + members: [parent, coparent, child] + state_code: IA + output: + # Income 5_350/mo: > A, > B, > C (5_272), <= D (5_380) -> index 3 + ia_cca_exit_fee_level: 3 + +- name: Special-needs family uses the higher special-needs thresholds. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + employment_income: 63_600 + coparent: + age: 30 + child: + age: 4 + is_disabled: true + spm_units: + spm_unit: + members: [parent, coparent, child] + households: + household: + members: [parent, coparent, child] + state_code: IA + output: + # Income 5_300/mo with a special-needs child uses SN thresholds: + # > A (4_842), <= B (5_272)? 5_300 > 5_272 -> > B, <= C (5_703) -> index 2. + # Under the basic thresholds the same income would be index 3 (> D 5_380? no, + # > C 5_272 -> index 3). The SN table keeps it at index 2 -> proves SN routing. + ia_cca_exit_fee_level: 2 + +# === Edge cases === +# The exit-threshold tables also run by family size 1-13 with the formula clipping +# size to 13, so a unit of 14+ uses the size-13 column. Size-13 exit-basic +# thresholds: A 11_329, B 15_592. + +- name: Exit fee family of thirteen uses the size-13 thresholds. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 40 + employment_income: 144_000 + c1: {age: 4} + c2: {age: 4} + c3: {age: 4} + c4: {age: 4} + c5: {age: 4} + c6: {age: 4} + c7: {age: 4} + c8: {age: 4} + c9: {age: 4} + c10: {age: 4} + c11: {age: 4} + c12: {age: 4} + spm_units: + spm_unit: + members: [parent, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12] + households: + household: + members: [parent, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12] + state_code: IA + output: + # Family of 13 basic; income 12_000/mo: > A (11_329), <= B (15_592) -> index 1 + ia_cca_exit_fee_level: 1 + +- name: Exit fee family of fourteen uses the size-13 column at the same level. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 40 + employment_income: 144_000 + c1: {age: 4} + c2: {age: 4} + c3: {age: 4} + c4: {age: 4} + c5: {age: 4} + c6: {age: 4} + c7: {age: 4} + c8: {age: 4} + c9: {age: 4} + c10: {age: 4} + c11: {age: 4} + c12: {age: 4} + c13: {age: 4} + spm_units: + spm_unit: + members: [parent, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13] + households: + household: + members: [parent, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13] + state_code: IA + output: + # Family of 14 clips to size 13 -> same thresholds, same income -> index 1 + ia_cca_exit_fee_level: 1 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_in_exit_tier.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_in_exit_tier.yaml new file mode 100644 index 00000000000..9e6dc4b6ff0 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_in_exit_tier.yaml @@ -0,0 +1,130 @@ +# ia_cca_in_exit_tier: SPMUnit, bool, MONTH (REQ-029 dispatch). +# True when the family is enrolled AND monthly income exceeds the ongoing CCA Plus +# limit (225% FPL). These families pay the percentage-of-cost CCA Exit fee rather +# than the sliding unit fee. 2025 family-of-3 225% FPL monthly = 4_996.88 +# (annual 59_962.50). + +- name: Enrolled family above 225 percent FPL is in the exit tier. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + employment_income: 60_000 + coparent: + age: 30 + child: + age: 4 + spm_units: + spm_unit: + members: [parent, coparent, child] + ia_cca_enrolled: true + households: + household: + members: [parent, coparent, child] + state_code: IA + output: + # Income 5_000/mo > 4_996.88 (225% FPL) and enrolled -> exit tier + ia_cca_in_exit_tier: true + +- name: Enrolled family below 225 percent FPL is not in the exit tier. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + employment_income: 57_600 + coparent: + age: 30 + child: + age: 4 + spm_units: + spm_unit: + members: [parent, coparent, child] + ia_cca_enrolled: true + households: + household: + members: [parent, coparent, child] + state_code: IA + output: + # Income 4_800/mo <= 4_996.88 (225% FPL) -> CCA Plus, not exit tier + ia_cca_in_exit_tier: false + +- name: Applicant above 225 percent FPL is not in the exit tier. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + employment_income: 60_000 + coparent: + age: 30 + child: + age: 4 + spm_units: + spm_unit: + members: [parent, coparent, child] + ia_cca_enrolled: false + households: + household: + members: [parent, coparent, child] + state_code: IA + output: + # Same income but not enrolled -> the exit tier requires enrollment -> false + ia_cca_in_exit_tier: false + +# === Edge cases === +# The exit-tier dispatch turns on at exactly 225% FPL (family of 3; 225% annual = +# 59_962.50). We bracket the boundary +-12/yr; the exact round-trip is float-fragile +# so we do not assert at it. + +- name: Enrolled family 12 dollars below 225 percent FPL is not in the exit tier. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + employment_income: 59_950 + coparent: + age: 30 + child: + age: 4 + spm_units: + spm_unit: + members: [parent, coparent, child] + ia_cca_enrolled: true + households: + household: + members: [parent, coparent, child] + state_code: IA + output: + # 59_950 (-$1/mo under 59_962.50) -> not over 225% FPL -> CCA Plus, not exit + ia_cca_in_exit_tier: false + +- name: Enrolled family 12 dollars above 225 percent FPL is in the exit tier. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + employment_income: 59_975 + coparent: + age: 30 + child: + age: 4 + spm_units: + spm_unit: + members: [parent, coparent, child] + ia_cca_enrolled: true + households: + household: + members: [parent, coparent, child] + state_code: IA + output: + # 59_975 (+$1/mo over 59_962.50) and enrolled -> exit tier + ia_cca_in_exit_tier: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_sliding_fee_level.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_sliding_fee_level.yaml new file mode 100644 index 00000000000..d1b0681fc6b --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_sliding_fee_level.yaml @@ -0,0 +1,186 @@ +# ia_cca_sliding_fee_level: SPMUnit, int index 0-27, MONTH (REQ-028). +# The family is at the first level whose monthly-income threshold (for its family +# size) is at or above its income; equivalently the index is the number of level +# thresholds the monthly income exceeds, capped at 27 (Level BB). +# Income is monthly ia_cca_countable_income = employment_income / 12. +# Family-of-3 monthly thresholds: A 2_044, B 2_152, C 2_212; AA 4_254, BB 6_000. + +- name: Income below the Level A threshold is Level A index 0. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + employment_income: 24_000 + coparent: + age: 30 + child: + age: 4 + spm_units: + spm_unit: + members: [parent, coparent, child] + households: + household: + members: [parent, coparent, child] + state_code: IA + output: + # Family of 3; income 2_000/mo <= A (2_044) -> index 0 + ia_cca_sliding_fee_level: 0 + +- name: Income between Level A and B thresholds is Level B index 1. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + employment_income: 25_200 + coparent: + age: 30 + child: + age: 4 + spm_units: + spm_unit: + members: [parent, coparent, child] + households: + household: + members: [parent, coparent, child] + state_code: IA + output: + # Income 2_100/mo: > A (2_044), <= B (2_152) -> index 1 + ia_cca_sliding_fee_level: 1 + +- name: Income between Level B and C thresholds is Level C index 2. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + employment_income: 26_400 + coparent: + age: 30 + child: + age: 4 + spm_units: + spm_unit: + members: [parent, coparent, child] + households: + household: + members: [parent, coparent, child] + state_code: IA + output: + # Income 2_200/mo: > B (2_152), <= C (2_212) -> index 2 + ia_cca_sliding_fee_level: 2 + +- name: Very high income within the CCA Plus range tops out at Level BB index 27. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + employment_income: 57_600 + coparent: + age: 30 + child: + age: 4 + spm_units: + spm_unit: + members: [parent, coparent, child] + households: + household: + members: [parent, coparent, child] + state_code: IA + output: + # Income 4_800/mo: > AA (4_254), <= BB (6_000) -> top index 27 (Level BB) + ia_cca_sliding_fee_level: 27 + +# === Edge cases === +# The income-threshold table runs by family size 1-13 (13 means "13 or more"). The +# formula clips family size to that range, so a unit of 14+ must use the size-13 +# column. These three cases test the minimum (size 1) and the size-13 clip ceiling. + +- name: Family of one uses the size-1 thresholds at Level A index 0. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + employment_income: 12_000 + spm_units: + spm_unit: + members: [parent] + households: + household: + members: [parent] + state_code: IA + output: + # Family of 1; income 1_000/mo <= A (1_192 for size 1) -> index 0 (minimum size) + ia_cca_sliding_fee_level: 0 + +- name: Family of thirteen uses the size-13 thresholds. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 40 + employment_income: 76_800 + c1: {age: 4} + c2: {age: 4} + c3: {age: 4} + c4: {age: 4} + c5: {age: 4} + c6: {age: 4} + c7: {age: 4} + c8: {age: 4} + c9: {age: 4} + c10: {age: 4} + c11: {age: 4} + c12: {age: 4} + spm_units: + spm_unit: + members: [parent, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12] + households: + household: + members: [parent, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12] + state_code: IA + output: + # Family of 13; income 6_400/mo: > A (6_303), <= B (6_635 size 13) -> index 1 + ia_cca_sliding_fee_level: 1 + +- name: Family of fourteen uses the size-13 column at the same level. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 40 + employment_income: 76_800 + c1: {age: 4} + c2: {age: 4} + c3: {age: 4} + c4: {age: 4} + c5: {age: 4} + c6: {age: 4} + c7: {age: 4} + c8: {age: 4} + c9: {age: 4} + c10: {age: 4} + c11: {age: 4} + c12: {age: 4} + c13: {age: 4} + spm_units: + spm_unit: + members: [parent, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13] + households: + household: + members: [parent, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13] + state_code: IA + output: + # Family of 14 clips to size 13 -> same thresholds, same income -> index 1 + # (confirms the clip(size, 1, 13) ceiling on the lookup table) + ia_cca_sliding_fee_level: 1 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_activity_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_activity_eligible.yaml new file mode 100644 index 00000000000..904d53aec31 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_activity_eligible.yaml @@ -0,0 +1,248 @@ +# ia_cca_activity_eligible: SPMUnit, bool, MONTH (REQ-013 / REQ-014 / REQ-015). +# IAC 441-170.2(2)"b": each parent must average at least 32 hours/week in an +# approved activity (28 hours/week if the family includes a child with special +# needs). Full-time students qualify; meets_ccdf_activity_test is the fallback for +# non-derivable activities. Uses weekly_hours_worked_before_lsr (default 40) so the +# labor-supply response does not create a circular dependency; tests that mean +# "not working enough" set weekly_hours_worked_before_lsr explicitly. +# Two-parent families simplify the coinciding-hours rule to a per-parent gate: +# every head/spouse must independently qualify. + +- name: Single parent working 32 hours meets the activity requirement. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + is_tax_unit_head_or_spouse: true + weekly_hours_worked_before_lsr: 32 + child: + age: 3 + tax_units: + tax_unit: + members: [parent, child] + spm_units: + spm_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: IA + output: + # 32 >= 32 -> activity eligible + ia_cca_activity_eligible: true + +- name: Single parent working 31 hours does not meet the basic requirement. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + is_tax_unit_head_or_spouse: true + weekly_hours_worked_before_lsr: 31 + child: + age: 3 + tax_units: + tax_unit: + members: [parent, child] + spm_units: + spm_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: IA + output: + # 31 < 32 and no special-needs child -> not activity eligible + ia_cca_activity_eligible: false + +- name: Family with a special-needs child uses the 28-hour threshold. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + is_tax_unit_head_or_spouse: true + weekly_hours_worked_before_lsr: 28 + child: + age: 3 + is_disabled: true + tax_units: + tax_unit: + members: [parent, child] + spm_units: + spm_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: IA + output: + # Special-needs child -> 28-hour threshold; 28 >= 28 -> activity eligible + # (would fail the 32-hour basic threshold) + ia_cca_activity_eligible: true + +- name: Special-needs family parent below 28 hours is not eligible. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + is_tax_unit_head_or_spouse: true + weekly_hours_worked_before_lsr: 27 + child: + age: 3 + is_disabled: true + tax_units: + tax_unit: + members: [parent, child] + spm_units: + spm_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: IA + output: + # 27 < 28 -> not activity eligible + ia_cca_activity_eligible: false + +- name: Two-parent family qualifies only when both parents meet the hours. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent1: + age: 30 + is_tax_unit_head_or_spouse: true + weekly_hours_worked_before_lsr: 40 + parent2: + age: 30 + is_tax_unit_head_or_spouse: true + weekly_hours_worked_before_lsr: 40 + child: + age: 3 + tax_units: + tax_unit: + members: [parent1, parent2, child] + spm_units: + spm_unit: + members: [parent1, parent2, child] + households: + household: + members: [parent1, parent2, child] + state_code: IA + output: + # Both parents 40 >= 32 -> activity eligible + ia_cca_activity_eligible: true + +- name: Two-parent family fails when one parent is below the hours threshold. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent1: + age: 30 + is_tax_unit_head_or_spouse: true + weekly_hours_worked_before_lsr: 40 + parent2: + age: 30 + is_tax_unit_head_or_spouse: true + weekly_hours_worked_before_lsr: 10 + child: + age: 3 + tax_units: + tax_unit: + members: [parent1, parent2, child] + spm_units: + spm_unit: + members: [parent1, parent2, child] + households: + household: + members: [parent1, parent2, child] + state_code: IA + output: + # Parent2 10 < 32 -> not all parents qualify -> not activity eligible + ia_cca_activity_eligible: false + +- name: Full-time student parent qualifies even without enough work hours. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 22 + is_tax_unit_head_or_spouse: true + weekly_hours_worked_before_lsr: 0 + is_full_time_student: true + child: + age: 3 + tax_units: + tax_unit: + members: [parent, child] + spm_units: + spm_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: IA + output: + # 0 work hours but full-time student -> approved activity -> eligible + ia_cca_activity_eligible: true + +- name: Idle parent with no approved activity is not eligible. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + is_tax_unit_head_or_spouse: true + weekly_hours_worked_before_lsr: 0 + child: + age: 3 + tax_units: + tax_unit: + members: [parent, child] + spm_units: + spm_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: IA + output: + # 0 hours, not a student, no fallback activity -> not activity eligible + ia_cca_activity_eligible: false + +- name: Parent meeting the CCDF activity fallback qualifies. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + is_tax_unit_head_or_spouse: true + weekly_hours_worked_before_lsr: 0 + child: + age: 3 + tax_units: + tax_unit: + members: [parent, child] + spm_units: + spm_unit: + members: [parent, child] + meets_ccdf_activity_test: true + households: + household: + members: [parent, child] + state_code: IA + output: + # Non-derivable approved activity (job search, PROMISE JOBS, training combo) + # routed through meets_ccdf_activity_test fallback -> eligible + ia_cca_activity_eligible: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_asset_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_asset_eligible.yaml new file mode 100644 index 00000000000..fba59ef9994 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_asset_eligible.yaml @@ -0,0 +1,59 @@ +# ia_cca_asset_eligible: SPMUnit, bool, MONTH (REQ-012). +# Iowa's resource limit is $1,000,000, applied instead of the much tighter federal +# CCDF asset check. Tests spm_unit_assets <= 1_000_000. + +- name: Resources below the one million dollar limit are eligible. + period: 2025-01 + absolute_error_margin: 0.1 + input: + spm_units: + spm_unit: + members: [parent] + spm_unit_assets: 500_000 + people: + parent: + age: 30 + households: + household: + members: [parent] + state_code: IA + output: + ia_cca_asset_eligible: true + +- name: Resources at exactly the limit are eligible. + period: 2025-01 + absolute_error_margin: 0.1 + input: + spm_units: + spm_unit: + members: [parent] + spm_unit_assets: 1_000_000 + people: + parent: + age: 30 + households: + household: + members: [parent] + state_code: IA + output: + # 1_000_000 <= 1_000_000 -> eligible (at the limit) + ia_cca_asset_eligible: true + +- name: Resources above the limit are not eligible. + period: 2025-01 + absolute_error_margin: 0.1 + input: + spm_units: + spm_unit: + members: [parent] + spm_unit_assets: 1_000_001 + people: + parent: + age: 30 + households: + household: + members: [parent] + state_code: IA + output: + # Just above the $1,000,000 limit -> ineligible + ia_cca_asset_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_eligible.yaml new file mode 100644 index 00000000000..fb69d30b72a --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_eligible.yaml @@ -0,0 +1,220 @@ +# ia_cca_eligible: SPMUnit, bool, MONTH (REQ-016). +# Eligible = has an eligible child AND asset-eligible AND +# ((income-eligible AND activity-eligible) OR income-exception). +# Families eligible without regard to income (FIP/protective/foster) bypass both +# the income and activity tests but still need an eligible child and assets below +# the limit. + +- name: Standard household meeting all tests is eligible. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + employment_income: 30_000 + is_tax_unit_head_or_spouse: true + weekly_hours_worked_before_lsr: 40 + child: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + tax_units: + tax_unit: + members: [parent, child] + spm_units: + spm_unit: + members: [parent, child] + ia_cca_enrolled: false + spm_unit_assets: 5_000 + households: + household: + members: [parent, child] + state_code: IA + output: + # Eligible child, income (2_500/mo) below 160% FPL (family 2 -> 2_820/mo), + # parent works 40 hrs, assets below $1M -> eligible + ia_cca_eligible: true + +- name: Household failing the income test is not eligible on the standard path. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + employment_income: 60_000 + is_tax_unit_head_or_spouse: true + weekly_hours_worked_before_lsr: 40 + child: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + tax_units: + tax_unit: + members: [parent, child] + spm_units: + spm_unit: + members: [parent, child] + ia_cca_enrolled: false + spm_unit_assets: 5_000 + households: + household: + members: [parent, child] + state_code: IA + output: + # Family of 2; income 5_000/mo > 160% FPL (2_820/mo) and not enrolled -> ineligible + ia_cca_eligible: false + +- name: FIP household over the income limit and not working is still eligible. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + employment_income: 90_000 + is_tax_unit_head_or_spouse: true + weekly_hours_worked_before_lsr: 0 + child: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + tax_units: + tax_unit: + members: [parent, child] + spm_units: + spm_unit: + members: [parent, child] + is_tanf_enrolled: true + spm_unit_assets: 5_000 + households: + household: + members: [parent, child] + state_code: IA + output: + # Income exception (FIP) bypasses the income and activity tests; eligible child + # and assets below $1M -> eligible despite high income and zero work hours + ia_cca_eligible: true + +- name: Household with no eligible child is not eligible. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + employment_income: 20_000 + is_tax_unit_head_or_spouse: true + weekly_hours_worked_before_lsr: 40 + teen: + age: 15 + is_tax_unit_dependent: true + immigration_status: CITIZEN + tax_units: + tax_unit: + members: [parent, teen] + spm_units: + spm_unit: + members: [parent, teen] + spm_unit_assets: 5_000 + households: + household: + members: [parent, teen] + state_code: IA + output: + # Only child is age 15 (> 13, not disabled) -> no eligible child -> ineligible + ia_cca_eligible: false + +- name: Household over the resource limit is not eligible. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + employment_income: 20_000 + is_tax_unit_head_or_spouse: true + weekly_hours_worked_before_lsr: 40 + child: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + tax_units: + tax_unit: + members: [parent, child] + spm_units: + spm_unit: + members: [parent, child] + is_tanf_enrolled: true + spm_unit_assets: 2_000_000 + households: + household: + members: [parent, child] + state_code: IA + output: + # FIP income exception, but resources $2M > $1M limit -> ineligible + # (the asset test is not bypassed by the income exception) + ia_cca_eligible: false + +# === Edge cases === + +- name: SPM unit with only an adult and no children is not eligible. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + employment_income: 24_000 + is_tax_unit_head_or_spouse: true + weekly_hours_worked_before_lsr: 40 + tax_units: + tax_unit: + members: [parent] + spm_units: + spm_unit: + members: [parent] + ia_cca_enrolled: false + spm_unit_assets: 5_000 + households: + household: + members: [parent] + state_code: IA + output: + # No children at all -> no eligible child -> ineligible (degenerate unit; the + # has_eligible_child gate must short-circuit without error) + ia_cca_eligible: false + +- name: Eligible child with zero care hours leaves the family eligible. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + employment_income: 24_000 + is_tax_unit_head_or_spouse: true + weekly_hours_worked_before_lsr: 40 + child: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 0 + tax_units: + tax_unit: + members: [parent, child] + spm_units: + spm_unit: + members: [parent, child] + ia_cca_enrolled: false + spm_unit_assets: 5_000 + households: + household: + members: [parent, child] + state_code: IA + output: + # Eligibility does not depend on care hours (only the benefit does); the child + # is age/dependency/immigration eligible -> family remains eligible. The zero + # care hours zero out the benefit, tested in the benefit file. + ia_cca_eligible: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_eligible_child.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_eligible_child.yaml new file mode 100644 index 00000000000..87c117f5302 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_eligible_child.yaml @@ -0,0 +1,226 @@ +# ia_cca_eligible_child: Person, bool, MONTH (REQ-001 / REQ-002 / REQ-004). +# IAC 441-170.2(2): child up to age 13 (basic), up to age 19 for a child with +# special needs; must be a dependent in the family; must be a U.S. citizen or +# qualified alien (is_ccdf_immigration_eligible_child). Protective-services and +# foster children are categorically eligible regardless of dependency/immigration. + +- name: Infant dependent citizen is an eligible child. + period: 2025-01 + input: + people: + parent: + age: 30 + immigration_status: CITIZEN + child: + age: 1 + is_tax_unit_dependent: true + immigration_status: CITIZEN + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: IA + output: + # Parent is not a child; child age 1 < 13, dependent, citizen -> eligible + ia_cca_eligible_child: [false, true] + +- name: Child just under the basic age limit is eligible. + period: 2025-01 + input: + people: + child: + age: 12 + is_tax_unit_dependent: true + immigration_status: CITIZEN + tax_units: + tax_unit: + members: [child] + households: + household: + members: [child] + state_code: IA + output: + # 12 < 13 -> eligible + ia_cca_eligible_child: true + +- name: Child at exactly age 13 is not eligible unless special needs. + period: 2025-01 + input: + people: + child: + age: 13 + is_tax_unit_dependent: true + immigration_status: CITIZEN + tax_units: + tax_unit: + members: [child] + households: + household: + members: [child] + state_code: IA + output: + # 13 is not < 13 and not disabled -> ineligible + ia_cca_eligible_child: false + +- name: Disabled child at age 13 is eligible under the special-needs limit. + period: 2025-01 + input: + people: + child: + age: 13 + is_disabled: true + is_tax_unit_dependent: true + immigration_status: CITIZEN + tax_units: + tax_unit: + members: [child] + households: + household: + members: [child] + state_code: IA + output: + # Disabled -> special-needs limit 19; 13 < 19 -> eligible + ia_cca_eligible_child: true + +- name: Disabled child just under the special-needs age limit is eligible. + period: 2025-01 + input: + people: + child: + age: 18 + is_disabled: true + is_tax_unit_dependent: true + immigration_status: CITIZEN + tax_units: + tax_unit: + members: [child] + households: + household: + members: [child] + state_code: IA + output: + # 18 < 19 -> eligible + ia_cca_eligible_child: true + +- name: Disabled child at exactly age 19 is not eligible. + period: 2025-01 + input: + people: + child: + age: 19 + is_disabled: true + is_tax_unit_dependent: true + immigration_status: CITIZEN + tax_units: + tax_unit: + members: [child] + households: + household: + members: [child] + state_code: IA + output: + # 19 is not < 19 -> ineligible even with special needs + ia_cca_eligible_child: false + +- name: Non-dependent child within the age limit is not a standard eligible child. + period: 2025-01 + input: + people: + child: + age: 5 + is_tax_unit_dependent: false + immigration_status: CITIZEN + tax_units: + tax_unit: + members: [child] + households: + household: + members: [child] + state_code: IA + output: + # Age-eligible but not a dependent and no categorical path -> ineligible + ia_cca_eligible_child: false + +- name: Child without immigration eligibility is not a standard eligible child. + period: 2025-01 + input: + people: + child: + age: 5 + is_tax_unit_dependent: true + immigration_status: UNDOCUMENTED + tax_units: + tax_unit: + members: [child] + households: + household: + members: [child] + state_code: IA + output: + # Age-eligible dependent but not immigration-eligible -> ineligible + ia_cca_eligible_child: false + +- name: Protective-services child is categorically eligible regardless of dependency. + period: 2025-01 + input: + people: + child: + age: 5 + is_tax_unit_dependent: false + immigration_status: UNDOCUMENTED + receives_or_needs_protective_services: true + tax_units: + tax_unit: + members: [child] + households: + household: + members: [child] + state_code: IA + output: + # Age-eligible and protective -> eligible despite non-dependent/non-citizen + ia_cca_eligible_child: true + +- name: Foster child is categorically eligible regardless of dependency. + period: 2025-01 + input: + people: + child: + age: 5 + is_tax_unit_dependent: false + immigration_status: UNDOCUMENTED + is_in_foster_care: true + tax_units: + tax_unit: + members: [child] + households: + household: + members: [child] + state_code: IA + output: + # Age-eligible and foster -> eligible despite non-dependent/non-citizen + ia_cca_eligible_child: true + +# === Edge cases === + +- name: Disabled child past the special-needs age limit is not eligible. + period: 2025-01 + input: + people: + child: + age: 20 + is_disabled: true + is_tax_unit_dependent: true + immigration_status: CITIZEN + tax_units: + tax_unit: + members: [child] + households: + household: + members: [child] + state_code: IA + output: + # 20 is well over the special-needs limit of 19 -> ineligible (upper boundary, + # confirms the age gate does not silently re-admit older disabled children) + ia_cca_eligible_child: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_income_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_income_eligible.yaml new file mode 100644 index 00000000000..2b42a4921e8 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_income_eligible.yaml @@ -0,0 +1,697 @@ +# ia_cca_income_eligible: SPMUnit, bool, MONTH (REQ-005 .. REQ-010). +# Three income tiers selected by the ia_cca_enrolled boolean input: +# - Applicants (not enrolled): initial limit = 160% FPL basic / 200% FPL special-needs. +# - Enrolled (ongoing): CCA Exit 250% FPL basic / 275% FPL special-needs. +# Each initial limit is capped at 85% of Iowa median family income when lower +# (smi_rate.yaml = 0.85); for the small families below the FPL limit always binds +# (85% MFI is far higher), so these cases test the FPL boundary directly. +# The comparison is MONTHLY: ia_cca_countable_income (employment_income / 12) is +# compared to spm_unit_fpg (auto-divided to monthly) * rate. Because both sides +# are divided by 12, an annual employment_income of (annual FPG * rate) lands +# exactly on the monthly limit, so the annual figures below equal the FPL band. +# 2025 FPG (CONTIGUOUS_US): first_person 15_650, additional_person 5_500. +# Family of 2 FPG = 21_150; 160% = 33_840. +# Family of 3 FPG = 26_650; 160% = 42_640; 200% = 53_300; 225% = 59_962.50; +# 250% = 66_625; 275% = 73_287.50. +# A family with a non-disabled child uses the basic limits; a family with a +# special-needs (disabled) child uses the special-needs limits. Only one parent +# earns, so countable income is that parent's employment income alone. + +# === Tier 1: initial applicant, basic 160% FPL === + +- name: Applicant family of two above 160 percent FPL is not income eligible. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + employment_income: 42_000 + child: + age: 2 + spm_units: + spm_unit: + members: [parent, child] + ia_cca_enrolled: false + households: + household: + members: [parent, child] + state_code: IA + output: + # Family of 2; 160% FPL = 33_840 (annual basis); 42_000 > 33_840 -> ineligible + ia_cca_income_eligible: false + +- name: Applicant family of three below 160 percent FPL is income eligible. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent1: + age: 30 + employment_income: 42_000 + parent2: + age: 30 + child: + age: 2 + spm_units: + spm_unit: + members: [parent1, parent2, child] + ia_cca_enrolled: false + households: + household: + members: [parent1, parent2, child] + state_code: IA + output: + # Family of 3; 160% FPL = 42_640; 42_000 < 42_640 -> eligible + ia_cca_income_eligible: true + +- name: Applicant family of three just below 160 percent FPL is income eligible. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent1: + age: 30 + employment_income: 42_600 + parent2: + age: 30 + child: + age: 2 + spm_units: + spm_unit: + members: [parent1, parent2, child] + ia_cca_enrolled: false + households: + household: + members: [parent1, parent2, child] + state_code: IA + output: + # 42_600 < 42_640 (160% FPL) -> eligible (boundary, just below) + ia_cca_income_eligible: true + +- name: Applicant family of three just above 160 percent FPL is not income eligible. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent1: + age: 30 + employment_income: 42_700 + parent2: + age: 30 + child: + age: 2 + spm_units: + spm_unit: + members: [parent1, parent2, child] + ia_cca_enrolled: false + households: + household: + members: [parent1, parent2, child] + state_code: IA + output: + # 42_700 > 42_640 (160% FPL) -> ineligible (not enrolled, basic child) + ia_cca_income_eligible: false + +# === Tier 1b: initial applicant, special-needs 200% FPL === + +- name: Applicant family with special-needs child uses the 200 percent FPL limit. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent1: + age: 30 + employment_income: 53_000 + parent2: + age: 30 + child: + age: 2 + is_disabled: true + spm_units: + spm_unit: + members: [parent1, parent2, child] + ia_cca_enrolled: false + households: + household: + members: [parent1, parent2, child] + state_code: IA + output: + # Family of 3 special-needs; 200% FPL = 53_300; 53_000 < 53_300 -> eligible + # (would be ineligible under the 160% basic limit of 42_640) + ia_cca_income_eligible: true + +- name: Applicant family with special-needs child above 200 percent FPL is not eligible. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent1: + age: 30 + employment_income: 53_500 + parent2: + age: 30 + child: + age: 2 + is_disabled: true + spm_units: + spm_unit: + members: [parent1, parent2, child] + ia_cca_enrolled: false + households: + household: + members: [parent1, parent2, child] + state_code: IA + output: + # 53_500 > 53_300 (200% FPL) -> ineligible + ia_cca_income_eligible: false + +# === Tier 2: enrolled (CCA Exit ceiling), basic 250% FPL === +# The enrolled income test uses the CCA Exit ceiling (250% basic). The CCA Plus +# (225%) vs CCA Exit (250%) distinction governs which COPAY mechanism applies, not +# income eligibility; see ia_cca_in_exit_tier and the copay tests. + +- name: Enrolled family above the initial limit but below 250 percent FPL stays eligible. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent1: + age: 30 + employment_income: 59_000 + parent2: + age: 30 + child: + age: 2 + spm_units: + spm_unit: + members: [parent1, parent2, child] + ia_cca_enrolled: true + households: + household: + members: [parent1, parent2, child] + state_code: IA + output: + # Enrolled basic; 250% FPL = 66_625; 59_000 < 66_625 -> eligible + # (would be ineligible as an applicant under 160% = 42_640) + ia_cca_income_eligible: true + +- name: Applicant at the same income above 160 percent FPL is not eligible. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent1: + age: 30 + employment_income: 59_000 + parent2: + age: 30 + child: + age: 2 + spm_units: + spm_unit: + members: [parent1, parent2, child] + ia_cca_enrolled: false + households: + household: + members: [parent1, parent2, child] + state_code: IA + output: + # Same 59_000 income but NOT enrolled -> initial 160% limit -> ineligible + ia_cca_income_eligible: false + +- name: Enrolled basic family just below 250 percent FPL stays eligible. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent1: + age: 30 + employment_income: 66_500 + parent2: + age: 30 + child: + age: 2 + spm_units: + spm_unit: + members: [parent1, parent2, child] + ia_cca_enrolled: true + households: + household: + members: [parent1, parent2, child] + state_code: IA + output: + # 66_500 < 66_625 (250% FPL) -> eligible (boundary, just below) + ia_cca_income_eligible: true + +- name: Enrolled basic family above 250 percent FPL is not eligible. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent1: + age: 30 + employment_income: 67_000 + parent2: + age: 30 + child: + age: 2 + spm_units: + spm_unit: + members: [parent1, parent2, child] + ia_cca_enrolled: true + households: + household: + members: [parent1, parent2, child] + state_code: IA + output: + # 67_000 > 66_625 (250% FPL) -> ineligible even when enrolled (basic care) + ia_cca_income_eligible: false + +# === Tier 3: enrolled special-needs, 275% FPL === + +- name: Enrolled special-needs family above 250 percent but below 275 percent FPL stays eligible. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent1: + age: 30 + employment_income: 72_000 + parent2: + age: 30 + child: + age: 2 + is_disabled: true + spm_units: + spm_unit: + members: [parent1, parent2, child] + ia_cca_enrolled: true + households: + household: + members: [parent1, parent2, child] + state_code: IA + output: + # Enrolled special-needs; 275% FPL = 73_287.50; 72_000 < 73_287.50 -> eligible + # (would be ineligible under the 250% basic limit of 66_625) + ia_cca_income_eligible: true + +- name: Enrolled special-needs family above 275 percent FPL is not eligible. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent1: + age: 30 + employment_income: 74_000 + parent2: + age: 30 + child: + age: 2 + is_disabled: true + spm_units: + spm_unit: + members: [parent1, parent2, child] + ia_cca_enrolled: true + households: + household: + members: [parent1, parent2, child] + state_code: IA + output: + # 74_000 > 73_287.50 (275% FPL) -> ineligible + ia_cca_income_eligible: false + +# === 85% MFI cap binds for the initial limit on a higher-income family === + +- name: Applicant family above the 85 percent MFI cap is not eligible. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent1: + age: 40 + employment_income: 130_000 + parent2: + age: 40 + child1: + age: 2 + child2: + age: 4 + spm_units: + spm_unit: + members: [parent1, parent2, child1, child2] + ia_cca_enrolled: false + households: + household: + members: [parent1, parent2, child1, child2] + state_code: IA + output: + # Family of 4; initial limit = min(160% FPL = 51_440, 85% MFI = 100_138). + # 130_000 exceeds both -> ineligible. Confirms the min(FPL, 85% MFI) cap is + # applied on the initial limit. + ia_cca_income_eligible: false + +# === Edge cases === +# Boundary tests use the MODEL's own monthly FPG (printed above). The income test +# compares monthly countable income (annual employment_income / 12) to monthly FPG +# (auto-divided) * rate. At EXACTLY the boundary the float32 round-trip +# (annual -> /12 income vs annual FPG -> /12 limit) drifts by a few thousandths, +# and the drift direction is value-dependent, so we never assert at the exact +# boundary; we bracket it +-12/yr (= +-$1/mo) instead. The existing +-100/yr cases +# above are loose; these tighten the band to the smallest reliable offset. + +- name: Applicant family of three 12 dollars below 160 percent FPL is eligible (tight boundary). + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent1: + age: 30 + employment_income: 42_628 + parent2: + age: 30 + child: + age: 2 + spm_units: + spm_unit: + members: [parent1, parent2, child] + ia_cca_enrolled: false + households: + household: + members: [parent1, parent2, child] + state_code: IA + output: + # 160% FPL annual = 42_640; 42_628 (-$1/mo) -> just below -> eligible + ia_cca_income_eligible: true + +- name: Applicant family of three 12 dollars above 160 percent FPL is not eligible (tight boundary). + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent1: + age: 30 + employment_income: 42_652 + parent2: + age: 30 + child: + age: 2 + spm_units: + spm_unit: + members: [parent1, parent2, child] + ia_cca_enrolled: false + households: + household: + members: [parent1, parent2, child] + state_code: IA + output: + # 42_652 (+$1/mo over the 42_640 limit) -> just above -> ineligible + ia_cca_income_eligible: false + +- name: Applicant family of three with zero income is income eligible. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent1: + age: 30 + employment_income: 0 + parent2: + age: 30 + child: + age: 2 + spm_units: + spm_unit: + members: [parent1, parent2, child] + ia_cca_enrolled: false + households: + household: + members: [parent1, parent2, child] + state_code: IA + output: + # 0 income is below every limit -> eligible (degenerate low boundary) + ia_cca_countable_income: 0 + ia_cca_income_eligible: true + +- name: Applicant family with very high income is not eligible under any tier. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent1: + age: 30 + employment_income: 500_000 + parent2: + age: 30 + child: + age: 2 + spm_units: + spm_unit: + members: [parent1, parent2, child] + ia_cca_enrolled: false + households: + household: + members: [parent1, parent2, child] + state_code: IA + output: + # 500_000/yr far exceeds the 160% FPL initial limit -> ineligible (degenerate + # high boundary; even the enrolled 250% ceiling could not reach this income) + ia_cca_income_eligible: false + +# Special-needs 200% FPL boundary (family of 3; 200% annual = 53_300), tight +-12/yr. +- name: Applicant special-needs family 12 dollars below 200 percent FPL is eligible. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent1: + age: 30 + employment_income: 53_288 + parent2: + age: 30 + child: + age: 2 + is_disabled: true + spm_units: + spm_unit: + members: [parent1, parent2, child] + ia_cca_enrolled: false + households: + household: + members: [parent1, parent2, child] + state_code: IA + output: + # 200% FPL annual = 53_300; 53_288 (-$1/mo) -> eligible + ia_cca_income_eligible: true + +- name: Applicant special-needs family 12 dollars above 200 percent FPL is not eligible. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent1: + age: 30 + employment_income: 53_312 + parent2: + age: 30 + child: + age: 2 + is_disabled: true + spm_units: + spm_unit: + members: [parent1, parent2, child] + ia_cca_enrolled: false + households: + household: + members: [parent1, parent2, child] + state_code: IA + output: + # 53_312 (+$1/mo over 53_300) -> ineligible + ia_cca_income_eligible: false + +# Enrolled basic 250% FPL boundary (family of 3; 250% annual = 66_625), tight +-12/yr. +- name: Enrolled basic family 12 dollars below 250 percent FPL is eligible. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent1: + age: 30 + employment_income: 66_612 + parent2: + age: 30 + child: + age: 2 + spm_units: + spm_unit: + members: [parent1, parent2, child] + ia_cca_enrolled: true + households: + household: + members: [parent1, parent2, child] + state_code: IA + output: + # 250% FPL annual = 66_625; 66_612 (-$1/mo) -> eligible + ia_cca_income_eligible: true + +- name: Enrolled basic family 12 dollars above 250 percent FPL is not eligible. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent1: + age: 30 + employment_income: 66_638 + parent2: + age: 30 + child: + age: 2 + spm_units: + spm_unit: + members: [parent1, parent2, child] + ia_cca_enrolled: true + households: + household: + members: [parent1, parent2, child] + state_code: IA + output: + # 66_638 (+$1/mo over 66_625) -> ineligible even when enrolled + ia_cca_income_eligible: false + +# Enrolled special-needs 275% FPL boundary (family of 3; 275% annual = 73_287.50). +- name: Enrolled special-needs family 12 dollars below 275 percent FPL is eligible. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent1: + age: 30 + employment_income: 73_275 + parent2: + age: 30 + child: + age: 2 + is_disabled: true + spm_units: + spm_unit: + members: [parent1, parent2, child] + ia_cca_enrolled: true + households: + household: + members: [parent1, parent2, child] + state_code: IA + output: + # 275% FPL annual = 73_287.50; 73_275 -> just below -> eligible + ia_cca_income_eligible: true + +- name: Enrolled special-needs family above 275 percent FPL is not eligible. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent1: + age: 30 + employment_income: 73_300 + parent2: + age: 30 + child: + age: 2 + is_disabled: true + spm_units: + spm_unit: + members: [parent1, parent2, child] + ia_cca_enrolled: true + households: + household: + members: [parent1, parent2, child] + state_code: IA + output: + # 73_300 (+$12.50/yr over 73_287.50) -> ineligible + ia_cca_income_eligible: false + +# === 85% MFI cap genuinely binds (only for very large families) === +# The federal SMI household-size adjustment caps at 6 persons, so 85% MFI plateaus +# while 160% FPL keeps rising. The cap therefore binds only at family size 16+. A +# 20-person unit puts 85% MFI ($13,994.91/mo) below 160% FPL ($16,020/mo), so income +# in that window fails the initial test even though it is below 160% FPL. (The +# existing family-of-4 case above never reaches the cap because its income exceeds +# both limits; these two cases exercise the min(FPL, 85% MFI) selection directly.) + +- name: Applicant large family between 85 percent MFI and 160 percent FPL is not eligible. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 40 + employment_income: 180_000 + c1: {age: 4} + c2: {age: 4} + c3: {age: 4} + c4: {age: 4} + c5: {age: 4} + c6: {age: 4} + c7: {age: 4} + c8: {age: 4} + c9: {age: 4} + c10: {age: 4} + c11: {age: 4} + c12: {age: 4} + c13: {age: 4} + c14: {age: 4} + c15: {age: 4} + c16: {age: 4} + c17: {age: 4} + c18: {age: 4} + c19: {age: 4} + spm_units: + spm_unit: + members: [parent, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18, c19] + ia_cca_enrolled: false + households: + household: + members: [parent, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18, c19] + state_code: IA + output: + # Family of 20; 15_000/mo income > 85% MFI (13_994.91/mo) but < 160% FPL + # (16_020/mo). initial limit = min(160% FPL, 85% MFI) = 85% MFI -> ineligible. + ia_cca_income_eligible: false + +- name: Applicant large family below the 85 percent MFI cap is eligible. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 40 + employment_income: 150_000 + c1: {age: 4} + c2: {age: 4} + c3: {age: 4} + c4: {age: 4} + c5: {age: 4} + c6: {age: 4} + c7: {age: 4} + c8: {age: 4} + c9: {age: 4} + c10: {age: 4} + c11: {age: 4} + c12: {age: 4} + c13: {age: 4} + c14: {age: 4} + c15: {age: 4} + c16: {age: 4} + c17: {age: 4} + c18: {age: 4} + c19: {age: 4} + spm_units: + spm_unit: + members: [parent, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18, c19] + ia_cca_enrolled: false + households: + household: + members: [parent, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18, c19] + state_code: IA + output: + # 12_500/mo income < 85% MFI (13_994.91/mo) -> below the binding cap -> eligible + ia_cca_income_eligible: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_income_exception.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_income_exception.yaml new file mode 100644 index 00000000000..601cf646434 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_income_exception.yaml @@ -0,0 +1,85 @@ +# ia_cca_income_exception: SPMUnit, bool, MONTH (REQ-011). +# Iowa serves certain families without regard to income (IAC 441-170.2(1)"b"): +# FIP/TANF recipients (via is_tanf_enrolled), families with a child in protective +# child care, and licensed foster parents needing care for a foster child. + +- name: FIP-enrolled family qualifies through the income exception. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + child: + age: 4 + spm_units: + spm_unit: + members: [parent, child] + is_tanf_enrolled: true + households: + household: + members: [parent, child] + state_code: IA + output: + ia_cca_income_exception: true + +- name: Family with a protective-services child qualifies. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + child: + age: 4 + receives_or_needs_protective_services: true + spm_units: + spm_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: IA + output: + ia_cca_income_exception: true + +- name: Foster-care family qualifies. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + child: + age: 4 + is_in_foster_care: true + spm_units: + spm_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: IA + output: + ia_cca_income_exception: true + +- name: Family without any exception path does not qualify. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + child: + age: 4 + spm_units: + spm_unit: + members: [parent, child] + is_tanf_enrolled: false + households: + household: + members: [parent, child] + state_code: IA + output: + # No FIP, no protective, no foster -> standard income-tested path applies + ia_cca_income_exception: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_age_group.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_age_group.yaml new file mode 100644 index 00000000000..dcd6d5829f2 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_age_group.yaml @@ -0,0 +1,148 @@ +# ia_cca_age_group: Person, Enum, MONTH (derived from age via +# age_group/age_group.yaml bracket parameter; school-age cutoff = age 5). +# IAC 441-170.4(7) definitions: infant and toddler = two weeks to three years; +# preschool = three years to school age (kindergarten entry, modeled as age 5); +# school age = a child attending school. +# age < 3 -> INFANT_TODDLER (bracket index 0) +# 3 <= age < 5 -> PRESCHOOL (bracket index 1) +# age >= 5 -> SCHOOL_AGE (bracket index 2) +# Special-needs variants (suffix _SN) are derived from the is_disabled flag, +# matching the rate-table breakdown keys (INFANT_TODDLER_SN, etc.). + +- name: Newborn is infant and toddler. + period: 2025-01 + input: + people: + person1: + age: 0 + households: + household: + members: [person1] + state_code: IA + output: + ia_cca_age_group: INFANT_TODDLER + +- name: Child at 1 year is infant and toddler. + period: 2025-01 + input: + people: + person1: + age: 1 + households: + household: + members: [person1] + state_code: IA + output: + # 1 < 3 -> INFANT_TODDLER + ia_cca_age_group: INFANT_TODDLER + +- name: Child just under 3 is infant and toddler. + period: 2025-01 + input: + people: + person1: + age: 2 + households: + household: + members: [person1] + state_code: IA + output: + # 2 < 3 -> INFANT_TODDLER (upper boundary of infant/toddler band) + ia_cca_age_group: INFANT_TODDLER + +- name: Child at exactly 3 years is preschool. + period: 2025-01 + input: + people: + person1: + age: 3 + households: + household: + members: [person1] + state_code: IA + output: + # 3 <= age < 5 -> PRESCHOOL (lower boundary of preschool band) + ia_cca_age_group: PRESCHOOL + +- name: Child at 4 years is preschool. + period: 2025-01 + input: + people: + person1: + age: 4 + households: + household: + members: [person1] + state_code: IA + output: + # 4 < 5 -> PRESCHOOL (upper boundary of preschool band) + ia_cca_age_group: PRESCHOOL + +- name: Child at exactly 5 years is school age. + period: 2025-01 + input: + people: + person1: + age: 5 + households: + household: + members: [person1] + state_code: IA + output: + # age >= 5 -> SCHOOL_AGE (kindergarten entry, lower boundary) + ia_cca_age_group: SCHOOL_AGE + +- name: Child at 10 years is school age. + period: 2025-01 + input: + people: + person1: + age: 10 + households: + household: + members: [person1] + state_code: IA + output: + ia_cca_age_group: SCHOOL_AGE + +- name: Disabled infant or toddler has special-needs variant. + period: 2025-01 + input: + people: + person1: + age: 1 + is_disabled: true + households: + household: + members: [person1] + state_code: IA + output: + ia_cca_age_group: INFANT_TODDLER_SN + +- name: Disabled preschooler has special-needs variant. + period: 2025-01 + input: + people: + person1: + age: 4 + is_disabled: true + households: + household: + members: [person1] + state_code: IA + output: + ia_cca_age_group: PRESCHOOL_SN + +- name: Disabled school-age child has special-needs variant. + period: 2025-01 + input: + people: + person1: + age: 8 + is_disabled: true + households: + household: + members: [person1] + state_code: IA + output: + ia_cca_age_group: SCHOOL_AGE_SN diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_children_in_care.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_children_in_care.yaml new file mode 100644 index 00000000000..3fede903f02 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_children_in_care.yaml @@ -0,0 +1,90 @@ +# ia_cca_children_in_care: SPMUnit, int, MONTH (REQ-027 gating helper). +# Counts eligible children with scheduled childcare hours (childcare_hours_per_week +# > 0). Used to set the sliding-fee children column and to gate the in-home rate. + +- name: Counts only eligible children who have childcare hours. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + child_in_care_1: + age: 2 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 30 + child_in_care_2: + age: 5 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 20 + child_no_hours: + age: 7 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 0 + child_over_age: + age: 14 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 30 + tax_units: + tax_unit: + members: [parent, child_in_care_1, child_in_care_2, child_no_hours, child_over_age] + spm_units: + spm_unit: + members: [parent, child_in_care_1, child_in_care_2, child_no_hours, child_over_age] + households: + household: + members: [parent, child_in_care_1, child_in_care_2, child_no_hours, child_over_age] + state_code: IA + output: + # Two eligible children have hours; the no-hours child and the age-14 (over the + # age-13 basic limit) child are not counted -> 2 + ia_cca_children_in_care: 2 + +- name: No children in care returns zero. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + child: + age: 5 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 0 + tax_units: + tax_unit: + members: [parent, child] + spm_units: + spm_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: IA + output: + ia_cca_children_in_care: 0 + +# === Edge cases === + +- name: Adult-only unit with no children returns zero. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + spm_units: + spm_unit: + members: [parent] + households: + household: + members: [parent] + state_code: IA + output: + # Degenerate unit (no children at all) -> count 0, no crash + ia_cca_children_in_care: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_countable_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_countable_income.yaml new file mode 100644 index 00000000000..7c38fd75426 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_countable_income.yaml @@ -0,0 +1,139 @@ +# ia_cca_countable_income: SPMUnit, USD, MONTH (REQ-032 / REQ-033 / REQ-034). +# Sums countable earned and unearned income, excluding the earnings of a child +# age 14 or younger and the earnings of a full-time student age 18 or younger +# (IAC 441-170.2(1)"d"). Definition period is MONTH, so annual income sources are +# auto-divided to monthly figures. + +- name: Parent earnings count in monthly countable income. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 35 + employment_income: 36_000 + child: + age: 4 + spm_units: + spm_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: IA + output: + # 36_000 / 12 = 3_000 per month + ia_cca_countable_income: 3_000 + +- name: Earnings of a child age 14 or younger are excluded. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 35 + employment_income: 36_000 + working_child: + age: 14 + employment_income: 12_000 + spm_units: + spm_unit: + members: [parent, working_child] + households: + household: + members: [parent, working_child] + state_code: IA + output: + # Child age 14 (<= 14) -> earnings excluded; only parent counts: 36_000/12 = 3_000 + ia_cca_countable_income: 3_000 + +- name: Earnings of a full-time student age 18 or younger are excluded. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 35 + employment_income: 36_000 + student_child: + age: 17 + employment_income: 9_000 + is_full_time_student: true + spm_units: + spm_unit: + members: [parent, student_child] + households: + household: + members: [parent, student_child] + state_code: IA + output: + # Child age 17 and full-time student -> earnings excluded; only parent: 3_000 + ia_cca_countable_income: 3_000 + +- name: Earnings of a non-student older minor are counted. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 35 + employment_income: 36_000 + teen: + age: 17 + employment_income: 12_000 + is_full_time_student: false + spm_units: + spm_unit: + members: [parent, teen] + households: + household: + members: [parent, teen] + state_code: IA + output: + # Age 17, not a student, and over 14 -> earnings counted: + # (36_000 + 12_000) / 12 = 4_000 + ia_cca_countable_income: 4_000 + +- name: Unearned income is counted in addition to earnings. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 35 + employment_income: 36_000 + social_security: 6_000 + child: + age: 4 + spm_units: + spm_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: IA + output: + # (36_000 earned + 6_000 social security) / 12 = 3_500 per month + ia_cca_countable_income: 3_500 + +# === Edge cases === + +- name: No income sources gives zero countable income. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + child: + age: 4 + spm_units: + spm_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: IA + output: + # No earned or unearned income for anyone -> 0 (degenerate low boundary) + ia_cca_countable_income: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_max_rate.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_max_rate.yaml new file mode 100644 index 00000000000..56924d75f68 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_max_rate.yaml @@ -0,0 +1,319 @@ +# ia_cca_max_rate: Person, USD per half-day unit, MONTH (REQ-019 .. REQ-027). +# Looks up the maximum half-day-unit rate by provider type x quality rating x +# age group (with the special-needs variant baked into the age-group enum). Only +# defined for an eligible child (defined_for = ia_cca_eligible_child), so each +# person here is an age-/dependency-/immigration-eligible child. In-home care is +# paid only when at least three children in the family need care, so the in-home +# cases use a family of three children in care; with fewer the rate is zeroed. +# Rates effective 2024-07-01 (IAC 441-170.4(7) Tables 1-5). + +# ===== Table 1: Licensed Center (4 quality tiers x 3 age groups x Basic/SN) ===== + +- name: Licensed center, no rating, all age groups, basic and special needs. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + it: + age: 1 + is_tax_unit_dependent: true + immigration_status: CITIZEN + ia_cca_provider_type: LICENSED_CENTER + ia_cca_quality_rating: NO_RATING + pre: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + ia_cca_provider_type: LICENSED_CENTER + ia_cca_quality_rating: NO_RATING + sa: + age: 8 + is_tax_unit_dependent: true + immigration_status: CITIZEN + ia_cca_provider_type: LICENSED_CENTER + ia_cca_quality_rating: NO_RATING + it_sn: + age: 1 + is_disabled: true + is_tax_unit_dependent: true + immigration_status: CITIZEN + ia_cca_provider_type: LICENSED_CENTER + ia_cca_quality_rating: NO_RATING + pre_sn: + age: 4 + is_disabled: true + is_tax_unit_dependent: true + immigration_status: CITIZEN + ia_cca_provider_type: LICENSED_CENTER + ia_cca_quality_rating: NO_RATING + sa_sn: + age: 8 + is_disabled: true + is_tax_unit_dependent: true + immigration_status: CITIZEN + ia_cca_provider_type: LICENSED_CENTER + ia_cca_quality_rating: NO_RATING + tax_units: + tax_unit: + members: [it, pre, sa, it_sn, pre_sn, sa_sn] + households: + household: + members: [it, pre, sa, it_sn, pre_sn, sa_sn] + state_code: IA + output: + # INFANT_TODDLER 25.66 / PRESCHOOL 19.50 / SCHOOL_AGE 16.50 + # SN: INFANT_TODDLER_SN 51.94 / PRESCHOOL_SN 30.43 / SCHOOL_AGE_SN 30.34 + ia_cca_max_rate: [25.66, 19.50, 16.50, 51.94, 30.43, 30.34] + +- name: Licensed center, quality rating 5, basic age groups. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + it: + age: 1 + is_tax_unit_dependent: true + immigration_status: CITIZEN + ia_cca_provider_type: LICENSED_CENTER + ia_cca_quality_rating: RATING_5 + pre: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + ia_cca_provider_type: LICENSED_CENTER + ia_cca_quality_rating: RATING_5 + sa: + age: 8 + is_tax_unit_dependent: true + immigration_status: CITIZEN + ia_cca_provider_type: LICENSED_CENTER + ia_cca_quality_rating: RATING_5 + tax_units: + tax_unit: + members: [it, pre, sa] + households: + household: + members: [it, pre, sa] + state_code: IA + output: + # RATING_5: INFANT_TODDLER 27.00 / PRESCHOOL 22.97 / SCHOOL_AGE 18.50 + ia_cca_max_rate: [27.00, 22.97, 18.50] + +- name: Licensed center, quality rating 1 or 2 and 3 or 4, preschool. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + pre12: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + ia_cca_provider_type: LICENSED_CENTER + ia_cca_quality_rating: RATING_1_OR_2 + pre34: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + ia_cca_provider_type: LICENSED_CENTER + ia_cca_quality_rating: RATING_3_OR_4 + tax_units: + tax_unit: + members: [pre12, pre34] + households: + household: + members: [pre12, pre34] + state_code: IA + output: + # PRESCHOOL: RATING_1_OR_2 20.25 / RATING_3_OR_4 21.50 + ia_cca_max_rate: [20.25, 21.50] + +# ===== Table 2: Child Development Home A or B ===== + +- name: Child development home A or B, no rating and rating 5, all age groups. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + it: + age: 1 + is_tax_unit_dependent: true + immigration_status: CITIZEN + ia_cca_provider_type: CHILD_DEVELOPMENT_HOME_AB + ia_cca_quality_rating: NO_RATING + pre: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + ia_cca_provider_type: CHILD_DEVELOPMENT_HOME_AB + ia_cca_quality_rating: NO_RATING + sa: + age: 8 + is_tax_unit_dependent: true + immigration_status: CITIZEN + ia_cca_provider_type: CHILD_DEVELOPMENT_HOME_AB + ia_cca_quality_rating: NO_RATING + it5: + age: 1 + is_tax_unit_dependent: true + immigration_status: CITIZEN + ia_cca_provider_type: CHILD_DEVELOPMENT_HOME_AB + ia_cca_quality_rating: RATING_5 + tax_units: + tax_unit: + members: [it, pre, sa, it5] + households: + household: + members: [it, pre, sa, it5] + state_code: IA + output: + # NO_RATING: IT 16.63 / PRE 15.00 / SA 15.00; RATING_5 IT 17.50 + ia_cca_max_rate: [16.63, 15.00, 15.00, 17.50] + +# ===== Table 3: Child Development Home C ===== + +- name: Child development home C, rating 3 or 4, all age groups. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + it: + age: 1 + is_tax_unit_dependent: true + immigration_status: CITIZEN + ia_cca_provider_type: CHILD_DEVELOPMENT_HOME_C + ia_cca_quality_rating: RATING_3_OR_4 + pre: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + ia_cca_provider_type: CHILD_DEVELOPMENT_HOME_C + ia_cca_quality_rating: RATING_3_OR_4 + sa: + age: 8 + is_tax_unit_dependent: true + immigration_status: CITIZEN + ia_cca_provider_type: CHILD_DEVELOPMENT_HOME_C + ia_cca_quality_rating: RATING_3_OR_4 + tax_units: + tax_unit: + members: [it, pre, sa] + households: + household: + members: [it, pre, sa] + state_code: IA + output: + # CDH C RATING_3_OR_4: IT 18.00 / PRE 17.50 / SA 16.44 + ia_cca_max_rate: [18.00, 17.50, 16.44] + +# ===== Table 4: Child Care Home (Not Registered) — no quality dimension ===== + +- name: Not-registered child care home, basic and special needs, no quality effect. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + it: + age: 1 + is_tax_unit_dependent: true + immigration_status: CITIZEN + ia_cca_provider_type: CHILD_CARE_HOME_NOT_REGISTERED + ia_cca_quality_rating: NO_RATING + pre: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + ia_cca_provider_type: CHILD_CARE_HOME_NOT_REGISTERED + ia_cca_quality_rating: NO_RATING + it_sn: + age: 1 + is_disabled: true + is_tax_unit_dependent: true + immigration_status: CITIZEN + ia_cca_provider_type: CHILD_CARE_HOME_NOT_REGISTERED + ia_cca_quality_rating: RATING_5 + tax_units: + tax_unit: + members: [it, pre, it_sn] + households: + household: + members: [it, pre, it_sn] + state_code: IA + output: + # No quality dimension: IT basic 16.63 / PRE basic 15.00 / IT_SN 24.95 + # (RATING_5 on the SN child is ignored — Table 4 has no quality tier) + ia_cca_max_rate: [16.63, 15.00, 24.95] + +# ===== Table 5: In-Home provider (minimum wage), only with 3+ children in care ===== + +- name: In-home provider rate applies when three children are in care. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + it: + age: 1 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 30 + ia_cca_provider_type: IN_HOME + pre: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 30 + ia_cca_provider_type: IN_HOME + sa: + age: 8 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 30 + ia_cca_provider_type: IN_HOME + tax_units: + tax_unit: + members: [parent, it, pre, sa] + spm_units: + spm_unit: + members: [parent, it, pre, sa] + households: + household: + members: [parent, it, pre, sa] + state_code: IA + output: + # 3 children in care -> in-home allowed; flat 36.25 per unit for all ages + # (parent is not an eligible child -> max_rate not defined -> 0) + ia_cca_max_rate: [0, 36.25, 36.25, 36.25] + +- name: In-home provider rate is zero when fewer than three children are in care. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + it: + age: 1 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 30 + ia_cca_provider_type: IN_HOME + pre: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 30 + ia_cca_provider_type: IN_HOME + tax_units: + tax_unit: + members: [parent, it, pre] + spm_units: + spm_unit: + members: [parent, it, pre] + households: + household: + members: [parent, it, pre] + state_code: IA + output: + # Only 2 children in care -> in-home not allowed -> rate zeroed + ia_cca_max_rate: [0, 0, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_monthly_units.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_monthly_units.yaml new file mode 100644 index 00000000000..726fea80ad2 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_monthly_units.yaml @@ -0,0 +1,68 @@ +# ia_cca_monthly_units: Person, float, MONTH (REQ-017). +# A unit of service is a half day of up to 5 hours of care (IAC 441-170.1). With +# no authorized-units input, the monthly units are inferred from the child's +# scheduled weekly childcare hours, annualized to monthly and divided by the +# 5-hour unit: monthly_units = childcare_hours_per_week * (52 / 12) / 5. +# Defined only for an eligible child. + +- name: Full-time hours convert to monthly half-day units. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + child: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 40 + tax_units: + tax_unit: + members: [child] + households: + household: + members: [child] + state_code: IA + output: + # 40 * (52 / 12) / 5 = 34.6667 + ia_cca_monthly_units: 34.6667 + +- name: Part-time hours convert to fewer monthly units. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + child: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 20 + tax_units: + tax_unit: + members: [child] + households: + household: + members: [child] + state_code: IA + output: + # 20 * (52 / 12) / 5 = 17.3333 + ia_cca_monthly_units: 17.3333 + +- name: No scheduled hours gives zero units. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + child: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 0 + tax_units: + tax_unit: + members: [child] + households: + household: + members: [child] + state_code: IA + output: + ia_cca_monthly_units: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_provider_type.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_provider_type.yaml new file mode 100644 index 00000000000..425c8b9dac6 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_provider_type.yaml @@ -0,0 +1,46 @@ +# ia_cca_provider_type: Person, Enum input, MONTH (REQ-020). +# Input variable identifying the child's provider type. Default is LICENSED_CENTER. + +- name: Defaults to licensed center. + period: 2025-01 + input: + people: + child: + age: 4 + households: + household: + members: [child] + state_code: IA + output: + ia_cca_provider_type: LICENSED_CENTER + +- name: Each provider type round-trips as set. + period: 2025-01 + input: + people: + a: + age: 4 + ia_cca_provider_type: LICENSED_CENTER + b: + age: 4 + ia_cca_provider_type: CHILD_DEVELOPMENT_HOME_AB + c: + age: 4 + ia_cca_provider_type: CHILD_DEVELOPMENT_HOME_C + d: + age: 4 + ia_cca_provider_type: CHILD_CARE_HOME_NOT_REGISTERED + e: + age: 4 + ia_cca_provider_type: IN_HOME + households: + household: + members: [a, b, c, d, e] + state_code: IA + output: + ia_cca_provider_type: + - LICENSED_CENTER + - CHILD_DEVELOPMENT_HOME_AB + - CHILD_DEVELOPMENT_HOME_C + - CHILD_CARE_HOME_NOT_REGISTERED + - IN_HOME diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_quality_rating.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_quality_rating.yaml new file mode 100644 index 00000000000..a5549d8a961 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_quality_rating.yaml @@ -0,0 +1,42 @@ +# ia_cca_quality_rating: Person, Enum input, MONTH (REQ-021). +# Input variable for the provider's IQ4K quality rating. Default is NO_RATING. + +- name: Defaults to no quality rating. + period: 2025-01 + input: + people: + child: + age: 4 + households: + household: + members: [child] + state_code: IA + output: + ia_cca_quality_rating: NO_RATING + +- name: Each quality rating round-trips as set. + period: 2025-01 + input: + people: + a: + age: 4 + ia_cca_quality_rating: NO_RATING + b: + age: 4 + ia_cca_quality_rating: RATING_1_OR_2 + c: + age: 4 + ia_cca_quality_rating: RATING_3_OR_4 + d: + age: 4 + ia_cca_quality_rating: RATING_5 + households: + household: + members: [a, b, c, d] + state_code: IA + output: + ia_cca_quality_rating: + - NO_RATING + - RATING_1_OR_2 + - RATING_3_OR_4 + - RATING_5 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_smi.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_smi.yaml new file mode 100644 index 00000000000..f9eadb03a3a --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_smi.yaml @@ -0,0 +1,29 @@ +# ia_cca_smi: SPMUnit, USD, MONTH (REQ-007). +# 85% of Iowa median family income, reused from the federal hhs_smi() helper and +# returned as a monthly figure. The SMI table is on a fiscal year starting Oct 1, +# so a January 2025 period uses the 2024-10-01 (FY2025) value: IA 4-person SMI = +# 113_549. Household size adjustment: first 0.52, persons 2-6 +0.16 each. + +- name: 85 percent MFI monthly cap for a family of three. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + parent1: + age: 30 + parent2: + age: 30 + child: + age: 4 + spm_units: + spm_unit: + members: [parent1, parent2, child] + households: + household: + members: [parent1, parent2, child] + state_code: IA + output: + # size adjustment (size 3) = 0.52 + 0.16 * 2 = 0.84 + # annual SMI = 113_549 * 0.84 = 95_381.16 + # 85% monthly = 95_381.16 * 0.85 / 12 = 6_756.17 + ia_cca_smi: 6_756.17 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_child_care_subsidies.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_child_care_subsidies.yaml new file mode 100644 index 00000000000..ba9b9bdbd11 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_child_care_subsidies.yaml @@ -0,0 +1,43 @@ +# ia_child_care_subsidies: SPMUnit, USD, YEAR (federal aggregator registration). +# Adds the monthly ia_cca benefit to an annual total so it flows into the federal +# child_care_subsidies variable. + +- name: Annual subsidy is twelve times the monthly CCA benefit. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 18_000 + is_tax_unit_head_or_spouse: true + weekly_hours_worked_before_lsr: 40 + child: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 40 + pre_subsidy_childcare_expenses: 12_000 + ia_cca_provider_type: LICENSED_CENTER + ia_cca_quality_rating: NO_RATING + tax_units: + tax_unit: + members: [parent, child] + spm_units: + spm_unit: + members: [parent, child] + ia_cca_enrolled: false + spm_unit_assets: 5_000 + households: + household: + members: [parent, child] + state_code: IA + output: + # Preschool licensed center NO_RATING ceiling = 19.50 * monthly_units(40) + # = 19.50 * 34.6667 = 676.00; expense 12_000/yr -> 1_000/mo capped at 676.00. + # Income 1_500/mo <= Level A (family 2, 1_619) -> copay 0. + # Monthly benefit = 676.00. Read at a YEAR period, the MONTH-defined ia_cca + # sums across the 12 months -> 676.00 * 12 = 8_112.00, and the YEAR aggregator + # adds it through to the same total. + ia_cca: 8_112.00 + ia_child_care_subsidies: 8_112.00 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/integration.yaml new file mode 100644 index 00000000000..082bd7a1055 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/integration.yaml @@ -0,0 +1,571 @@ +# Iowa CCA integration tests — full pipeline from eligibility through benefit. +# Scenarios mirror the program's worked examples (family of 3, young children, +# center-based) across the three income tiers (CCA / CCA Plus / CCA Exit), plus a +# special-needs scenario and an in-home (3+ children, minimum-wage rate) scenario. +# +# Benefit pipeline (ia_cca): +# per child: child_ceiling = ia_cca_max_rate (per half-day unit) * ia_cca_monthly_units +# ia_cca_monthly_units(hpw) = hpw * (52 / 12) / 5; monthly_units(40) = 34.6667 +# ia_cca = max( sum_over_in-care_children min(expense, ceiling) - copay, 0 ) +# Income tests are monthly: ia_cca_countable_income = employment_income / 12. +# 2025 FPG: family of 2 = 21_150; family of 3 = 26_650; family of 4 = 32_150. +# pre_subsidy_childcare_expenses is YEAR-defined, so the annual figures below are +# divided to a monthly cost inside the MONTH benefit formula (12_000/yr -> 1_000/mo). +# +# The dollar figures here are recomputed from the modeled formulas (which infer +# monthly units from scheduled hours) rather than transcribed from the overview +# PDF's authorized-unit accounting, so they differ from the PDF's illustrative +# $1,808 / $1,620 / $1,540 / $447 figures while exercising the same tiers. + +# === Case 1: CCA tier (applicant), copay waived at fee Level A === +- name: Case 1, single parent two young children licensed center, CCA Level A no copay. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 28 + employment_income: 24_000 + is_tax_unit_head_or_spouse: true + weekly_hours_worked_before_lsr: 40 + immigration_status: CITIZEN + infant: + age: 1 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 40 + pre_subsidy_childcare_expenses: 12_000 + ia_cca_provider_type: LICENSED_CENTER + ia_cca_quality_rating: NO_RATING + preschooler: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 40 + pre_subsidy_childcare_expenses: 12_000 + ia_cca_provider_type: LICENSED_CENTER + ia_cca_quality_rating: NO_RATING + tax_units: + tax_unit: + members: [parent, infant, preschooler] + spm_units: + spm_unit: + members: [parent, infant, preschooler] + ia_cca_enrolled: false + spm_unit_assets: 5_000 + households: + household: + members: [parent, infant, preschooler] + state_code: IA + output: + # Eligibility: infant (1) and preschooler (4) are eligible children. + ia_cca_eligible_child: [false, true, true] + # Family of 3; income 2_000/mo <= 160% FPL (3_553.33) -> income eligible. + ia_cca_income_eligible: true + ia_cca_eligible: true + # Age groups: parent SCHOOL_AGE, infant INFANT_TODDLER, preschooler PRESCHOOL. + ia_cca_age_group: [SCHOOL_AGE, INFANT_TODDLER, PRESCHOOL] + # Rates (NO_RATING licensed center): infant 25.66, preschool 19.50. + ia_cca_max_rate: [0, 25.66, 19.50] + # Income 2_000/mo <= Level A (2_044, family 3) -> Level A unit fee 0 -> copay 0. + ia_cca_copay: 0 + # Each expense 12_000/yr -> 1_000/mo. Ceilings: + # infant 25.66 * 34.6667 = 889.55 (< 1_000/mo expense -> capped to 889.55), + # preschool 19.50 * 34.6667 = 676.00 (< 1_000/mo expense -> capped to 676.00). + # Benefit = (889.55 + 676.00) - 0 = 1_565.55 + ia_cca: 1_565.55 + +# === Case 2: CCA tier (applicant), sliding unit fee at Level B, two children === +- name: Case 2, single parent two children licensed center, CCA Level B copay. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 28 + employment_income: 25_200 + is_tax_unit_head_or_spouse: true + weekly_hours_worked_before_lsr: 40 + immigration_status: CITIZEN + infant: + age: 1 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 40 + pre_subsidy_childcare_expenses: 12_000 + ia_cca_provider_type: LICENSED_CENTER + ia_cca_quality_rating: NO_RATING + preschooler: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 40 + pre_subsidy_childcare_expenses: 12_000 + ia_cca_provider_type: LICENSED_CENTER + ia_cca_quality_rating: NO_RATING + tax_units: + tax_unit: + members: [parent, infant, preschooler] + spm_units: + spm_unit: + members: [parent, infant, preschooler] + ia_cca_enrolled: false + spm_unit_assets: 5_000 + households: + household: + members: [parent, infant, preschooler] + state_code: IA + output: + ia_cca_eligible: true + # Income 2_100/mo: > Level A (2_044), <= Level B (2_152) -> Level B. + # 2 children in care -> column 2 -> unit fee 0.45. + # focal_units = monthly_units(40) = 34.6667; copay = 0.45 * 34.6667 = 15.60. + ia_cca_copay: 15.60 + # Same ceilings as Case 1: total capped 889.55 + 676.00 = 1_565.55. + # Benefit = 1_565.55 - 15.60 = 1_549.95 + ia_cca: 1_549.95 + +# === Case 3: CCA Plus tier (enrolled, 160-225% FPL), sliding fee Level BB === +- name: Case 3, enrolled family one preschooler licensed center, CCA Plus. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 57_600 + is_tax_unit_head_or_spouse: true + weekly_hours_worked_before_lsr: 40 + immigration_status: CITIZEN + coparent: + age: 30 + is_tax_unit_head_or_spouse: true + weekly_hours_worked_before_lsr: 40 + immigration_status: CITIZEN + preschooler: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 30 + pre_subsidy_childcare_expenses: 12_000 + ia_cca_provider_type: LICENSED_CENTER + ia_cca_quality_rating: NO_RATING + tax_units: + tax_unit: + members: [parent, coparent, preschooler] + spm_units: + spm_unit: + members: [parent, coparent, preschooler] + ia_cca_enrolled: true + spm_unit_assets: 5_000 + households: + household: + members: [parent, coparent, preschooler] + state_code: IA + output: + ia_cca_eligible: true + # Family of 3 enrolled; income 4_800/mo <= 250% FPL (5_552.08) -> eligible. + ia_cca_income_eligible: true + # 4_800/mo <= 225% FPL (4_996.88) -> NOT exit tier -> sliding unit fee. + ia_cca_in_exit_tier: false + # Sliding level: 4_800/mo > AA (4_254), <= BB (6_000) -> Level BB. + # 1 child in care -> column 1 -> unit fee 6.70. + # focal_units = monthly_units(30) = 26.0; copay = 6.70 * 26.0 = 174.20. + ia_cca_copay: 174.20 + # Preschool ceiling = 19.50 * 26.0 = 507.00; expense 12_000/yr -> 1_000/mo, + # capped to the 507.00 ceiling. + # Benefit = max(507.00 - 174.20, 0) = 332.80 + ia_cca: 332.80 + +# === Case 4: CCA Exit tier (enrolled, >225% FPL), percentage-of-cost copay === +- name: Case 4, enrolled family one preschooler licensed center, CCA Exit 60 percent. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 62_400 + is_tax_unit_head_or_spouse: true + weekly_hours_worked_before_lsr: 40 + immigration_status: CITIZEN + coparent: + age: 30 + is_tax_unit_head_or_spouse: true + weekly_hours_worked_before_lsr: 40 + immigration_status: CITIZEN + preschooler: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 40 + pre_subsidy_childcare_expenses: 12_000 + ia_cca_provider_type: LICENSED_CENTER + ia_cca_quality_rating: NO_RATING + tax_units: + tax_unit: + members: [parent, coparent, preschooler] + spm_units: + spm_unit: + members: [parent, coparent, preschooler] + ia_cca_enrolled: true + spm_unit_assets: 5_000 + households: + household: + members: [parent, coparent, preschooler] + state_code: IA + output: + ia_cca_eligible: true + # Family of 3 enrolled; income 5_200/mo <= 250% FPL (5_552.08) -> eligible. + ia_cca_income_eligible: true + # 5_200/mo > 225% FPL (4_996.88) and enrolled -> CCA Exit tier. + ia_cca_in_exit_tier: true + # Exit basic level: > A (4_842), > B (5_057), <= C (5_272) -> Level C -> 60%. + # Expense 12_000/yr -> 1_000/mo. Exit copay = 60% of the child's monthly cost + # of care = 0.60 * 1_000 = 600. + ia_cca_copay: 600.00 + # Preschool ceiling = 19.50 * 34.6667 = 676.00; monthly expense 1_000 capped to + # 676.00. Benefit = max(676.00 - 600.00, 0) = 76.00 + ia_cca: 76.00 + +# === Case 5: Special-needs branch (applicant, 200% FPL, SN rate) === +- name: Case 5, single parent disabled preschooler, special-needs rate and limit. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 36_000 + is_tax_unit_head_or_spouse: true + weekly_hours_worked_before_lsr: 30 + immigration_status: CITIZEN + child: + age: 4 + is_disabled: true + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 40 + pre_subsidy_childcare_expenses: 12_000 + ia_cca_provider_type: LICENSED_CENTER + ia_cca_quality_rating: NO_RATING + tax_units: + tax_unit: + members: [parent, child] + spm_units: + spm_unit: + members: [parent, child] + ia_cca_enrolled: false + spm_unit_assets: 5_000 + households: + household: + members: [parent, child] + state_code: IA + output: + # Disabled preschooler -> special-needs age group and limits. + ia_cca_age_group: [SCHOOL_AGE, PRESCHOOL_SN] + # Family of 2 special-needs; income 3_000/mo <= 200% FPL (3_525) -> eligible + # (would fail the 160% basic limit of 2_820/mo). + ia_cca_income_eligible: true + # Special-needs child -> 28-hour activity threshold; parent works 30 -> eligible. + ia_cca_activity_eligible: true + ia_cca_eligible: true + # Licensed center NO_RATING PRESCHOOL_SN rate = 30.43. + ia_cca_max_rate: [0, 30.43] + # Sliding level (family 2): income 3_000/mo -> Level W; 1 child -> column 1 -> 5.45. + # focal_units = monthly_units(40) = 34.6667; copay = 5.45 * 34.6667 = 188.93. + ia_cca_copay: 188.93 + # Ceiling = 30.43 * 34.6667 = 1_054.91; expense 12_000/yr -> 1_000/mo < ceiling + # -> capped to 1_000. Benefit = max(1_000 - 188.93, 0) = 811.07 + ia_cca: 811.07 + +# === Case 6: In-Home provider, three children in care, minimum-wage rate === +- name: Case 6, single parent three children in-home minimum wage rate. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 24_000 + is_tax_unit_head_or_spouse: true + weekly_hours_worked_before_lsr: 40 + immigration_status: CITIZEN + child1: + age: 1 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 30 + pre_subsidy_childcare_expenses: 12_000 + ia_cca_provider_type: IN_HOME + child2: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 30 + pre_subsidy_childcare_expenses: 12_000 + ia_cca_provider_type: IN_HOME + child3: + age: 8 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 30 + pre_subsidy_childcare_expenses: 12_000 + ia_cca_provider_type: IN_HOME + tax_units: + tax_unit: + members: [parent, child1, child2, child3] + spm_units: + spm_unit: + members: [parent, child1, child2, child3] + ia_cca_enrolled: false + spm_unit_assets: 5_000 + households: + household: + members: [parent, child1, child2, child3] + state_code: IA + output: + ia_cca_eligible: true + # Three eligible children in care -> in-home rate allowed (>= 3). + ia_cca_children_in_care: 3 + # In-home flat rate 36.25 per half-day unit for each child (parent gets 0). + ia_cca_max_rate: [0, 36.25, 36.25, 36.25] + # Family of 4; income 2_000/mo <= Level A (2_470, family 4) -> copay 0. + ia_cca_copay: 0 + # Per-child ceiling = 36.25 * monthly_units(30) = 36.25 * 26.0 = 942.50; + # each expense 12_000/yr -> 1_000/mo > ceiling -> capped to 942.50 per child; + # 3 children -> 2_827.50. Benefit = 2_827.50 - 0 = 2_827.50 + ia_cca: 2_827.50 + +# === Case 7: In-home with only two children — rate gated off, no benefit === +- name: Case 7, in-home with only two children pays no in-home benefit. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 24_000 + is_tax_unit_head_or_spouse: true + weekly_hours_worked_before_lsr: 40 + immigration_status: CITIZEN + child1: + age: 1 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 30 + pre_subsidy_childcare_expenses: 800 + ia_cca_provider_type: IN_HOME + child2: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 30 + pre_subsidy_childcare_expenses: 800 + ia_cca_provider_type: IN_HOME + tax_units: + tax_unit: + members: [parent, child1, child2] + spm_units: + spm_unit: + members: [parent, child1, child2] + ia_cca_enrolled: false + spm_unit_assets: 5_000 + households: + household: + members: [parent, child1, child2] + state_code: IA + output: + # Family is otherwise eligible, but only 2 children -> in-home rate gated to 0. + ia_cca_eligible: true + ia_cca_children_in_care: 2 + ia_cca_max_rate: [0, 0, 0] + # Ceiling 0 for both children -> capped expense 0 -> benefit 0. + ia_cca: 0 + +# === Case 8: Ineligible — child over the age limit === +- name: Case 8, household with only an over-age child is ineligible. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + employment_income: 20_000 + is_tax_unit_head_or_spouse: true + weekly_hours_worked_before_lsr: 40 + immigration_status: CITIZEN + teen: + age: 13 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 30 + pre_subsidy_childcare_expenses: 600 + ia_cca_provider_type: LICENSED_CENTER + ia_cca_quality_rating: NO_RATING + tax_units: + tax_unit: + members: [parent, teen] + spm_units: + spm_unit: + members: [parent, teen] + ia_cca_enrolled: false + spm_unit_assets: 5_000 + households: + household: + members: [parent, teen] + state_code: IA + output: + # Child age 13 (not < 13, not disabled) -> no eligible child. + ia_cca_eligible_child: [false, false] + ia_cca_eligible: false + ia_cca: 0 + +# === Case 9: Non-Iowa household — defined_for zeroes the benefit === +- name: Case 9, identical household outside Iowa receives no benefit. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 28 + employment_income: 24_000 + is_tax_unit_head_or_spouse: true + weekly_hours_worked_before_lsr: 40 + immigration_status: CITIZEN + preschooler: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 40 + pre_subsidy_childcare_expenses: 900 + ia_cca_provider_type: LICENSED_CENTER + ia_cca_quality_rating: NO_RATING + tax_units: + tax_unit: + members: [parent, preschooler] + spm_units: + spm_unit: + members: [parent, preschooler] + ia_cca_enrolled: false + spm_unit_assets: 5_000 + households: + household: + members: [parent, preschooler] + state_code: NE + output: + # Nebraska household -> ia_cca defined_for StateCode.IA zeroes the benefit + # and ia_cca_eligible is false. + ia_cca_eligible: false + ia_cca: 0 + +# === Edge cases === + +# === Case 10: Eligible child with zero care hours — eligible but no benefit === +- name: Case 10, eligible child with zero care hours yields a zero benefit. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 24_000 + is_tax_unit_head_or_spouse: true + weekly_hours_worked_before_lsr: 40 + immigration_status: CITIZEN + child: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 0 + pre_subsidy_childcare_expenses: 12_000 + ia_cca_provider_type: LICENSED_CENTER + ia_cca_quality_rating: NO_RATING + tax_units: + tax_unit: + members: [parent, child] + spm_units: + spm_unit: + members: [parent, child] + ia_cca_enrolled: false + spm_unit_assets: 5_000 + households: + household: + members: [parent, child] + state_code: IA + output: + # The family is eligible (age/dependency/immigration), but the child is not in + # care (0 hours) -> the per-child contribution is gated to $0 -> benefit 0. + ia_cca_eligible: true + ia_cca_children_in_care: 0 + ia_cca: 0 + +# === Case 11: Income above every tier — ineligible, no benefit === +- name: Case 11, very high income family receives no benefit. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 500_000 + is_tax_unit_head_or_spouse: true + weekly_hours_worked_before_lsr: 40 + immigration_status: CITIZEN + child: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 40 + pre_subsidy_childcare_expenses: 12_000 + ia_cca_provider_type: LICENSED_CENTER + ia_cca_quality_rating: NO_RATING + tax_units: + tax_unit: + members: [parent, child] + spm_units: + spm_unit: + members: [parent, child] + ia_cca_enrolled: false + spm_unit_assets: 5_000 + households: + household: + members: [parent, child] + state_code: IA + output: + # 500_000/yr far exceeds the 160% FPL initial limit -> income ineligible -> + # not eligible -> benefit 0 (degenerate high-income boundary) + ia_cca_income_eligible: false + ia_cca_eligible: false + ia_cca: 0 + +# === Case 12: Adult-only unit with no children — degenerate, no benefit === +- name: Case 12, adult-only unit with no children receives no benefit. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 24_000 + is_tax_unit_head_or_spouse: true + weekly_hours_worked_before_lsr: 40 + immigration_status: CITIZEN + tax_units: + tax_unit: + members: [parent] + spm_units: + spm_unit: + members: [parent] + ia_cca_enrolled: false + spm_unit_assets: 5_000 + households: + household: + members: [parent] + state_code: IA + output: + # No children -> no eligible child -> ineligible -> benefit 0. The full pipeline + # must run on a unit with zero eligible children without error. + ia_cca_children_in_care: 0 + ia_cca_eligible: false + ia_cca: 0 diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_copay.py b/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_copay.py new file mode 100644 index 00000000000..86a69b3fed5 --- /dev/null +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_copay.py @@ -0,0 +1,60 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.ia.hhs.cca.copay.ia_cca_sliding_fee_level import ( + SLIDING_FEE_LEVELS, +) +from policyengine_us.variables.gov.states.ia.hhs.cca.copay.ia_cca_exit_fee_level import ( + EXIT_FEE_LEVELS, +) + + +class ia_cca_copay(Variable): + value_type = float + entity = SPMUnit + unit = USD + label = "Iowa CCA family fee" + definition_period = MONTH + defined_for = StateCode.IA + reference = "https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=11" + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.ia.hhs.cca.copay + person = spm_unit.members + is_eligible_child = person("ia_cca_eligible_child", period) + in_care = person("childcare_hours_per_week", period.this_year) > 0 + is_in_care_child = is_eligible_child & in_care + children_in_care = spm_unit("ia_cca_children_in_care", period) + + # --- Mechanism A: CCA and CCA Plus sliding unit fee --- + # A flat fee per half-day unit, assessed on only one child (the one + # receiving the most units), with the unit-fee column set by the + # number of children in care (one, two, or three or more). + sliding_level = spm_unit("ia_cca_sliding_fee_level", period) + children_col = clip(children_in_care, 1, 3).astype(int) + unit_fee = 0 + for i, level in enumerate(SLIDING_FEE_LEVELS): + unit_fee = unit_fee + where( + sliding_level == i, + p.sliding_fee.unit_fee[level][children_col], + 0, + ) + monthly_units = person("ia_cca_monthly_units", period) + focal_units = spm_unit.max(monthly_units * is_in_care_child) + sliding_copay = unit_fee * focal_units + + # --- Mechanism B: CCA Exit percentage of cost of care --- + # A percentage of the cost of care for each child in care, summed + # across children. + exit_level = spm_unit("ia_cca_exit_fee_level", period) + exit_pct = 0 + for i, level in enumerate(EXIT_FEE_LEVELS): + exit_pct = exit_pct + where(exit_level == i, p.exit.fee_pct[level], 0) + child_expense = person("pre_subsidy_childcare_expenses", period) + exit_copay = exit_pct * spm_unit.sum(child_expense * is_in_care_child) + + in_exit_tier = spm_unit("ia_cca_in_exit_tier", period) + copay = where(in_exit_tier, exit_copay, sliding_copay) + + # Families eligible without regard to income pay no fee + # (IAC 441-170.2(1)"b"; family fee chart). + income_exception = spm_unit("ia_cca_income_exception", period) + return where(income_exception, 0, copay) diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_exit_fee_level.py b/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_exit_fee_level.py new file mode 100644 index 00000000000..fb59f061b59 --- /dev/null +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_exit_fee_level.py @@ -0,0 +1,31 @@ +from policyengine_us.model_api import * + +# CCA Exit fee levels A through D, in ascending income order. +EXIT_FEE_LEVELS = ["A", "B", "C", "D"] + + +class ia_cca_exit_fee_level(Variable): + value_type = int + entity = SPMUnit + label = "Iowa CCA Exit fee level index" + definition_period = MONTH + defined_for = StateCode.IA + reference = "https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=12" + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.ia.hhs.cca.copay.exit + income = spm_unit("ia_cca_countable_income", period) + size = spm_unit("spm_unit_size", period.this_year) + capped_size = clip(size, 1, 13).astype(int) + # CCA Exit uses a separate income threshold table for basic care and + # special-needs care. + has_special_needs_child = spm_unit("ia_cca_has_special_needs_child", period) + # A family is at the first level whose monthly-income threshold for + # its family size is at or above its income. + level_index = 0 + for level in EXIT_FEE_LEVELS: + basic_threshold = p.income_thresholds_basic[level][capped_size] + sn_threshold = p.income_thresholds_special_needs[level][capped_size] + threshold = where(has_special_needs_child, sn_threshold, basic_threshold) + level_index = level_index + (income > threshold) + return min_(level_index, len(EXIT_FEE_LEVELS) - 1) diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_in_exit_tier.py b/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_in_exit_tier.py new file mode 100644 index 00000000000..a21b16b1903 --- /dev/null +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_in_exit_tier.py @@ -0,0 +1,21 @@ +from policyengine_us.model_api import * + + +class ia_cca_in_exit_tier(Variable): + value_type = bool + entity = SPMUnit + label = "Iowa CCA family is in the CCA Exit tier" + definition_period = MONTH + defined_for = StateCode.IA + reference = "https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=12" + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.ia.hhs.cca.income.fpl_rate + # CCA Exit applies to enrolled families whose income exceeds the + # ongoing CCA Plus limit (225% FPL); these families pay the + # percentage-of-cost CCA Exit fee instead of the sliding unit fee. + enrolled = spm_unit("ia_cca_enrolled", period) + income = spm_unit("ia_cca_countable_income", period) + fpg = spm_unit("spm_unit_fpg", period) + plus_limit = fpg * p.plus_basic + return enrolled & (income > plus_limit) diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_sliding_fee_level.py b/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_sliding_fee_level.py new file mode 100644 index 00000000000..f453120b08e --- /dev/null +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_sliding_fee_level.py @@ -0,0 +1,62 @@ +from policyengine_us.model_api import * + +# Sliding-fee levels A through BB, in ascending income order. +SLIDING_FEE_LEVELS = [ + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z", + "AA", + "BB", +] + + +class ia_cca_sliding_fee_level(Variable): + value_type = int + entity = SPMUnit + label = "Iowa CCA and CCA Plus sliding-fee level index" + definition_period = MONTH + defined_for = StateCode.IA + reference = "https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=11" + + def formula(spm_unit, period, parameters): + thresholds = parameters( + period + ).gov.states.ia.hhs.cca.copay.sliding_fee.income_thresholds + income = spm_unit("ia_cca_countable_income", period) + # The income thresholds run by family size 1 through 13 (13 means + # 13 or more). + size = spm_unit("spm_unit_size", period.this_year) + capped_size = clip(size, 1, 13).astype(int) + # A family is at the first level whose monthly-income threshold for + # its family size is at or above its income. Equivalently, the level + # index is the number of level thresholds the income exceeds. + level_index = 0 + for level in SLIDING_FEE_LEVELS: + threshold = thresholds[level][capped_size] + level_index = level_index + (income > threshold) + # Cap at the last level (index 27); incomes above the BB threshold + # exceed the CCA Plus income limit and are screened out upstream. + return min_(level_index, len(SLIDING_FEE_LEVELS) - 1) diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_activity_eligible.py b/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_activity_eligible.py new file mode 100644 index 00000000000..fbbf372248a --- /dev/null +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_activity_eligible.py @@ -0,0 +1,42 @@ +from policyengine_us.model_api import * + + +class ia_cca_activity_eligible(Variable): + value_type = bool + entity = SPMUnit + label = "Eligible for Iowa CCA based on activity requirements" + definition_period = MONTH + defined_for = StateCode.IA + reference = "https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=6" + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.ia.hhs.cca.activity_requirements + # Each parent must average at least 32 hours per week in an approved + # activity, or 28 hours when the family includes a child with + # special needs (IAC 441-170.2(2)"b"(2)). + has_special_needs_child = spm_unit("ia_cca_has_special_needs_child", period) + min_hours = where( + has_special_needs_child, + p.weekly_hours_special_needs, + p.weekly_hours, + ) + person = spm_unit.members + is_head_or_spouse = person("is_tax_unit_head_or_spouse", period.this_year) + # Use the pre-labor-supply-response hours to avoid a circular + # dependency with the labor supply response. + hours_worked = person("weekly_hours_worked_before_lsr", period.this_year) + meets_work_requirement = hours_worked >= spm_unit.project(min_hours) + is_student = person("is_full_time_student", period.this_year) + individually_eligible = meets_work_requirement | is_student + # Each parent must independently meet an approved activity + # (170.2(2)"b"). We simplify the "coinciding hours" rule for + # two-parent families to a per-parent hours gate. Require at least + # one head/spouse so a unit of only dependents does not pass. + n_parents = spm_unit.sum(is_head_or_spouse) + n_qualifying = spm_unit.sum(is_head_or_spouse & individually_eligible) + all_parents_qualify = (n_parents >= 1) & (n_qualifying >= n_parents) + # Fallback for approved activities we cannot derive directly + # (PROMISE JOBS, time-limited job search or medical incapacity, + # combined employment and training). + fallback = spm_unit("meets_ccdf_activity_test", period.this_year) + return all_parents_qualify | fallback diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_asset_eligible.py b/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_asset_eligible.py new file mode 100644 index 00000000000..c9f3f8b9c02 --- /dev/null +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_asset_eligible.py @@ -0,0 +1,19 @@ +from policyengine_us.model_api import * + + +class ia_cca_asset_eligible(Variable): + value_type = bool + entity = SPMUnit + label = "Eligible for Iowa CCA based on resources" + definition_period = MONTH + defined_for = StateCode.IA + reference = "https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=5" + + def formula(spm_unit, period, parameters): + # Iowa's $1 million resource limit is far above the federal CCDF + # asset limit, so we apply Iowa's own limit rather than reusing the + # federal CCDF asset check (which would impose a much tighter test + # than Iowa law allows). Resources are a stock, read annually. + assets = spm_unit("spm_unit_assets", period.this_year) + asset_limit = parameters(period).gov.states.ia.hhs.cca.eligibility.asset_limit + return assets <= asset_limit diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_eligible.py b/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_eligible.py new file mode 100644 index 00000000000..9d313fe15c9 --- /dev/null +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_eligible.py @@ -0,0 +1,22 @@ +from policyengine_us.model_api import * + + +class ia_cca_eligible(Variable): + value_type = bool + entity = SPMUnit + label = "Eligible for Iowa CCA" + definition_period = MONTH + defined_for = StateCode.IA + reference = "https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=3" + + def formula(spm_unit, period, parameters): + has_eligible_child = add(spm_unit, period, ["ia_cca_eligible_child"]) > 0 + asset_eligible = spm_unit("ia_cca_asset_eligible", period) + income_eligible = spm_unit("ia_cca_income_eligible", period) + activity_eligible = spm_unit("ia_cca_activity_eligible", period) + # Families eligible without regard to income (FIP, protective, + # foster) bypass both the income test and the activity need-for- + # service test (IAC 441-170.2(1)"b"). + income_exception = spm_unit("ia_cca_income_exception", period) + standard_path = income_eligible & activity_eligible + return has_eligible_child & asset_eligible & (standard_path | income_exception) diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_eligible_child.py b/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_eligible_child.py new file mode 100644 index 00000000000..939a1bd2846 --- /dev/null +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_eligible_child.py @@ -0,0 +1,34 @@ +from policyengine_us.model_api import * + + +class ia_cca_eligible_child(Variable): + value_type = bool + entity = Person + label = "Eligible child for Iowa CCA" + definition_period = MONTH + defined_for = StateCode.IA + reference = "https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=5" + + def formula(person, period, parameters): + p = parameters(period).gov.states.ia.hhs.cca.eligibility + # Iowa serves a child up to age 13, or up to age 19 for a child + # with special needs (IAC 441-170.2(2)"a"). `age` is YEAR-defined. + age = person("age", period.this_year) + is_disabled = person("is_disabled", period.this_year) + age_eligible = where( + is_disabled, + age < p.special_needs_age_limit, + age < p.child_age_limit, + ) + is_dependent = person("is_tax_unit_dependent", period.this_year) + immigration_eligible = person( + "is_ccdf_immigration_eligible_child", period.this_year + ) + standard_eligible = age_eligible & is_dependent & immigration_eligible + # A child in protective or foster care qualifies through the + # income-exception path regardless of dependency or immigration + # status (IAC 441-170.2(1)"b"). + protective = person("receives_or_needs_protective_services", period) + foster = person("is_in_foster_care", period) + categorical_eligible = age_eligible & (protective | foster) + return standard_eligible | categorical_eligible diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_has_special_needs_child.py b/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_has_special_needs_child.py new file mode 100644 index 00000000000..2b4fb4ca9ac --- /dev/null +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_has_special_needs_child.py @@ -0,0 +1,16 @@ +from policyengine_us.model_api import * + + +class ia_cca_has_special_needs_child(Variable): + value_type = bool + entity = SPMUnit + label = "Iowa CCA family includes a child with special needs" + definition_period = MONTH + defined_for = StateCode.IA + reference = "https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=3" + + def formula(spm_unit, period, parameters): + person = spm_unit.members + is_eligible_child = person("ia_cca_eligible_child", period) + is_disabled = person("is_disabled", period.this_year) + return spm_unit.any(is_eligible_child & is_disabled) diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_income_eligible.py b/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_income_eligible.py new file mode 100644 index 00000000000..e5a311a9235 --- /dev/null +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_income_eligible.py @@ -0,0 +1,38 @@ +from policyengine_us.model_api import * + + +class ia_cca_income_eligible(Variable): + value_type = bool + entity = SPMUnit + label = "Eligible for Iowa CCA based on income" + definition_period = MONTH + defined_for = StateCode.IA + reference = "https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=3" + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.ia.hhs.cca.income.fpl_rate + countable_income = spm_unit("ia_cca_countable_income", period) + # spm_unit_fpg is YEAR-defined; the bare monthly period auto-divides + # it to a monthly poverty guideline, matching the monthly income. + fpg = spm_unit("spm_unit_fpg", period) + enrolled = spm_unit("ia_cca_enrolled", period) + # The family standard rises if any eligible child receives + # special-needs care. + has_special_needs_child = spm_unit("ia_cca_has_special_needs_child", period) + + initial_fpl_rate = where( + has_special_needs_child, + p.initial_special_needs, + p.initial_basic, + ) + # Already-enrolled families stay eligible up to the higher ongoing + # CCA Exit limit (250% basic / 275% special-needs FPL). + exit_fpl_rate = where( + has_special_needs_child, + p.exit_special_needs, + p.exit_basic, + ) + initial_limit = min_(fpg * initial_fpl_rate, spm_unit("ia_cca_smi", period)) + exit_limit = fpg * exit_fpl_rate + income_limit = where(enrolled, exit_limit, initial_limit) + return countable_income <= income_limit diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_income_exception.py b/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_income_exception.py new file mode 100644 index 00000000000..4f0c561391a --- /dev/null +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_income_exception.py @@ -0,0 +1,25 @@ +from policyengine_us.model_api import * + + +class ia_cca_income_exception(Variable): + value_type = bool + entity = SPMUnit + label = "Iowa CCA eligible without regard to income" + definition_period = MONTH + defined_for = StateCode.IA + reference = "https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=3" + + def formula(spm_unit, period, parameters): + # Iowa serves certain families without regard to income + # (IAC 441-170.2(1)"b"): FIP/TANF recipients, families with a child + # in protective child care, and licensed foster parents needing + # care for a foster child. We use is_tanf_enrolled (a bare input) + # for the FIP path to break the CCAP-to-TANF circular dependency. + # PROMISE JOBS and court-ordered care have no PolicyEngine input at + # the moment, so we don't model those exception paths. + on_fip = spm_unit("is_tanf_enrolled", period) + person = spm_unit.members + protective = person("receives_or_needs_protective_services", period) + foster = person("is_in_foster_care", period) + has_protective_or_foster = spm_unit.any(protective | foster) + return on_fip | has_protective_or_foster diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca.py b/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca.py new file mode 100644 index 00000000000..417e9ff9c93 --- /dev/null +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca.py @@ -0,0 +1,29 @@ +from policyengine_us.model_api import * + + +class ia_cca(Variable): + value_type = float + entity = SPMUnit + unit = USD + label = "Iowa CCA benefit amount" + definition_period = MONTH + defined_for = "ia_cca_eligible" + reference = "https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=14" + + def formula(spm_unit, period, parameters): + person = spm_unit.members + is_in_care = person("ia_cca_eligible_child", period) & ( + person("childcare_hours_per_week", period.this_year) > 0 + ) + # Iowa pays the provider's charge for each child, not to exceed the + # maximum rate ceiling (rate per half-day unit times the child's + # monthly units of care), then subtracts the family fee + # (IAC 441-170.4(7)"a"). + child_charge = person("pre_subsidy_childcare_expenses", period) + max_rate = person("ia_cca_max_rate", period) + monthly_units = person("ia_cca_monthly_units", period) + child_ceiling = max_rate * monthly_units + child_capped = min_(child_charge, child_ceiling) + total_capped = spm_unit.sum(child_capped * is_in_care) + copay = spm_unit("ia_cca_copay", period) + return max_(total_capped - copay, 0) diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_age_group.py b/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_age_group.py new file mode 100644 index 00000000000..c914ee5d4a2 --- /dev/null +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_age_group.py @@ -0,0 +1,39 @@ +from policyengine_us.model_api import * + + +class IACCAAgeGroup(Enum): + INFANT_TODDLER = "Infant and Toddler" + INFANT_TODDLER_SN = "Infant and Toddler with Special Needs" + PRESCHOOL = "Preschool" + PRESCHOOL_SN = "Preschool with Special Needs" + SCHOOL_AGE = "School Age" + SCHOOL_AGE_SN = "School Age with Special Needs" + + +class ia_cca_age_group(Variable): + value_type = Enum + entity = Person + possible_values = IACCAAgeGroup + default_value = IACCAAgeGroup.PRESCHOOL + definition_period = MONTH + label = "Iowa CCA child care rate age group" + defined_for = StateCode.IA + reference = "https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=15" + + def formula(person, period, parameters): + p = parameters(period).gov.states.ia.hhs.cca.age_group + # `age` is YEAR-defined; read the annual value inside this monthly + # formula. The bracket returns the base group from the child's age + # in years: 0 = infant and toddler (under 3), 1 = preschool + # (3 to school age), 2 = school age (5 and older). + age = person("age", period.this_year) + base_group = p.age_group.calc(age) + # Iowa pays a higher special-needs rate for a child who receives + # special-needs care; we use the disability flag as the proxy. The + # special-needs variant of each base group sits at the next enum + # index (0=INFANT_TODDLER, 1=INFANT_TODDLER_SN, 2=PRESCHOOL, + # 3=PRESCHOOL_SN, 4=SCHOOL_AGE, 5=SCHOOL_AGE_SN). + is_disabled = person("is_disabled", period.this_year) + standard_index = base_group * 2 + sn_index = standard_index + 1 + return where(is_disabled, sn_index, standard_index) diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_children_in_care.py b/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_children_in_care.py new file mode 100644 index 00000000000..ffb49201893 --- /dev/null +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_children_in_care.py @@ -0,0 +1,18 @@ +from policyengine_us.model_api import * + + +class ia_cca_children_in_care(Variable): + value_type = int + entity = SPMUnit + label = "Iowa CCA number of children in care" + definition_period = MONTH + defined_for = StateCode.IA + reference = "https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=15" + + def formula(spm_unit, period, parameters): + person = spm_unit.members + is_eligible_child = person("ia_cca_eligible_child", period) + # A child is in care if they have childcare hours scheduled. + # `childcare_hours_per_week` is YEAR-defined. + in_care = person("childcare_hours_per_week", period.this_year) > 0 + return spm_unit.sum(is_eligible_child & in_care) diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_countable_income.py b/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_countable_income.py new file mode 100644 index 00000000000..f572e558c68 --- /dev/null +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_countable_income.py @@ -0,0 +1,28 @@ +from policyengine_us.model_api import * + + +class ia_cca_countable_income(Variable): + value_type = float + entity = SPMUnit + label = "Iowa CCA countable income" + definition_period = MONTH + unit = USD + defined_for = StateCode.IA + reference = "https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=3" + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.ia.hhs.cca.income + person = spm_unit.members + # Iowa excludes the earnings of a child age 14 or younger, and the + # earnings of a child 18 or younger who is a full-time student + # (IAC 441-170.2(1)"d"). `age` is YEAR-defined, so read the annual + # value inside this monthly formula. + age = person("age", period.this_year) + is_full_time_student = person("is_full_time_student", period.this_year) + excluded_minor = (age <= p.minor_earnings_age) | ( + (age <= p.minor_student_age) & is_full_time_student + ) + earned_per_person = add(person, period, p.countable_income.earned_sources) + counted_earned = spm_unit.sum(earned_per_person * ~excluded_minor) + unearned_income = add(spm_unit, period, p.countable_income.unearned_sources) + return counted_earned + unearned_income diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_enrolled.py b/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_enrolled.py new file mode 100644 index 00000000000..39ac60c83d7 --- /dev/null +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_enrolled.py @@ -0,0 +1,16 @@ +from policyengine_us.model_api import * + + +class ia_cca_enrolled(Variable): + value_type = bool + entity = SPMUnit + definition_period = MONTH + label = "Currently enrolled in Iowa CCA" + documentation = ( + "Whether the family is already enrolled in Iowa Child Care " + "Assistance. Iowa applies the higher ongoing income limits " + "(CCA Plus and CCA Exit) only to families enrolled at annual " + "redetermination; new applicants face the lower initial limits." + ) + defined_for = StateCode.IA + reference = "https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=3" diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_max_rate.py b/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_max_rate.py new file mode 100644 index 00000000000..2b19be28dde --- /dev/null +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_max_rate.py @@ -0,0 +1,50 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.ia.hhs.cca.ia_cca_provider_type import ( + IACCAProviderType, +) + + +class ia_cca_max_rate(Variable): + value_type = float + entity = Person + unit = USD + label = "Iowa CCA maximum half-day-unit rate per child" + definition_period = MONTH + defined_for = "ia_cca_eligible_child" + reference = "https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=14" + + def formula(person, period, parameters): + p = parameters(period).gov.states.ia.hhs.cca.payment + provider_type = person("ia_cca_provider_type", period) + quality_rating = person("ia_cca_quality_rating", period) + age_group = person("ia_cca_age_group", period) + + licensed_center_rate = p.rates.licensed_center[quality_rating][age_group] + child_dev_home_ab_rate = p.rates.child_dev_home_ab[quality_rating][age_group] + child_dev_home_c_rate = p.rates.child_dev_home_c[quality_rating][age_group] + not_registered_rate = p.rates.child_care_home_not_registered[age_group] + + # Iowa pays the in-home (Table 5) minimum-wage rate only when at + # least three children in the family require care + # (IAC 441-170.4(8)"d"). + children_in_care = person.spm_unit("ia_cca_children_in_care", period) + in_home_allowed = children_in_care >= p.in_home_min_children + in_home_rate = where(in_home_allowed, p.in_home_rate, 0) + + return select( + [ + provider_type == IACCAProviderType.LICENSED_CENTER, + provider_type == IACCAProviderType.CHILD_DEVELOPMENT_HOME_AB, + provider_type == IACCAProviderType.CHILD_DEVELOPMENT_HOME_C, + provider_type == IACCAProviderType.CHILD_CARE_HOME_NOT_REGISTERED, + provider_type == IACCAProviderType.IN_HOME, + ], + [ + licensed_center_rate, + child_dev_home_ab_rate, + child_dev_home_c_rate, + not_registered_rate, + in_home_rate, + ], + default=licensed_center_rate, + ) diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_monthly_units.py b/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_monthly_units.py new file mode 100644 index 00000000000..9f64b554628 --- /dev/null +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_monthly_units.py @@ -0,0 +1,21 @@ +from policyengine_us.model_api import * + + +class ia_cca_monthly_units(Variable): + value_type = float + entity = Person + label = "Iowa CCA half-day units of care per month per child" + definition_period = MONTH + defined_for = "ia_cca_eligible_child" + reference = "https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=3" + + def formula(person, period, parameters): + # A unit of service is a half day of up to 5 hours of care + # (IAC 441-170.1). PolicyEngine has no authorized-units input, so we + # infer the monthly units of care from the child's scheduled child + # care hours: weekly hours, annualized to a monthly figure, divided + # by the hours in a unit. `childcare_hours_per_week` is YEAR-defined. + hours_per_unit = parameters(period).gov.states.ia.hhs.cca.payment.hours_per_unit + weekly_hours = person("childcare_hours_per_week", period.this_year) + monthly_hours = weekly_hours * (WEEKS_IN_YEAR / MONTHS_IN_YEAR) + return monthly_hours / hours_per_unit diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_provider_type.py b/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_provider_type.py new file mode 100644 index 00000000000..9e7f42ff243 --- /dev/null +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_provider_type.py @@ -0,0 +1,20 @@ +from policyengine_us.model_api import * + + +class IACCAProviderType(Enum): + LICENSED_CENTER = "Licensed Center" + CHILD_DEVELOPMENT_HOME_AB = "Child Development Home A or B" + CHILD_DEVELOPMENT_HOME_C = "Child Development Home C" + CHILD_CARE_HOME_NOT_REGISTERED = "Child Care Home (Not Registered)" + IN_HOME = "In-Home Provider" + + +class ia_cca_provider_type(Variable): + value_type = Enum + entity = Person + possible_values = IACCAProviderType + default_value = IACCAProviderType.LICENSED_CENTER + definition_period = MONTH + label = "Iowa CCA child care provider type" + defined_for = StateCode.IA + reference = "https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=14" diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_quality_rating.py b/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_quality_rating.py new file mode 100644 index 00000000000..3e1313a41dc --- /dev/null +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_quality_rating.py @@ -0,0 +1,19 @@ +from policyengine_us.model_api import * + + +class IACCAQualityRating(Enum): + RATING_1_OR_2 = "Quality Rating 1 or 2" + RATING_3_OR_4 = "Quality Rating 3 or 4" + RATING_5 = "Quality Rating 5" + NO_RATING = "No Quality Rating" + + +class ia_cca_quality_rating(Variable): + value_type = Enum + entity = Person + possible_values = IACCAQualityRating + default_value = IACCAQualityRating.NO_RATING + definition_period = MONTH + label = "Iowa CCA provider quality rating" + defined_for = StateCode.IA + reference = "https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=15" diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_smi.py b/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_smi.py new file mode 100644 index 00000000000..42be278195c --- /dev/null +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_smi.py @@ -0,0 +1,27 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.hhs.hhs_smi import smi + + +class ia_cca_smi(Variable): + value_type = float + entity = SPMUnit + label = "Iowa CCA 85% median family income income cap" + unit = USD + definition_period = MONTH + defined_for = StateCode.IA + reference = "https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=3" + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.ia.hhs.cca.income + year = period.start.year + month = period.start.month + # The federal SMI table is published on a fiscal-year basis + # beginning each October 1. + if month >= 10: + instant_str = f"{year}-10-01" + else: + instant_str = f"{year - 1}-10-01" + size = spm_unit("spm_unit_size", period.this_year) + state = spm_unit.household("state_code_str", period.this_year) + annual_smi = smi(size, state, instant_str, parameters) + return annual_smi * p.smi_rate / MONTHS_IN_YEAR diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/ia_child_care_subsidies.py b/policyengine_us/variables/gov/states/ia/hhs/cca/ia_child_care_subsidies.py new file mode 100644 index 00000000000..d06ec32ae40 --- /dev/null +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/ia_child_care_subsidies.py @@ -0,0 +1,11 @@ +from policyengine_us.model_api import * + + +class ia_child_care_subsidies(Variable): + value_type = float + entity = SPMUnit + label = "Iowa child care subsidies" + unit = USD + definition_period = YEAR + defined_for = StateCode.IA + adds = ["ia_cca"] From 2cc0f0fe41c52c40326471a689c9e9c844560cbc Mon Sep 17 00:00:00 2001 From: Ziming Date: Mon, 8 Jun 2026 23:28:55 -0400 Subject: [PATCH 3/7] Expand IA CCAP changelog fragment description Co-Authored-By: Claude Opus 4.8 (1M context) --- changelog.d/ia-ccap.added.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/ia-ccap.added.md b/changelog.d/ia-ccap.added.md index 3101011fda4..b230488fdcd 100644 --- a/changelog.d/ia-ccap.added.md +++ b/changelog.d/ia-ccap.added.md @@ -1 +1 @@ -Add Iowa Child Care Assistance (CCAP) program. +Add Iowa Child Care Assistance (CCA / CCAP) - 3-tier CCDF child care subsidy with full provider rate matrix and sliding/percentage copays. From c71660ee30cfdb9a2ccd0780cefa52d0a1f738e3 Mon Sep 17 00:00:00 2001 From: Ziming Date: Mon, 8 Jun 2026 23:57:25 -0400 Subject: [PATCH 4/7] Review-fix round 1: address critical issues from /review-program - Fix citation 170.4(8)"d" -> 170.4(7)"d" (in-home care; no 170.4(8) exists) - Fix #page anchors on income exclusion / minor-age params (p.4/p.5) - Add ia_cca_has_special_needs_child unit test (was untested) - Use age-based child eligibility per IAC 170.2(1)"e"(3) (drop is_tax_unit_dependent) - Make ia_cca_max_rate provider selection explicit (no silent default) - Strip internal REQ- markers from tests; add ia_cca.yaml + matrix/negative-income cases Co-Authored-By: Claude Opus 4.8 (1M context) --- .../countable_income/earned_sources.yaml | 4 +- .../countable_income/unearned_sources.yaml | 2 +- .../ia/hhs/cca/income/minor_earnings_age.yaml | 2 +- .../ia/hhs/cca/income/minor_student_age.yaml | 2 +- .../states/ia/hhs/cca/income/smi_rate.yaml | 2 +- .../hhs/cca/payment/in_home_min_children.yaml | 2 +- .../ia/hhs/cca/payment/in_home_rate.yaml | 8 +- .../cca/payment/rates/child_dev_home_c.yaml | 4 +- .../states/ia/hhs/cca/copay/ia_cca_copay.yaml | 2 +- .../hhs/cca/copay/ia_cca_exit_fee_level.yaml | 2 +- .../ia/hhs/cca/copay/ia_cca_in_exit_tier.yaml | 2 +- .../cca/copay/ia_cca_sliding_fee_level.yaml | 2 +- .../eligibility/ia_cca_activity_eligible.yaml | 2 +- .../eligibility/ia_cca_asset_eligible.yaml | 2 +- .../hhs/cca/eligibility/ia_cca_eligible.yaml | 2 +- .../eligibility/ia_cca_eligible_child.yaml | 19 ++- .../ia_cca_has_special_needs_child.yaml | 113 ++++++++++++++ .../eligibility/ia_cca_income_eligible.yaml | 2 +- .../eligibility/ia_cca_income_exception.yaml | 2 +- .../gov/states/ia/hhs/cca/ia_cca.yaml | 144 ++++++++++++++++++ .../ia/hhs/cca/ia_cca_children_in_care.yaml | 2 +- .../ia/hhs/cca/ia_cca_countable_income.yaml | 2 +- .../states/ia/hhs/cca/ia_cca_max_rate.yaml | 118 +++++++++++++- .../ia/hhs/cca/ia_cca_monthly_units.yaml | 2 +- .../ia/hhs/cca/ia_cca_provider_type.yaml | 2 +- .../ia/hhs/cca/ia_cca_quality_rating.yaml | 2 +- .../gov/states/ia/hhs/cca/ia_cca_smi.yaml | 2 +- .../cca/eligibility/ia_cca_eligible_child.py | 9 +- .../cca/eligibility/ia_cca_income_eligible.py | 12 ++ .../eligibility/ia_cca_income_exception.py | 2 +- .../ia/hhs/cca/ia_cca_countable_income.py | 2 +- .../gov/states/ia/hhs/cca/ia_cca_enrolled.py | 10 +- .../gov/states/ia/hhs/cca/ia_cca_max_rate.py | 7 +- .../gov/states/ia/hhs/cca/ia_cca_smi.py | 5 + 34 files changed, 449 insertions(+), 48 deletions(-) create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_has_special_needs_child.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca.yaml diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/income/countable_income/earned_sources.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/countable_income/earned_sources.yaml index fc41ae07a9a..2c1b6ab215c 100644 --- a/policyengine_us/parameters/gov/states/ia/hhs/cca/income/countable_income/earned_sources.yaml +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/countable_income/earned_sources.yaml @@ -12,6 +12,6 @@ metadata: label: Iowa CCA countable earned income sources reference: - title: IAC 441-170.2(1)"c" — Income counted (wages, self-employment net profit) - href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=3 + href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=4 - title: IAC 441-170.2(1)"d" — Income excluded (child age 14 or under; student 18 or under) - href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=3 + href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=4 diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/income/countable_income/unearned_sources.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/countable_income/unearned_sources.yaml index a1bcddad40b..0b46a01b18c 100644 --- a/policyengine_us/parameters/gov/states/ia/hhs/cca/income/countable_income/unearned_sources.yaml +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/countable_income/unearned_sources.yaml @@ -30,4 +30,4 @@ metadata: # separate PolicyEngine variable at the moment and are therefore not counted. reference: - title: IAC 441-170.2(1)"c" — Income counted (Census median-income sources) - href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=3 + href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=4 diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/income/minor_earnings_age.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/minor_earnings_age.yaml index 4a74f904fd3..c0c115154fe 100644 --- a/policyengine_us/parameters/gov/states/ia/hhs/cca/income/minor_earnings_age.yaml +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/minor_earnings_age.yaml @@ -8,4 +8,4 @@ metadata: label: Iowa CCA excluded-earnings child age reference: - title: IAC 441-170.2(1)"d" — Income excluded (earnings of a child age 14 or younger) - href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=3 + href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=4 diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/income/minor_student_age.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/minor_student_age.yaml index c3850de9f03..6e624d8c25e 100644 --- a/policyengine_us/parameters/gov/states/ia/hhs/cca/income/minor_student_age.yaml +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/minor_student_age.yaml @@ -8,4 +8,4 @@ metadata: label: Iowa CCA excluded-earnings student age reference: - title: IAC 441-170.2(1)"d" — Income excluded (earnings of a student 18 or younger) - href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=3 + href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=5 diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/income/smi_rate.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/smi_rate.yaml index 2551b4a777d..aee9eb55647 100644 --- a/policyengine_us/parameters/gov/states/ia/hhs/cca/income/smi_rate.yaml +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/smi_rate.yaml @@ -1,4 +1,4 @@ -description: Iowa caps initial Child Care Assistance income at this share of the state median family income when that cap is lower than the federal poverty level standard. +description: Iowa caps initial Child Care Assistance income at this share of Iowa's median family income (MFI) when that cap is lower than the federal poverty level standard. Iowa publishes no standalone MFI figure, so the model uses the federal CCDF State Median Income (SMI) table as a proxy for the statutory MFI standard. values: 2024-07-01: 0.85 diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/payment/in_home_min_children.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/payment/in_home_min_children.yaml index b10e9f7b178..3d2038f1b86 100644 --- a/policyengine_us/parameters/gov/states/ia/hhs/cca/payment/in_home_min_children.yaml +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/payment/in_home_min_children.yaml @@ -7,5 +7,5 @@ metadata: period: year label: Iowa CCA in-home minimum children needing care reference: - - title: IAC 441-170.4(8)"d" — In-home care (three or more children require care) + - title: IAC 441-170.4(7)"d" — In-home care (three or more children require care) href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=15 diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/payment/in_home_rate.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/payment/in_home_rate.yaml index d089636c78a..e6c57c78ba2 100644 --- a/policyengine_us/parameters/gov/states/ia/hhs/cca/payment/in_home_rate.yaml +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/payment/in_home_rate.yaml @@ -1,4 +1,4 @@ -description: Iowa pays this maximum half-day-unit rate for in-home child care (Table 5) under the Child Care Assistance program. The rule sets the rate at the minimum wage amount (Iowa minimum wage of $7.25 per hour times the 5-hour unit equals $36.25), the same for basic and special-needs care across all age groups. +description: Iowa pays this maximum half-day-unit rate for in-home child care (Table 5) under the Child Care Assistance program, the same for basic and special-needs care across all age groups. The published Provider Rate Letter (Table 5) sets the rate at $36.25 per half-day unit; the regulation describes it as the minimum wage amount (Iowa minimum wage of $7.25 per hour times the 5-hour unit equals $36.25). values: 2024-07-01: 36.25 @@ -7,7 +7,7 @@ metadata: period: year label: Iowa CCA in-home half-day-unit rate reference: - - title: IAC 441-170.4(8)"d" — In-home care rate (minimum wage amount; paid only with 3 or more children needing care) - href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=15 - - title: Iowa HHS CCA Maximum Rates, Table 5, In-Home ($36.25 per half-day unit, effective July 1, 2024) + - title: Iowa CCR&R Provider Rate Letter, Table 5, In-Home ($36.25 per half-day unit, effective July 1, 2024) href: https://iowaccrr.org/resources/files/BGP/149%20CCA%20Rates.pdf#page=2 + - title: IAC 441-170.4(7)"d" — In-home care rate (minimum wage amount; paid only with 3 or more children needing care) + href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=15 diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/payment/rates/child_dev_home_c.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/payment/rates/child_dev_home_c.yaml index f46a3acd911..43fedabc622 100644 --- a/policyengine_us/parameters/gov/states/ia/hhs/cca/payment/rates/child_dev_home_c.yaml +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/payment/rates/child_dev_home_c.yaml @@ -7,8 +7,8 @@ metadata: - ia_cca_quality_rating - ia_cca_age_group reference: - - title: IAC 441-170.4(7) — Table 3, Child Development Home C (effective July 1, 2024) - href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=14 + - title: IAC 441-170.4(7) — Table 3, Child Development Home C (effective July 1, 2024; table straddles pp.14-15, with the preschool and school-age rows on p.15) + href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=15 NO_RATING: INFANT_TODDLER: diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_copay.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_copay.yaml index 58949225323..da92952921f 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_copay.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_copay.yaml @@ -1,4 +1,4 @@ -# ia_cca_copay: SPMUnit, USD, MONTH (REQ-028 / REQ-029 / REQ-030). +# ia_cca_copay: SPMUnit, USD, MONTH. # Two mechanisms dispatched by ia_cca_in_exit_tier (enrolled & income > 225% FPL): # A. CCA / CCA Plus sliding unit fee = unit_fee[level][children_col] * focal_units, # where focal_units = the most monthly units of any in-care eligible child diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_exit_fee_level.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_exit_fee_level.yaml index 4c375aefb64..3f4616d29d7 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_exit_fee_level.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_exit_fee_level.yaml @@ -1,4 +1,4 @@ -# ia_cca_exit_fee_level: SPMUnit, int index 0-3, MONTH (REQ-029). +# ia_cca_exit_fee_level: SPMUnit, int index 0-3, MONTH. # Selects the CCA Exit fee level (A=0 .. D=3) from monthly income and family size, # using the basic thresholds for a basic-care family and the special-needs # thresholds when the family includes a special-needs child. diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_in_exit_tier.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_in_exit_tier.yaml index 9e6dc4b6ff0..3616fca1e64 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_in_exit_tier.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_in_exit_tier.yaml @@ -1,4 +1,4 @@ -# ia_cca_in_exit_tier: SPMUnit, bool, MONTH (REQ-029 dispatch). +# ia_cca_in_exit_tier: SPMUnit, bool, MONTH (dispatch). # True when the family is enrolled AND monthly income exceeds the ongoing CCA Plus # limit (225% FPL). These families pay the percentage-of-cost CCA Exit fee rather # than the sliding unit fee. 2025 family-of-3 225% FPL monthly = 4_996.88 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_sliding_fee_level.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_sliding_fee_level.yaml index d1b0681fc6b..46ea3a20645 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_sliding_fee_level.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_sliding_fee_level.yaml @@ -1,4 +1,4 @@ -# ia_cca_sliding_fee_level: SPMUnit, int index 0-27, MONTH (REQ-028). +# ia_cca_sliding_fee_level: SPMUnit, int index 0-27, MONTH. # The family is at the first level whose monthly-income threshold (for its family # size) is at or above its income; equivalently the index is the number of level # thresholds the monthly income exceeds, capped at 27 (Level BB). diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_activity_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_activity_eligible.yaml index 904d53aec31..8809ef2e6bf 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_activity_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_activity_eligible.yaml @@ -1,4 +1,4 @@ -# ia_cca_activity_eligible: SPMUnit, bool, MONTH (REQ-013 / REQ-014 / REQ-015). +# ia_cca_activity_eligible: SPMUnit, bool, MONTH. # IAC 441-170.2(2)"b": each parent must average at least 32 hours/week in an # approved activity (28 hours/week if the family includes a child with special # needs). Full-time students qualify; meets_ccdf_activity_test is the fallback for diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_asset_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_asset_eligible.yaml index fba59ef9994..8d64d7d766e 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_asset_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_asset_eligible.yaml @@ -1,4 +1,4 @@ -# ia_cca_asset_eligible: SPMUnit, bool, MONTH (REQ-012). +# ia_cca_asset_eligible: SPMUnit, bool, MONTH. # Iowa's resource limit is $1,000,000, applied instead of the much tighter federal # CCDF asset check. Tests spm_unit_assets <= 1_000_000. diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_eligible.yaml index fb69d30b72a..1d3166f24b5 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_eligible.yaml @@ -1,4 +1,4 @@ -# ia_cca_eligible: SPMUnit, bool, MONTH (REQ-016). +# ia_cca_eligible: SPMUnit, bool, MONTH. # Eligible = has an eligible child AND asset-eligible AND # ((income-eligible AND activity-eligible) OR income-exception). # Families eligible without regard to income (FIP/protective/foster) bypass both diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_eligible_child.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_eligible_child.yaml index 87c117f5302..3b8a3544e58 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_eligible_child.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_eligible_child.yaml @@ -1,8 +1,10 @@ -# ia_cca_eligible_child: Person, bool, MONTH (REQ-001 / REQ-002 / REQ-004). +# ia_cca_eligible_child: Person, bool, MONTH. # IAC 441-170.2(2): child up to age 13 (basic), up to age 19 for a child with -# special needs; must be a dependent in the family; must be a U.S. citizen or -# qualified alien (is_ccdf_immigration_eligible_child). Protective-services and -# foster children are categorically eligible regardless of dependency/immigration. +# special needs; child status is age-based (not tax-dependency-based, since +# 170.2(1)"e"(3) includes children living with a non-legally-responsible +# caretaker); must be a U.S. citizen or qualified alien +# (is_ccdf_immigration_eligible_child). Protective-services and foster children +# are categorically eligible regardless of immigration status. - name: Infant dependent citizen is an eligible child. period: 2025-01 @@ -124,7 +126,7 @@ # 19 is not < 19 -> ineligible even with special needs ia_cca_eligible_child: false -- name: Non-dependent child within the age limit is not a standard eligible child. +- name: Non-dependent child within the age limit is still an eligible child. period: 2025-01 input: people: @@ -140,8 +142,11 @@ members: [child] state_code: IA output: - # Age-eligible but not a dependent and no categorical path -> ineligible - ia_cca_eligible_child: false + # Child status is age-based, not dependency-based. Iowa's family-size + # definition (170.2(1)"e"(3)) includes a child living with a person not + # legally responsible for support, so an age-eligible citizen child who is + # not a tax dependent is still eligible. + ia_cca_eligible_child: true - name: Child without immigration eligibility is not a standard eligible child. period: 2025-01 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_has_special_needs_child.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_has_special_needs_child.yaml new file mode 100644 index 00000000000..7ec169df30c --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_has_special_needs_child.yaml @@ -0,0 +1,113 @@ +# ia_cca_has_special_needs_child: SPMUnit, bool, MONTH. +# IAC 441-170.2(2)"a": a family includes a child with special needs when an +# age-eligible child (under the special-needs age limit of 19) is disabled. +# Formula: spm_unit.any(ia_cca_eligible_child & is_disabled). The is_eligible_child +# filter binds so that disabled adults and over-age disabled children do not flip +# the flag true. + +- name: Age-eligible disabled child makes the family special-needs. + period: 2025-01 + input: + people: + parent: + age: 30 + immigration_status: CITIZEN + child: + age: 10 + is_disabled: true + is_tax_unit_dependent: true + immigration_status: CITIZEN + tax_units: + tax_unit: + members: [parent, child] + spm_units: + spm_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: IA + output: + # Child age 10 < 19, dependent citizen -> eligible child; disabled -> true + ia_cca_has_special_needs_child: true + +- name: Over-age disabled child does not make the family special-needs. + period: 2025-01 + input: + people: + parent: + age: 45 + immigration_status: CITIZEN + child: + age: 20 + is_disabled: true + is_tax_unit_dependent: true + immigration_status: CITIZEN + tax_units: + tax_unit: + members: [parent, child] + spm_units: + spm_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: IA + output: + # Age 20 > special-needs limit 19 -> not an eligible child, so the disabled + # flag is filtered out -> false (proves the is_eligible_child filter binds) + ia_cca_has_special_needs_child: false + +- name: Disabled adult with a non-disabled eligible child is not special-needs. + period: 2025-01 + input: + people: + parent: + age: 40 + is_disabled: true + immigration_status: CITIZEN + child: + age: 8 + is_disabled: false + is_tax_unit_dependent: true + immigration_status: CITIZEN + tax_units: + tax_unit: + members: [parent, child] + spm_units: + spm_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: IA + output: + # The disabled member is an adult (not an eligible child) and the eligible + # child is not disabled -> false (proves the flag keys off the child) + ia_cca_has_special_needs_child: false + +- name: Family with no disabled member is not special-needs. + period: 2025-01 + input: + people: + parent: + age: 30 + immigration_status: CITIZEN + child: + age: 5 + is_disabled: false + is_tax_unit_dependent: true + immigration_status: CITIZEN + tax_units: + tax_unit: + members: [parent, child] + spm_units: + spm_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: IA + output: + # No disabled member -> false + ia_cca_has_special_needs_child: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_income_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_income_eligible.yaml index 2b42a4921e8..50a243ba4ce 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_income_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_income_eligible.yaml @@ -1,4 +1,4 @@ -# ia_cca_income_eligible: SPMUnit, bool, MONTH (REQ-005 .. REQ-010). +# ia_cca_income_eligible: SPMUnit, bool, MONTH. # Three income tiers selected by the ia_cca_enrolled boolean input: # - Applicants (not enrolled): initial limit = 160% FPL basic / 200% FPL special-needs. # - Enrolled (ongoing): CCA Exit 250% FPL basic / 275% FPL special-needs. diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_income_exception.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_income_exception.yaml index 601cf646434..b63fbd521c1 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_income_exception.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_income_exception.yaml @@ -1,4 +1,4 @@ -# ia_cca_income_exception: SPMUnit, bool, MONTH (REQ-011). +# ia_cca_income_exception: SPMUnit, bool, MONTH. # Iowa serves certain families without regard to income (IAC 441-170.2(1)"b"): # FIP/TANF recipients (via is_tanf_enrolled), families with a child in protective # child care, and licensed foster parents needing care for a foster child. diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca.yaml new file mode 100644 index 00000000000..99e6e7c8d28 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca.yaml @@ -0,0 +1,144 @@ +# ia_cca: SPMUnit, USD, MONTH. +# Isolates the benefit formula's three moving parts: +# per child: child_capped = min(pre_subsidy_childcare_expenses, max_rate * units) +# total = sum over in-care eligible children of child_capped +# benefit = max(total - ia_cca_copay, 0) +# monthly_units(hpw) = hpw * (52 / 12) / 5; monthly_units(40) = 34.6667, (30) = 26.0. +# 2025 FPG: family of 2 = 21_150; family of 3 = 26_650. +# pre_subsidy_childcare_expenses is YEAR-defined; 12_000/yr -> 1_000/mo in the +# MONTH benefit formula. + +- name: Positive benefit, charge capped at the rate ceiling with no copay. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 28 + employment_income: 24_000 + is_tax_unit_head_or_spouse: true + weekly_hours_worked_before_lsr: 40 + immigration_status: CITIZEN + infant: + age: 1 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 40 + pre_subsidy_childcare_expenses: 12_000 + ia_cca_provider_type: LICENSED_CENTER + ia_cca_quality_rating: NO_RATING + preschooler: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 40 + pre_subsidy_childcare_expenses: 12_000 + ia_cca_provider_type: LICENSED_CENTER + ia_cca_quality_rating: NO_RATING + tax_units: + tax_unit: + members: [parent, infant, preschooler] + spm_units: + spm_unit: + members: [parent, infant, preschooler] + ia_cca_enrolled: false + spm_unit_assets: 5_000 + households: + household: + members: [parent, infant, preschooler] + state_code: IA + output: + # Income 2_000/mo <= Level A (2_044, family 3) -> copay 0. + ia_cca_copay: 0 + # Ceilings: infant 25.66 * 34.6667 = 889.55; preschool 19.50 * 34.6667 = 676.00. + # Each 1_000/mo expense exceeds its ceiling -> capped to the ceiling. + # Benefit = (889.55 + 676.00) - 0 = 1_565.55 + ia_cca: 1_565.55 + +- name: Copay exceeds the rate ceiling so the benefit floors at zero. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 62_400 + is_tax_unit_head_or_spouse: true + weekly_hours_worked_before_lsr: 40 + immigration_status: CITIZEN + coparent: + age: 30 + is_tax_unit_head_or_spouse: true + weekly_hours_worked_before_lsr: 40 + immigration_status: CITIZEN + preschooler: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 40 + pre_subsidy_childcare_expenses: 24_000 + ia_cca_provider_type: LICENSED_CENTER + ia_cca_quality_rating: NO_RATING + tax_units: + tax_unit: + members: [parent, coparent, preschooler] + spm_units: + spm_unit: + members: [parent, coparent, preschooler] + ia_cca_enrolled: true + spm_unit_assets: 5_000 + households: + household: + members: [parent, coparent, preschooler] + state_code: IA + output: + # Family of 3 enrolled; income 5_200/mo > 225% FPL (4_996.88) -> CCA Exit tier, + # Exit basic Level C -> 60% percentage-of-cost copay. + ia_cca_in_exit_tier: true + # Expense 24_000/yr -> 2_000/mo. Exit copay = 60% * 2_000 = 1_200. + ia_cca_copay: 1_200.00 + # Preschool ceiling = 19.50 * 34.6667 = 676.00; child_capped = min(2_000, 676) = 676. + # Benefit = max(676.00 - 1_200.00, 0) = 0 (copay exceeds ceiling -> floored at 0) + ia_cca: 0 + +- name: Negative countable income does not inflate the benefit beyond the ceiling. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + self_employment_income: -60_000 + is_tax_unit_head_or_spouse: true + weekly_hours_worked_before_lsr: 40 + immigration_status: CITIZEN + preschooler: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 40 + pre_subsidy_childcare_expenses: 12_000 + ia_cca_provider_type: LICENSED_CENTER + ia_cca_quality_rating: NO_RATING + tax_units: + tax_unit: + members: [parent, preschooler] + spm_units: + spm_unit: + members: [parent, preschooler] + ia_cca_enrolled: false + spm_unit_assets: 5_000 + households: + household: + members: [parent, preschooler] + state_code: IA + output: + # Negative self-employment income -> zero/negative countable income, well under + # the 160% FPL limit -> income eligible; parent works 40 hrs -> activity eligible. + ia_cca_eligible: true + # Countable income <= sliding-fee Level A -> maximum subsidy, copay 0. + ia_cca_copay: 0 + # Preschool ceiling = 19.50 * 34.6667 = 676.00; expense 1_000/mo capped to 676.00. + # Benefit stays at the rate ceiling (max subsidy) and is not inflated by the + # negative income. Benefit = max(676.00 - 0, 0) = 676.00 + ia_cca: 676.00 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_children_in_care.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_children_in_care.yaml index 3fede903f02..cb2d6091da2 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_children_in_care.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_children_in_care.yaml @@ -1,4 +1,4 @@ -# ia_cca_children_in_care: SPMUnit, int, MONTH (REQ-027 gating helper). +# ia_cca_children_in_care: SPMUnit, int, MONTH (gating helper). # Counts eligible children with scheduled childcare hours (childcare_hours_per_week # > 0). Used to set the sliding-fee children column and to gate the in-home rate. diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_countable_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_countable_income.yaml index 7c38fd75426..5d57d8c036e 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_countable_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_countable_income.yaml @@ -1,4 +1,4 @@ -# ia_cca_countable_income: SPMUnit, USD, MONTH (REQ-032 / REQ-033 / REQ-034). +# ia_cca_countable_income: SPMUnit, USD, MONTH. # Sums countable earned and unearned income, excluding the earnings of a child # age 14 or younger and the earnings of a full-time student age 18 or younger # (IAC 441-170.2(1)"d"). Definition period is MONTH, so annual income sources are diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_max_rate.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_max_rate.yaml index 56924d75f68..7b14a50916b 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_max_rate.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_max_rate.yaml @@ -1,4 +1,4 @@ -# ia_cca_max_rate: Person, USD per half-day unit, MONTH (REQ-019 .. REQ-027). +# ia_cca_max_rate: Person, USD per half-day unit, MONTH. # Looks up the maximum half-day-unit rate by provider type x quality rating x # age group (with the special-needs variant baked into the age-group enum). Only # defined for an eligible child (defined_for = ia_cca_eligible_child), so each @@ -169,6 +169,81 @@ # NO_RATING: IT 16.63 / PRE 15.00 / SA 15.00; RATING_5 IT 17.50 ia_cca_max_rate: [16.63, 15.00, 15.00, 17.50] +- name: Child development home A or B, rating 1 or 2, all age groups. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + it: + age: 1 + is_tax_unit_dependent: true + immigration_status: CITIZEN + ia_cca_provider_type: CHILD_DEVELOPMENT_HOME_AB + ia_cca_quality_rating: RATING_1_OR_2 + pre: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + ia_cca_provider_type: CHILD_DEVELOPMENT_HOME_AB + ia_cca_quality_rating: RATING_1_OR_2 + sa: + age: 8 + is_tax_unit_dependent: true + immigration_status: CITIZEN + ia_cca_provider_type: CHILD_DEVELOPMENT_HOME_AB + ia_cca_quality_rating: RATING_1_OR_2 + tax_units: + tax_unit: + members: [it, pre, sa] + households: + household: + members: [it, pre, sa] + state_code: IA + output: + # RATING_1_OR_2: IT 16.63 / PRE 15.50 / SA 15.25 + ia_cca_max_rate: [16.63, 15.50, 15.25] + +- name: Child development home A or B, rating 3 or 4, basic age groups and special needs. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + it: + age: 1 + is_tax_unit_dependent: true + immigration_status: CITIZEN + ia_cca_provider_type: CHILD_DEVELOPMENT_HOME_AB + ia_cca_quality_rating: RATING_3_OR_4 + pre: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + ia_cca_provider_type: CHILD_DEVELOPMENT_HOME_AB + ia_cca_quality_rating: RATING_3_OR_4 + sa: + age: 8 + is_tax_unit_dependent: true + immigration_status: CITIZEN + ia_cca_provider_type: CHILD_DEVELOPMENT_HOME_AB + ia_cca_quality_rating: RATING_3_OR_4 + pre_sn: + age: 4 + is_disabled: true + is_tax_unit_dependent: true + immigration_status: CITIZEN + ia_cca_provider_type: CHILD_DEVELOPMENT_HOME_AB + ia_cca_quality_rating: RATING_3_OR_4 + tax_units: + tax_unit: + members: [it, pre, sa, pre_sn] + households: + household: + members: [it, pre, sa, pre_sn] + state_code: IA + output: + # RATING_3_OR_4: IT 16.63 / PRE 16.00 / SA 15.50 / PRESCHOOL_SN 24.00 + ia_cca_max_rate: [16.63, 16.00, 15.50, 24.00] + # ===== Table 3: Child Development Home C ===== - name: Child development home C, rating 3 or 4, all age groups. @@ -205,6 +280,47 @@ # CDH C RATING_3_OR_4: IT 18.00 / PRE 17.50 / SA 16.44 ia_cca_max_rate: [18.00, 17.50, 16.44] +- name: Child development home C, rating 1 or 2, basic age groups and special needs. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + it: + age: 1 + is_tax_unit_dependent: true + immigration_status: CITIZEN + ia_cca_provider_type: CHILD_DEVELOPMENT_HOME_C + ia_cca_quality_rating: RATING_1_OR_2 + pre: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + ia_cca_provider_type: CHILD_DEVELOPMENT_HOME_C + ia_cca_quality_rating: RATING_1_OR_2 + sa: + age: 8 + is_tax_unit_dependent: true + immigration_status: CITIZEN + ia_cca_provider_type: CHILD_DEVELOPMENT_HOME_C + ia_cca_quality_rating: RATING_1_OR_2 + it_sn: + age: 1 + is_disabled: true + is_tax_unit_dependent: true + immigration_status: CITIZEN + ia_cca_provider_type: CHILD_DEVELOPMENT_HOME_C + ia_cca_quality_rating: RATING_1_OR_2 + tax_units: + tax_unit: + members: [it, pre, sa, it_sn] + households: + household: + members: [it, pre, sa, it_sn] + state_code: IA + output: + # RATING_1_OR_2: IT 18.00 / PRE 17.00 / SA 15.75 / INFANT_TODDLER_SN 27.00 + ia_cca_max_rate: [18.00, 17.00, 15.75, 27.00] + # ===== Table 4: Child Care Home (Not Registered) — no quality dimension ===== - name: Not-registered child care home, basic and special needs, no quality effect. diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_monthly_units.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_monthly_units.yaml index 726fea80ad2..fab9c30625b 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_monthly_units.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_monthly_units.yaml @@ -1,4 +1,4 @@ -# ia_cca_monthly_units: Person, float, MONTH (REQ-017). +# ia_cca_monthly_units: Person, float, MONTH. # A unit of service is a half day of up to 5 hours of care (IAC 441-170.1). With # no authorized-units input, the monthly units are inferred from the child's # scheduled weekly childcare hours, annualized to monthly and divided by the diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_provider_type.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_provider_type.yaml index 425c8b9dac6..f09681310de 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_provider_type.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_provider_type.yaml @@ -1,4 +1,4 @@ -# ia_cca_provider_type: Person, Enum input, MONTH (REQ-020). +# ia_cca_provider_type: Person, Enum input, MONTH. # Input variable identifying the child's provider type. Default is LICENSED_CENTER. - name: Defaults to licensed center. diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_quality_rating.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_quality_rating.yaml index a5549d8a961..7fea18f1020 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_quality_rating.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_quality_rating.yaml @@ -1,4 +1,4 @@ -# ia_cca_quality_rating: Person, Enum input, MONTH (REQ-021). +# ia_cca_quality_rating: Person, Enum input, MONTH. # Input variable for the provider's IQ4K quality rating. Default is NO_RATING. - name: Defaults to no quality rating. diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_smi.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_smi.yaml index f9eadb03a3a..9b6adb17497 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_smi.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_smi.yaml @@ -1,4 +1,4 @@ -# ia_cca_smi: SPMUnit, USD, MONTH (REQ-007). +# ia_cca_smi: SPMUnit, USD, MONTH. # 85% of Iowa median family income, reused from the federal hhs_smi() helper and # returned as a monthly figure. The SMI table is on a fiscal year starting Oct 1, # so a January 2025 period uses the 2024-10-01 (FY2025) value: IA 4-person SMI = diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_eligible_child.py b/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_eligible_child.py index 939a1bd2846..4c71841b6da 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_eligible_child.py +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_eligible_child.py @@ -20,11 +20,16 @@ def formula(person, period, parameters): age < p.special_needs_age_limit, age < p.child_age_limit, ) - is_dependent = person("is_tax_unit_dependent", period.this_year) immigration_eligible = person( "is_ccdf_immigration_eligible_child", period.this_year ) - standard_eligible = age_eligible & is_dependent & immigration_eligible + # Child status is age-based, not tax-dependency-based. Iowa's + # family-size definition (IAC 441-170.2(1)"e"(3)) explicitly includes + # "a child or children who live with a person or persons not legally + # responsible for the child's support," so a non-dependent minor + # living with a non-parent caretaker is still an eligible child; + # gating on is_tax_unit_dependent would wrongly exclude them. + standard_eligible = age_eligible & immigration_eligible # A child in protective or foster care qualifies through the # income-exception path regardless of dependency or immigration # status (IAC 441-170.2(1)"b"). diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_income_eligible.py b/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_income_eligible.py index e5a311a9235..8c85f653700 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_income_eligible.py +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_income_eligible.py @@ -34,5 +34,17 @@ def formula(spm_unit, period, parameters): ) initial_limit = min_(fpg * initial_fpl_rate, spm_unit("ia_cca_smi", period)) exit_limit = fpg * exit_fpl_rate + # IAC 441-170.2(1)"a" sets three income tiers: initial (a(1), + # min of 160%/200% FPL and 85% MFI), ongoing CCA Plus (a(2), + # min of 225% FPL and 85% MFI), and ongoing CCA Exit (a(3), + # 250%/275% FPL with no MFI cap). We collapse the two ongoing tiers + # into the higher CCA Exit ceiling for enrolled families, so the a(2) + # 225%-FPL-with-85%-MFI-cap intermediate ceiling is not represented. + # The 85% MFI cap is correctly absent from a(3). The omitted a(2) cap + # only binds below the 225% FPL standard at very large family sizes + # (where 85% MFI falls below 225% FPL), and the model uses the 225% + # FPL standard solely as the copay-mechanism dispatch boundary + # (CCA/CCA Plus sliding fee vs. CCA Exit percentage), never as an + # income-eligibility ceiling, so this simplification is immaterial. income_limit = where(enrolled, exit_limit, initial_limit) return countable_income <= income_limit diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_income_exception.py b/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_income_exception.py index 4f0c561391a..d64800ff22a 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_income_exception.py +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_income_exception.py @@ -7,7 +7,7 @@ class ia_cca_income_exception(Variable): label = "Iowa CCA eligible without regard to income" definition_period = MONTH defined_for = StateCode.IA - reference = "https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=3" + reference = "https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=4" def formula(spm_unit, period, parameters): # Iowa serves certain families without regard to income diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_countable_income.py b/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_countable_income.py index f572e558c68..aab18373d08 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_countable_income.py +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_countable_income.py @@ -8,7 +8,7 @@ class ia_cca_countable_income(Variable): definition_period = MONTH unit = USD defined_for = StateCode.IA - reference = "https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=3" + reference = "https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=4" def formula(spm_unit, period, parameters): p = parameters(period).gov.states.ia.hhs.cca.income diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_enrolled.py b/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_enrolled.py index 39ac60c83d7..7c938c3a442 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_enrolled.py +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_enrolled.py @@ -6,11 +6,9 @@ class ia_cca_enrolled(Variable): entity = SPMUnit definition_period = MONTH label = "Currently enrolled in Iowa CCA" - documentation = ( - "Whether the family is already enrolled in Iowa Child Care " - "Assistance. Iowa applies the higher ongoing income limits " - "(CCA Plus and CCA Exit) only to families enrolled at annual " - "redetermination; new applicants face the lower initial limits." - ) + # Whether the family is already enrolled in Iowa Child Care Assistance. + # Iowa applies the higher ongoing income limits (CCA Plus and CCA Exit) + # only to families enrolled at annual redetermination; new applicants + # face the lower initial limits. defined_for = StateCode.IA reference = "https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=3" diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_max_rate.py b/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_max_rate.py index 2b19be28dde..64c71a4f14c 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_max_rate.py +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_max_rate.py @@ -26,7 +26,7 @@ def formula(person, period, parameters): # Iowa pays the in-home (Table 5) minimum-wage rate only when at # least three children in the family require care - # (IAC 441-170.4(8)"d"). + # (IAC 441-170.4(7)"d"). children_in_care = person.spm_unit("ia_cca_children_in_care", period) in_home_allowed = children_in_care >= p.in_home_min_children in_home_rate = where(in_home_allowed, p.in_home_rate, 0) @@ -46,5 +46,8 @@ def formula(person, period, parameters): not_registered_rate, in_home_rate, ], - default=licensed_center_rate, + # Every IACCAProviderType value is matched explicitly above, so a + # future unmapped provider type surfaces as a zero rate (a visible + # gap) rather than silently inheriting the licensed-center rate. + default=0, ) diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_smi.py b/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_smi.py index 42be278195c..f100d2ebb9d 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_smi.py +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_smi.py @@ -13,6 +13,11 @@ class ia_cca_smi(Variable): def formula(spm_unit, period, parameters): p = parameters(period).gov.states.ia.hhs.cca.income + # IAC 441-170.2(1)"a"(1)"3" caps income at 85% of Iowa's "median + # family income (MFI)." Iowa publishes no standalone MFI figure, so we + # use the federal CCDF State Median Income (SMI) table as a proxy for + # the statutory MFI standard. The 0.85 share is taken directly from + # the regulation. year = period.start.year month = period.start.month # The federal SMI table is published on a fiscal-year basis From e3d034e020305150a6fe13e16dc90350a0774fac Mon Sep 17 00:00:00 2001 From: Ziming Date: Tue, 9 Jun 2026 00:20:32 -0400 Subject: [PATCH 5/7] Review-fix round 2: correct copay fee-level lookup (off-by-one) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CCA/CCA Plus sliding-fee and CCA Exit fee-level lookups used sum(income > threshold), placing families whose income falls strictly between two thresholds one level too high (over-charging copay). The fee charts assign the level via "first row greater than income, use the row above" — each printed threshold is the inclusive floor of its level. Corrected both to max(sum(income >= threshold) - 1, 0). This makes CCA Exit Level A (33%) reachable. Recomputed affected copay/level/integration test expectations against the corrected logic. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../states/ia/hhs/cca/copay/ia_cca_copay.yaml | 60 ++++++++++--------- .../hhs/cca/copay/ia_cca_exit_fee_level.yaml | 34 ++++++----- .../cca/copay/ia_cca_sliding_fee_level.yaml | 34 ++++++----- .../gov/states/ia/hhs/cca/ia_cca.yaml | 8 +-- .../gov/states/ia/hhs/cca/integration.yaml | 52 ++++++++-------- .../ia/hhs/cca/copay/ia_cca_exit_fee_level.py | 13 ++-- .../ia/hhs/cca/copay/ia_cca_in_exit_tier.py | 4 ++ .../hhs/cca/copay/ia_cca_sliding_fee_level.py | 14 +++-- 8 files changed, 120 insertions(+), 99 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_copay.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_copay.yaml index da92952921f..fd0e2dcb2ba 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_copay.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_copay.yaml @@ -42,7 +42,7 @@ # Family of 2; income 1_500/mo <= Level A threshold (1_619 for size 2) -> fee 0 ia_cca_copay: 0 -- name: Sliding fee at Level B with one child in care. +- name: Income between Level A and B floors stays at Level A with a zero one-child fee. period: 2025-01 absolute_error_margin: 0.01 input: @@ -69,12 +69,11 @@ members: [parent, coparent, child] state_code: IA output: - # Family of 3; income 2_100/mo: > A (2_044), <= B (2_152) -> Level B (index 1). - # 1 child in care -> column 1 -> unit fee 0.20. - # focal_units = monthly_units(40) = 34.6667; copay = 0.20 * 34.6667 = 6.9333 - ia_cca_copay: 6.93 + # Family of 3; income 2_100/mo: >= A floor (2_044), < B floor (2_152) -> Level A + # (index 0). 1 child in care -> column 1 -> Level A unit fee 0.00 -> copay 0. + ia_cca_copay: 0 -- name: Sliding fee at Level B with two children in care uses the two-child column. +- name: Income between Level A and B floors gives a zero two-child fee at Level A. period: 2025-01 absolute_error_margin: 0.01 input: @@ -104,12 +103,11 @@ members: [parent, child1, child2] state_code: IA output: - # Family of 3; income 2_100/mo -> Level B. 2 children in care -> column 2 -> 0.45. - # focal_units = monthly_units(40) = 34.6667; copay = 0.45 * 34.6667 = 15.60. - # Fee is still assessed on only one child (the most units), via focal_units. - ia_cca_copay: 15.60 + # Family of 3; income 2_100/mo -> Level A (index 0). 2 children in care -> + # column 2 -> Level A unit fee 0.00 -> copay 0. + ia_cca_copay: 0 -- name: Sliding fee at Level C with three children in care uses the three-or-more column. +- name: Sliding fee at Level B with three children in care uses the three-or-more column. period: 2025-01 absolute_error_margin: 0.01 input: @@ -144,17 +142,19 @@ members: [parent, child1, child2, child3] state_code: IA output: - # Family of 4; income 2_650/mo: > B (2_600), <= C (2_673) -> Level C (index 2). - # 3 children in care -> column 3 (three or more) -> 0.95. - # focal_units = monthly_units(40) = 34.6667; copay = 0.95 * 34.6667 = 32.9333 - ia_cca_copay: 32.93 + # Family of 4; income 2_650/mo: >= B floor (2_600), < C floor (2_673) -> Level B + # (index 1). 3 children in care -> column 3 (three or more) -> 0.70. + # focal_units = monthly_units(40) = 34.6667; copay = 0.70 * 34.6667 = 24.2667 + ia_cca_copay: 24.27 # ===== Mechanism B: CCA Exit percentage of cost of care ===== -# Note: CCA Exit Level A (33%) is unreachable in practice — entering the exit tier -# requires income above 225% FPL, which always exceeds the exit Level A income -# threshold, so the lowest reachable exit level is B (45%). +# Each printed exit threshold is the inclusive floor of its level, so a family whose +# income lands between the Level A and Level B floors pays the Level A (33%) exit +# fee. Entering the exit tier requires income above 225% FPL; for a family of 3 that +# 225% FPL line (4_996.88/mo) sits between the exit Level A floor (4_842) and the +# Level B floor (5_057), so Level A IS reachable just above the dispatch boundary. -- name: CCA Exit fee at Level B is 45 percent of the cost of care. +- name: CCA Exit fee at Level A is 33 percent of the cost of care. period: 2025-01 absolute_error_margin: 0.01 input: @@ -183,11 +183,11 @@ state_code: IA output: # Family of 3; income 5_000/mo > 225% FPL (4_996.88) -> exit tier. - # Exit basic: > A (4_842), <= B (5_057) -> Level B -> 45%. - # Expense 12_000/yr -> 1_000/mo; copay = 0.45 * 1_000 = 450 - ia_cca_copay: 450.00 + # Exit basic: >= A floor (4_842), < B floor (5_057) -> Level A -> 33%. + # Expense 12_000/yr -> 1_000/mo; copay = 0.33 * 1_000 = 330 + ia_cca_copay: 330.00 -- name: CCA Exit fee at Level C is 60 percent across all children in care. +- name: CCA Exit fee at Level B is 45 percent across all children in care. period: 2025-01 absolute_error_margin: 0.01 input: @@ -220,10 +220,10 @@ state_code: IA output: # Family of 3; income 5_100/mo > 225% FPL (4_996.88) -> exit tier. - # Exit basic: > A (4_842), > B (5_057), <= C (5_272) -> Level C -> 60%. + # Exit basic: >= B floor (5_057), < C floor (5_272) -> Level B -> 45%. # The exit fee applies to EACH child in care, summed. Expenses 12_000/yr and - # 9_600/yr -> 1_000/mo and 800/mo; copay = 0.60 * (1_000 + 800) = 1_080 - ia_cca_copay: 1_080.00 + # 9_600/yr -> 1_000/mo and 800/mo; copay = 0.45 * (1_000 + 800) = 810 + ia_cca_copay: 810.00 - name: CCA Exit special-needs fee uses the special-needs income thresholds. period: 2025-01 @@ -256,9 +256,11 @@ output: # Family of 3 special-needs; income 5_400/mo > 225% FPL (4_996.88) -> exit tier. # Exit special-needs thresholds (family 3): A 4_842, B 5_272, C 5_703. - # 5_400: > A, > B (5_272), <= C (5_703) -> Level C -> 60%. - # Expense 12_000/yr -> 1_000/mo; copay = 0.60 * 1_000 = 600 - ia_cca_copay: 600.00 + # 5_400: >= B floor (5_272), < C floor (5_703) -> Level B -> 45%. + # Under the basic thresholds the same income would be Level C (>= C floor 5_272); + # the SN table holds it at Level B -> proves SN routing. + # Expense 12_000/yr -> 1_000/mo; copay = 0.45 * 1_000 = 450 + ia_cca_copay: 450.00 # ===== Copay exemption for the no-income-test population ===== diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_exit_fee_level.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_exit_fee_level.yaml index 3f4616d29d7..0b321fb3aff 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_exit_fee_level.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_exit_fee_level.yaml @@ -1,7 +1,9 @@ # ia_cca_exit_fee_level: SPMUnit, int index 0-3, MONTH. # Selects the CCA Exit fee level (A=0 .. D=3) from monthly income and family size, # using the basic thresholds for a basic-care family and the special-needs -# thresholds when the family includes a special-needs child. +# thresholds when the family includes a special-needs child. Each printed threshold +# is the inclusive floor of its level: the index is the highest level whose floor is +# at or below income, i.e. max(thresholds met or exceeded - 1, 0). # Family-of-3 exit basic thresholds: A 4_842, B 5_057, C 5_272, D 5_380. # Family-of-3 exit special-needs thresholds: A 4_842, B 5_272, C 5_703, D 5_918. @@ -29,7 +31,7 @@ # (This income alone is not in the exit tier; the index is still defined.) ia_cca_exit_fee_level: 0 -- name: Exit income between A and B is Level B index 1. +- name: Exit income between A and B is Level A index 0. period: 2025-01 absolute_error_margin: 0.1 input: @@ -49,10 +51,10 @@ members: [parent, coparent, child] state_code: IA output: - # Income 5_000/mo: > A (4_842), <= B (5_057) -> index 1 - ia_cca_exit_fee_level: 1 + # Income 5_000/mo: >= A floor (4_842), < B floor (5_057) -> Level A index 0 + ia_cca_exit_fee_level: 0 -- name: Exit income between C and D is Level D index 3. +- name: Exit income between C and D is Level C index 2. period: 2025-01 absolute_error_margin: 0.1 input: @@ -72,8 +74,8 @@ members: [parent, coparent, child] state_code: IA output: - # Income 5_350/mo: > A, > B, > C (5_272), <= D (5_380) -> index 3 - ia_cca_exit_fee_level: 3 + # Income 5_350/mo: >= C floor (5_272), < D floor (5_380) -> Level C index 2 + ia_cca_exit_fee_level: 2 - name: Special-needs family uses the higher special-needs thresholds. period: 2025-01 @@ -97,10 +99,11 @@ state_code: IA output: # Income 5_300/mo with a special-needs child uses SN thresholds: - # > A (4_842), <= B (5_272)? 5_300 > 5_272 -> > B, <= C (5_703) -> index 2. - # Under the basic thresholds the same income would be index 3 (> D 5_380? no, - # > C 5_272 -> index 3). The SN table keeps it at index 2 -> proves SN routing. - ia_cca_exit_fee_level: 2 + # >= B floor (5_272), < C floor (5_703) -> Level B index 1. + # Under the basic thresholds the same income would be Level C index 2 + # (>= C floor 5_272, < D floor 5_380). The SN table holds it at index 1 + # -> proves SN routing. + ia_cca_exit_fee_level: 1 # === Edge cases === # The exit-threshold tables also run by family size 1-13 with the formula clipping @@ -135,8 +138,9 @@ members: [parent, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12] state_code: IA output: - # Family of 13 basic; income 12_000/mo: > A (11_329), <= B (15_592) -> index 1 - ia_cca_exit_fee_level: 1 + # Family of 13 basic; income 12_000/mo: >= A floor (11_329), < B floor (15_592) + # -> Level A index 0 + ia_cca_exit_fee_level: 0 - name: Exit fee family of fourteen uses the size-13 column at the same level. period: 2025-01 @@ -167,5 +171,5 @@ members: [parent, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13] state_code: IA output: - # Family of 14 clips to size 13 -> same thresholds, same income -> index 1 - ia_cca_exit_fee_level: 1 + # Family of 14 clips to size 13 -> same thresholds, same income -> Level A index 0 + ia_cca_exit_fee_level: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_sliding_fee_level.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_sliding_fee_level.yaml index 46ea3a20645..6a62903f741 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_sliding_fee_level.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_sliding_fee_level.yaml @@ -1,7 +1,8 @@ # ia_cca_sliding_fee_level: SPMUnit, int index 0-27, MONTH. -# The family is at the first level whose monthly-income threshold (for its family -# size) is at or above its income; equivalently the index is the number of level -# thresholds the monthly income exceeds, capped at 27 (Level BB). +# Per the fee chart's lookup rule, each printed threshold is the inclusive floor of +# its own level: the family is at the highest level whose floor is at or below its +# monthly income. The index is max(number of thresholds met or exceeded - 1, 0), +# capped at 27 (Level BB). # Income is monthly ia_cca_countable_income = employment_income / 12. # Family-of-3 monthly thresholds: A 2_044, B 2_152, C 2_212; AA 4_254, BB 6_000. @@ -28,7 +29,7 @@ # Family of 3; income 2_000/mo <= A (2_044) -> index 0 ia_cca_sliding_fee_level: 0 -- name: Income between Level A and B thresholds is Level B index 1. +- name: Income between Level A and B thresholds stays at Level A index 0. period: 2025-01 absolute_error_margin: 0.1 input: @@ -48,10 +49,10 @@ members: [parent, coparent, child] state_code: IA output: - # Income 2_100/mo: > A (2_044), <= B (2_152) -> index 1 - ia_cca_sliding_fee_level: 1 + # Income 2_100/mo: >= A floor (2_044), < B floor (2_152) -> Level A index 0 + ia_cca_sliding_fee_level: 0 -- name: Income between Level B and C thresholds is Level C index 2. +- name: Income between Level B and C thresholds is Level B index 1. period: 2025-01 absolute_error_margin: 0.1 input: @@ -71,10 +72,10 @@ members: [parent, coparent, child] state_code: IA output: - # Income 2_200/mo: > B (2_152), <= C (2_212) -> index 2 - ia_cca_sliding_fee_level: 2 + # Income 2_200/mo: >= B floor (2_152), < C floor (2_212) -> Level B index 1 + ia_cca_sliding_fee_level: 1 -- name: Very high income within the CCA Plus range tops out at Level BB index 27. +- name: High income within the CCA Plus range lands at Level AA index 26. period: 2025-01 absolute_error_margin: 0.1 input: @@ -94,8 +95,8 @@ members: [parent, coparent, child] state_code: IA output: - # Income 4_800/mo: > AA (4_254), <= BB (6_000) -> top index 27 (Level BB) - ia_cca_sliding_fee_level: 27 + # Income 4_800/mo: >= AA floor (4_254), < BB floor (6_000) -> Level AA index 26 + ia_cca_sliding_fee_level: 26 # === Edge cases === # The income-threshold table runs by family size 1-13 (13 means "13 or more"). The @@ -149,8 +150,9 @@ members: [parent, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12] state_code: IA output: - # Family of 13; income 6_400/mo: > A (6_303), <= B (6_635 size 13) -> index 1 - ia_cca_sliding_fee_level: 1 + # Family of 13; income 6_400/mo: >= A floor (6_303), < B floor (6_635 size 13) + # -> Level A index 0 + ia_cca_sliding_fee_level: 0 - name: Family of fourteen uses the size-13 column at the same level. period: 2025-01 @@ -181,6 +183,6 @@ members: [parent, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13] state_code: IA output: - # Family of 14 clips to size 13 -> same thresholds, same income -> index 1 + # Family of 14 clips to size 13 -> same thresholds, same income -> Level A index 0 # (confirms the clip(size, 1, 13) ceiling on the lookup table) - ia_cca_sliding_fee_level: 1 + ia_cca_sliding_fee_level: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca.yaml index 99e6e7c8d28..35e90911350 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca.yaml @@ -93,12 +93,12 @@ state_code: IA output: # Family of 3 enrolled; income 5_200/mo > 225% FPL (4_996.88) -> CCA Exit tier, - # Exit basic Level C -> 60% percentage-of-cost copay. + # Exit basic Level B -> 45% percentage-of-cost copay. ia_cca_in_exit_tier: true - # Expense 24_000/yr -> 2_000/mo. Exit copay = 60% * 2_000 = 1_200. - ia_cca_copay: 1_200.00 + # Expense 24_000/yr -> 2_000/mo. Exit copay = 45% * 2_000 = 900. + ia_cca_copay: 900.00 # Preschool ceiling = 19.50 * 34.6667 = 676.00; child_capped = min(2_000, 676) = 676. - # Benefit = max(676.00 - 1_200.00, 0) = 0 (copay exceeds ceiling -> floored at 0) + # Benefit = max(676.00 - 900.00, 0) = 0 (copay exceeds ceiling -> floored at 0) ia_cca: 0 - name: Negative countable income does not inflate the benefit beyond the ceiling. diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/integration.yaml index 082bd7a1055..6a59f84cf7f 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/integration.yaml @@ -75,8 +75,8 @@ # Benefit = (889.55 + 676.00) - 0 = 1_565.55 ia_cca: 1_565.55 -# === Case 2: CCA tier (applicant), sliding unit fee at Level B, two children === -- name: Case 2, single parent two children licensed center, CCA Level B copay. +# === Case 2: CCA tier (applicant), income between Level A and B floors -> Level A === +- name: Case 2, single parent two children licensed center, CCA Level A no copay. period: 2025-01 absolute_error_margin: 0.01 input: @@ -117,13 +117,12 @@ state_code: IA output: ia_cca_eligible: true - # Income 2_100/mo: > Level A (2_044), <= Level B (2_152) -> Level B. - # 2 children in care -> column 2 -> unit fee 0.45. - # focal_units = monthly_units(40) = 34.6667; copay = 0.45 * 34.6667 = 15.60. - ia_cca_copay: 15.60 + # Income 2_100/mo: >= Level A floor (2_044), < Level B floor (2_152) -> Level A. + # 2 children in care -> column 2 -> Level A unit fee 0.00 -> copay 0. + ia_cca_copay: 0 # Same ceilings as Case 1: total capped 889.55 + 676.00 = 1_565.55. - # Benefit = 1_565.55 - 15.60 = 1_549.95 - ia_cca: 1_549.95 + # Benefit = 1_565.55 - 0 = 1_565.55 + ia_cca: 1_565.55 # === Case 3: CCA Plus tier (enrolled, 160-225% FPL), sliding fee Level BB === - name: Case 3, enrolled family one preschooler licensed center, CCA Plus. @@ -168,17 +167,17 @@ ia_cca_income_eligible: true # 4_800/mo <= 225% FPL (4_996.88) -> NOT exit tier -> sliding unit fee. ia_cca_in_exit_tier: false - # Sliding level: 4_800/mo > AA (4_254), <= BB (6_000) -> Level BB. - # 1 child in care -> column 1 -> unit fee 6.70. - # focal_units = monthly_units(30) = 26.0; copay = 6.70 * 26.0 = 174.20. - ia_cca_copay: 174.20 + # Sliding level: 4_800/mo >= AA floor (4_254), < BB floor (6_000) -> Level AA. + # 1 child in care -> column 1 -> unit fee 6.45. + # focal_units = monthly_units(30) = 26.0; copay = 6.45 * 26.0 = 167.70. + ia_cca_copay: 167.70 # Preschool ceiling = 19.50 * 26.0 = 507.00; expense 12_000/yr -> 1_000/mo, # capped to the 507.00 ceiling. - # Benefit = max(507.00 - 174.20, 0) = 332.80 - ia_cca: 332.80 + # Benefit = max(507.00 - 167.70, 0) = 339.30 + ia_cca: 339.30 # === Case 4: CCA Exit tier (enrolled, >225% FPL), percentage-of-cost copay === -- name: Case 4, enrolled family one preschooler licensed center, CCA Exit 60 percent. +- name: Case 4, enrolled family one preschooler licensed center, CCA Exit 45 percent. period: 2025-01 absolute_error_margin: 0.01 input: @@ -220,13 +219,13 @@ ia_cca_income_eligible: true # 5_200/mo > 225% FPL (4_996.88) and enrolled -> CCA Exit tier. ia_cca_in_exit_tier: true - # Exit basic level: > A (4_842), > B (5_057), <= C (5_272) -> Level C -> 60%. - # Expense 12_000/yr -> 1_000/mo. Exit copay = 60% of the child's monthly cost - # of care = 0.60 * 1_000 = 600. - ia_cca_copay: 600.00 + # Exit basic level: >= B floor (5_057), < C floor (5_272) -> Level B -> 45%. + # Expense 12_000/yr -> 1_000/mo. Exit copay = 45% of the child's monthly cost + # of care = 0.45 * 1_000 = 450. + ia_cca_copay: 450.00 # Preschool ceiling = 19.50 * 34.6667 = 676.00; monthly expense 1_000 capped to - # 676.00. Benefit = max(676.00 - 600.00, 0) = 76.00 - ia_cca: 76.00 + # 676.00. Benefit = max(676.00 - 450.00, 0) = 226.00 + ia_cca: 226.00 # === Case 5: Special-needs branch (applicant, 200% FPL, SN rate) === - name: Case 5, single parent disabled preschooler, special-needs rate and limit. @@ -272,12 +271,13 @@ ia_cca_eligible: true # Licensed center NO_RATING PRESCHOOL_SN rate = 30.43. ia_cca_max_rate: [0, 30.43] - # Sliding level (family 2): income 3_000/mo -> Level W; 1 child -> column 1 -> 5.45. - # focal_units = monthly_units(40) = 34.6667; copay = 5.45 * 34.6667 = 188.93. - ia_cca_copay: 188.93 + # Sliding level (family 2): income 3_000/mo -> Level V (>= V floor 2_938, + # < W floor 3_021); 1 child -> column 1 -> 5.20. + # focal_units = monthly_units(40) = 34.6667; copay = 5.20 * 34.6667 = 180.27. + ia_cca_copay: 180.27 # Ceiling = 30.43 * 34.6667 = 1_054.91; expense 12_000/yr -> 1_000/mo < ceiling - # -> capped to 1_000. Benefit = max(1_000 - 188.93, 0) = 811.07 - ia_cca: 811.07 + # -> capped to 1_000. Benefit = max(1_000 - 180.27, 0) = 819.73 + ia_cca: 819.73 # === Case 6: In-Home provider, three children in care, minimum-wage rate === - name: Case 6, single parent three children in-home minimum wage rate. diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_exit_fee_level.py b/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_exit_fee_level.py index fb59f061b59..bc991e4f397 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_exit_fee_level.py +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_exit_fee_level.py @@ -20,12 +20,17 @@ def formula(spm_unit, period, parameters): # CCA Exit uses a separate income threshold table for basic care and # special-needs care. has_special_needs_child = spm_unit("ia_cca_has_special_needs_child", period) - # A family is at the first level whose monthly-income threshold for - # its family size is at or above its income. - level_index = 0 + # Per the CCA Exit fee chart's lookup rule (#page=2): move down the + # family-size column to the "first row with an amount greater than the + # monthly family income" and "use the row above" to set the fee. Each + # printed threshold is the inclusive floor of its own level, so the level + # index is the highest level whose floor is at-or-below income: + # (number of thresholds the income meets or exceeds) - 1. + levels_met = 0 for level in EXIT_FEE_LEVELS: basic_threshold = p.income_thresholds_basic[level][capped_size] sn_threshold = p.income_thresholds_special_needs[level][capped_size] threshold = where(has_special_needs_child, sn_threshold, basic_threshold) - level_index = level_index + (income > threshold) + levels_met = levels_met + (income >= threshold) + level_index = max_(levels_met - 1, 0) return min_(level_index, len(EXIT_FEE_LEVELS) - 1) diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_in_exit_tier.py b/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_in_exit_tier.py index a21b16b1903..c0ec37911b7 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_in_exit_tier.py +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_in_exit_tier.py @@ -14,6 +14,10 @@ def formula(spm_unit, period, parameters): # CCA Exit applies to enrolled families whose income exceeds the # ongoing CCA Plus limit (225% FPL); these families pay the # percentage-of-cost CCA Exit fee instead of the sliding unit fee. + # This dispatch boundary omits the a(2) "min(225% FPL, 85% MFI)" cap; + # the 85% MFI ceiling only binds for very large families and is + # immaterial here (see ia_cca_income_eligible for the documented + # omission). enrolled = spm_unit("ia_cca_enrolled", period) income = spm_unit("ia_cca_countable_income", period) fpg = spm_unit("spm_unit_fpg", period) diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_sliding_fee_level.py b/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_sliding_fee_level.py index f453120b08e..3d270d94900 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_sliding_fee_level.py +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_sliding_fee_level.py @@ -50,13 +50,17 @@ def formula(spm_unit, period, parameters): # 13 or more). size = spm_unit("spm_unit_size", period.this_year) capped_size = clip(size, 1, 13).astype(int) - # A family is at the first level whose monthly-income threshold for - # its family size is at or above its income. Equivalently, the level - # index is the number of level thresholds the income exceeds. - level_index = 0 + # Per the fee chart's lookup rule (#page=1): move down the family-size + # column to the "first row with an amount greater than the monthly + # family income" and "use the row above" to set the fee. Each printed + # threshold is therefore the inclusive floor of its own level, so the + # level index is the highest level whose floor is at-or-below income: + # (number of thresholds the income meets or exceeds) - 1. + levels_met = 0 for level in SLIDING_FEE_LEVELS: threshold = thresholds[level][capped_size] - level_index = level_index + (income > threshold) + levels_met = levels_met + (income >= threshold) + level_index = max_(levels_met - 1, 0) # Cap at the last level (index 27); incomes above the BB threshold # exceed the CCA Plus income limit and are screened out upstream. return min_(level_index, len(SLIDING_FEE_LEVELS) - 1) From 40e83a805990a27588ba403c4469813efe82168f Mon Sep 17 00:00:00 2001 From: Ziming Date: Tue, 9 Jun 2026 17:19:18 -0400 Subject: [PATCH 6/7] Review-fix round 3: countable income, per-child exit fee, fee-chart scan - Count SSI in countable income (excluded only in FIP-linked cases per IAC 441-170.2(1)"d"(25)-(26), which route through the income-exception path); drop tax_exempt_interest_income (already inside the interest_income umbrella) - Floor each person's net self-employment subtotal at zero per 170.2(1)"c"(1) (new self_employment_sources.yaml split) - Restrict the minor-earnings exclusions to non-head/spouse persons so a teen parent's own wages stay counted - Make ia_cca_exit_fee_level Person-level: the Basic vs Special Needs exit table is selected per child per the fee chart, and ia_cca_copay sums per-child percentages - Replace the count-based fee-level lookup with the chart's literal first-row-greater scan (the BB row sits below AA for family sizes 9-13, so the counting shortcut landed those sizes one level too high) - Fix need-for-service comment citations (170.2(2)"b"(3)/(9)/(6)-(7)), inverted threshold descriptions, and two page anchors - Tests: +15 cases (exit Levels C/D, mixed Basic/SN family, sliding-fee BB-window regressions, SSI/SE-loss/interest/teen-head countable income); boundary-test names corrected to actual offsets Co-Authored-By: Claude Fable 5 --- .../copay/exit/income_thresholds_basic.yaml | 2 +- .../exit/income_thresholds_special_needs.yaml | 2 +- .../copay/sliding_fee/income_thresholds.yaml | 2 +- .../countable_income/earned_sources.yaml | 9 +- .../self_employment_sources.yaml | 14 ++ .../countable_income/unearned_sources.yaml | 16 +- .../ia/hhs/cca/income/minor_student_age.yaml | 4 +- .../states/ia/hhs/cca/copay/ia_cca_copay.yaml | 160 +++++++++++++++--- .../hhs/cca/copay/ia_cca_exit_fee_level.yaml | 134 ++++++++++++--- .../ia/hhs/cca/copay/ia_cca_in_exit_tier.yaml | 12 +- .../cca/copay/ia_cca_sliding_fee_level.yaml | 133 ++++++++++++++- .../hhs/cca/eligibility/ia_cca_eligible.yaml | 11 +- .../eligibility/ia_cca_income_eligible.yaml | 19 ++- .../gov/states/ia/hhs/cca/ia_cca.yaml | 16 +- .../ia/hhs/cca/ia_cca_countable_income.yaml | 129 +++++++++++++- .../ia/hhs/cca/ia_child_care_subsidies.yaml | 4 +- .../gov/states/ia/hhs/cca/integration.yaml | 4 +- .../states/ia/hhs/cca/copay/ia_cca_copay.py | 8 +- .../ia/hhs/cca/copay/ia_cca_exit_fee_level.py | 40 +++-- .../hhs/cca/copay/ia_cca_sliding_fee_level.py | 26 +-- .../ia/hhs/cca/eligibility/ia_cca_eligible.py | 11 +- .../ia_cca_has_special_needs_child.py | 2 +- .../ia/hhs/cca/ia_cca_countable_income.py | 21 ++- 23 files changed, 637 insertions(+), 142 deletions(-) create mode 100644 policyengine_us/parameters/gov/states/ia/hhs/cca/income/countable_income/self_employment_sources.yaml diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/copay/exit/income_thresholds_basic.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/copay/exit/income_thresholds_basic.yaml index 356af035c2e..df1d3defbad 100644 --- a/policyengine_us/parameters/gov/states/ia/hhs/cca/copay/exit/income_thresholds_basic.yaml +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/copay/exit/income_thresholds_basic.yaml @@ -1,4 +1,4 @@ -description: Iowa assigns a CCA Exit family using basic care to a fee level (A through D) under the Child Care Assistance program based on monthly income and family size. A family is at the first level whose monthly-income threshold for its family size is at or above the family's monthly income. +description: Iowa assigns each child receiving basic care in a CCA Exit family to a fee level (A through D) under the Child Care Assistance program based on the family's monthly income and family size, by moving down the family-size column to the first row with an amount greater than the family's monthly income and using the row above; each printed threshold is the inclusive floor of its own level. metadata: period: month unit: currency-USD diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/copay/exit/income_thresholds_special_needs.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/copay/exit/income_thresholds_special_needs.yaml index ed2ae9dd756..0356bc6c08d 100644 --- a/policyengine_us/parameters/gov/states/ia/hhs/cca/copay/exit/income_thresholds_special_needs.yaml +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/copay/exit/income_thresholds_special_needs.yaml @@ -1,4 +1,4 @@ -description: Iowa assigns a CCA Exit family using special-needs care to a fee level (A through D) under the Child Care Assistance program based on monthly income and family size. A family is at the first level whose monthly-income threshold for its family size is at or above the family's monthly income. +description: Iowa assigns each child receiving special-needs care in a CCA Exit family to a fee level (A through D) under the Child Care Assistance program based on the family's monthly income and family size, by moving down the family-size column to the first row with an amount greater than the family's monthly income and using the row above; each printed threshold is the inclusive floor of its own level. metadata: period: month unit: currency-USD diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/copay/sliding_fee/income_thresholds.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/copay/sliding_fee/income_thresholds.yaml index 0bcaa3167a3..4b33120ddc5 100644 --- a/policyengine_us/parameters/gov/states/ia/hhs/cca/copay/sliding_fee/income_thresholds.yaml +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/copay/sliding_fee/income_thresholds.yaml @@ -1,4 +1,4 @@ -description: Iowa assigns a CCA and CCA Plus family to a sliding-fee level (A through BB) under the Child Care Assistance program based on monthly income and family size. A family is at the first level whose monthly-income threshold for its family size is at or above the family's monthly income. +description: Iowa assigns a CCA and CCA Plus family to a sliding-fee level (A through BB) under the Child Care Assistance program based on monthly income and family size, by moving down the family-size column to the first row with an amount greater than the family's monthly income and using the row above. metadata: period: month unit: currency-USD diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/income/countable_income/earned_sources.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/countable_income/earned_sources.yaml index 2c1b6ab215c..beb3f2bbe61 100644 --- a/policyengine_us/parameters/gov/states/ia/hhs/cca/income/countable_income/earned_sources.yaml +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/countable_income/earned_sources.yaml @@ -1,17 +1,12 @@ -description: Iowa counts these earned income sources under the Child Care Assistance program. Iowa excludes the earnings of a child age 14 or younger and the earnings of a child 18 or younger who is a full-time student, so these sources are summed only for persons who are not excluded minors. +description: Iowa counts these wage and salary income sources under the Child Care Assistance program. Iowa excludes the earnings of a child age 14 or younger and the earnings of a child 18 or younger who is a full-time student, so these sources are summed only for persons who are not excluded minors; self-employment sources are listed separately because a net self-employment loss cannot offset other income. values: 2024-07-01: - employment_income - - self_employment_income - - sstb_self_employment_income - - farm_operations_income metadata: unit: list period: year label: Iowa CCA countable earned income sources reference: - - title: IAC 441-170.2(1)"c" — Income counted (wages, self-employment net profit) - href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=4 - - title: IAC 441-170.2(1)"d" — Income excluded (child age 14 or under; student 18 or under) + - title: IAC 441-170.2(1)"c" — Income counted (wages); "d"(13) child-14 and "d"(35) student-18 earnings exclusions (pp. 4-5) href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=4 diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/income/countable_income/self_employment_sources.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/countable_income/self_employment_sources.yaml new file mode 100644 index 00000000000..39b3c886228 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/countable_income/self_employment_sources.yaml @@ -0,0 +1,14 @@ +description: Iowa counts the net profit from these self-employment income sources under the Child Care Assistance program; a net self-employment loss cannot offset other earned or unearned income, so the formula floors their per-person total at zero. +values: + 2024-07-01: + - self_employment_income + - sstb_self_employment_income + - farm_operations_income + +metadata: + unit: list + period: year + label: Iowa CCA countable self-employment income sources + reference: + - title: IAC 441-170.2(1)"c"(1) — Net profit from self-employment; a net loss cannot be offset from other earned or unearned income + href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=4 diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/income/countable_income/unearned_sources.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/countable_income/unearned_sources.yaml index 0b46a01b18c..7e24009bf25 100644 --- a/policyengine_us/parameters/gov/states/ia/hhs/cca/income/countable_income/unearned_sources.yaml +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/countable_income/unearned_sources.yaml @@ -2,10 +2,10 @@ description: Iowa counts these unearned income sources under the Child Care Assi values: 2024-07-01: - social_security # includes retirement, survivors, and disability + - ssi # counted; excluded only in FIP-linked cases (see metadata note) - pension_income # pensions and annuities - dividend_income - - interest_income - - tax_exempt_interest_income + - interest_income # includes taxable and tax-exempt interest - estate_income # estate and trust income - rental_income # net rental and royalty income - unemployment_compensation @@ -23,11 +23,13 @@ metadata: label: Iowa CCA countable unearned income sources # Iowa counts public assistance generally, but the model omits TANF/FIP cash # dollars here to avoid the CCAP-to-TANF circular dependency (FIP recipients - # qualify through the income-exception path instead). Iowa excludes SSI except - # in specified FIP-linked cases; we don't track that linkage at the moment, so - # SSI is conservatively left out of countable income. AmeriCorps living - # allowance, permanent disability insurance, and royalty income have no - # separate PolicyEngine variable at the moment and are therefore not counted. + # qualify through the income-exception path instead). SSI is counted: Iowa + # excludes it only in specified FIP-linked cases (IAC 441-170.2(1)"d"(25)-(26)), + # and families in those cases qualify through the income-exception path where + # countable income is moot, so counting SSI for everyone else matches the rule. + # AmeriCorps living allowance, permanent disability insurance, and royalty + # income have no separate PolicyEngine variable at the moment and are + # therefore not counted. reference: - title: IAC 441-170.2(1)"c" — Income counted (Census median-income sources) href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=4 diff --git a/policyengine_us/parameters/gov/states/ia/hhs/cca/income/minor_student_age.yaml b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/minor_student_age.yaml index 6e624d8c25e..92afd0a750f 100644 --- a/policyengine_us/parameters/gov/states/ia/hhs/cca/income/minor_student_age.yaml +++ b/policyengine_us/parameters/gov/states/ia/hhs/cca/income/minor_student_age.yaml @@ -1,4 +1,4 @@ -description: Iowa excludes the earnings of a full-time student this age or younger from countable income under the Child Care Assistance program. +description: Iowa excludes the earnings of a child this age or younger who is a full-time student from countable income under the Child Care Assistance program. values: 2024-07-01: 18 @@ -7,5 +7,5 @@ metadata: period: year label: Iowa CCA excluded-earnings student age reference: - - title: IAC 441-170.2(1)"d" — Income excluded (earnings of a student 18 or younger) + - title: IAC 441-170.2(1)"d"(35) — Income excluded (earnings of a child 18 or younger who is a full-time student) href: https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=5 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_copay.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_copay.yaml index fd0e2dcb2ba..3b227fbac56 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_copay.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_copay.yaml @@ -3,7 +3,10 @@ # A. CCA / CCA Plus sliding unit fee = unit_fee[level][children_col] * focal_units, # where focal_units = the most monthly units of any in-care eligible child # and children_col = clip(children in care, 1, 3). -# B. CCA Exit = exit_pct[level] * sum of childcare expenses for children in care. +# B. CCA Exit = sum over children in care of exit_pct[child's level] * the +# child's childcare expense. The exit fee is per child, not family-wide: +# ia_cca_exit_fee_level is Person-level, and each child's level uses the +# basic or special-needs exit table per the child's own is_disabled flag. # Families eligible without regard to income (FIP/protective/foster) pay no fee. # monthly_units(hpw) = hpw * (52 / 12) / 5; monthly_units(40 hrs/wk) = 34.6667. # 2025 FPG: family of 3 = 26_650 (225% monthly = 4_996.88); family of 4 = 32_150. @@ -148,11 +151,16 @@ ia_cca_copay: 24.27 # ===== Mechanism B: CCA Exit percentage of cost of care ===== -# Each printed exit threshold is the inclusive floor of its level, so a family whose -# income lands between the Level A and Level B floors pays the Level A (33%) exit -# fee. Entering the exit tier requires income above 225% FPL; for a family of 3 that -# 225% FPL line (4_996.88/mo) sits between the exit Level A floor (4_842) and the -# Level B floor (5_057), so Level A IS reachable just above the dispatch boundary. +# The exit fee is charged per child: each in-care child's own fee level sets the +# percentage applied to that child's cost of care, and the per-child fees are +# summed. Each printed exit threshold is the inclusive floor of its level, so a +# child whose family income lands between the Level A and Level B floors pays the +# Level A (33%) exit fee. Entering the exit tier requires income above 225% FPL; +# for a family of 3 that 225% FPL line (4_996.88/mo) sits between the exit Level A +# floor (4_842) and the Level B floor (5_057), so Level A IS reachable just above +# the dispatch boundary. +# Family-of-3 exit basic floors: A 4_842, B 5_057, C 5_272, D 5_380. +# Exit fee percentages: A 33%, B 45%, C 60%, D 60%. - name: CCA Exit fee at Level A is 33 percent of the cost of care. period: 2025-01 @@ -183,9 +191,10 @@ state_code: IA output: # Family of 3; income 5_000/mo > 225% FPL (4_996.88) -> exit tier. - # Exit basic: >= A floor (4_842), < B floor (5_057) -> Level A -> 33%. + # The in-care child is basic: >= A floor (4_842), < B floor (5_057) + # -> Level A -> 33%. # Expense 12_000/yr -> 1_000/mo; copay = 0.33 * 1_000 = 330 - ia_cca_copay: 330.00 + ia_cca_copay: 330 - name: CCA Exit fee at Level B is 45 percent across all children in care. period: 2025-01 @@ -220,12 +229,13 @@ state_code: IA output: # Family of 3; income 5_100/mo > 225% FPL (4_996.88) -> exit tier. - # Exit basic: >= B floor (5_057), < C floor (5_272) -> Level B -> 45%. - # The exit fee applies to EACH child in care, summed. Expenses 12_000/yr and - # 9_600/yr -> 1_000/mo and 800/mo; copay = 0.45 * (1_000 + 800) = 810 - ia_cca_copay: 810.00 + # Both in-care children are basic: >= B floor (5_057), < C floor (5_272) + # -> Level B -> 45% each. The per-child fees are summed: expenses 12_000/yr + # and 9_600/yr -> 1_000/mo and 800/mo; + # copay = 0.45 * 1_000 + 0.45 * 800 = 810 + ia_cca_copay: 810 -- name: CCA Exit special-needs fee uses the special-needs income thresholds. +- name: CCA Exit fee for a special-needs child uses the child's special-needs thresholds. period: 2025-01 absolute_error_margin: 0.01 input: @@ -254,13 +264,125 @@ members: [parent, coparent, child] state_code: IA output: - # Family of 3 special-needs; income 5_400/mo > 225% FPL (4_996.88) -> exit tier. - # Exit special-needs thresholds (family 3): A 4_842, B 5_272, C 5_703. - # 5_400: >= B floor (5_272), < C floor (5_703) -> Level B -> 45%. - # Under the basic thresholds the same income would be Level C (>= C floor 5_272); - # the SN table holds it at Level B -> proves SN routing. + # Family of 3; income 5_400/mo > 225% FPL (4_996.88) -> exit tier. + # The disabled in-care child uses the SN table (family 3: A 4_842, B 5_272, + # C 5_703): >= B floor (5_272), < C floor (5_703) -> Level B -> 45%. + # Under the basic table 5_400 would be Level D (>= D floor 5_380) at 60%; + # the child's own SN routing holds the fee at 45%. The parents land at + # basic Level D, but only in-care children's percentages enter the sum. # Expense 12_000/yr -> 1_000/mo; copay = 0.45 * 1_000 = 450 - ia_cca_copay: 450.00 + ia_cca_copay: 450 + +- name: CCA Exit fee at Level C is 60 percent of the cost of care. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 64_200 + coparent: + age: 30 + child: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 40 + pre_subsidy_childcare_expenses: 12_000 + tax_units: + tax_unit: + members: [parent, coparent, child] + spm_units: + spm_unit: + members: [parent, coparent, child] + ia_cca_enrolled: true + households: + household: + members: [parent, coparent, child] + state_code: IA + output: + # Family of 3; income 5_350/mo > 225% FPL (4_996.88) -> exit tier. + # The in-care child is basic: >= C floor (5_272), < D floor (5_380) + # -> Level C -> 60%. + # Expense 12_000/yr -> 1_000/mo; copay = 0.60 * 1_000 = 600 + ia_cca_copay: 600 + +- name: CCA Exit fee beyond the Level D floor stays at 60 percent. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 66_000 + coparent: + age: 30 + child: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 40 + pre_subsidy_childcare_expenses: 12_000 + tax_units: + tax_unit: + members: [parent, coparent, child] + spm_units: + spm_unit: + members: [parent, coparent, child] + ia_cca_enrolled: true + households: + household: + members: [parent, coparent, child] + state_code: IA + output: + # Family of 3; income 5_500/mo > 225% FPL (4_996.88) -> exit tier. + # The in-care child is basic: 5_500 >= D floor (5_380), so no row exceeds + # income and the last level (D) applies -> 60%, same percentage as Level C. + # Expense 12_000/yr -> 1_000/mo; copay = 0.60 * 1_000 = 600 + ia_cca_copay: 600 + +- name: Mixed basic and special-needs children pay different per-child exit percentages. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 74_400 + coparent: + age: 30 + basic_child: + age: 8 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 40 + pre_subsidy_childcare_expenses: 9_600 + sn_child: + age: 4 + is_disabled: true + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_hours_per_week: 40 + pre_subsidy_childcare_expenses: 9_600 + tax_units: + tax_unit: + members: [parent, coparent, basic_child, sn_child] + spm_units: + spm_unit: + members: [parent, coparent, basic_child, sn_child] + ia_cca_enrolled: true + households: + household: + members: [parent, coparent, basic_child, sn_child] + state_code: IA + output: + # Family of 4; income 6_200/mo > 225% FPL (32_150/yr -> 6_028.13/mo) + # -> exit tier. The basic child uses the family-4 basic table: >= B floor + # (6_110), < C floor (6_370) -> Level B -> 45%. The SN child uses the + # family-4 SN table: >= A floor (5_850), < B floor (6_370) -> Level A + # -> 33%. Expenses 9_600/yr -> 800/mo each; + # copay = 0.45 * 800 + 0.33 * 800 = 624 + ia_cca_copay: 624 # ===== Copay exemption for the no-income-test population ===== diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_exit_fee_level.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_exit_fee_level.yaml index 0b321fb3aff..c32b683208c 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_exit_fee_level.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_exit_fee_level.yaml @@ -1,13 +1,16 @@ -# ia_cca_exit_fee_level: SPMUnit, int index 0-3, MONTH. -# Selects the CCA Exit fee level (A=0 .. D=3) from monthly income and family size, -# using the basic thresholds for a basic-care family and the special-needs -# thresholds when the family includes a special-needs child. Each printed threshold -# is the inclusive floor of its level: the index is the highest level whose floor is -# at or below income, i.e. max(thresholds met or exceeded - 1, 0). +# ia_cca_exit_fee_level: Person, int index 0-3, MONTH. +# Assigns each person a CCA Exit fee level (A=0 .. D=3) from the family's monthly +# income and size, selecting the basic or special-needs threshold table from the +# person's own is_disabled flag (the special-needs-care proxy). Per the fee chart's +# lookup rule, move down the family-size column to the first row with an amount +# greater than the monthly income and use the row above; each printed threshold is +# the inclusive floor of its level. Outputs are per-person lists in people order. # Family-of-3 exit basic thresholds: A 4_842, B 5_057, C 5_272, D 5_380. # Family-of-3 exit special-needs thresholds: A 4_842, B 5_272, C 5_703, D 5_918. +# Family-of-4 exit basic thresholds: A 5_850, B 6_110, C 6_370, D 6_500. +# Family-of-4 exit special-needs thresholds: A 5_850, B 6_370, C 6_890, D 7_150. -- name: Low exit income lands at Level A index 0. +- name: Low exit income lands every person at Level A index 0. period: 2025-01 absolute_error_margin: 0.1 input: @@ -27,11 +30,12 @@ members: [parent, coparent, child] state_code: IA output: - # Family of 3 basic; income 4_800/mo <= A (4_842) -> index 0 + # Family of 3, all basic; income 4_800/mo < A floor (4_842) -> index 0 for + # parent, coparent, and child. # (This income alone is not in the exit tier; the index is still defined.) - ia_cca_exit_fee_level: 0 + ia_cca_exit_fee_level: [0, 0, 0] -- name: Exit income between A and B is Level A index 0. +- name: Exit income between the A and B floors is Level A index 0 for every person. period: 2025-01 absolute_error_margin: 0.1 input: @@ -52,9 +56,10 @@ state_code: IA output: # Income 5_000/mo: >= A floor (4_842), < B floor (5_057) -> Level A index 0 - ia_cca_exit_fee_level: 0 + # for parent, coparent, and child. + ia_cca_exit_fee_level: [0, 0, 0] -- name: Exit income between C and D is Level C index 2. +- name: Exit income between the C and D floors is Level C index 2 for every person. period: 2025-01 absolute_error_margin: 0.1 input: @@ -75,9 +80,59 @@ state_code: IA output: # Income 5_350/mo: >= C floor (5_272), < D floor (5_380) -> Level C index 2 - ia_cca_exit_fee_level: 2 + # for parent, coparent, and child. + ia_cca_exit_fee_level: [2, 2, 2] -- name: Special-needs family uses the higher special-needs thresholds. +- name: Exit income a cent below the B floor stays at Level A. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + employment_income: 60_683.88 + coparent: + age: 30 + child: + age: 4 + spm_units: + spm_unit: + members: [parent, coparent, child] + households: + household: + members: [parent, coparent, child] + state_code: IA + output: + # Income 5_056.99/mo: the B row (5_057) is the first row greater than income, + # so the row above (A) applies -> Level A index 0. + ia_cca_exit_fee_level: [0, 0, 0] + +- name: Exit income a cent above the B floor moves to Level B. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + employment_income: 60_684.12 + coparent: + age: 30 + child: + age: 4 + spm_units: + spm_unit: + members: [parent, coparent, child] + households: + household: + members: [parent, coparent, child] + state_code: IA + output: + # Income 5_057.01/mo: the C row (5_272) is now the first row greater than + # income, so the row above (B) applies -> Level B index 1; the printed B + # threshold is the inclusive floor of its own level. + ia_cca_exit_fee_level: [1, 1, 1] + +- name: Special-needs child uses the higher special-needs thresholds while basic persons do not. period: 2025-01 absolute_error_margin: 0.1 input: @@ -98,12 +153,40 @@ members: [parent, coparent, child] state_code: IA output: - # Income 5_300/mo with a special-needs child uses SN thresholds: - # >= B floor (5_272), < C floor (5_703) -> Level B index 1. - # Under the basic thresholds the same income would be Level C index 2 - # (>= C floor 5_272, < D floor 5_380). The SN table holds it at index 1 - # -> proves SN routing. - ia_cca_exit_fee_level: 1 + # Income 5_300/mo. The non-disabled parent and coparent use the basic table: + # >= C floor (5_272), < D floor (5_380) -> Level C index 2. The disabled + # child uses the SN table: >= B floor (5_272), < C floor (5_703) -> Level B + # index 1. The same income lands one level lower on the SN table -> proves + # per-person SN routing. + ia_cca_exit_fee_level: [2, 2, 1] + +- name: Mixed basic and special-needs children land on different levels at the same income. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + employment_income: 74_400 + coparent: + age: 30 + basic_child: + age: 8 + sn_child: + age: 4 + is_disabled: true + spm_units: + spm_unit: + members: [parent, coparent, basic_child, sn_child] + households: + household: + members: [parent, coparent, basic_child, sn_child] + state_code: IA + output: + # Family of 4; income 6_200/mo. Basic table (parent, coparent, basic_child): + # >= B floor (6_110), < C floor (6_370) -> Level B index 1. SN table + # (sn_child): >= A floor (5_850), < B floor (6_370) -> Level A index 0. + ia_cca_exit_fee_level: [1, 1, 1, 0] # === Edge cases === # The exit-threshold tables also run by family size 1-13 with the formula clipping @@ -138,9 +221,9 @@ members: [parent, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12] state_code: IA output: - # Family of 13 basic; income 12_000/mo: >= A floor (11_329), < B floor (15_592) - # -> Level A index 0 - ia_cca_exit_fee_level: 0 + # Family of 13, all basic; income 12_000/mo: >= A floor (11_329), + # < B floor (15_592) -> Level A index 0 for every person. + ia_cca_exit_fee_level: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - name: Exit fee family of fourteen uses the size-13 column at the same level. period: 2025-01 @@ -171,5 +254,6 @@ members: [parent, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13] state_code: IA output: - # Family of 14 clips to size 13 -> same thresholds, same income -> Level A index 0 - ia_cca_exit_fee_level: 0 + # Family of 14 clips to size 13 -> same thresholds, same income + # -> Level A index 0 for every person. + ia_cca_exit_fee_level: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_in_exit_tier.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_in_exit_tier.yaml index 3616fca1e64..90fa598f64c 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_in_exit_tier.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_in_exit_tier.yaml @@ -78,10 +78,10 @@ # === Edge cases === # The exit-tier dispatch turns on at exactly 225% FPL (family of 3; 225% annual = -# 59_962.50). We bracket the boundary +-12/yr; the exact round-trip is float-fragile -# so we do not assert at it. +# 59_962.50). We bracket the boundary +-12.50/yr; the exact round-trip is +# float-fragile so we do not assert at it. -- name: Enrolled family 12 dollars below 225 percent FPL is not in the exit tier. +- name: Enrolled family 12.50 dollars below 225 percent FPL is not in the exit tier. period: 2025-01 absolute_error_margin: 0.1 input: @@ -102,10 +102,10 @@ members: [parent, coparent, child] state_code: IA output: - # 59_950 (-$1/mo under 59_962.50) -> not over 225% FPL -> CCA Plus, not exit + # 59_950 (-$12.50/yr under 59_962.50) -> not over 225% FPL -> CCA Plus, not exit ia_cca_in_exit_tier: false -- name: Enrolled family 12 dollars above 225 percent FPL is in the exit tier. +- name: Enrolled family 12.50 dollars above 225 percent FPL is in the exit tier. period: 2025-01 absolute_error_margin: 0.1 input: @@ -126,5 +126,5 @@ members: [parent, coparent, child] state_code: IA output: - # 59_975 (+$1/mo over 59_962.50) and enrolled -> exit tier + # 59_975 (+$12.50/yr over 59_962.50) and enrolled -> exit tier ia_cca_in_exit_tier: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_sliding_fee_level.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_sliding_fee_level.yaml index 6a62903f741..9f4c2cc8379 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_sliding_fee_level.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_sliding_fee_level.yaml @@ -1,10 +1,16 @@ # ia_cca_sliding_fee_level: SPMUnit, int index 0-27, MONTH. -# Per the fee chart's lookup rule, each printed threshold is the inclusive floor of -# its own level: the family is at the highest level whose floor is at or below its -# monthly income. The index is max(number of thresholds met or exceeded - 1, 0), -# capped at 27 (Level BB). +# Per the fee chart's lookup rule, the formula moves down the family-size column to +# the first row with an amount greater than the family's monthly income and uses the +# row above (each printed threshold is the inclusive floor of its own level). Income +# below the Level A floor stays at Level A index 0; income that no row exceeds (at +# or above every floor) takes Level BB index 27. The literal scan matters because +# the chart's BB row is BELOW the AA row for family sizes 9-13 (e.g., size 10: +# BB 9_500 < AA 10_457): in the band between those two floors the first row greater +# than income is still Y/Z/AA, so the family lands one level lower than a +# count-of-floors-met shortcut would place it. # Income is monthly ia_cca_countable_income = employment_income / 12. # Family-of-3 monthly thresholds: A 2_044, B 2_152, C 2_212; AA 4_254, BB 6_000. +# Family-of-10 top floors: Y 9_903, Z 10_172, AA 10_457, BB 9_500. - name: Income below the Level A threshold is Level A index 0. period: 2025-01 @@ -98,6 +104,28 @@ # Income 4_800/mo: >= AA floor (4_254), < BB floor (6_000) -> Level AA index 26 ia_cca_sliding_fee_level: 26 +- name: Income at or above every floor lands at Level BB index 27 for a family of two. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 30 + employment_income: 61_200 + child: + age: 4 + spm_units: + spm_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: IA + output: + # Family of 2 (monotonic column); income 5_100/mo >= BB floor (5_000, the + # highest size-2 floor), so no row exceeds it -> last level, BB index 27 + ia_cca_sliding_fee_level: 27 + # === Edge cases === # The income-threshold table runs by family size 1-13 (13 means "13 or more"). The # formula clips family size to that range, so a unit of 14+ must use the size-13 @@ -186,3 +214,100 @@ # Family of 14 clips to size 13 -> same thresholds, same income -> Level A index 0 # (confirms the clip(size, 1, 13) ceiling on the lookup table) ia_cca_sliding_fee_level: 0 + +# === Non-monotonic BB row (family sizes 9-13) === +# For sizes 9-13 the chart's BB floor sits BELOW the AA floor (size 10: BB 9_500 < +# AA 10_457). In the band between those floors the first row greater than income is +# still Z or AA, so the literal scan keeps the family at Y or Z; a count-of-floors-met +# shortcut would also count the out-of-order BB floor and land one level too high. +# Size-10 floors: Y 9_903, Z 10_172, AA 10_457, BB 9_500. + +- name: Family of ten between the BB and Z floors lands at Level Y index 24. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 40 + employment_income: 120_000 + c1: {age: 4} + c2: {age: 4} + c3: {age: 4} + c4: {age: 4} + c5: {age: 4} + c6: {age: 4} + c7: {age: 4} + c8: {age: 4} + c9: {age: 4} + spm_units: + spm_unit: + members: [parent, c1, c2, c3, c4, c5, c6, c7, c8, c9] + households: + household: + members: [parent, c1, c2, c3, c4, c5, c6, c7, c8, c9] + state_code: IA + output: + # Family of 10; income 10_000/mo: >= Y floor (9_903) and >= BB floor (9_500), + # < Z floor (10_172) -> first row greater is Z, row above is Y index 24. + # The old count-of-floors lookup also counted the BB floor and gave Z index 25. + ia_cca_sliding_fee_level: 24 + +- name: Family of ten just below the AA floor lands at Level Z index 25. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 40 + employment_income: 125_472 + c1: {age: 4} + c2: {age: 4} + c3: {age: 4} + c4: {age: 4} + c5: {age: 4} + c6: {age: 4} + c7: {age: 4} + c8: {age: 4} + c9: {age: 4} + spm_units: + spm_unit: + members: [parent, c1, c2, c3, c4, c5, c6, c7, c8, c9] + households: + household: + members: [parent, c1, c2, c3, c4, c5, c6, c7, c8, c9] + state_code: IA + output: + # Family of 10; income 10_456/mo: >= Z floor (10_172) and >= BB floor (9_500), + # < AA floor (10_457) -> first row greater is AA, row above is Z index 25. + # The old count-of-floors lookup also counted the BB floor and gave AA index 26. + ia_cca_sliding_fee_level: 25 + +- name: Family of ten at or above every floor lands at Level BB index 27. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 40 + employment_income: 126_000 + c1: {age: 4} + c2: {age: 4} + c3: {age: 4} + c4: {age: 4} + c5: {age: 4} + c6: {age: 4} + c7: {age: 4} + c8: {age: 4} + c9: {age: 4} + spm_units: + spm_unit: + members: [parent, c1, c2, c3, c4, c5, c6, c7, c8, c9] + households: + household: + members: [parent, c1, c2, c3, c4, c5, c6, c7, c8, c9] + state_code: IA + output: + # Family of 10; income 10_500/mo: >= AA floor (10_457, the highest size-10 + # floor) and >= BB floor (9_500), so no row exceeds it -> last level, BB + # index 27 + ia_cca_sliding_fee_level: 27 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_eligible.yaml index 1d3166f24b5..dae084fcb7c 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_eligible.yaml @@ -1,9 +1,11 @@ # ia_cca_eligible: SPMUnit, bool, MONTH. # Eligible = has an eligible child AND asset-eligible AND # ((income-eligible AND activity-eligible) OR income-exception). -# Families eligible without regard to income (FIP/protective/foster) bypass both -# the income and activity tests but still need an eligible child and assets below -# the limit. +# Families eligible without regard to income (FIP/protective/foster) skip the +# income test and satisfy the need-for-service requirement through their +# qualifying condition (protective and foster care are themselves needs for +# service; FIP enrollment proxies PROMISE JOBS participation), but still need an +# eligible child and assets below the limit. - name: Standard household meeting all tests is eligible. period: 2025-01 @@ -93,7 +95,8 @@ members: [parent, child] state_code: IA output: - # Income exception (FIP) bypasses the income and activity tests; eligible child + # Income exception (FIP) skips the income test and satisfies the need for + # service through FIP enrollment (the PROMISE JOBS proxy); eligible child # and assets below $1M -> eligible despite high income and zero work hours ia_cca_eligible: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_income_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_income_eligible.yaml index 50a243ba4ce..d1757c033be 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_income_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/eligibility/ia_cca_income_eligible.yaml @@ -357,8 +357,9 @@ # (auto-divided) * rate. At EXACTLY the boundary the float32 round-trip # (annual -> /12 income vs annual FPG -> /12 limit) drifts by a few thousandths, # and the drift direction is value-dependent, so we never assert at the exact -# boundary; we bracket it +-12/yr (= +-$1/mo) instead. The existing +-100/yr cases -# above are loose; these tighten the band to the smallest reliable offset. +# boundary; we bracket it +-12 to +-13/yr (about +-$1/mo) instead. The existing +# +-100/yr cases above are loose; these tighten the band to the smallest reliable +# offset. - name: Applicant family of three 12 dollars below 160 percent FPL is eligible (tight boundary). period: 2025-01 @@ -509,8 +510,8 @@ # 53_312 (+$1/mo over 53_300) -> ineligible ia_cca_income_eligible: false -# Enrolled basic 250% FPL boundary (family of 3; 250% annual = 66_625), tight +-12/yr. -- name: Enrolled basic family 12 dollars below 250 percent FPL is eligible. +# Enrolled basic 250% FPL boundary (family of 3; 250% annual = 66_625), tight +-13/yr. +- name: Enrolled basic family 13 dollars below 250 percent FPL is eligible. period: 2025-01 absolute_error_margin: 0.1 input: @@ -531,10 +532,10 @@ members: [parent1, parent2, child] state_code: IA output: - # 250% FPL annual = 66_625; 66_612 (-$1/mo) -> eligible + # 250% FPL annual = 66_625; 66_612 (-$13/yr) -> just below -> eligible ia_cca_income_eligible: true -- name: Enrolled basic family 12 dollars above 250 percent FPL is not eligible. +- name: Enrolled basic family 13 dollars above 250 percent FPL is not eligible. period: 2025-01 absolute_error_margin: 0.1 input: @@ -555,11 +556,11 @@ members: [parent1, parent2, child] state_code: IA output: - # 66_638 (+$1/mo over 66_625) -> ineligible even when enrolled + # 66_638 (+$13/yr over 66_625) -> just above -> ineligible even when enrolled ia_cca_income_eligible: false # Enrolled special-needs 275% FPL boundary (family of 3; 275% annual = 73_287.50). -- name: Enrolled special-needs family 12 dollars below 275 percent FPL is eligible. +- name: Enrolled special-needs family 12.50 dollars below 275 percent FPL is eligible. period: 2025-01 absolute_error_margin: 0.1 input: @@ -581,7 +582,7 @@ members: [parent1, parent2, child] state_code: IA output: - # 275% FPL annual = 73_287.50; 73_275 -> just below -> eligible + # 275% FPL annual = 73_287.50; 73_275 (-$12.50/yr) -> just below -> eligible ia_cca_income_eligible: true - name: Enrolled special-needs family above 275 percent FPL is not eligible. diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca.yaml index 35e90911350..727f4f8fbc0 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca.yaml @@ -96,12 +96,12 @@ # Exit basic Level B -> 45% percentage-of-cost copay. ia_cca_in_exit_tier: true # Expense 24_000/yr -> 2_000/mo. Exit copay = 45% * 2_000 = 900. - ia_cca_copay: 900.00 + ia_cca_copay: 900 # Preschool ceiling = 19.50 * 34.6667 = 676.00; child_capped = min(2_000, 676) = 676. # Benefit = max(676.00 - 900.00, 0) = 0 (copay exceeds ceiling -> floored at 0) ia_cca: 0 -- name: Negative countable income does not inflate the benefit beyond the ceiling. +- name: Net self-employment loss floors countable income at zero without inflating the benefit. period: 2025-01 absolute_error_margin: 0.01 input: @@ -133,12 +133,14 @@ members: [parent, preschooler] state_code: IA output: - # Negative self-employment income -> zero/negative countable income, well under - # the 160% FPL limit -> income eligible; parent works 40 hrs -> activity eligible. + # The per-person net self-employment loss is floored at zero (a loss cannot + # offset other income), so countable income is exactly 0 -> well under the + # 160% FPL limit -> income eligible; parent works 40 hrs -> activity eligible. + ia_cca_countable_income: 0 ia_cca_eligible: true - # Countable income <= sliding-fee Level A -> maximum subsidy, copay 0. + # Countable income 0 <= sliding-fee Level A -> maximum subsidy, copay 0. ia_cca_copay: 0 # Preschool ceiling = 19.50 * 34.6667 = 676.00; expense 1_000/mo capped to 676.00. # Benefit stays at the rate ceiling (max subsidy) and is not inflated by the - # negative income. Benefit = max(676.00 - 0, 0) = 676.00 - ia_cca: 676.00 + # self-employment loss. Benefit = max(676.00 - 0, 0) = 676.00 + ia_cca: 676 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_countable_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_countable_income.yaml index 5d57d8c036e..d7f43f3e150 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_countable_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_countable_income.yaml @@ -1,8 +1,12 @@ # ia_cca_countable_income: SPMUnit, USD, MONTH. -# Sums countable earned and unearned income, excluding the earnings of a child -# age 14 or younger and the earnings of a full-time student age 18 or younger -# (IAC 441-170.2(1)"d"). Definition period is MONTH, so annual income sources are -# auto-divided to monthly figures. +# Sums countable earned and unearned income. Each person's net self-employment +# subtotal is floored at zero — a net self-employment loss cannot offset wages +# or unearned income (IAC 441-170.2(1)"c"(1)). The earnings of a child age 14 +# or younger and of a full-time student age 18 or younger are excluded, but +# only for persons who are not the tax-unit head or spouse, since both +# exclusions cover a child's earnings — a teen parent's own wages stay counted +# (IAC 441-170.2(1)"d"(13) and (35)). Definition period is MONTH, so annual +# income sources are auto-divided to monthly figures. - name: Parent earnings count in monthly countable income. period: 2025-01 @@ -116,6 +120,98 @@ # (36_000 earned + 6_000 social security) / 12 = 3_500 per month ia_cca_countable_income: 3_500 +- name: SSI is counted in unearned income. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 35 + employment_income: 36_000 + child: + age: 4 + ssi: 500 # ssi is MONTH-defined, so this is the January amount + spm_units: + spm_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: IA + output: + # 36_000 / 12 = 3_000 earned + 500 monthly SSI = 3_500 per month + ia_cca_countable_income: 3_500 + +- name: Tax-exempt interest is counted exactly once via the interest income umbrella. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 35 + taxable_interest_income: 1_200 + tax_exempt_interest_income: 1_200 + child: + age: 4 + spm_units: + spm_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: IA + output: + # interest_income = 1_200 taxable + 1_200 tax-exempt = 2_400 per year + # -> 2_400 / 12 = 200 per month; double-counting tax-exempt interest + # through a separate unearned source would give (2_400 + 1_200) / 12 = 300 + ia_cca_countable_income: 200 + +- name: A net self-employment loss does not offset wages. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 35 + employment_income: 36_000 + self_employment_income: -12_000 + child: + age: 4 + spm_units: + spm_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: IA + output: + # Self-employment subtotal floored at zero: max(-12_000, 0) = 0, so + # countable income is the wages alone: 36_000 / 12 = 3_000 per month + # (offsetting the loss would give (36_000 - 12_000) / 12 = 2_000) + ia_cca_countable_income: 3_000 + +- name: A family with only a self-employment loss has zero countable income. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 35 + self_employment_income: -12_000 + child: + age: 4 + spm_units: + spm_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: IA + output: + # Self-employment subtotal floored at zero: max(-12_000, 0) = 0; no other + # income -> 0 per month, not -12_000 / 12 = -1_000 + ia_cca_countable_income: 0 + # === Edge cases === - name: No income sources gives zero countable income. @@ -137,3 +233,28 @@ output: # No earned or unearned income for anyone -> 0 (degenerate low boundary) ia_cca_countable_income: 0 + +- name: An 18-year-old full-time-student single parent's own wages are counted. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 18 + employment_income: 24_000 + is_full_time_student: true + baby: + age: 1 + spm_units: + spm_unit: + members: [parent, baby] + households: + household: + members: [parent, baby] + state_code: IA + output: + # The student exclusion covers a child's earnings only: as the tax-unit + # head, the 18-year-old parent is not an excluded minor even though they + # are a full-time student age 18 (contrast with the dependent 17-year-old + # full-time-student case above) -> 24_000 / 12 = 2_000 per month + ia_cca_countable_income: 2_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_child_care_subsidies.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_child_care_subsidies.yaml index ba9b9bdbd11..d83159ad781 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_child_care_subsidies.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_child_care_subsidies.yaml @@ -39,5 +39,5 @@ # Monthly benefit = 676.00. Read at a YEAR period, the MONTH-defined ia_cca # sums across the 12 months -> 676.00 * 12 = 8_112.00, and the YEAR aggregator # adds it through to the same total. - ia_cca: 8_112.00 - ia_child_care_subsidies: 8_112.00 + ia_cca: 8_112 + ia_child_care_subsidies: 8_112 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/integration.yaml index 6a59f84cf7f..57d6b841619 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/integration.yaml @@ -222,10 +222,10 @@ # Exit basic level: >= B floor (5_057), < C floor (5_272) -> Level B -> 45%. # Expense 12_000/yr -> 1_000/mo. Exit copay = 45% of the child's monthly cost # of care = 0.45 * 1_000 = 450. - ia_cca_copay: 450.00 + ia_cca_copay: 450 # Preschool ceiling = 19.50 * 34.6667 = 676.00; monthly expense 1_000 capped to # 676.00. Benefit = max(676.00 - 450.00, 0) = 226.00 - ia_cca: 226.00 + ia_cca: 226 # === Case 5: Special-needs branch (applicant, 200% FPL, SN rate) === - name: Case 5, single parent disabled preschooler, special-needs rate and limit. diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_copay.py b/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_copay.py index 86a69b3fed5..332c58237b1 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_copay.py +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_copay.py @@ -43,13 +43,15 @@ def formula(spm_unit, period, parameters): # --- Mechanism B: CCA Exit percentage of cost of care --- # A percentage of the cost of care for each child in care, summed - # across children. - exit_level = spm_unit("ia_cca_exit_fee_level", period) + # across children. The fee chart picks the Basic or Special Needs + # threshold table for each child separately, so the percentage is + # resolved per child before summing. + exit_level = person("ia_cca_exit_fee_level", period) exit_pct = 0 for i, level in enumerate(EXIT_FEE_LEVELS): exit_pct = exit_pct + where(exit_level == i, p.exit.fee_pct[level], 0) child_expense = person("pre_subsidy_childcare_expenses", period) - exit_copay = exit_pct * spm_unit.sum(child_expense * is_in_care_child) + exit_copay = spm_unit.sum(exit_pct * child_expense * is_in_care_child) in_exit_tier = spm_unit("ia_cca_in_exit_tier", period) copay = where(in_exit_tier, exit_copay, sliding_copay) diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_exit_fee_level.py b/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_exit_fee_level.py index bc991e4f397..513939152da 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_exit_fee_level.py +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_exit_fee_level.py @@ -6,31 +6,35 @@ class ia_cca_exit_fee_level(Variable): value_type = int - entity = SPMUnit + entity = Person label = "Iowa CCA Exit fee level index" definition_period = MONTH defined_for = StateCode.IA reference = "https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=12" - def formula(spm_unit, period, parameters): + def formula(person, period, parameters): p = parameters(period).gov.states.ia.hhs.cca.copay.exit - income = spm_unit("ia_cca_countable_income", period) - size = spm_unit("spm_unit_size", period.this_year) + income = person.spm_unit("ia_cca_countable_income", period) + size = person.spm_unit("spm_unit_size", period.this_year) capped_size = clip(size, 1, 13).astype(int) - # CCA Exit uses a separate income threshold table for basic care and - # special-needs care. - has_special_needs_child = spm_unit("ia_cca_has_special_needs_child", period) + # The fee chart selects the income-threshold table for each child + # separately: the special-needs table for a child receiving + # special-needs care (the disability flag is the proxy) and the + # basic table otherwise, so the level is resolved per person. + is_special_needs = person("is_disabled", period.this_year) # Per the CCA Exit fee chart's lookup rule (#page=2): move down the - # family-size column to the "first row with an amount greater than the - # monthly family income" and "use the row above" to set the fee. Each - # printed threshold is the inclusive floor of its own level, so the level - # index is the highest level whose floor is at-or-below income: - # (number of thresholds the income meets or exceeds) - 1. - levels_met = 0 - for level in EXIT_FEE_LEVELS: + # family-size column to the "first row with an amount greater than + # the monthly family income" and "use the row above" to set the fee. + # Walking the rows bottom-up and keeping the lowest row whose + # threshold exceeds income implements that scan literally; incomes + # below the Level A floor stay at Level A, and incomes at or above + # the last floor take the last level. + num_levels = len(EXIT_FEE_LEVELS) + first_greater = num_levels + for index in reversed(range(num_levels)): + level = EXIT_FEE_LEVELS[index] basic_threshold = p.income_thresholds_basic[level][capped_size] sn_threshold = p.income_thresholds_special_needs[level][capped_size] - threshold = where(has_special_needs_child, sn_threshold, basic_threshold) - levels_met = levels_met + (income >= threshold) - level_index = max_(levels_met - 1, 0) - return min_(level_index, len(EXIT_FEE_LEVELS) - 1) + threshold = where(is_special_needs, sn_threshold, basic_threshold) + first_greater = where(threshold > income, index, first_greater) + return max_(first_greater - 1, 0) diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_sliding_fee_level.py b/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_sliding_fee_level.py index 3d270d94900..300377c5bf4 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_sliding_fee_level.py +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_sliding_fee_level.py @@ -52,15 +52,17 @@ def formula(spm_unit, period, parameters): capped_size = clip(size, 1, 13).astype(int) # Per the fee chart's lookup rule (#page=1): move down the family-size # column to the "first row with an amount greater than the monthly - # family income" and "use the row above" to set the fee. Each printed - # threshold is therefore the inclusive floor of its own level, so the - # level index is the highest level whose floor is at-or-below income: - # (number of thresholds the income meets or exceeds) - 1. - levels_met = 0 - for level in SLIDING_FEE_LEVELS: - threshold = thresholds[level][capped_size] - levels_met = levels_met + (income >= threshold) - level_index = max_(levels_met - 1, 0) - # Cap at the last level (index 27); incomes above the BB threshold - # exceed the CCA Plus income limit and are screened out upstream. - return min_(level_index, len(SLIDING_FEE_LEVELS) - 1) + # family income" and "use the row above" to set the fee. The scan is + # implemented literally — walking the rows bottom-up and keeping the + # lowest row whose threshold exceeds income — because the chart's BB + # row is below the AA row for family sizes 9 through 13, so a + # count-of-thresholds-met shortcut would land those sizes one level + # too high in the band between the BB and AA floors. Incomes below + # the Level A floor stay at Level A; incomes no row exceeds (at or + # above every floor) take the last level (BB). + num_levels = len(SLIDING_FEE_LEVELS) + first_greater = num_levels + for index in reversed(range(num_levels)): + threshold = thresholds[SLIDING_FEE_LEVELS[index]][capped_size] + first_greater = where(threshold > income, index, first_greater) + return max_(first_greater - 1, 0) diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_eligible.py b/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_eligible.py index 9d313fe15c9..b33a7a3ab35 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_eligible.py +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_eligible.py @@ -15,8 +15,15 @@ def formula(spm_unit, period, parameters): income_eligible = spm_unit("ia_cca_income_eligible", period) activity_eligible = spm_unit("ia_cca_activity_eligible", period) # Families eligible without regard to income (FIP, protective, - # foster) bypass both the income test and the activity need-for- - # service test (IAC 441-170.2(1)"b"). + # foster) skip the income test (IAC 441-170.2(1)"b") and satisfy + # the need-for-service requirement through their qualifying + # condition: protective child care and licensed foster care are + # themselves needs for service (IAC 441-170.2(2)"b"(3) and (9)), + # and a FIP family qualifies through employment with no + # minimum-hours requirement, education, or PROMISE JOBS + # participation (IAC 441-170.2(2)"b"(6)-(7)) — we don't track + # PROMISE JOBS participation at the moment, so FIP enrollment + # serves as the proxy for need. income_exception = spm_unit("ia_cca_income_exception", period) standard_path = income_eligible & activity_eligible return has_eligible_child & asset_eligible & (standard_path | income_exception) diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_has_special_needs_child.py b/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_has_special_needs_child.py index 2b4fb4ca9ac..c1081264469 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_has_special_needs_child.py +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_has_special_needs_child.py @@ -7,7 +7,7 @@ class ia_cca_has_special_needs_child(Variable): label = "Iowa CCA family includes a child with special needs" definition_period = MONTH defined_for = StateCode.IA - reference = "https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=3" + reference = "https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=1" def formula(spm_unit, period, parameters): person = spm_unit.members diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_countable_income.py b/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_countable_income.py index aab18373d08..189ecde4020 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_countable_income.py +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_countable_income.py @@ -15,14 +15,25 @@ def formula(spm_unit, period, parameters): person = spm_unit.members # Iowa excludes the earnings of a child age 14 or younger, and the # earnings of a child 18 or younger who is a full-time student - # (IAC 441-170.2(1)"d"). `age` is YEAR-defined, so read the annual - # value inside this monthly formula. + # (IAC 441-170.2(1)"d"(13) and (35)). Both exclusions cover a child's + # earnings, so they never apply to a tax-unit head or spouse — a + # teen parent's own wages stay counted. `age` is YEAR-defined, so + # read the annual value inside this monthly formula. age = person("age", period.this_year) is_full_time_student = person("is_full_time_student", period.this_year) - excluded_minor = (age <= p.minor_earnings_age) | ( - (age <= p.minor_student_age) & is_full_time_student + is_head_or_spouse = person("is_tax_unit_head_or_spouse", period.this_year) + excluded_minor = ~is_head_or_spouse & ( + (age <= p.minor_earnings_age) + | ((age <= p.minor_student_age) & is_full_time_student) ) - earned_per_person = add(person, period, p.countable_income.earned_sources) + wage_income = add(person, period, p.countable_income.earned_sources) + # A net loss in self-employment income cannot offset other earned or + # unearned income (IAC 441-170.2(1)"c"(1)), so the per-person + # self-employment total is floored at zero. + self_employment = add( + person, period, p.countable_income.self_employment_sources + ) + earned_per_person = wage_income + max_(self_employment, 0) counted_earned = spm_unit.sum(earned_per_person * ~excluded_minor) unearned_income = add(spm_unit, period, p.countable_income.unearned_sources) return counted_earned + unearned_income From 7711778f1fc5376019579bc5df8f2f6708260e11 Mon Sep 17 00:00:00 2001 From: Ziming Date: Tue, 9 Jun 2026 17:45:25 -0400 Subject: [PATCH 7/7] Review-fix round 4: day-based unit counting and a(2) MFI cap on dispatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Count half-day units per day when the care schedule is known (childcare_days_per_week / childcare_hours_per_day inputs): one unit for a day of up to 5 hours, two units for a longer day, per IAC 441-170.1 ("per 24-hour period") and 170.4(7)"a" (full-day rate = 2 half-day rates). Falls back to total-hours proration when only weekly hours are reported, preserving existing test and microsim behavior for hours-only inputs - Apply the IAC 441-170.2(1)"a"(2) ceiling — min(225% FPL, 85% MFI) — to the CCA Plus / CCA Exit copay-mechanism dispatch; the MFI cap binds below 225% FPL at family sizes 10+ with current parameters - Tests: +7 cases (part-day, full-day, 5-hour-boundary, 10-hour-day, zero-hours schedules; size-10 dispatch on both sides of the MFI cap) Co-Authored-By: Claude Fable 5 --- .../ia/hhs/cca/copay/ia_cca_in_exit_tier.yaml | 73 ++++++++++- .../ia/hhs/cca/ia_cca_monthly_units.yaml | 124 +++++++++++++++++- .../ia/hhs/cca/copay/ia_cca_in_exit_tier.py | 14 +- .../cca/eligibility/ia_cca_income_eligible.py | 15 +-- .../states/ia/hhs/cca/ia_cca_monthly_units.py | 26 +++- 5 files changed, 221 insertions(+), 31 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_in_exit_tier.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_in_exit_tier.yaml index 90fa598f64c..d49aa9dd266 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_in_exit_tier.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/copay/ia_cca_in_exit_tier.yaml @@ -1,8 +1,11 @@ # ia_cca_in_exit_tier: SPMUnit, bool, MONTH (dispatch). # True when the family is enrolled AND monthly income exceeds the ongoing CCA Plus -# limit (225% FPL). These families pay the percentage-of-cost CCA Exit fee rather -# than the sliding unit fee. 2025 family-of-3 225% FPL monthly = 4_996.88 -# (annual 59_962.50). +# ceiling — the lesser of 225% FPL and 85% of Iowa median family income +# (IAC 441-170.2(1)"a"(2)). These families pay the percentage-of-cost CCA Exit fee +# rather than the sliding unit fee. The 85% MFI cap binds below 225% FPL only at +# very large family sizes (10+ with current parameters). 2025 family-of-3 225% FPL +# monthly = 4_996.88 (annual 59_962.50); 2025 family-of-10 figures: 85% SMI +# monthly = 11_582.00, 225% FPL monthly = 12_215.62. - name: Enrolled family above 225 percent FPL is in the exit tier. period: 2025-01 @@ -128,3 +131,67 @@ output: # 59_975 (+$12.50/yr over 59_962.50) and enrolled -> exit tier ia_cca_in_exit_tier: true + +- name: Large enrolled family between the 85 percent MFI cap and 225 percent FPL is in the exit tier. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 35 + employment_income: 141_600 + coparent: + age: 35 + c1: {age: 6} + c2: {age: 6} + c3: {age: 6} + c4: {age: 6} + c5: {age: 6} + c6: {age: 6} + c7: {age: 6} + c8: {age: 6} + spm_units: + spm_unit: + members: [parent, coparent, c1, c2, c3, c4, c5, c6, c7, c8] + ia_cca_enrolled: true + households: + household: + members: [parent, coparent, c1, c2, c3, c4, c5, c6, c7, c8] + state_code: IA + output: + # Family of 10; income 11_800/mo. The CCA Plus ceiling is + # min(225% FPL 12_215.62, 85% MFI 11_582.00) = 11_582.00, so income + # above the MFI cap moves the family into the exit tier even though it + # is below 225% FPL. + ia_cca_in_exit_tier: true + +- name: Large enrolled family below the 85 percent MFI cap stays on the sliding fee. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 35 + employment_income: 138_000 + coparent: + age: 35 + c1: {age: 6} + c2: {age: 6} + c3: {age: 6} + c4: {age: 6} + c5: {age: 6} + c6: {age: 6} + c7: {age: 6} + c8: {age: 6} + spm_units: + spm_unit: + members: [parent, coparent, c1, c2, c3, c4, c5, c6, c7, c8] + ia_cca_enrolled: true + households: + household: + members: [parent, coparent, c1, c2, c3, c4, c5, c6, c7, c8] + state_code: IA + output: + # Family of 10; income 11_500/mo <= min(12_215.62, 11_582.00) + # -> CCA Plus sliding fee, not exit tier + ia_cca_in_exit_tier: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_monthly_units.yaml b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_monthly_units.yaml index fab9c30625b..f526367cffc 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_monthly_units.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ia/hhs/cca/ia_cca_monthly_units.yaml @@ -1,9 +1,11 @@ # ia_cca_monthly_units: Person, float, MONTH. -# A unit of service is a half day of up to 5 hours of care (IAC 441-170.1). With -# no authorized-units input, the monthly units are inferred from the child's -# scheduled weekly childcare hours, annualized to monthly and divided by the -# 5-hour unit: monthly_units = childcare_hours_per_week * (52 / 12) / 5. -# Defined only for an eligible child. +# A unit of service is a half day of up to 5 hours of care per 24-hour period, +# and a full day bills as two half-day units (IAC 441-170.1; 170.4(7)"a"). +# When the care schedule is known (childcare_days_per_week > 0), units are +# counted per day: one unit for a day of up to 5 hours, two for a longer day, +# times days per week, annualized monthly (x 52 / 12). When only weekly hours +# are reported, units fall back to total-hours proration: +# childcare_hours_per_week * (52 / 12) / 5. Defined only for an eligible child. - name: Full-time hours convert to monthly half-day units. period: 2025-01 @@ -66,3 +68,115 @@ state_code: IA output: ia_cca_monthly_units: 0 + +- name: A four-hour day bills one unit per day, not a prorated fraction. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + child: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_days_per_week: 5 + childcare_hours_per_day: 4 + tax_units: + tax_unit: + members: [child] + households: + household: + members: [child] + state_code: IA + output: + # 4 hours <= 5 -> 1 unit per day; 5 days * 1 * (52 / 12) = 21.6667. + # Hours proration would give 20 * (52 / 12) / 5 = 17.3333, understating + # the units for a part-day schedule. + ia_cca_monthly_units: 21.6667 + +- name: An eight-hour day bills two units as a full day. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + child: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_days_per_week: 5 + childcare_hours_per_day: 8 + tax_units: + tax_unit: + members: [child] + households: + household: + members: [child] + state_code: IA + output: + # 8 hours > 5 -> full day = 2 units; 5 days * 2 * (52 / 12) = 43.3333 + ia_cca_monthly_units: 43.3333 + +- name: A five-hour day is exactly one unit. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + child: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_days_per_week: 5 + childcare_hours_per_day: 5 + tax_units: + tax_unit: + members: [child] + households: + household: + members: [child] + state_code: IA + output: + # A unit is up to 5 hours, so a 5-hour day stays at 1 unit: + # 5 days * 1 * (52 / 12) = 21.6667 + ia_cca_monthly_units: 21.6667 + +- name: A ten-hour day still bills two units. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + child: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_days_per_week: 3 + childcare_hours_per_day: 10 + tax_units: + tax_unit: + members: [child] + households: + household: + members: [child] + state_code: IA + output: + # 10 hours > 5 -> full day = 2 units; 3 days * 2 * (52 / 12) = 26 + ia_cca_monthly_units: 26 + +- name: Days scheduled with zero hours give zero units. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + child: + age: 4 + is_tax_unit_dependent: true + immigration_status: CITIZEN + childcare_days_per_week: 5 + childcare_hours_per_day: 0 + tax_units: + tax_unit: + members: [child] + households: + household: + members: [child] + state_code: IA + output: + ia_cca_monthly_units: 0 diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_in_exit_tier.py b/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_in_exit_tier.py index c0ec37911b7..d0d6a4a5966 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_in_exit_tier.py +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/copay/ia_cca_in_exit_tier.py @@ -12,14 +12,14 @@ class ia_cca_in_exit_tier(Variable): def formula(spm_unit, period, parameters): p = parameters(period).gov.states.ia.hhs.cca.income.fpl_rate # CCA Exit applies to enrolled families whose income exceeds the - # ongoing CCA Plus limit (225% FPL); these families pay the - # percentage-of-cost CCA Exit fee instead of the sliding unit fee. - # This dispatch boundary omits the a(2) "min(225% FPL, 85% MFI)" cap; - # the 85% MFI ceiling only binds for very large families and is - # immaterial here (see ia_cca_income_eligible for the documented - # omission). + # ongoing CCA Plus ceiling — the lesser of 225% FPL and 85% of Iowa + # median family income (IAC 441-170.2(1)"a"(2)); these families pay + # the percentage-of-cost CCA Exit fee instead of the sliding unit + # fee. The 85% MFI cap binds below 225% FPL only at very large + # family sizes (10 and up with current parameters). enrolled = spm_unit("ia_cca_enrolled", period) income = spm_unit("ia_cca_countable_income", period) fpg = spm_unit("spm_unit_fpg", period) - plus_limit = fpg * p.plus_basic + smi_cap = spm_unit("ia_cca_smi", period) + plus_limit = min_(fpg * p.plus_basic, smi_cap) return enrolled & (income > plus_limit) diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_income_eligible.py b/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_income_eligible.py index 8c85f653700..82aed853007 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_income_eligible.py +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/eligibility/ia_cca_income_eligible.py @@ -37,14 +37,11 @@ def formula(spm_unit, period, parameters): # IAC 441-170.2(1)"a" sets three income tiers: initial (a(1), # min of 160%/200% FPL and 85% MFI), ongoing CCA Plus (a(2), # min of 225% FPL and 85% MFI), and ongoing CCA Exit (a(3), - # 250%/275% FPL with no MFI cap). We collapse the two ongoing tiers - # into the higher CCA Exit ceiling for enrolled families, so the a(2) - # 225%-FPL-with-85%-MFI-cap intermediate ceiling is not represented. - # The 85% MFI cap is correctly absent from a(3). The omitted a(2) cap - # only binds below the 225% FPL standard at very large family sizes - # (where 85% MFI falls below 225% FPL), and the model uses the 225% - # FPL standard solely as the copay-mechanism dispatch boundary - # (CCA/CCA Plus sliding fee vs. CCA Exit percentage), never as an - # income-eligibility ceiling, so this simplification is immaterial. + # 250%/275% FPL with no MFI cap). For ELIGIBILITY, enrolled families + # are covered up to the higher a(3) CCA Exit ceiling (the 85% MFI cap + # is correctly absent from a(3)); a family over the a(2) CCA Plus + # ceiling is not ineligible — it moves to the CCA Exit tier. The a(2) + # ceiling, including its 85% MFI cap, is applied where it has effect: + # as the copay-mechanism dispatch boundary in ia_cca_in_exit_tier. income_limit = where(enrolled, exit_limit, initial_limit) return countable_income <= income_limit diff --git a/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_monthly_units.py b/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_monthly_units.py index 9f64b554628..4ef366e3d2a 100644 --- a/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_monthly_units.py +++ b/policyengine_us/variables/gov/states/ia/hhs/cca/ia_cca_monthly_units.py @@ -10,12 +10,24 @@ class ia_cca_monthly_units(Variable): reference = "https://www.legis.iowa.gov/docs/iac/chapter/441.170.pdf#page=3" def formula(person, period, parameters): - # A unit of service is a half day of up to 5 hours of care - # (IAC 441-170.1). PolicyEngine has no authorized-units input, so we - # infer the monthly units of care from the child's scheduled child - # care hours: weekly hours, annualized to a monthly figure, divided - # by the hours in a unit. `childcare_hours_per_week` is YEAR-defined. + # A unit of service is a half day of up to 5 hours of care per + # 24-hour period, and a full day bills as two half-day units + # (IAC 441-170.1; 170.4(7)"a" sets the half-day rate at the + # provider's full-day rate divided by 2). When the care schedule is + # known, units are counted per day: one unit for a day of up to + # 5 hours, two units for a longer day. PolicyEngine has no + # authorized-units input, so when only weekly hours are reported + # the units are prorated over 5-hour blocks instead. hours_per_unit = parameters(period).gov.states.ia.hhs.cca.payment.hours_per_unit weekly_hours = person("childcare_hours_per_week", period.this_year) - monthly_hours = weekly_hours * (WEEKS_IN_YEAR / MONTHS_IN_YEAR) - return monthly_hours / hours_per_unit + days_per_week = person("childcare_days_per_week", period.this_year) + weeks_per_month = WEEKS_IN_YEAR / MONTHS_IN_YEAR + daily_hours = weekly_hours / where(days_per_week > 0, days_per_week, 1) + units_per_day = select( + [daily_hours <= 0, daily_hours <= hours_per_unit], + [0, 1], + default=2, + ) + schedule_units = days_per_week * units_per_day * weeks_per_month + prorated_units = weekly_hours * weeks_per_month / hours_per_unit + return where(days_per_week > 0, schedule_units, prorated_units)