From 39eece2907fbef05fc673437eebb6738237fe278 Mon Sep 17 00:00:00 2001 From: Ziming Date: Sun, 21 Jun 2026 21:41:12 -0400 Subject: [PATCH 1/6] Initialize North Dakota CCAP implementation Closes #8708 Co-Authored-By: Claude Opus 4.8 (1M context) --- changelog.d/nd-ccap.added.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/nd-ccap.added.md diff --git a/changelog.d/nd-ccap.added.md b/changelog.d/nd-ccap.added.md new file mode 100644 index 00000000000..88f7bf0922b --- /dev/null +++ b/changelog.d/nd-ccap.added.md @@ -0,0 +1 @@ +Implemented North Dakota Child Care Assistance Program (CCAP). From 69a1b05d8c942d798ad8c90395976d955b7f7cff Mon Sep 17 00:00:00 2001 From: Ziming Date: Sun, 21 Jun 2026 23:37:02 -0400 Subject: [PATCH 2/6] Implement North Dakota CCAP (ref #8708) Child Care Assistance Program (NDAC 75-02-01.3; Manual Service Chapter 400-28, ML 3930): two-tier SMI income test (75% applicant / 85% enrolled), provider max-rate table (4 provider types x 4 age groups x FT/PT), stepped-%SMI copay (waived <=30% SMI, capped 7%), special-needs +10% SMR, QRIS step bonuses, infant/toddler provider bonus. Wired into federal CCDF child_care_subsidies and programs.yaml. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../hhs/ccdf/child_care_subsidy_programs.yaml | 1 + .../dhs/ccap/age_group/infant_max_months.yaml | 15 + .../ccap/age_group/preschool_min_months.yaml | 14 + .../ccap/age_group/toddler_max_months.yaml | 15 + .../states/nd/dhs/ccap/copay/max_rate.yaml | 13 + .../gov/states/nd/dhs/ccap/copay/rate.yaml | 37 ++ .../dhs/ccap/copay/waiver_smi_threshold.yaml | 11 + .../dhs/ccap/eligibility/child_age_limit.yaml | 19 + .../eligibility/disabled_child_age_limit.yaml | 15 + .../dhs/ccap/income/continuing_smi_rate.yaml | 13 + .../nd/dhs/ccap/income/initial_smi_rate.yaml | 13 + .../states/nd/dhs/ccap/income/sources.yaml | 95 ++++ .../states/nd/dhs/ccap/rates/full_time.yaml | 56 ++ .../dhs/ccap/rates/infant_toddler_bonus.yaml | 27 + ...infant_toddler_bonus_min_weekly_hours.yaml | 18 + .../states/nd/dhs/ccap/rates/part_time.yaml | 55 ++ .../dhs/ccap/rates/qris_step_bonus_rate.yaml | 33 ++ .../ccap/rates/special_needs_multiplier.yaml | 11 + .../time_category/full_time_min_hours.yaml | 14 + policyengine_us/programs.yaml | 6 + .../nd/dhs/ccap/copay/nd_ccap_copay.yaml | 249 +++++++++ .../nd_ccap_activity_eligible.yaml | 113 ++++ .../ccap/eligibility/nd_ccap_eligible.yaml | 131 +++++ .../eligibility/nd_ccap_eligible_child.yaml | 154 ++++++ .../eligibility/nd_ccap_income_eligible.yaml | 148 +++++ .../nd_ccap_parent_in_eligible_activity.yaml | 90 +++ .../nd_ccap_child_support_deduction.yaml | 67 +++ .../ccap/income/nd_ccap_countable_income.yaml | 110 ++++ .../gov/states/nd/dhs/ccap/integration.yaml | 514 ++++++++++++++++++ .../nd/dhs/ccap/rates/nd_ccap_age_group.yaml | 145 +++++ .../dhs/ccap/rates/nd_ccap_base_subsidy.yaml | 84 +++ .../rates/nd_ccap_infant_toddler_bonus.yaml | 121 +++++ .../rates/nd_ccap_provider_qris_step.yaml | 73 +++ .../ccap/rates/nd_ccap_qris_step_bonus.yaml | 172 ++++++ .../ccap/rates/nd_ccap_state_max_rate.yaml | 304 +++++++++++ .../dhs/ccap/rates/nd_ccap_time_category.yaml | 61 +++ .../states/nd/dhs/ccap/copay/nd_ccap_copay.py | 30 + .../eligibility/nd_ccap_activity_eligible.py | 22 + .../dhs/ccap/eligibility/nd_ccap_eligible.py | 19 + .../eligibility/nd_ccap_eligible_child.py | 33 ++ .../eligibility/nd_ccap_income_eligible.py | 25 + .../nd_ccap_parent_in_eligible_activity.py | 28 + .../income/nd_ccap_child_support_deduction.py | 20 + .../ccap/income/nd_ccap_countable_income.py | 24 + .../dhs/ccap/income/nd_ccap_gross_income.py | 12 + .../states/nd/dhs/ccap/is_nd_ccap_enrolled.py | 10 + .../gov/states/nd/dhs/ccap/nd_ccap.py | 22 + .../nd/dhs/ccap/nd_child_care_subsidies.py | 11 + .../nd/dhs/ccap/rates/nd_ccap_age_group.py | 43 ++ .../nd/dhs/ccap/rates/nd_ccap_base_subsidy.py | 27 + .../rates/nd_ccap_infant_toddler_bonus.py | 36 ++ .../ccap/rates/nd_ccap_provider_qris_step.py | 20 + .../dhs/ccap/rates/nd_ccap_provider_type.py | 19 + .../dhs/ccap/rates/nd_ccap_qris_step_bonus.py | 34 ++ .../dhs/ccap/rates/nd_ccap_state_max_rate.py | 48 ++ .../dhs/ccap/rates/nd_ccap_time_category.py | 28 + 56 files changed, 3528 insertions(+) create mode 100644 policyengine_us/parameters/gov/states/nd/dhs/ccap/age_group/infant_max_months.yaml create mode 100644 policyengine_us/parameters/gov/states/nd/dhs/ccap/age_group/preschool_min_months.yaml create mode 100644 policyengine_us/parameters/gov/states/nd/dhs/ccap/age_group/toddler_max_months.yaml create mode 100644 policyengine_us/parameters/gov/states/nd/dhs/ccap/copay/max_rate.yaml create mode 100644 policyengine_us/parameters/gov/states/nd/dhs/ccap/copay/rate.yaml create mode 100644 policyengine_us/parameters/gov/states/nd/dhs/ccap/copay/waiver_smi_threshold.yaml create mode 100644 policyengine_us/parameters/gov/states/nd/dhs/ccap/eligibility/child_age_limit.yaml create mode 100644 policyengine_us/parameters/gov/states/nd/dhs/ccap/eligibility/disabled_child_age_limit.yaml create mode 100644 policyengine_us/parameters/gov/states/nd/dhs/ccap/income/continuing_smi_rate.yaml create mode 100644 policyengine_us/parameters/gov/states/nd/dhs/ccap/income/initial_smi_rate.yaml create mode 100644 policyengine_us/parameters/gov/states/nd/dhs/ccap/income/sources.yaml create mode 100644 policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/full_time.yaml create mode 100644 policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/infant_toddler_bonus.yaml create mode 100644 policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/infant_toddler_bonus_min_weekly_hours.yaml create mode 100644 policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/part_time.yaml create mode 100644 policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/qris_step_bonus_rate.yaml create mode 100644 policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/special_needs_multiplier.yaml create mode 100644 policyengine_us/parameters/gov/states/nd/dhs/ccap/time_category/full_time_min_hours.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/copay/nd_ccap_copay.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_activity_eligible.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_eligible.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_eligible_child.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_income_eligible.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_parent_in_eligible_activity.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/income/nd_ccap_child_support_deduction.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/income/nd_ccap_countable_income.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/integration.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_age_group.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_base_subsidy.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_infant_toddler_bonus.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_provider_qris_step.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_qris_step_bonus.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_state_max_rate.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_time_category.yaml create mode 100644 policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_copay.py create mode 100644 policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_activity_eligible.py create mode 100644 policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_eligible.py create mode 100644 policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_eligible_child.py create mode 100644 policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_income_eligible.py create mode 100644 policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_parent_in_eligible_activity.py create mode 100644 policyengine_us/variables/gov/states/nd/dhs/ccap/income/nd_ccap_child_support_deduction.py create mode 100644 policyengine_us/variables/gov/states/nd/dhs/ccap/income/nd_ccap_countable_income.py create mode 100644 policyengine_us/variables/gov/states/nd/dhs/ccap/income/nd_ccap_gross_income.py create mode 100644 policyengine_us/variables/gov/states/nd/dhs/ccap/is_nd_ccap_enrolled.py create mode 100644 policyengine_us/variables/gov/states/nd/dhs/ccap/nd_ccap.py create mode 100644 policyengine_us/variables/gov/states/nd/dhs/ccap/nd_child_care_subsidies.py create mode 100644 policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_age_group.py create mode 100644 policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_base_subsidy.py create mode 100644 policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_infant_toddler_bonus.py create mode 100644 policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_provider_qris_step.py create mode 100644 policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_provider_type.py create mode 100644 policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_qris_step_bonus.py create mode 100644 policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_state_max_rate.py create mode 100644 policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_time_category.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 ab4fddc1e7c..ca80187d366 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 @@ -33,6 +33,7 @@ values: - va_child_care_subsidies # Virginia Child Care Subsidy Program - wa_child_care_subsidies # Washington Working Connections Child Care - wv_child_care_subsidies # West Virginia Child Care Assistance Program + - nd_child_care_subsidies # North Dakota Child Care Assistance Program metadata: unit: list diff --git a/policyengine_us/parameters/gov/states/nd/dhs/ccap/age_group/infant_max_months.yaml b/policyengine_us/parameters/gov/states/nd/dhs/ccap/age_group/infant_max_months.yaml new file mode 100644 index 00000000000..638fe36ed30 --- /dev/null +++ b/policyengine_us/parameters/gov/states/nd/dhs/ccap/age_group/infant_max_months.yaml @@ -0,0 +1,15 @@ +description: North Dakota classifies children below this age in months as infants for reimbursement rates under the Child Care Assistance Program. + +# The rate-table column "Infant" covers birth through 17 months (under 18 +# months), matching the manual band of birth through the month the child turns +# 18 months (400-28-100-30). +values: + 2026-01-01: 18 + +metadata: + unit: month + period: year + label: North Dakota CCAP infant maximum age in months + reference: + - title: North Dakota CCAP Policy Manual, Basis for Allowable Child Care Rate of Payments 400-28-100-30 + href: https://www.nd.gov/dhs/policymanuals/40028/40028.htm diff --git a/policyengine_us/parameters/gov/states/nd/dhs/ccap/age_group/preschool_min_months.yaml b/policyengine_us/parameters/gov/states/nd/dhs/ccap/age_group/preschool_min_months.yaml new file mode 100644 index 00000000000..d9c3f7153d7 --- /dev/null +++ b/policyengine_us/parameters/gov/states/nd/dhs/ccap/age_group/preschool_min_months.yaml @@ -0,0 +1,14 @@ +description: North Dakota classifies children at or above this age in months as preschool age, until they reach school age, for reimbursement rates under the Child Care Assistance Program. + +# The rate-table column "Preschool" starts at three years (36 months) and runs +# until the child reaches school age (400-28-100-30). +values: + 2026-01-01: 36 + +metadata: + unit: month + period: year + label: North Dakota CCAP preschool minimum age in months + reference: + - title: North Dakota CCAP Policy Manual, Basis for Allowable Child Care Rate of Payments 400-28-100-30 + href: https://www.nd.gov/dhs/policymanuals/40028/40028.htm diff --git a/policyengine_us/parameters/gov/states/nd/dhs/ccap/age_group/toddler_max_months.yaml b/policyengine_us/parameters/gov/states/nd/dhs/ccap/age_group/toddler_max_months.yaml new file mode 100644 index 00000000000..e13409c5a36 --- /dev/null +++ b/policyengine_us/parameters/gov/states/nd/dhs/ccap/age_group/toddler_max_months.yaml @@ -0,0 +1,15 @@ +description: North Dakota classifies children below this age in months as toddlers for reimbursement rates under the Child Care Assistance Program. + +# The rate-table column "Toddler" covers 18 months through 35 months (under 36 +# months), matching the manual band of 18 months through the month the child +# turns three years old (400-28-100-30). +values: + 2026-01-01: 36 + +metadata: + unit: month + period: year + label: North Dakota CCAP toddler maximum age in months + reference: + - title: North Dakota CCAP Policy Manual, Basis for Allowable Child Care Rate of Payments 400-28-100-30 + href: https://www.nd.gov/dhs/policymanuals/40028/40028.htm diff --git a/policyengine_us/parameters/gov/states/nd/dhs/ccap/copay/max_rate.yaml b/policyengine_us/parameters/gov/states/nd/dhs/ccap/copay/max_rate.yaml new file mode 100644 index 00000000000..c7549b70cb8 --- /dev/null +++ b/policyengine_us/parameters/gov/states/nd/dhs/ccap/copay/max_rate.yaml @@ -0,0 +1,13 @@ +description: North Dakota caps the family monthly co-payment at this share of countable monthly income under the Child Care Assistance Program. +values: + 2022-10-01: 0.07 + +metadata: + unit: /1 + period: year + label: North Dakota CCAP family co-payment maximum rate + reference: + - title: DN 241 (10-2022) Sliding Fee Schedule, Child Care Assistance Program + href: https://ndlegis.gov/assembly/68-2023/testimony/SHUMSER-2190-20230118-13895-F-HOGAN_KATHY.pdf + - title: 45 CFR 98.45(k) + href: https://www.ecfr.gov/current/title-45/section-98.45#p-98.45(k) diff --git a/policyengine_us/parameters/gov/states/nd/dhs/ccap/copay/rate.yaml b/policyengine_us/parameters/gov/states/nd/dhs/ccap/copay/rate.yaml new file mode 100644 index 00000000000..ff2a04a98a2 --- /dev/null +++ b/policyengine_us/parameters/gov/states/nd/dhs/ccap/copay/rate.yaml @@ -0,0 +1,37 @@ +description: North Dakota sets the family monthly co-payment as this share of countable monthly income, by income as a share of the state median income, under the Child Care Assistance Program. + +# PENDING validation against current Power BI worksheet. North Dakota does not +# publish a static co-pay percentage schedule; the family monthly co-pay is +# computed through the HHS Power BI "Worksheet for Calculating Copay". The +# bracket structure below recovers the schedule documented in DN 241 (10-2022): +# the co-pay is waived at or below 30% SMI, then rises through the sliding-fee +# levels to a 7% ceiling above 40% SMI. Replace these percentages once the +# current Power BI values are confirmed. Brackets are keyed on income as a share +# of the state median income; each "above X%" threshold is shifted by +0.0001 so +# that income exactly at a band's ceiling stays in the lower band. + +brackets: + - threshold: + 2022-10-01: 0 + amount: + 2022-10-01: 0 # at or below 30% SMI: waived + - threshold: + 2022-10-01: 0.3001 # above 30% up to 40% SMI + amount: + 2022-10-01: 0.06 + - threshold: + 2022-10-01: 0.4001 # above 40% SMI (capped at 7%) + amount: + 2022-10-01: 0.07 + +metadata: + type: single_amount + threshold_unit: /1 + amount_unit: /1 + period: year + label: North Dakota CCAP family co-payment rate by SMI share + reference: + - title: North Dakota CCAP Policy Manual, Co-pay Determination 400-28-90-10 + href: https://www.nd.gov/dhs/policymanuals/40028/40028.htm + - title: DN 241 (10-2022) Sliding Fee Schedule, Child Care Assistance Program + href: https://ndlegis.gov/assembly/68-2023/testimony/SHUMSER-2190-20230118-13895-F-HOGAN_KATHY.pdf diff --git a/policyengine_us/parameters/gov/states/nd/dhs/ccap/copay/waiver_smi_threshold.yaml b/policyengine_us/parameters/gov/states/nd/dhs/ccap/copay/waiver_smi_threshold.yaml new file mode 100644 index 00000000000..e29df499ff4 --- /dev/null +++ b/policyengine_us/parameters/gov/states/nd/dhs/ccap/copay/waiver_smi_threshold.yaml @@ -0,0 +1,11 @@ +description: North Dakota waives the family co-payment for families with income at or below this share of the state median income under the Child Care Assistance Program. +values: + 2022-10-01: 0.3 + +metadata: + unit: /1 + period: year + label: North Dakota CCAP co-payment waiver SMI threshold + reference: + - title: North Dakota CCAP Policy Manual, Co-pay Waived 400-28-90-20 + href: https://www.nd.gov/dhs/policymanuals/40028/40028.htm diff --git a/policyengine_us/parameters/gov/states/nd/dhs/ccap/eligibility/child_age_limit.yaml b/policyengine_us/parameters/gov/states/nd/dhs/ccap/eligibility/child_age_limit.yaml new file mode 100644 index 00000000000..17c8c1f6668 --- /dev/null +++ b/policyengine_us/parameters/gov/states/nd/dhs/ccap/eligibility/child_age_limit.yaml @@ -0,0 +1,19 @@ +description: North Dakota limits eligibility to children below this age under the Child Care Assistance Program. + +# Children are eligible through the month of their 13th birthday (under 13). +# Effective April 1, 2026, eligibility runs through the month the child turns +# 12 (under 12), aligning to state law. The 2020 entry is placed at +# PolicyEngine's backdating floor. +values: + 2020-01-01: 13 + 2026-04-01: 12 + +metadata: + unit: year + period: year + label: North Dakota CCAP child age limit + reference: + - title: North Dakota CCAP Policy Manual, Eligible Children 400-28-35-02 + href: https://www.nd.gov/dhs/policymanuals/40028/40028.htm + - title: North Dakota HHS Child Care Assistance Program updates 2026 + href: https://www.hhs.nd.gov/ec-news/child-care-assistance-program-ccap-updates-2026 diff --git a/policyengine_us/parameters/gov/states/nd/dhs/ccap/eligibility/disabled_child_age_limit.yaml b/policyengine_us/parameters/gov/states/nd/dhs/ccap/eligibility/disabled_child_age_limit.yaml new file mode 100644 index 00000000000..c8d8d3fb950 --- /dev/null +++ b/policyengine_us/parameters/gov/states/nd/dhs/ccap/eligibility/disabled_child_age_limit.yaml @@ -0,0 +1,15 @@ +description: North Dakota limits eligibility to special-needs children below this age under the Child Care Assistance Program. + +# Children who are physically or mentally incapable of self-care, or in need of +# supervised care under a court order, are eligible through the month of their +# 19th birthday (under 19). +values: + 2020-01-01: 19 + +metadata: + unit: year + period: year + label: North Dakota CCAP special-needs child age limit + reference: + - title: North Dakota CCAP Policy Manual, Eligible Children 400-28-35-02 + href: https://www.nd.gov/dhs/policymanuals/40028/40028.htm diff --git a/policyengine_us/parameters/gov/states/nd/dhs/ccap/income/continuing_smi_rate.yaml b/policyengine_us/parameters/gov/states/nd/dhs/ccap/income/continuing_smi_rate.yaml new file mode 100644 index 00000000000..41f21d2ceee --- /dev/null +++ b/policyengine_us/parameters/gov/states/nd/dhs/ccap/income/continuing_smi_rate.yaml @@ -0,0 +1,13 @@ +description: North Dakota limits continuing eligibility to families with income at or below this share of the state median income under the Child Care Assistance Program. +values: + 2024-07-01: 0.85 + +metadata: + unit: /1 + period: year + label: North Dakota CCAP continuing income limit rate + reference: + - title: North Dakota CCAP Policy Manual, Graduated Eligibility 400-28-25-15 + href: https://www.nd.gov/dhs/policymanuals/40028/40028.htm + - title: North Dakota HHS Apply for Help, Child Care Assistance Program + href: https://www.hhs.nd.gov/applyforhelp/ccap diff --git a/policyengine_us/parameters/gov/states/nd/dhs/ccap/income/initial_smi_rate.yaml b/policyengine_us/parameters/gov/states/nd/dhs/ccap/income/initial_smi_rate.yaml new file mode 100644 index 00000000000..8dfa6cb8164 --- /dev/null +++ b/policyengine_us/parameters/gov/states/nd/dhs/ccap/income/initial_smi_rate.yaml @@ -0,0 +1,13 @@ +description: North Dakota limits initial eligibility to families with income at or below this share of the state median income under the Child Care Assistance Program. +values: + 2024-07-01: 0.75 + +metadata: + unit: /1 + period: year + label: North Dakota CCAP initial income limit rate + reference: + - title: North Dakota CCAP Policy Manual, Graduated Eligibility 400-28-25-15 + href: https://www.nd.gov/dhs/policymanuals/40028/40028.htm + - title: North Dakota HHS Apply for Help, Child Care Assistance Program + href: https://www.hhs.nd.gov/applyforhelp/ccap diff --git a/policyengine_us/parameters/gov/states/nd/dhs/ccap/income/sources.yaml b/policyengine_us/parameters/gov/states/nd/dhs/ccap/income/sources.yaml new file mode 100644 index 00000000000..39227e59c5c --- /dev/null +++ b/policyengine_us/parameters/gov/states/nd/dhs/ccap/income/sources.yaml @@ -0,0 +1,95 @@ +description: North Dakota counts these income sources as countable income under the Child Care Assistance Program. +values: + 2024-07-01: + # Earned income + - employment_income + - self_employment_income + # Unearned income + - rental_income + # social_security already includes the disability, retirement, survivors, + # and dependents components, so social_security_disability is not listed + # separately to avoid double-counting Social Security Disability Insurance. + - social_security + - unemployment_compensation + - workers_compensation + - veterans_benefits + # Regular periodic withdrawals from annuities, pensions, and other + # retirement plans are countable income (exclusion #8 exception). + - pension_income + - alimony_income + - child_support_received + + # ---------------------------------------------------------------------- + # The 46 income types excluded by 400-28-65-15 (NDAC 75-02-01.3-07) are + # listed here as comments. Most have no corresponding PolicyEngine income + # variable; those that do are deliberately omitted from the counted list. + # #1 Non-recurring lump-sum unearned payments (retroactive SSA/SSI/UI/ + # TANF/RRB/VA/WSI, inheritance, gambling winnings, settlements, + # mineral leasing bonuses, severance, trust income). + # #2 Earned income received as a non-recurring lump sum (bonuses, + # re-enlistment bonuses). + # #3 Tribal payments and Individual Indian Monies (IIM) accounts. + # #4 Reimbursements / third-party payments (HUD, HAP, ERAP, Tribal + # LIHEAP, General Assistance, TANF supportive services, medical, + # child care, employment/training, adoption assistance subsidies). + # #5 Child or spousal support of a TANF recipient assigned to the Child + # Support Division. + # #6 Children's earned income (excluded in the formula, not here). + # #7 In-kind income (goods, commodities, store credits, services). + # #8 Dividends and interest from savings, checking, and investments + # (so interest_income and dividend_income are NOT counted). The + # interest portion of regular annuity/pension/retirement withdrawals + # is counted and is captured by pension_income above. + # #9 Deposits where the client is only a signatory without ownership. + # #10 Cooperative distributions (patronage dividends). + # #11 Withdrawals from medical savings, HRA, and FSA accounts. + # #12 Foster care payments. + # #13 Subsidized guardianship payments. + # #14 Benefit / fundraiser money disbursed by a third party. + # #15 Federal benefits received as a representative payee for a + # non-household member. + # #16 Job Corps income, allowances, and bonuses. + # #17 Refunds of rental, storage, utility, or provider deposits. + # #18 Homestead Tax Credit refunds. + # #19 Property tax relief. + # #20 Loans that require repayment (written agreement). + # #21 Monies used for care of a non-household member. + # #22 Census income. + # #23 Trade Adjustment Assistance (TAA) payments. + # #24 PASS set-aside income (Title XVI / SSI). + # #25 Monetary gifts for special occasions. + # #26 Infrequent or irregular income (earned and unearned). + # #27 Gift cards and gift certificates. + # #28 National School Lunch Act programs (school lunch, summer food, + # commodity distribution, CACFP). + # #29 Child Nutrition Act programs (school breakfast, special milk, WIC). + # #30 Uniform Relocation Assistance payments. + # #31 National and Community Service Act volunteer payments (AmeriCorps, + # VISTA, NCCC, RSVP, Foster Grandparents, Senior Companion). + # #32 Disaster Relief Act assistance (FEMA, Red Cross, Salvation Army). + # #33 WIOA / Youthbuild allowances (counted only for those 19+). + # #34 LIHEAP payments. + # #35 SNAP benefits. + # #36 Child Care Assistance Program payments (provider receipts are + # self-employment income, already counted above). + # #37 Federally funded student financial assistance (Pell, SEOG, + # Stafford, Perkins, federal work-study, BIA grants); work-required + # internship/stipend income is counted as earned income. + # #38 Reduction in basic pay for veteran's educational assistance (MGIB). + # #39 Older Americans Act programs (Experience Works, etc.). + # #40-46 Remaining federally required disregards, including state/federal + # tax refunds and the Earned Income Tax Credit (#42) and combat pay + # (#46). + # TANF cash assistance is countable per the manual but is omitted here to + # avoid the Child Care Assistance Program to Temporary Assistance for Needy + # Families circular dependency through the dependent care deduction. + +metadata: + unit: list + period: year + label: North Dakota CCAP countable income sources + reference: + - title: North Dakota CCAP Policy Manual, Income Exclusions 400-28-65-15 + href: https://www.nd.gov/dhs/policymanuals/40028/40028.htm + - title: NDAC 75-02-01.3-07 + href: https://ndlegis.gov/prod/acdata/pdf/75-02-01.3.pdf diff --git a/policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/full_time.yaml b/policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/full_time.yaml new file mode 100644 index 00000000000..0067c229c44 --- /dev/null +++ b/policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/full_time.yaml @@ -0,0 +1,56 @@ +description: North Dakota provides these full-time monthly maximum reimbursement rates by provider type and age group under the Child Care Assistance Program. + +# Statewide flat full-time (25+ hours/week) monthly maximums effective +# January 1, 2026, from the 2025 Market Rate Survey. Infant and toddler rates +# are set at the 75th percentile of market rate; preschool and school-age rates +# at the 50th percentile. + +metadata: + period: month + unit: currency-USD + label: North Dakota CCAP full-time monthly maximum rates + breakdown: + - nd_ccap_provider_type + - nd_ccap_age_group + reference: + - title: North Dakota CCAP Policy Manual, Basis for Allowable Child Care Rate of Payments 400-28-100-30 + href: https://www.nd.gov/dhs/policymanuals/40028/40028.htm + - title: North Dakota HHS Child Care Assistance Program provider rates + href: https://www.hhs.nd.gov/human-services/providers/ccap + +CENTER: + INFANT: + 2026-01-01: 1_240 + TODDLER: + 2026-01-01: 1_124 + PRESCHOOL: + 2026-01-01: 940 + SCHOOL_AGE: + 2026-01-01: 800 +LICENSED_FAMILY: + INFANT: + 2026-01-01: 900 + TODDLER: + 2026-01-01: 880 + PRESCHOOL: + 2026-01-01: 740 + SCHOOL_AGE: + 2026-01-01: 700 +SELF_DECLARED_TRIBAL: + INFANT: + 2026-01-01: 646 + TODDLER: + 2026-01-01: 600 + PRESCHOOL: + 2026-01-01: 531 + SCHOOL_AGE: + 2026-01-01: 529 +APPROVED_RELATIVE: + INFANT: + 2026-01-01: 422 + TODDLER: + 2026-01-01: 398 + PRESCHOOL: + 2026-01-01: 351 + SCHOOL_AGE: + 2026-01-01: 348 diff --git a/policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/infant_toddler_bonus.yaml b/policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/infant_toddler_bonus.yaml new file mode 100644 index 00000000000..dfd223cdc2d --- /dev/null +++ b/policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/infant_toddler_bonus.yaml @@ -0,0 +1,27 @@ +description: North Dakota pays providers this additional monthly amount per infant or toddler under the Child Care Assistance Program. + +# Infant/toddler bonus, paid to North Dakota licensed, tribally licensed, or +# military licensed providers as a separate payment on top of the service-month +# subsidy, for children attending 40 or more hours per month. Effective +# January 1, 2026. Preschool and school-age children receive no bonus. + +metadata: + period: month + unit: currency-USD + label: North Dakota CCAP infant/toddler provider bonus + breakdown: + - nd_ccap_age_group + reference: + - title: North Dakota CCAP Policy Manual, Basis for Allowable Child Care Rate of Payments 400-28-100-30 + href: https://www.nd.gov/dhs/policymanuals/40028/40028.htm + - title: North Dakota HHS Child Care Assistance Program updates 2026 + href: https://www.hhs.nd.gov/ec-news/child-care-assistance-program-ccap-updates-2026 + +INFANT: + 2026-01-01: 200 +TODDLER: + 2026-01-01: 115 +PRESCHOOL: + 2026-01-01: 0 +SCHOOL_AGE: + 2026-01-01: 0 diff --git a/policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/infant_toddler_bonus_min_weekly_hours.yaml b/policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/infant_toddler_bonus_min_weekly_hours.yaml new file mode 100644 index 00000000000..04ea1ae6109 --- /dev/null +++ b/policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/infant_toddler_bonus_min_weekly_hours.yaml @@ -0,0 +1,18 @@ +description: North Dakota requires an infant or toddler to attend at least this many child care hours per week to qualify the provider for the infant/toddler bonus under the Child Care Assistance Program. + +# The bonus requires attendance of 40 or more hours per month. PolicyEngine +# tracks weekly child care hours, so the monthly threshold is approximated as +# 40 hours per month divided by the manual's 4.3 weeks-per-month conversion +# factor (400-28-70-05), giving roughly 9.3 hours per week. +values: + 2026-01-01: 9.3 + +metadata: + unit: hour + period: week + label: North Dakota CCAP infant/toddler bonus minimum weekly hours + reference: + - title: North Dakota HHS Child Care Assistance Program updates 2026 + href: https://www.hhs.nd.gov/ec-news/child-care-assistance-program-ccap-updates-2026 + - title: North Dakota CCAP Policy Manual, Income Conversion 400-28-70-05 + href: https://www.nd.gov/dhs/policymanuals/40028/40028.htm diff --git a/policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/part_time.yaml b/policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/part_time.yaml new file mode 100644 index 00000000000..71320755f51 --- /dev/null +++ b/policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/part_time.yaml @@ -0,0 +1,55 @@ +description: North Dakota provides these part-time monthly maximum reimbursement rates by provider type and age group under the Child Care Assistance Program. + +# Statewide flat part-time (less than 25 hours/week) monthly maximums effective +# January 1, 2026, from the 2025 Market Rate Survey. All part-time rates are set +# at the 50th percentile of market rate. + +metadata: + period: month + unit: currency-USD + label: North Dakota CCAP part-time monthly maximum rates + breakdown: + - nd_ccap_provider_type + - nd_ccap_age_group + reference: + - title: North Dakota CCAP Policy Manual, Basis for Allowable Child Care Rate of Payments 400-28-100-30 + href: https://www.nd.gov/dhs/policymanuals/40028/40028.htm + - title: North Dakota HHS Child Care Assistance Program provider rates + href: https://www.hhs.nd.gov/human-services/providers/ccap + +CENTER: + INFANT: + 2026-01-01: 546 + TODDLER: + 2026-01-01: 529 + PRESCHOOL: + 2026-01-01: 489 + SCHOOL_AGE: + 2026-01-01: 416 +LICENSED_FAMILY: + INFANT: + 2026-01-01: 416 + TODDLER: + 2026-01-01: 393 + PRESCHOOL: + 2026-01-01: 385 + SCHOOL_AGE: + 2026-01-01: 364 +SELF_DECLARED_TRIBAL: + INFANT: + 2026-01-01: 284 + TODDLER: + 2026-01-01: 283 + PRESCHOOL: + 2026-01-01: 276 + SCHOOL_AGE: + 2026-01-01: 275 +APPROVED_RELATIVE: + INFANT: + 2026-01-01: 186 + TODDLER: + 2026-01-01: 187 + PRESCHOOL: + 2026-01-01: 182 + SCHOOL_AGE: + 2026-01-01: 181 diff --git a/policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/qris_step_bonus_rate.yaml b/policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/qris_step_bonus_rate.yaml new file mode 100644 index 00000000000..bb8e4100298 --- /dev/null +++ b/policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/qris_step_bonus_rate.yaml @@ -0,0 +1,33 @@ +description: North Dakota pays providers this additional share of the state maximum rate by quality rating step under the Child Care Assistance Program. + +# Quality Rating and Improvement System (QRIS) step bonus, paid to providers as +# a separate payment on top of the service-month subsidy. Per 400-28-100-30 +# (revised 6/1/2023, ML #3819): step 2 = +5%, step 3 = +10%, step 4 = +15% of +# the state maximum rate. Effective January 1, 2026 the step 2 bonus is +# eliminated and steps 3 and 4 drop to +5% and +10%. + +metadata: + period: year + unit: /1 + label: North Dakota CCAP QRIS step bonus rate + breakdown: + - nd_ccap_provider_qris_step + reference: + - title: North Dakota CCAP Policy Manual, Basis for Allowable Child Care Rate of Payments 400-28-100-30 + href: https://www.nd.gov/dhs/policymanuals/40028/40028.htm + - title: North Dakota HHS Child Care Assistance Program updates 2026 + href: https://www.hhs.nd.gov/ec-news/child-care-assistance-program-ccap-updates-2026 + +STEP_1: + 2023-06-01: 0 +STEP_2: + 2023-06-01: 0.05 + 2026-01-01: 0 +STEP_3: + 2023-06-01: 0.1 + 2026-01-01: 0.05 +STEP_4: + 2023-06-01: 0.15 + 2026-01-01: 0.1 +UNRATED: + 2023-06-01: 0 diff --git a/policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/special_needs_multiplier.yaml b/policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/special_needs_multiplier.yaml new file mode 100644 index 00000000000..a6e0b184803 --- /dev/null +++ b/policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/special_needs_multiplier.yaml @@ -0,0 +1,11 @@ +description: North Dakota multiplies the state maximum rate by this factor for special-needs children attending a provider with a quality rating of step 2 or higher under the Child Care Assistance Program. +values: + 2023-06-01: 1.1 + +metadata: + unit: /1 + period: year + label: North Dakota CCAP special needs rate multiplier + reference: + - title: North Dakota CCAP Policy Manual, Basis for Allowable Child Care Rate of Payments 400-28-100-30 + href: https://www.nd.gov/dhs/policymanuals/40028/40028.htm diff --git a/policyengine_us/parameters/gov/states/nd/dhs/ccap/time_category/full_time_min_hours.yaml b/policyengine_us/parameters/gov/states/nd/dhs/ccap/time_category/full_time_min_hours.yaml new file mode 100644 index 00000000000..bb69c7b6b1a --- /dev/null +++ b/policyengine_us/parameters/gov/states/nd/dhs/ccap/time_category/full_time_min_hours.yaml @@ -0,0 +1,14 @@ +description: North Dakota classifies child care of at least this many hours per week as full-time under the Child Care Assistance Program. + +# Full-time level of care is 25 or more hours per week; part-time is 1 to fewer +# than 25 hours per week (400-28-80-50). +values: + 2020-01-01: 25 + +metadata: + unit: hour + period: week + label: North Dakota CCAP full-time minimum weekly hours + reference: + - title: North Dakota CCAP Policy Manual, Determining Level of Care 400-28-80-50 + href: https://www.nd.gov/dhs/policymanuals/40028/40028.htm diff --git a/policyengine_us/programs.yaml b/policyengine_us/programs.yaml index 7d90c813240..ca1289af387 100644 --- a/policyengine_us/programs.yaml +++ b/policyengine_us/programs.yaml @@ -654,6 +654,12 @@ programs: full_name: Georgia Childcare and Parent Services variable: ga_caps parameter_prefix: gov.states.ga.decal.caps + - state: ND + status: in_progress + name: North Dakota CCAP + full_name: North Dakota Child Care Assistance Program + variable: nd_ccap + parameter_prefix: gov.states.nd.dhs.ccap - id: head_start name: Head Start diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/copay/nd_ccap_copay.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/copay/nd_ccap_copay.yaml new file mode 100644 index 00000000000..78fdc1e67ae --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/copay/nd_ccap_copay.yaml @@ -0,0 +1,249 @@ +# Tests for nd_ccap_copay (SPMUnit, MONTH, USD). +# The family monthly co-payment is a share of countable monthly income by +# income-to-SMI band, capped at 7% of income, and waived at or below 30% SMI or +# for TANF recipients (400-28-90-10/-20; DN 241 10-2022). The bracket +# percentages are PENDING validation against the HHS Power BI worksheet (the +# copay/rate.yaml parameter); when those values change, update these expected +# copays together. +# 2-person ND monthly SMI for 2026 = $7,762.97 (30% = $2,328.89, 40% = $3,105.19). +# Countable income is set directly and the unit is made eligible so copay +# (defined_for = nd_ccap_eligible) is computed. + +- name: Case 1, income at or below 30% SMI, co-payment waived. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + person2: + age: 3 + spm_units: + spm_unit: + members: [person1, person2] + nd_ccap_countable_income: 2_000 # ratio 0.2576, at or below 30% SMI + nd_ccap_income_eligible: true + is_ccdf_asset_eligible: true + nd_ccap_activity_eligible: true + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_copay: 0 + +- name: Case 2, income exactly at 30% SMI, co-payment waived (at or below). + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + person2: + age: 3 + spm_units: + spm_unit: + members: [person1, person2] + nd_ccap_countable_income: 2_328.89 # exactly 30% SMI + nd_ccap_income_eligible: true + is_ccdf_asset_eligible: true + nd_ccap_activity_eligible: true + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_copay: 0 + +- name: Case 3, income in the 30-40% SMI band, 6% of income. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + person2: + age: 3 + spm_units: + spm_unit: + members: [person1, person2] + nd_ccap_countable_income: 2_717.04 # ratio 0.35 + nd_ccap_income_eligible: true + is_ccdf_asset_eligible: true + nd_ccap_activity_eligible: true + households: + household: + members: [person1, person2] + state_code: ND + output: + # 2,717.04 * 0.06 = 163.02 + nd_ccap_copay: 163.02 + +- name: Case 4, income above 40% SMI, capped at 7% of income. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + person2: + age: 3 + spm_units: + spm_unit: + members: [person1, person2] + nd_ccap_countable_income: 3_881.49 # ratio 0.50 + nd_ccap_income_eligible: true + is_ccdf_asset_eligible: true + nd_ccap_activity_eligible: true + households: + household: + members: [person1, person2] + state_code: ND + output: + # 3,881.49 * 0.07 = 271.70 + nd_ccap_copay: 271.70 + +- name: Case 5, TANF recipient, co-payment waived regardless of income. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + person2: + age: 3 + spm_units: + spm_unit: + members: [person1, person2] + nd_ccap_countable_income: 5_000 # well above 40% SMI + nd_ccap_income_eligible: true + is_ccdf_asset_eligible: true + nd_ccap_activity_eligible: true + is_tanf_enrolled: true + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_copay: 0 + +- name: Case 6, negative countable income produces no co-payment. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + person2: + age: 3 + spm_units: + spm_unit: + members: [person1, person2] + nd_ccap_countable_income: -5_000 # floored at zero in the formula + nd_ccap_income_eligible: true + is_ccdf_asset_eligible: true + nd_ccap_activity_eligible: true + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_copay: 0 + +- name: Case 7, zero countable income, co-payment waived. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + person2: + age: 3 + spm_units: + spm_unit: + members: [person1, person2] + nd_ccap_countable_income: 0 # ratio 0, well below 30% SMI + nd_ccap_income_eligible: true + is_ccdf_asset_eligible: true + nd_ccap_activity_eligible: true + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_copay: 0 + +- name: Case 8, income just above 30% SMI but at the band ceiling, still waived. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + person2: + age: 3 + spm_units: + spm_unit: + members: [person1, person2] + # ratio 0.3001 sits exactly on the 6% band threshold; the bracket + # applies 6% only strictly above 0.3001, so this lands in the 0% band. + nd_ccap_countable_income: 2_329.6672 + nd_ccap_income_eligible: true + is_ccdf_asset_eligible: true + nd_ccap_activity_eligible: true + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_copay: 0 + +- name: Case 9, income at exactly 40% SMI, still in the 6% band (not yet 7%). + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + person2: + age: 3 + spm_units: + spm_unit: + members: [person1, person2] + nd_ccap_countable_income: 3_105.1882 # exactly 40% SMI, ratio 0.40 + nd_ccap_income_eligible: true + is_ccdf_asset_eligible: true + nd_ccap_activity_eligible: true + households: + household: + members: [person1, person2] + state_code: ND + output: + # ratio 0.40 < 0.4001, so still the 6% band: 3,105.1882 * 0.06 = 186.31. + nd_ccap_copay: 186.31 + +- name: Case 10, income at the 40% band ceiling, still 6% (7% applies only above 40.01%). + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + person2: + age: 3 + spm_units: + spm_unit: + members: [person1, person2] + # ratio 0.4001 sits exactly on the 7% band threshold; the bracket + # applies 7% only strictly above 0.4001, so this stays in the 6% band. + nd_ccap_countable_income: 3_105.9644 + nd_ccap_income_eligible: true + is_ccdf_asset_eligible: true + nd_ccap_activity_eligible: true + households: + household: + members: [person1, person2] + state_code: ND + output: + # 3,105.9644 * 0.06 = 186.36 (the 7% cap does not yet bind). + nd_ccap_copay: 186.36 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_activity_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_activity_eligible.yaml new file mode 100644 index 00000000000..5cb288d51f8 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_activity_eligible.yaml @@ -0,0 +1,113 @@ +# Tests for nd_ccap_activity_eligible (SPMUnit, MONTH, bool). +# Every caretaker (head or spouse) must be in an allowable activity, and the +# unit must have at least one head or spouse (400-28-55-05). A caretaker who is +# available (not in an activity) makes the unit ineligible, the proxy for the +# two-caretaker availability rule (NDAC 75-02-01.3-11). is_tax_unit_head_or_spouse +# is set directly to isolate the all-caretakers-active aggregation. + +- name: Case 1, single working caretaker plus a child, eligible. + period: 2026-01 + input: + people: + person1: + age: 30 + employment_income: 24_000 + is_tax_unit_head_or_spouse: true + person2: + age: 3 + is_tax_unit_head_or_spouse: false + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_activity_eligible: true + +- name: Case 2, two caretakers both working, eligible. + period: 2026-01 + input: + people: + person1: + age: 32 + employment_income: 24_000 + is_tax_unit_head_or_spouse: true + person2: + age: 30 + employment_income: 18_000 + is_tax_unit_head_or_spouse: true + person3: + age: 3 + is_tax_unit_head_or_spouse: false + spm_units: + spm_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: ND + output: + nd_ccap_activity_eligible: true + +- name: Case 3, one of two caretakers not in an activity, ineligible. + period: 2026-01 + input: + people: + person1: + age: 32 + employment_income: 24_000 + is_tax_unit_head_or_spouse: true + person2: + age: 30 # no activity + is_tax_unit_head_or_spouse: true + person3: + age: 3 + is_tax_unit_head_or_spouse: false + spm_units: + spm_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: ND + output: + nd_ccap_activity_eligible: false + +- name: Case 4, single caretaker with no activity, ineligible. + period: 2026-01 + input: + people: + person1: + age: 30 # no earnings, school, TANF, or disability + is_tax_unit_head_or_spouse: true + person2: + age: 3 + is_tax_unit_head_or_spouse: false + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_activity_eligible: false + +- name: Case 5, no head or spouse in the unit, ineligible. + period: 2026-01 + input: + people: + person1: + age: 3 + is_tax_unit_head_or_spouse: false + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_activity_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_eligible.yaml new file mode 100644 index 00000000000..0e17b2d81b1 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_eligible.yaml @@ -0,0 +1,131 @@ +# Tests for nd_ccap_eligible (SPMUnit, MONTH, bool). +# The unit is eligible only when it has an eligible child AND is income +# eligible AND asset eligible AND activity eligible. Upstream booleans are set +# directly to truth-table each AND factor; the eligible child is driven by age. + +- name: Case 1, all four conditions met, eligible. + period: 2026-01 + input: + people: + person1: + age: 30 + person2: + age: 3 + spm_units: + spm_unit: + members: [person1, person2] + nd_ccap_income_eligible: true + is_ccdf_asset_eligible: true + nd_ccap_activity_eligible: true + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_eligible: true + +- name: Case 2, no eligible child (only adults), ineligible. + period: 2026-01 + input: + people: + person1: + age: 30 + person2: + age: 28 + spm_units: + spm_unit: + members: [person1, person2] + nd_ccap_income_eligible: true + is_ccdf_asset_eligible: true + nd_ccap_activity_eligible: true + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_eligible: false + +- name: Case 3, income ineligible, ineligible. + period: 2026-01 + input: + people: + person1: + age: 30 + person2: + age: 3 + spm_units: + spm_unit: + members: [person1, person2] + nd_ccap_income_eligible: false + is_ccdf_asset_eligible: true + nd_ccap_activity_eligible: true + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_eligible: false + +- name: Case 4, over asset limit, ineligible. + period: 2026-01 + input: + people: + person1: + age: 30 + person2: + age: 3 + spm_units: + spm_unit: + members: [person1, person2] + nd_ccap_income_eligible: true + is_ccdf_asset_eligible: false + nd_ccap_activity_eligible: true + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_eligible: false + +- name: Case 5, activity ineligible, ineligible. + period: 2026-01 + input: + people: + person1: + age: 30 + person2: + age: 3 + spm_units: + spm_unit: + members: [person1, person2] + nd_ccap_income_eligible: true + is_ccdf_asset_eligible: true + nd_ccap_activity_eligible: false + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_eligible: false + +- name: Case 6, otherwise-eligible family outside North Dakota, ineligible. + period: 2026-01 + input: + people: + person1: + age: 30 + person2: + age: 3 + spm_units: + spm_unit: + members: [person1, person2] + nd_ccap_income_eligible: true + is_ccdf_asset_eligible: true + nd_ccap_activity_eligible: true + households: + household: + members: [person1, person2] + state_code: MN + output: + # defined_for = StateCode.ND zeroes eligibility outside North Dakota. + nd_ccap_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_eligible_child.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_eligible_child.yaml new file mode 100644 index 00000000000..7d80a5bcd62 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_eligible_child.yaml @@ -0,0 +1,154 @@ +# Tests for nd_ccap_eligible_child (Person, MONTH, bool). +# A child is eligible if under the age limit AND a US citizen or lawful +# permanent resident (400-28-35-02, 400-28-50-25). The general age limit is +# 13 (under 13) through 2026-03-31 and 12 (under 12) from 2026-04-01; the +# special-needs / court-order limit is 19. Because test periods must be the +# first month or a whole year, the 12-limit era is exercised at 2027-01. + +- name: Case 1, citizen infant, eligible. + period: 2026-01 + input: + people: + person1: + age: 1 + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_eligible_child: true + +- name: Case 2, citizen age 12, under the 13 limit, eligible. + period: 2026-01 + input: + people: + person1: + age: 12 + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_eligible_child: true + +- name: Case 3, citizen age 13, at the 13 limit, ineligible. + period: 2026-01 + input: + people: + person1: + age: 13 + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_eligible_child: false + +- name: Case 4, age 12 eff 2027 (under-12 era), at the 12 limit, ineligible. + period: 2027-01 + input: + people: + person1: + age: 12 + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_eligible_child: false + +- name: Case 5, age 11 eff 2027 (under-12 era), under the 12 limit, eligible. + period: 2027-01 + input: + people: + person1: + age: 11 + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_eligible_child: true + +- name: Case 6, disabled age 15, under the 19 special-needs limit, eligible. + period: 2026-01 + input: + people: + person1: + age: 15 + is_disabled: true + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_eligible_child: true + +- name: Case 7, disabled age 19, at the 19 limit, ineligible. + period: 2026-01 + input: + people: + person1: + age: 19 + is_disabled: true + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_eligible_child: false + +- name: Case 8, non-disabled age 15, over the general limit, ineligible. + period: 2026-01 + input: + people: + person1: + age: 15 + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_eligible_child: false + +- name: Case 9, lawful permanent resident infant, eligible. + period: 2026-01 + input: + people: + person1: + age: 1 + immigration_status: LEGAL_PERMANENT_RESIDENT + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_eligible_child: true + +- name: Case 10, undocumented infant, immigration-ineligible. + period: 2026-01 + input: + people: + person1: + age: 1 + immigration_status: UNDOCUMENTED + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_eligible_child: false + +- name: Case 11, refugee infant, immigration-ineligible (narrower than CCDF). + period: 2026-01 + input: + people: + person1: + age: 1 + immigration_status: REFUGEE + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_eligible_child: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_income_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_income_eligible.yaml new file mode 100644 index 00000000000..4c43ad80bd6 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_income_eligible.yaml @@ -0,0 +1,148 @@ +# Tests for nd_ccap_income_eligible (SPMUnit, MONTH, bool). +# Initial applicants (is_nd_ccap_enrolled = false) are tested against 75% of +# the monthly state median income; enrolled recipients (is_nd_ccap_enrolled = +# true) are tested against 85% (400-28-25-15). Countable income is set +# directly to isolate the two-tier comparison. +# 2-person ND monthly SMI for 2026 (uprated) = $7,762.97/mo: +# 75% = $5,822.23/mo, 85% = $6,598.53/mo. +# nd_ccap_countable_income is a MONTH variable, so values are monthly. + +- name: Case 1, applicant just below 75% SMI, income eligible. + period: 2026-01 + input: + people: + person1: + age: 30 + person2: + age: 3 + spm_units: + spm_unit: + members: [person1, person2] + nd_ccap_countable_income: 5_800 # below 5,822.23 (75% SMI) + is_nd_ccap_enrolled: false + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_income_eligible: true + +- name: Case 2, applicant just above 75% SMI, not income eligible as applicant. + period: 2026-01 + input: + people: + person1: + age: 30 + person2: + age: 3 + spm_units: + spm_unit: + members: [person1, person2] + nd_ccap_countable_income: 5_900 # above 5,822.23 (75% SMI) + is_nd_ccap_enrolled: false + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_income_eligible: false + +- name: Case 3, enrolled family at the same 5,900 income, eligible under 85% tier. + period: 2026-01 + input: + people: + person1: + age: 30 + person2: + age: 3 + spm_units: + spm_unit: + members: [person1, person2] + nd_ccap_countable_income: 5_900 # above 75% but below 6,598.53 (85% SMI) + is_nd_ccap_enrolled: true + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_income_eligible: true + +- name: Case 4, enrolled family just above 85% SMI, not income eligible. + period: 2026-01 + input: + people: + person1: + age: 30 + person2: + age: 3 + spm_units: + spm_unit: + members: [person1, person2] + nd_ccap_countable_income: 6_700 # above 6,598.53 (85% SMI) + is_nd_ccap_enrolled: true + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_income_eligible: false + +- name: Case 5, applicant at exactly 75% SMI, income eligible (at or below). + period: 2026-01 + input: + people: + person1: + age: 30 + person2: + age: 3 + spm_units: + spm_unit: + members: [person1, person2] + nd_ccap_countable_income: 5_822.22 # at 75% SMI (5,822.23) + is_nd_ccap_enrolled: false + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_income_eligible: true + +- name: Case 6, enrolled family at exactly 85% SMI, income eligible (at or below). + period: 2026-01 + input: + people: + person1: + age: 30 + person2: + age: 3 + spm_units: + spm_unit: + members: [person1, person2] + nd_ccap_countable_income: 6_598.525 # exactly 85% SMI; <= passes + is_nd_ccap_enrolled: true + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_income_eligible: true + +- name: Case 7, enrolled family just above 85% SMI, not income eligible. + period: 2026-01 + input: + people: + person1: + age: 30 + person2: + age: 3 + spm_units: + spm_unit: + members: [person1, person2] + nd_ccap_countable_income: 6_598.53 # just above 85% SMI (6,598.525) + is_nd_ccap_enrolled: true + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_income_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_parent_in_eligible_activity.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_parent_in_eligible_activity.yaml new file mode 100644 index 00000000000..14831d63be2 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_parent_in_eligible_activity.yaml @@ -0,0 +1,90 @@ +# Tests for nd_ccap_parent_in_eligible_activity (Person, MONTH, bool). +# A parent is in an allowable activity with positive wages, nonzero +# self-employment income, full-time student status, TANF enrollment, or a +# disability; there is no minimum number of working hours (400-28-55-05). + +- name: Case 1, employed parent, in an eligible activity. + period: 2026-01 + input: + people: + person1: + age: 30 + employment_income: 24_000 # 2,000/mo + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_parent_in_eligible_activity: true + +- name: Case 2, self-employed with a business loss, still in an eligible activity. + period: 2026-01 + input: + people: + person1: + age: 30 + self_employment_income: -6_000 # loss still evidences self-employment + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_parent_in_eligible_activity: true + +- name: Case 3, full-time student, in an eligible activity. + period: 2026-01 + input: + people: + person1: + age: 22 + is_full_time_student: true + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_parent_in_eligible_activity: true + +- name: Case 4, disabled parent, in an eligible activity (incapacity). + period: 2026-01 + input: + people: + person1: + age: 40 + is_disabled: true + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_parent_in_eligible_activity: true + +- name: Case 5, TANF-enrolled parent, in an eligible activity. + period: 2026-01 + input: + people: + person1: + age: 30 + spm_units: + spm_unit: + members: [person1] + is_tanf_enrolled: true + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_parent_in_eligible_activity: true + +- name: Case 6, no earnings, school, TANF, or disability, not in an activity. + period: 2026-01 + input: + people: + person1: + age: 30 + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_parent_in_eligible_activity: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/income/nd_ccap_child_support_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/income/nd_ccap_child_support_deduction.yaml new file mode 100644 index 00000000000..766959d22cd --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/income/nd_ccap_child_support_deduction.yaml @@ -0,0 +1,67 @@ +# Tests for nd_ccap_child_support_deduction (SPMUnit, MONTH, USD). +# The only deduction from countable income is court-ordered child and spousal +# support paid by a counted unit member (400-28-65-30, NDAC 75-02-01.3-09), +# modeled as child_support_expense plus alimony_expense. These are annual +# person-level inputs read with the bare monthly period, so Core auto-divides +# them to monthly amounts. + +- name: Case 1, no support paid, no deduction. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + person2: + age: 3 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_child_support_deduction: 0 + +- name: Case 2, court-ordered child support paid. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + child_support_expense: 12_000 # 1,000/mo + person2: + age: 3 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_child_support_deduction: 1_000 + +- name: Case 3, child support and spousal support both deducted. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + child_support_expense: 12_000 # 1,000/mo + alimony_expense: 6_000 # 500/mo + person2: + age: 3 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: ND + output: + # (12,000 + 6,000) / 12 = 1,500 + nd_ccap_child_support_deduction: 1_500 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/income/nd_ccap_countable_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/income/nd_ccap_countable_income.yaml new file mode 100644 index 00000000000..d4d764636dc --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/income/nd_ccap_countable_income.yaml @@ -0,0 +1,110 @@ +# Tests for nd_ccap_countable_income (SPMUnit, MONTH, USD). +# Countable income is gross countable income minus the earned income of +# household members under 18 and minus the court-ordered child/spousal support +# deduction, floored at zero (400-28-65-15, 400-28-65-30). Annual person-level +# income inputs are read with the bare monthly period, so Core auto-divides +# them to monthly amounts. + +- name: Case 1, single earner, gross equals countable. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + employment_income: 36_000 # 3,000/mo + person2: + age: 10 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_countable_income: 3_000 + +- name: Case 2, minor's earned income is excluded. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + employment_income: 36_000 # 3,000/mo + person2: + age: 10 + employment_income: 6_000 # 500/mo, excluded as a minor's earnings + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: ND + output: + # 3,000 + 500 - 500 (minor earnings excluded) = 3,000 + nd_ccap_countable_income: 3_000 + +- name: Case 3, court-ordered child support paid is deducted. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + employment_income: 36_000 # 3,000/mo + child_support_expense: 12_000 # 1,000/mo deducted + person2: + age: 10 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: ND + output: + # 3,000 - 1,000 = 2,000 + nd_ccap_countable_income: 2_000 + +- name: Case 4, Social Security counts as unearned income. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + social_security: 24_000 # 2,000/mo + person2: + age: 10 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_countable_income: 2_000 + +- name: Case 5, negative self-employment income is floored at zero. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + self_employment_income: -120_000 # -10,000/mo + person2: + age: 10 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_countable_income: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/integration.yaml new file mode 100644 index 00000000000..21a49fbfa8d --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/integration.yaml @@ -0,0 +1,514 @@ +# Integration tests for North Dakota CCAP (Child Care Assistance Program). +# End-to-end scenarios verifying the eligibility chain, the two-tier income +# test, the state maximum rate lookup, the co-payment, the special-needs +10% +# raise inside the expense cap, and the additive QRIS step and infant/toddler +# provider bonuses that flow into the final nd_ccap and into the federal +# child_care_subsidies total. +# +# All monetary rates are the statewide flat monthly maximums effective +# 2026-01-01. 2-person ND monthly SMI for 2026 = $7,762.97 (75% = $5,822.23, +# 85% = $6,598.53, 30% = $2,328.89, 40% = $3,105.19). The co-pay bracket +# percentages (6% in the 30-40% band, 7% above 40%) are PENDING validation +# against the HHS Power BI worksheet; update these expectations together with +# the copay/rate.yaml parameter. + +- name: Case 1, single working parent, infant in a step-3 center, positive subsidy. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + employment_income: 30_000 # 2,500/mo + person2: + age: 0.5 + nd_ccap_provider_type: CENTER + nd_ccap_provider_qris_step: STEP_3 + childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_pre_subsidy_childcare_expenses: 24_000 # 2,000/mo + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_eligible: true + # countable income 2,500/mo; ratio 0.322 -> 6% band; copay 2,500 * 0.06 = 150. + # CENTER infant full-time rate 1,240; expenses 2,000 capped at 1,240. + # base = 1,240 - 150 = 1,090. + # QRIS step 3 (2026) bonus 1,240 * 0.05 = 62; infant bonus 200. + # nd_ccap = 1,090 + 62 + 200 = 1,352. + nd_ccap: 1_352 + # nd_child_care_subsidies is the YEAR aggregator (adds nd_ccap); read at a + # month period it reports the monthly value 1,352, confirming nd_ccap flows + # into the federal child_care_subsidies total. + nd_child_care_subsidies: 1_352 + +- name: Case 2, applicant over 75% SMI is ineligible but the same family enrolled qualifies. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + employment_income: 74_400 # 6,200/mo, between 75% and 85% SMI + person2: + age: 0.5 + nd_ccap_provider_type: CENTER + childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_pre_subsidy_childcare_expenses: 24_000 # 2,000/mo + is_nd_ccap_enrolled: false + households: + household: + members: [person1, person2] + state_code: ND + output: + # 6,200/mo > 75% SMI (5,822.23), so an applicant fails the income test. + nd_ccap_income_eligible: false + nd_ccap_eligible: false + nd_ccap: 0 + +- name: Case 3, same family enrolled qualifies under the 85% tier. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + employment_income: 74_400 # 6,200/mo, below 85% SMI (6,598.53) + person2: + age: 0.5 + nd_ccap_provider_type: CENTER + childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_pre_subsidy_childcare_expenses: 24_000 # 2,000/mo + is_nd_ccap_enrolled: true + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_income_eligible: true + nd_ccap_eligible: true + # ratio 0.799 > 40% -> copay capped at 7%: 6,200 * 0.07 = 434. + # CENTER infant full-time 1,240; expenses 2,000 capped at 1,240. + # base = 1,240 - 434 = 806; infant bonus 200; nd_ccap = 1,006. + nd_ccap: 1_006 + +- name: Case 4, family at or below 30% SMI has the co-payment waived. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + employment_income: 24_000 # 2,000/mo, ratio 0.258 at or below 30% SMI + person2: + age: 0.5 + nd_ccap_provider_type: CENTER + childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_pre_subsidy_childcare_expenses: 7_200 # 600/mo, below the max rate + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_eligible: true + nd_ccap_copay: 0 + # base is capped at the family's billed expenses 600 (< max rate 1,240), + # copay 0; infant bonus 200; nd_ccap = 600 + 200 = 800. + nd_ccap_base_subsidy: 600 + nd_ccap: 800 + +- name: Case 5, family above 40% SMI is capped at a 7% co-payment. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + employment_income: 48_000 # 4,000/mo, ratio 0.515 above 40% SMI + person2: + age: 0.5 + nd_ccap_provider_type: CENTER + childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_pre_subsidy_childcare_expenses: 24_000 # 2,000/mo + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_eligible: true + # copay capped at 7%: 4,000 * 0.07 = 280. + nd_ccap_copay: 280 + # base = 1,240 - 280 = 960; infant bonus 200; nd_ccap = 1,160. + nd_ccap: 1_160 + +- name: Case 6, special-needs +10% raises the rate ceiling above the base case. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + employment_income: 24_000 # 2,000/mo, copay waived (<= 30% SMI) + person2: + age: 0.5 + is_disabled: true + nd_ccap_provider_type: CENTER + nd_ccap_provider_qris_step: STEP_2 + childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_pre_subsidy_childcare_expenses: 15_600 # 1,300/mo, between 1,240 and 1,364 + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_eligible: true + # disabled child at QRIS step 2: state max rate 1,240 * 1.10 = 1,364. + # expenses 1,300 cap the rate, so base = min(1,364, 1,300) - 0 = 1,300. + # QRIS step 2 bonus is 0 in 2026; infant bonus 200; nd_ccap = 1,500. + nd_ccap_state_max_rate: [0, 1_364] + nd_ccap: 1_500 + +- name: Case 7, the same family without special needs receives a smaller subsidy. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + employment_income: 24_000 # 2,000/mo, copay waived + person2: + age: 0.5 + nd_ccap_provider_type: CENTER + nd_ccap_provider_qris_step: STEP_2 + childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_pre_subsidy_childcare_expenses: 15_600 # 1,300/mo + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_eligible: true + # no +10%: state max rate 1,240; expenses 1,300 do not bind; base = 1,240. + # infant bonus 200; nd_ccap = 1,440 (less than the 1,500 special-needs case). + nd_ccap: 1_440 + +- name: Case 8, QRIS step-4 bonus and toddler bonus add on top of an expense-capped base. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + employment_income: 24_000 # 2,000/mo, copay waived + person2: + age: 2 + nd_ccap_provider_type: CENTER + nd_ccap_provider_qris_step: STEP_4 + childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_pre_subsidy_childcare_expenses: 6_000 # 500/mo, well below the rates + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_eligible: true + # CENTER toddler full-time rate 1,124; expenses 500 cap the base at 500. + # base = 500 - 0 = 500. + nd_ccap_base_subsidy: 500 + # bonuses are NOT capped at expenses and NOT reduced by copay: + # QRIS step 4 (2026) 1,124 * 0.10 = 112.4; toddler bonus 115. + # nd_ccap = 500 + 112.4 + 115 = 727.4 (exceeds the 500 expense cap). + nd_ccap: 727.4 + +- name: Case 9, ineligible family with income above 85% SMI receives nothing. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + employment_income: 90_000 # 7,500/mo, above 85% SMI + person2: + age: 0.5 + nd_ccap_provider_type: CENTER + childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_pre_subsidy_childcare_expenses: 24_000 + is_nd_ccap_enrolled: true + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_income_eligible: false + nd_ccap_eligible: false + nd_ccap: 0 + +- name: Case 10, family with no eligible child (only adults) receives nothing. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + employment_income: 24_000 + person2: + age: 25 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_pre_subsidy_childcare_expenses: 24_000 + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_eligible: false + nd_ccap: 0 + +- name: Case 11, negative self-employment income does not inflate the subsidy. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + self_employment_income: -120_000 # -10,000/mo loss; still an eligible activity + person2: + age: 0.5 + nd_ccap_provider_type: CENTER + childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_pre_subsidy_childcare_expenses: 24_000 # 2,000/mo + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_eligible: true + # countable income floored at 0, so copay 0 and income eligible. + nd_ccap_countable_income: 0 + nd_ccap_copay: 0 + # base = min(1,240, 2,000) - 0 = 1,240; infant bonus 200; nd_ccap = 1,440. + nd_ccap: 1_440 + +- name: Case 12, otherwise-eligible family outside North Dakota receives nothing. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + employment_income: 24_000 + person2: + age: 0.5 + nd_ccap_provider_type: CENTER + childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_pre_subsidy_childcare_expenses: 24_000 + households: + household: + members: [person1, person2] + state_code: MN + output: + # defined_for = StateCode.ND zeroes the benefit and eligibility outside ND. + nd_ccap_eligible: false + nd_ccap: 0 + +- name: Case 13, zero billed expenses, base subsidy is zero but the bonuses still pay. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + employment_income: 24_000 # 2,000/mo, copay waived (<= 30% SMI) + person2: + age: 0.5 + nd_ccap_provider_type: CENTER + nd_ccap_provider_qris_step: STEP_3 + childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_pre_subsidy_childcare_expenses: 0 # no billed expenses + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_eligible: true + # base = min(rate 1,240, expenses 0) - 0 = 0; the expense cap zeroes it. + nd_ccap_base_subsidy: 0 + # the provider bonuses are NOT capped at expenses: QRIS step 3 (2026) + # 1,240 * 0.05 = 62; infant bonus 200. nd_ccap = 0 + 62 + 200 = 262. + nd_ccap: 262 + +- name: Case 14, large eight-person family with two infants and a toddler in care. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + employment_income: 30_000 # 2,500/mo + person2: + age: 33 + employment_income: 18_000 # 1,500/mo + person3: + age: 0.5 + nd_ccap_provider_type: CENTER + childcare_hours_per_week: 40 + person4: + age: 1.0 + nd_ccap_provider_type: CENTER + childcare_hours_per_week: 40 + person5: + age: 2.0 + nd_ccap_provider_type: CENTER + childcare_hours_per_week: 40 + person6: + age: 6.0 + is_in_k12_school: true + person7: + age: 8.0 + is_in_k12_school: true + person8: + age: 10.0 + is_in_k12_school: true + spm_units: + spm_unit: + members: + [person1, person2, person3, person4, person5, person6, person7, person8] + spm_unit_pre_subsidy_childcare_expenses: 120_000 # 10,000/mo, does not bind + households: + household: + members: + [person1, person2, person3, person4, person5, person6, person7, person8] + state_code: ND + output: + nd_ccap_eligible: true + # countable income 4,000/mo; an eight-person family has a much higher SMI, + # so the ratio is well below 30% and the co-payment is waived. + nd_ccap_copay: 0 + # in-care rates: two infants 1,240 + one toddler 1,124 = 3,604. The three + # school-age children carry the default Center part-time rate 416 each + # (no "not in care" flag), so the pooled cap is 3,604 + 3 * 416 = 4,852; + # expenses 10,000 do not bind. base = 4,852 - 0 = 4,852. + nd_ccap_base_subsidy: 4_852 + # infant/toddler bonuses: 200 + 200 + 115 = 515; no QRIS step set. + # nd_ccap = 4,852 + 515 = 5,367. + nd_ccap: 5_367 + +- name: Case 15, disabled infant at a QRIS step-4 center stacks special needs and bonuses. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + employment_income: 24_000 # 2,000/mo, copay waived + person2: + age: 0.5 + is_disabled: true + nd_ccap_provider_type: CENTER + nd_ccap_provider_qris_step: STEP_4 + childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_pre_subsidy_childcare_expenses: 24_000 # 2,000/mo, does not bind + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_eligible: true + # special-needs +10% raises the rate ceiling: 1,240 * 1.10 = 1,364. + nd_ccap_state_max_rate: [0, 1_364] + # base = min(1,364, 2,000) - 0 = 1,364. + nd_ccap_base_subsidy: 1_364 + # the QRIS bonus uses the base rate excluding the +10%, so it does not + # compound: 1,240 * 0.10 = 124 (not 1,364 * 0.10); infant bonus 200. + # nd_ccap = 1,364 + 124 + 200 = 1,688. + nd_ccap: 1_688 + +- name: Case 16, single adult with no child is ineligible (no eligible child). + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + employment_income: 24_000 + spm_units: + spm_unit: + members: [person1] + spm_unit_pre_subsidy_childcare_expenses: 12_000 + households: + household: + members: [person1] + state_code: ND + output: + # A one-person unit has no eligible child, so the unit is ineligible. + nd_ccap_eligible: false + nd_ccap: 0 + +- name: Case 17, very high income family far above 85% SMI receives nothing. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + employment_income: 600_000 # 50,000/mo, far above 85% SMI + person2: + age: 0.5 + nd_ccap_provider_type: CENTER + nd_ccap_provider_qris_step: STEP_4 + childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_pre_subsidy_childcare_expenses: 24_000 + is_nd_ccap_enrolled: true # even the enrolled 85% tier fails + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_income_eligible: false + nd_ccap_eligible: false + # no subsidy and no provider bonuses flow when the unit is ineligible. + nd_ccap: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_age_group.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_age_group.yaml new file mode 100644 index 00000000000..28d5951c1dc --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_age_group.yaml @@ -0,0 +1,145 @@ +# Tests for nd_ccap_age_group (Person, MONTH, Enum). +# Age groups follow the HHS rate-table headers, derived from age in months +# (age * 12) per 400-28-100-30 / the rate table: +# INFANT: birth through 17 months (< 18) +# TODDLER: 18 through 35 months (< 36) +# PRESCHOOL: 3 years and older (>= 36 months) and not in K-12 school +# SCHOOL_AGE: in K-12 school (default) +# Preschool vs school age is distinguished by is_in_k12_school, not age alone +# (RI / IN pattern), so a child age 6+ who is not flagged as in school maps to +# preschool, and the school-age cases set is_in_k12_school. + +- name: Case 1, age 0, infant. + period: 2026-01 + input: + people: + person1: + age: 0 + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_age_group: INFANT + +- name: Case 2, age 1 (12 months), still infant. + period: 2026-01 + input: + people: + person1: + age: 1.0 + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_age_group: INFANT + +- name: Case 3, exactly 17 months (1.4166), still infant. + period: 2026-01 + input: + people: + person1: + age: 1.4166 # 17 months, < 18 infant cutoff + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_age_group: INFANT + +- name: Case 4, exactly 18 months (1.5), switches to toddler. + period: 2026-01 + input: + people: + person1: + age: 1.5 # 18 months, infant cutoff is < 18 so this is toddler + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_age_group: TODDLER + +- name: Case 5, exactly 35 months (2.9166), still toddler. + period: 2026-01 + input: + people: + person1: + age: 2.9166 # 35 months, < 36 toddler cutoff + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_age_group: TODDLER + +- name: Case 6, exactly 36 months (3.0), switches to preschool. + period: 2026-01 + input: + people: + person1: + age: 3.0 # 36 months, toddler cutoff is < 36 so this is preschool + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_age_group: PRESCHOOL + +- name: Case 7, age 4, preschool (not in school). + period: 2026-01 + input: + people: + person1: + age: 4.0 # is_in_k12_school imputes school only from age 5, so age 4 is preschool + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_age_group: PRESCHOOL + +- name: Case 7b, age 6 explicitly not in school, preschool. + period: 2026-01 + input: + people: + person1: + age: 6.0 + is_in_k12_school: false # override the age-5+ school imputation + households: + household: + members: [person1] + state_code: ND + output: + # Preschool runs from 3 years until the child is in K-12 school, so a + # 6-year-old not yet in school is classified preschool by the rate table. + nd_ccap_age_group: PRESCHOOL + +- name: Case 8, age 7 in K-12 school, school age. + period: 2026-01 + input: + people: + person1: + age: 7.0 + is_in_k12_school: true + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_age_group: SCHOOL_AGE + +- name: Case 9, age 10 in K-12 school, school age. + period: 2026-01 + input: + people: + person1: + age: 10.0 + is_in_k12_school: true + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_age_group: SCHOOL_AGE diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_base_subsidy.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_base_subsidy.yaml new file mode 100644 index 00000000000..8cf31958ea7 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_base_subsidy.yaml @@ -0,0 +1,84 @@ +# Tests for nd_ccap_base_subsidy (SPMUnit, MONTH, USD). +# The base subsidy is the lesser of the summed per-child state maximum rates +# and the family's billed child care expenses, minus the co-payment, floored at +# zero (400-28-100-05). The special-needs +10% is already inside the state +# maximum rate, so it sits within the expense cap. The two provider bonuses are +# excluded from the base subsidy. defined_for = nd_ccap_eligible, so the unit +# must be eligible (a working parent with an eligible child in North Dakota). + +- name: Case 1, rate-limited base, co-payment subtracted. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + employment_income: 48_000 # 4,000/mo, ratio 0.515 -> copay 280 + person2: + age: 0.5 + nd_ccap_provider_type: CENTER + childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_pre_subsidy_childcare_expenses: 24_000 # 2,000/mo, above the rate + households: + household: + members: [person1, person2] + state_code: ND + output: + # rate 1,240 < expenses 2,000, so capped at 1,240; copay 280. + # base = 1,240 - 280 = 960. + nd_ccap_base_subsidy: 960 + +- name: Case 2, expense-limited base, co-payment waived. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + employment_income: 24_000 # 2,000/mo, copay waived (<= 30% SMI) + person2: + age: 0.5 + nd_ccap_provider_type: CENTER + childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_pre_subsidy_childcare_expenses: 7_200 # 600/mo, below the rate + households: + household: + members: [person1, person2] + state_code: ND + output: + # expenses 600 < rate 1,240, so capped at 600; copay 0. + # base = 600 - 0 = 600. + nd_ccap_base_subsidy: 600 + +- name: Case 3, co-payment exceeds the capped rate, base floored at zero. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + employment_income: 60_000 # 5,000/mo, ratio 0.644 -> copay 350 + person2: + age: 8 + is_in_k12_school: true + nd_ccap_provider_type: APPROVED_RELATIVE + childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_pre_subsidy_childcare_expenses: 24_000 # 2,000/mo + is_nd_ccap_enrolled: true # 5,000/mo passes the 85% tier + households: + household: + members: [person1, person2] + state_code: ND + output: + # approved-relative school-age full-time rate 348; copay 5,000 * 0.07 = 350. + # base = max(min(2,000, 348) - 350, 0) = max(-2, 0) = 0. + nd_ccap_base_subsidy: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_infant_toddler_bonus.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_infant_toddler_bonus.yaml new file mode 100644 index 00000000000..815f8ea710f --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_infant_toddler_bonus.yaml @@ -0,0 +1,121 @@ +# Tests for nd_ccap_infant_toddler_bonus (Person, MONTH, USD). +# A flat per-child provider payment of $200 for an infant or $115 for a toddler +# attending 40 or more hours per month, paid to providers other than approved +# relatives (400-28-100-30; HHS 2026 update). Effective 2026-01-01. The 40 +# hours/month condition is approximated as 9.3 child care hours per week. +# Preschool and school-age children receive no bonus. defined_for = +# nd_ccap_eligible_child. + +- name: Case 1, infant attending 40+ hours, $200 bonus. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 0.5 + nd_ccap_provider_type: CENTER + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_infant_toddler_bonus: 200 + +- name: Case 2, toddler attending 40+ hours, $115 bonus. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 2 + nd_ccap_provider_type: CENTER + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_infant_toddler_bonus: 115 + +- name: Case 3, infant below the weekly-hours threshold, no bonus. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 0.5 + nd_ccap_provider_type: CENTER + childcare_hours_per_week: 9.0 # below the 9.3 approximation of 40 hours/month + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_infant_toddler_bonus: 0 + +- name: Case 4, infant at exactly the weekly-hours threshold, $200 bonus. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 0.5 + nd_ccap_provider_type: CENTER + childcare_hours_per_week: 9.3 # at the threshold + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_infant_toddler_bonus: 200 + +- name: Case 5, infant at an approved-relative provider, no bonus. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 0.5 + nd_ccap_provider_type: APPROVED_RELATIVE + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: ND + output: + # Approved relatives are not eligible for the licensed-provider bonus. + nd_ccap_infant_toddler_bonus: 0 + +- name: Case 6, preschool child, no bonus. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 4 + nd_ccap_provider_type: CENTER + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_infant_toddler_bonus: 0 + +- name: Case 7, school-age child, no bonus. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 8 + is_in_k12_school: true + nd_ccap_provider_type: CENTER + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_infant_toddler_bonus: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_provider_qris_step.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_provider_qris_step.yaml new file mode 100644 index 00000000000..96bd1e19088 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_provider_qris_step.yaml @@ -0,0 +1,73 @@ +# Tests for nd_ccap_provider_qris_step (Person, MONTH, Enum input). +# This is an input with no formula; the cases confirm every QRIS step value +# (STEP_1..STEP_4 and the UNRATED default) round-trips. The step drives the +# QRIS bonus rate and gates the special-needs +10% (400-28-100-30). + +- name: Case 1, step 1. + period: 2026-01 + input: + people: + person1: + age: 3 + nd_ccap_provider_qris_step: STEP_1 + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_provider_qris_step: STEP_1 + +- name: Case 2, step 2. + period: 2026-01 + input: + people: + person1: + age: 3 + nd_ccap_provider_qris_step: STEP_2 + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_provider_qris_step: STEP_2 + +- name: Case 3, step 3. + period: 2026-01 + input: + people: + person1: + age: 3 + nd_ccap_provider_qris_step: STEP_3 + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_provider_qris_step: STEP_3 + +- name: Case 4, step 4. + period: 2026-01 + input: + people: + person1: + age: 3 + nd_ccap_provider_qris_step: STEP_4 + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_provider_qris_step: STEP_4 + +- name: Case 5, unrated default when not set. + period: 2026-01 + input: + people: + person1: + age: 3 + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_provider_qris_step: UNRATED diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_qris_step_bonus.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_qris_step_bonus.yaml new file mode 100644 index 00000000000..893f5bcd4be --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_qris_step_bonus.yaml @@ -0,0 +1,172 @@ +# Tests for nd_ccap_qris_step_bonus (Person, MONTH, USD). +# The QRIS step bonus is a separate provider payment equal to the base state +# maximum rate (full-time or part-time, excluding the special-needs +10%) +# times the quality-step bonus rate (400-28-100-30). It is date-varying: +# 2023-06-01 era: step 2 = +5%, step 3 = +10%, step 4 = +15% +# 2026-01-01 era: step 2 = +0%, step 3 = +5%, step 4 = +10% +# The 2026 monthly rate table backfills to the 2025 era (the pre-2026 rate +# schedule is not separately available), so the 2025 cases use the same base +# rate but the earlier bonus percentages. Center infant full-time base = 1,240; +# part-time base = 546. defined_for = nd_ccap_eligible_child. + +# --- 2026 era (step 2 eliminated; step 3 +5%, step 4 +10%) --- + +- name: Case 1, step 1 has no bonus. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 0.5 + nd_ccap_provider_type: CENTER + nd_ccap_provider_qris_step: STEP_1 + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_qris_step_bonus: 0 + +- name: Case 2, step 2 eliminated in 2026, no bonus. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 0.5 + nd_ccap_provider_type: CENTER + nd_ccap_provider_qris_step: STEP_2 + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_qris_step_bonus: 0 + +- name: Case 3, step 3 in 2026 is +5% of the base rate. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 0.5 + nd_ccap_provider_type: CENTER + nd_ccap_provider_qris_step: STEP_3 + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: ND + output: + # 1,240 * 0.05 = 62 + nd_ccap_qris_step_bonus: 62 + +- name: Case 4, step 4 in 2026 is +10% of the base rate. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 0.5 + nd_ccap_provider_type: CENTER + nd_ccap_provider_qris_step: STEP_4 + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: ND + output: + # 1,240 * 0.10 = 124 + nd_ccap_qris_step_bonus: 124 + +- name: Case 5, unrated provider has no bonus. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 0.5 + nd_ccap_provider_type: CENTER + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_qris_step_bonus: 0 + +- name: Case 6, step 4 part-time uses the part-time base rate. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 0.5 + nd_ccap_provider_type: CENTER + nd_ccap_provider_qris_step: STEP_4 + childcare_hours_per_week: 20 # part-time + households: + household: + members: [person1] + state_code: ND + output: + # part-time base 546 * 0.10 = 54.6 + nd_ccap_qris_step_bonus: 54.6 + +# --- 2025 era (step 2 +5%, step 3 +10%, step 4 +15%) --- + +- name: Case 7, step 2 in 2025 is +5% of the base rate. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 0.5 + nd_ccap_provider_type: CENTER + nd_ccap_provider_qris_step: STEP_2 + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: ND + output: + # 1,240 * 0.05 = 62 + nd_ccap_qris_step_bonus: 62 + +- name: Case 8, step 3 in 2025 is +10% of the base rate. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 0.5 + nd_ccap_provider_type: CENTER + nd_ccap_provider_qris_step: STEP_3 + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: ND + output: + # 1,240 * 0.10 = 124 + nd_ccap_qris_step_bonus: 124 + +- name: Case 9, step 4 in 2025 is +15% of the base rate. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 0.5 + nd_ccap_provider_type: CENTER + nd_ccap_provider_qris_step: STEP_4 + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: ND + output: + # 1,240 * 0.15 = 186 + nd_ccap_qris_step_bonus: 186 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_state_max_rate.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_state_max_rate.yaml new file mode 100644 index 00000000000..a00311f442d --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_state_max_rate.yaml @@ -0,0 +1,304 @@ +# Tests for nd_ccap_state_max_rate (Person, MONTH, USD). +# The monthly state maximum rate is looked up by provider type, age group, and +# level of care (full-time vs part-time), then multiplied by 1.10 for a +# disabled child at a QRIS step-2-or-higher provider (400-28-100-30). Rates are +# the statewide flat monthly maximums effective 2026-01-01. defined_for = +# nd_ccap_eligible_child, so each child is a citizen within the age limit. +# Full-time requires 25+ child care hours per week; part-time is under 25. +# Every provider type and age group is exercised at least once. + +# --- Full-time, every provider type and age group --- + +- name: Case 1, Center infant full-time. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 0.5 + nd_ccap_provider_type: CENTER + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_state_max_rate: 1_240 + +- name: Case 2, Center toddler full-time. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 2 + nd_ccap_provider_type: CENTER + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_state_max_rate: 1_124 + +- name: Case 3, Center preschool full-time. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 4 + nd_ccap_provider_type: CENTER + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_state_max_rate: 940 + +- name: Case 4, Center school-age full-time. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 8 + is_in_k12_school: true + nd_ccap_provider_type: CENTER + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_state_max_rate: 800 + +- name: Case 5, Licensed family infant full-time. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 0.5 + nd_ccap_provider_type: LICENSED_FAMILY + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_state_max_rate: 900 + +- name: Case 6, Licensed family toddler full-time. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 2 + nd_ccap_provider_type: LICENSED_FAMILY + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_state_max_rate: 880 + +- name: Case 7, Self-declared or tribal preschool full-time. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 4 + nd_ccap_provider_type: SELF_DECLARED_TRIBAL + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_state_max_rate: 531 + +- name: Case 8, Self-declared or tribal school-age full-time. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 8 + is_in_k12_school: true + nd_ccap_provider_type: SELF_DECLARED_TRIBAL + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_state_max_rate: 529 + +- name: Case 9, Approved relative infant full-time. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 0.5 + nd_ccap_provider_type: APPROVED_RELATIVE + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_state_max_rate: 422 + +- name: Case 10, Approved relative school-age full-time. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 8 + is_in_k12_school: true + nd_ccap_provider_type: APPROVED_RELATIVE + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_state_max_rate: 348 + +# --- Part-time rates --- + +- name: Case 11, Center infant part-time. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 0.5 + nd_ccap_provider_type: CENTER + childcare_hours_per_week: 20 # under 25, part-time + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_state_max_rate: 546 + +- name: Case 12, Licensed family preschool part-time. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 4 + nd_ccap_provider_type: LICENSED_FAMILY + childcare_hours_per_week: 20 + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_state_max_rate: 385 + +- name: Case 13, Approved relative toddler part-time. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 2 + nd_ccap_provider_type: APPROVED_RELATIVE + childcare_hours_per_week: 20 + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_state_max_rate: 187 + +# --- Special-needs +10% (gated on disability AND QRIS step 2+) --- + +- name: Case 14, disabled child at QRIS step 2 gets the +10% raise. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 0.5 + is_disabled: true + nd_ccap_provider_type: CENTER + nd_ccap_provider_qris_step: STEP_2 + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: ND + output: + # Center infant full-time 1,240 * 1.10 = 1,364 + nd_ccap_state_max_rate: 1_364 + +- name: Case 15, disabled child at QRIS step 1 does not get the +10% raise. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 0.5 + is_disabled: true + nd_ccap_provider_type: CENTER + nd_ccap_provider_qris_step: STEP_1 + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: ND + output: + # Step 1 fails the step-2-or-higher gate, so no +10%: still 1,240 + nd_ccap_state_max_rate: 1_240 + +- name: Case 16, non-disabled child at QRIS step 4 does not get the +10% raise. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 0.5 + nd_ccap_provider_type: CENTER + nd_ccap_provider_qris_step: STEP_4 + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: ND + output: + # Not disabled, so the special-needs gate fails: still 1,240 + nd_ccap_state_max_rate: 1_240 + +# --- Degenerate / edge inputs --- + +- name: Case 17, child with zero child care hours falls to the part-time rate. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 0.5 + nd_ccap_provider_type: CENTER + childcare_hours_per_week: 0 # no hours in care + households: + household: + members: [person1] + state_code: ND + output: + # We do not track an explicit "not in care" flag, so a child with zero + # hours is classified part-time (hours < 25) and receives the Center infant + # part-time rate 546 rather than zero. Families with a child not in care + # would supply zero pre-subsidy expenses, so the expense cap in + # nd_ccap_base_subsidy keeps the benefit from counting unused capacity. + nd_ccap_state_max_rate: 546 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_time_category.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_time_category.yaml new file mode 100644 index 00000000000..8389ee69d5f --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_time_category.yaml @@ -0,0 +1,61 @@ +# Tests for nd_ccap_time_category (Person, MONTH, Enum). +# Full-time level of care is 25 or more hours per week; part-time is 1 to fewer +# than 25 hours per week (400-28-80-50). childcare_hours_per_week is a YEAR +# variable read with period.this_year, so the weekly-hours value is passed +# directly. + +- name: Case 1, 40 hours per week, full time. + period: 2026-01 + input: + people: + person1: + age: 3 + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_time_category: FULL_TIME + +- name: Case 2, exactly 25 hours per week, full time (at the threshold). + period: 2026-01 + input: + people: + person1: + age: 3 + childcare_hours_per_week: 25 + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_time_category: FULL_TIME + +- name: Case 3, 24 hours per week, part time (just below the threshold). + period: 2026-01 + input: + people: + person1: + age: 3 + childcare_hours_per_week: 24 + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_time_category: PART_TIME + +- name: Case 4, 10 hours per week, part time. + period: 2026-01 + input: + people: + person1: + age: 3 + childcare_hours_per_week: 10 + households: + household: + members: [person1] + state_code: ND + output: + nd_ccap_time_category: PART_TIME diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_copay.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_copay.py new file mode 100644 index 00000000000..0d5414a9c21 --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_copay.py @@ -0,0 +1,30 @@ +from policyengine_us.model_api import * + + +class nd_ccap_copay(Variable): + value_type = float + entity = SPMUnit + unit = USD + label = "North Dakota CCAP family co-payment" + definition_period = MONTH + defined_for = "nd_ccap_eligible" + reference = "https://www.nd.gov/dhs/policymanuals/40028/40028.htm" + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.nd.dhs.ccap.copay + # Income is floored at zero so a self-employment loss cannot produce a + # negative co-payment. + countable_income = max_(spm_unit("nd_ccap_countable_income", period), 0) + # hhs_smi is an annual dollar amount; the bare monthly period + # auto-divides it to a monthly value. + monthly_smi = spm_unit("hhs_smi", period) + income_to_smi = where(monthly_smi > 0, countable_income / monthly_smi, 0) + copay_rate = p.rate.calc(income_to_smi) + copay = min_(countable_income * copay_rate, countable_income * p.max_rate) + # The co-payment is waived for families at or below 30% of the state + # median income and for TANF recipients (400-28-90-20). Diversion and + # Crossroads recipients are also waived but are not tracked at the + # moment. + is_tanf_enrolled = spm_unit("is_tanf_enrolled", period) + waived = (income_to_smi <= p.waiver_smi_threshold) | is_tanf_enrolled + return where(waived, 0, copay) diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_activity_eligible.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_activity_eligible.py new file mode 100644 index 00000000000..0f669701ed1 --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_activity_eligible.py @@ -0,0 +1,22 @@ +from policyengine_us.model_api import * + + +class nd_ccap_activity_eligible(Variable): + value_type = bool + entity = SPMUnit + label = "North Dakota CCAP activity eligible" + definition_period = MONTH + defined_for = StateCode.ND + reference = "https://www.nd.gov/dhs/policymanuals/40028/40028.htm" + + def formula(spm_unit, period, parameters): + # All caretakers (head and spouse) must be in an allowable activity. + # Requiring every caretaker to be active also serves as the proxy for + # the two-caretaker availability rule (NDAC 75-02-01.3-11): if a + # caretaker is available (not in an activity), the unit is ineligible. + person = spm_unit.members + is_head_or_spouse = person("is_tax_unit_head_or_spouse", period.this_year) + in_eligible_activity = person("nd_ccap_parent_in_eligible_activity", period) + has_head_or_spouse = spm_unit.sum(is_head_or_spouse) >= 1 + all_covered = spm_unit.sum(is_head_or_spouse & ~in_eligible_activity) == 0 + return has_head_or_spouse & all_covered diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_eligible.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_eligible.py new file mode 100644 index 00000000000..d7291d41d21 --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_eligible.py @@ -0,0 +1,19 @@ +from policyengine_us.model_api import * + + +class nd_ccap_eligible(Variable): + value_type = bool + entity = SPMUnit + label = "North Dakota CCAP eligible" + definition_period = MONTH + defined_for = StateCode.ND + reference = "https://www.nd.gov/dhs/policymanuals/40028/40028.htm" + + def formula(spm_unit, period, parameters): + has_eligible_child = add(spm_unit, period, ["nd_ccap_eligible_child"]) > 0 + income_eligible = spm_unit("nd_ccap_income_eligible", period) + # The $1,000,000 self-certified asset limit matches the federal CCDF + # limit, so we reuse is_ccdf_asset_eligible (400-28-65-05). + asset_eligible = spm_unit("is_ccdf_asset_eligible", period) + activity_eligible = spm_unit("nd_ccap_activity_eligible", period) + return has_eligible_child & income_eligible & asset_eligible & activity_eligible diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_eligible_child.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_eligible_child.py new file mode 100644 index 00000000000..1eea30031e7 --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_eligible_child.py @@ -0,0 +1,33 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.household.demographic.person.immigration_status import ( + ImmigrationStatus, +) + + +class nd_ccap_eligible_child(Variable): + value_type = bool + entity = Person + label = "North Dakota CCAP eligible child" + definition_period = MONTH + defined_for = StateCode.ND + reference = "https://www.nd.gov/dhs/policymanuals/40028/40028.htm" + + def formula(person, period, parameters): + p = parameters(period).gov.states.nd.dhs.ccap.eligibility + age = person("age", period.this_year) + # Children with a disability (or who need supervised care under a court + # order) remain eligible through the higher special-needs age limit. + # is_disabled proxies the manual's special-needs status; the + # court-order pathway is not tracked at the moment (400-28-35-02). + is_disabled = person("is_disabled", period.this_year) + age_limit = where(is_disabled, p.disabled_child_age_limit, p.child_age_limit) + age_eligible = age < age_limit + # The child (not the caretaker) must be a United States citizen or an + # alien lawfully admitted for permanent residence (400-28-50-25). This + # is narrower than the federal CCDF immigration test, so we do not + # reuse is_ccdf_immigration_eligible_child. + immigration_status = person("immigration_status", period.this_year) + immigration_eligible = (immigration_status == ImmigrationStatus.CITIZEN) | ( + immigration_status == ImmigrationStatus.LEGAL_PERMANENT_RESIDENT + ) + return age_eligible & immigration_eligible diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_income_eligible.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_income_eligible.py new file mode 100644 index 00000000000..52dda864958 --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_income_eligible.py @@ -0,0 +1,25 @@ +from policyengine_us.model_api import * + + +class nd_ccap_income_eligible(Variable): + value_type = bool + entity = SPMUnit + label = "North Dakota CCAP income eligible" + definition_period = MONTH + defined_for = StateCode.ND + reference = "https://www.nd.gov/dhs/policymanuals/40028/40028.htm" + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.nd.dhs.ccap.income + countable_income = spm_unit("nd_ccap_countable_income", period) + enrolled = spm_unit("is_nd_ccap_enrolled", period) + # Initial applicants are tested against 75% of the state median income; + # enrolled recipients are tested against 85% under the graduated + # eligibility rule (400-28-25-15). hhs_smi is an annual dollar amount, + # so reading it with the bare monthly period auto-divides it to a + # monthly value. + monthly_smi = spm_unit("hhs_smi", period) + initial_limit = monthly_smi * p.initial_smi_rate + continuing_limit = monthly_smi * p.continuing_smi_rate + income_limit = where(enrolled, continuing_limit, initial_limit) + return countable_income <= income_limit diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_parent_in_eligible_activity.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_parent_in_eligible_activity.py new file mode 100644 index 00000000000..a4d1971b879 --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_parent_in_eligible_activity.py @@ -0,0 +1,28 @@ +from policyengine_us.model_api import * + + +class nd_ccap_parent_in_eligible_activity(Variable): + value_type = bool + entity = Person + label = "North Dakota CCAP parent in an eligible activity" + definition_period = MONTH + defined_for = StateCode.ND + reference = "https://www.nd.gov/dhs/policymanuals/40028/40028.htm" + + def formula(person, period, parameters): + # Allowable activities include employment, self-employment, education + # or training, high school or GED completion, and work-study or + # internship; there is no minimum number of working hours, since + # activity is documented through proof of income (400-28-55-05). A + # parent qualifies with positive wages, nonzero self-employment income + # (a business loss still evidences active self-employment), full-time + # student status, TANF enrollment, or a disability (incapacity). We do + # not capture not-yet-paid new employment or active job search at the + # moment. + has_earnings = (person("employment_income", period) > 0) | ( + person("self_employment_income", period) != 0 + ) + is_student = person("is_full_time_student", period.this_year) + is_disabled = person("is_disabled", period.this_year) + is_tanf_enrolled = person.spm_unit("is_tanf_enrolled", period) + return has_earnings | is_student | is_disabled | is_tanf_enrolled diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/income/nd_ccap_child_support_deduction.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/income/nd_ccap_child_support_deduction.py new file mode 100644 index 00000000000..d98357d625c --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/income/nd_ccap_child_support_deduction.py @@ -0,0 +1,20 @@ +from policyengine_us.model_api import * + + +class nd_ccap_child_support_deduction(Variable): + value_type = float + entity = SPMUnit + unit = USD + label = "North Dakota CCAP court-ordered support deduction" + definition_period = MONTH + defined_for = StateCode.ND + reference = "https://www.nd.gov/dhs/policymanuals/40028/40028.htm" + + def formula(spm_unit, period, parameters): + # The only deduction from countable income is court-ordered child or + # spousal support paid by a counted unit member (400-28-65-30, + # 400-28-70, NDAC 75-02-01.3-09). PolicyEngine tracks support paid as + # annual person-level inputs, read here with the bare monthly period so + # Core auto-divides them to a monthly amount. Private (non-court- + # ordered) support and garnishment fees are not separately tracked. + return add(spm_unit, period, ["child_support_expense", "alimony_expense"]) diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/income/nd_ccap_countable_income.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/income/nd_ccap_countable_income.py new file mode 100644 index 00000000000..4cebda9564d --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/income/nd_ccap_countable_income.py @@ -0,0 +1,24 @@ +from policyengine_us.model_api import * + + +class nd_ccap_countable_income(Variable): + value_type = float + entity = SPMUnit + unit = USD + label = "North Dakota CCAP countable income" + definition_period = MONTH + defined_for = StateCode.ND + reference = "https://www.nd.gov/dhs/policymanuals/40028/40028.htm" + + def formula(spm_unit, period, parameters): + gross_income = spm_unit("nd_ccap_gross_income", period) + # The earned income of household members under age 18 is excluded + # (400-28-65-15 #6). + person = spm_unit.members + is_minor = person("age", period.this_year) < 18 + minor_earned_income = spm_unit.sum( + is_minor + * add(person, period, ["employment_income", "self_employment_income"]) + ) + child_support_deduction = spm_unit("nd_ccap_child_support_deduction", period) + return max_(gross_income - minor_earned_income - child_support_deduction, 0) diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/income/nd_ccap_gross_income.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/income/nd_ccap_gross_income.py new file mode 100644 index 00000000000..476076f0ad0 --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/income/nd_ccap_gross_income.py @@ -0,0 +1,12 @@ +from policyengine_us.model_api import * + + +class nd_ccap_gross_income(Variable): + value_type = float + entity = SPMUnit + unit = USD + label = "North Dakota CCAP gross countable income" + definition_period = MONTH + defined_for = StateCode.ND + reference = "https://www.nd.gov/dhs/policymanuals/40028/40028.htm" + adds = "gov.states.nd.dhs.ccap.income.sources" diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/is_nd_ccap_enrolled.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/is_nd_ccap_enrolled.py new file mode 100644 index 00000000000..c6c7552c588 --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/is_nd_ccap_enrolled.py @@ -0,0 +1,10 @@ +from policyengine_us.model_api import * + + +class is_nd_ccap_enrolled(Variable): + value_type = bool + entity = SPMUnit + definition_period = MONTH + label = "Enrolled in North Dakota CCAP" + defined_for = StateCode.ND + reference = "https://www.nd.gov/dhs/policymanuals/40028/40028.htm" diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/nd_ccap.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/nd_ccap.py new file mode 100644 index 00000000000..ba8cf2aee5a --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/nd_ccap.py @@ -0,0 +1,22 @@ +from policyengine_us.model_api import * + + +class nd_ccap(Variable): + value_type = float + entity = SPMUnit + unit = USD + label = "North Dakota CCAP benefit amount" + definition_period = MONTH + defined_for = "nd_ccap_eligible" + reference = "https://www.nd.gov/dhs/policymanuals/40028/40028.htm" + + def formula(spm_unit, period, parameters): + # The benefit is the base subsidy (capped at billed expenses and net of + # the co-payment) plus the additive QRIS step bonus and infant/toddler + # bonus. The two bonuses are separate provider payments, so they are + # not capped at the family's billed expenses and not reduced by the + # co-payment (400-28-100-30). + base_subsidy = spm_unit("nd_ccap_base_subsidy", period) + qris_step_bonus = add(spm_unit, period, ["nd_ccap_qris_step_bonus"]) + infant_toddler_bonus = add(spm_unit, period, ["nd_ccap_infant_toddler_bonus"]) + return base_subsidy + qris_step_bonus + infant_toddler_bonus diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/nd_child_care_subsidies.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/nd_child_care_subsidies.py new file mode 100644 index 00000000000..7fc7f0aae72 --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/nd_child_care_subsidies.py @@ -0,0 +1,11 @@ +from policyengine_us.model_api import * + + +class nd_child_care_subsidies(Variable): + value_type = float + entity = SPMUnit + label = "North Dakota child care subsidies" + unit = USD + definition_period = YEAR + defined_for = StateCode.ND + adds = ["nd_ccap"] diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_age_group.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_age_group.py new file mode 100644 index 00000000000..6bfafdcecd5 --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_age_group.py @@ -0,0 +1,43 @@ +from policyengine_us.model_api import * + + +class NDCCAPAgeGroup(Enum): + INFANT = "Infant" + TODDLER = "Toddler" + PRESCHOOL = "Preschool" + SCHOOL_AGE = "School age" + + +class nd_ccap_age_group(Variable): + value_type = Enum + entity = Person + possible_values = NDCCAPAgeGroup + default_value = NDCCAPAgeGroup.SCHOOL_AGE + definition_period = MONTH + label = "North Dakota CCAP rate-table age group" + defined_for = StateCode.ND + reference = "https://www.nd.gov/dhs/policymanuals/40028/40028.htm" + + def formula(person, period, parameters): + p = parameters(period).gov.states.nd.dhs.ccap.age_group + # The rate table is keyed on the HHS web age-group headers: Infant + # (birth through 17 months), Toddler (18 through 35 months), Preschool + # (three years through school age), and Other/School age. These + # boundaries differ slightly from the manual's 400-28-100-30 bands at + # the infant/toddler and preschool edges; the rate table the rate + # column is looked up from governs. + age_months = person("age", period.this_year) * MONTHS_IN_YEAR + is_in_school = person("is_in_k12_school", period.this_year) + return select( + [ + age_months < p.infant_max_months, + age_months < p.toddler_max_months, + (age_months >= p.preschool_min_months) & ~is_in_school, + ], + [ + NDCCAPAgeGroup.INFANT, + NDCCAPAgeGroup.TODDLER, + NDCCAPAgeGroup.PRESCHOOL, + ], + default=NDCCAPAgeGroup.SCHOOL_AGE, + ) diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_base_subsidy.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_base_subsidy.py new file mode 100644 index 00000000000..f3e1c47b72f --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_base_subsidy.py @@ -0,0 +1,27 @@ +from policyengine_us.model_api import * + + +class nd_ccap_base_subsidy(Variable): + value_type = float + entity = SPMUnit + unit = USD + label = "North Dakota CCAP base subsidy" + definition_period = MONTH + defined_for = "nd_ccap_eligible" + reference = "https://www.nd.gov/dhs/policymanuals/40028/40028.htm" + + def formula(spm_unit, period, parameters): + # The base subsidy is the lesser of the summed per-child state maximum + # rates and the family's billed child care expenses, minus the monthly + # co-payment, floored at zero (400-28-100-05). The special-needs +10% + # is already folded into nd_ccap_state_max_rate, so it sits inside the + # expense cap. The expense cap pools the per-child maximum rates across + # the unit (the IN / RI / MA convention), because billed expenses are a + # single SPM-unit input. + maximum_monthly_rate = add(spm_unit, period, ["nd_ccap_state_max_rate"]) + pre_subsidy_childcare_expenses = spm_unit( + "spm_unit_pre_subsidy_childcare_expenses", period + ) + capped_expenses = min_(pre_subsidy_childcare_expenses, maximum_monthly_rate) + copay = spm_unit("nd_ccap_copay", period) + return max_(capped_expenses - copay, 0) diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_infant_toddler_bonus.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_infant_toddler_bonus.py new file mode 100644 index 00000000000..fc1bd87ce2a --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_infant_toddler_bonus.py @@ -0,0 +1,36 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.nd.dhs.ccap.rates.nd_ccap_provider_type import ( + NDCCAPProviderType, +) + + +class nd_ccap_infant_toddler_bonus(Variable): + value_type = float + entity = Person + unit = USD + label = "North Dakota CCAP infant/toddler bonus per child" + definition_period = MONTH + defined_for = "nd_ccap_eligible_child" + reference = "https://www.nd.gov/dhs/policymanuals/40028/40028.htm" + + def formula(person, period, parameters): + p = parameters(period).gov.states.nd.dhs.ccap.rates + # The infant/toddler bonus is a flat per-child provider payment for + # children attending 40 or more hours per month, paid to North Dakota + # licensed, tribally licensed, or military licensed providers + # (400-28-100-30). The bonus is additive: it is not capped at the + # family's billed expenses and not reduced by the co-payment. The + # parameter is zero for preschool and school-age children, so the age + # filter is handled by the lookup. + age_group = person("nd_ccap_age_group", period) + bonus_amount = p.infant_toddler_bonus[age_group] + # PolicyEngine tracks weekly child care hours, so the 40-hours-per-month + # attendance condition is approximated against a weekly threshold. + hours = person("childcare_hours_per_week", period.this_year) + meets_hours = hours >= p.infant_toddler_bonus_min_weekly_hours + # We do not track the provider's licensure jurisdiction, so we assume + # the licensure condition is met for all providers other than approved + # relatives. + provider_type = person("nd_ccap_provider_type", period) + eligible_provider = provider_type != NDCCAPProviderType.APPROVED_RELATIVE + return bonus_amount * meets_hours * eligible_provider diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_provider_qris_step.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_provider_qris_step.py new file mode 100644 index 00000000000..d67f55c0362 --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_provider_qris_step.py @@ -0,0 +1,20 @@ +from policyengine_us.model_api import * + + +class NDCCAPProviderQRISStep(Enum): + STEP_1 = "Step 1" + STEP_2 = "Step 2" + STEP_3 = "Step 3" + STEP_4 = "Step 4" + UNRATED = "Unrated" + + +class nd_ccap_provider_qris_step(Variable): + value_type = Enum + entity = Person + possible_values = NDCCAPProviderQRISStep + default_value = NDCCAPProviderQRISStep.UNRATED + definition_period = MONTH + label = "North Dakota CCAP provider QRIS step rating" + defined_for = StateCode.ND + reference = "https://www.nd.gov/dhs/policymanuals/40028/40028.htm" diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_provider_type.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_provider_type.py new file mode 100644 index 00000000000..597e69fe9df --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_provider_type.py @@ -0,0 +1,19 @@ +from policyengine_us.model_api import * + + +class NDCCAPProviderType(Enum): + CENTER = "Center" + LICENSED_FAMILY = "Licensed family or group" + SELF_DECLARED_TRIBAL = "Self-declared or tribal" + APPROVED_RELATIVE = "Approved relative" + + +class nd_ccap_provider_type(Variable): + value_type = Enum + entity = Person + possible_values = NDCCAPProviderType + default_value = NDCCAPProviderType.CENTER + definition_period = MONTH + label = "North Dakota CCAP child care provider type" + defined_for = StateCode.ND + reference = "https://www.nd.gov/dhs/policymanuals/40028/40028.htm" diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_qris_step_bonus.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_qris_step_bonus.py new file mode 100644 index 00000000000..6fb7fdbf661 --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_qris_step_bonus.py @@ -0,0 +1,34 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.nd.dhs.ccap.rates.nd_ccap_time_category import ( + NDCCAPTimeCategory, +) + + +class nd_ccap_qris_step_bonus(Variable): + value_type = float + entity = Person + unit = USD + label = "North Dakota CCAP QRIS step bonus per child" + definition_period = MONTH + defined_for = "nd_ccap_eligible_child" + reference = "https://www.nd.gov/dhs/policymanuals/40028/40028.htm" + + def formula(person, period, parameters): + p = parameters(period).gov.states.nd.dhs.ccap.rates + # The QRIS step bonus is a separate provider payment computed as a + # share of the base state maximum rate by quality step (400-28-100-30). + # It uses the base rate excluding the special-needs +10% to avoid + # compounding the two provisions. The bonus is additive: it is not + # capped at the family's billed expenses and not reduced by the + # co-payment. + provider_type = person("nd_ccap_provider_type", period) + age_group = person("nd_ccap_age_group", period) + time_category = person("nd_ccap_time_category", period) + base_rate = where( + time_category == NDCCAPTimeCategory.FULL_TIME, + p.full_time[provider_type][age_group], + p.part_time[provider_type][age_group], + ) + qris_step = person("nd_ccap_provider_qris_step", period) + bonus_rate = p.qris_step_bonus_rate[qris_step] + return base_rate * bonus_rate diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_state_max_rate.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_state_max_rate.py new file mode 100644 index 00000000000..0ee82660359 --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_state_max_rate.py @@ -0,0 +1,48 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.nd.dhs.ccap.rates.nd_ccap_time_category import ( + NDCCAPTimeCategory, +) +from policyengine_us.variables.gov.states.nd.dhs.ccap.rates.nd_ccap_provider_qris_step import ( + NDCCAPProviderQRISStep, +) + + +class nd_ccap_state_max_rate(Variable): + value_type = float + entity = Person + unit = USD + label = "North Dakota CCAP state maximum rate per child" + definition_period = MONTH + defined_for = "nd_ccap_eligible_child" + reference = "https://www.nd.gov/dhs/policymanuals/40028/40028.htm" + + def formula(person, period, parameters): + p = parameters(period).gov.states.nd.dhs.ccap.rates + provider_type = person("nd_ccap_provider_type", period) + age_group = person("nd_ccap_age_group", period) + time_category = person("nd_ccap_time_category", period) + full_time_rate = p.full_time[provider_type][age_group] + part_time_rate = p.part_time[provider_type][age_group] + base_rate = where( + time_category == NDCCAPTimeCategory.FULL_TIME, + full_time_rate, + part_time_rate, + ) + # A child who meets the definition of disability and attends a provider + # with a QRIS rating of step 2 or higher receives an additional 10% of + # the state maximum rate (400-28-100-30). is_disabled proxies the + # required written special-needs verification; the manual's reference + # to KRS definitions is a copy-paste artifact, so we cite the North + # Dakota manual. The 10% raises the rate ceiling, so it folds inside + # the expense cap applied in nd_ccap_base_subsidy. + is_disabled = person("is_disabled", period.this_year) + qris_step = person("nd_ccap_provider_qris_step", period) + step_2_or_higher = ( + (qris_step == NDCCAPProviderQRISStep.STEP_2) + | (qris_step == NDCCAPProviderQRISStep.STEP_3) + | (qris_step == NDCCAPProviderQRISStep.STEP_4) + ) + special_needs_multiplier = where( + is_disabled & step_2_or_higher, p.special_needs_multiplier, 1 + ) + return base_rate * special_needs_multiplier diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_time_category.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_time_category.py new file mode 100644 index 00000000000..73c52af5a04 --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_time_category.py @@ -0,0 +1,28 @@ +from policyengine_us.model_api import * + + +class NDCCAPTimeCategory(Enum): + FULL_TIME = "Full time" + PART_TIME = "Part time" + + +class nd_ccap_time_category(Variable): + value_type = Enum + entity = Person + possible_values = NDCCAPTimeCategory + default_value = NDCCAPTimeCategory.PART_TIME + definition_period = MONTH + label = "North Dakota CCAP level of care" + defined_for = StateCode.ND + reference = "https://www.nd.gov/dhs/policymanuals/40028/40028.htm" + + def formula(person, period, parameters): + p = parameters(period).gov.states.nd.dhs.ccap.time_category + # Full-time level of care is 25 or more hours per week; part-time is + # 1 to fewer than 25 hours per week (400-28-80-50). + hours = person("childcare_hours_per_week", period.this_year) + return where( + hours >= p.full_time_min_hours, + NDCCAPTimeCategory.FULL_TIME, + NDCCAPTimeCategory.PART_TIME, + ) From 12d49c511d8a8ed5f784b4ff48b02891b16a0e86 Mon Sep 17 00:00:00 2001 From: Ziming Date: Mon, 22 Jun 2026 00:07:51 -0400 Subject: [PATCH 3/6] Review-fix round 1: address critical issues from /review-program - C1: revert child age limit to under-13 (unsourced 2026-04-01 under-12 entry removed) - C2: infant/toddler bonus gated on QRIS step 2+ and licensed provider; drop unsourced hours gate - C3: children's earned-income exclusion corrected to under-19 (NDAC 75-02-01.3-08(12)) - A1: document incapacity (400-28-35-15) basis for disabled-caretaker activity - A2: add top-level nd_ccap unit test Co-Authored-By: Claude Opus 4.8 (1M context) --- .../dhs/ccap/eligibility/child_age_limit.yaml | 8 +- .../child_earned_income_exclusion_age.yaml | 13 +++ ...infant_toddler_bonus_min_weekly_hours.yaml | 18 ---- .../eligibility/nd_ccap_eligible_child.yaml | 15 ++-- .../ccap/income/nd_ccap_countable_income.yaml | 52 ++++++++++- .../gov/states/nd/dhs/ccap/integration.yaml | 15 +++- .../gov/states/nd/dhs/ccap/nd_ccap.yaml | 90 +++++++++++++++++++ .../rates/nd_ccap_infant_toddler_bonus.yaml | 56 ++++++++---- .../nd_ccap_parent_in_eligible_activity.py | 10 ++- .../ccap/income/nd_ccap_countable_income.py | 7 +- .../rates/nd_ccap_infant_toddler_bonus.py | 40 +++++---- 11 files changed, 247 insertions(+), 77 deletions(-) create mode 100644 policyengine_us/parameters/gov/states/nd/dhs/ccap/income/child_earned_income_exclusion_age.yaml delete mode 100644 policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/infant_toddler_bonus_min_weekly_hours.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/nd_ccap.yaml diff --git a/policyengine_us/parameters/gov/states/nd/dhs/ccap/eligibility/child_age_limit.yaml b/policyengine_us/parameters/gov/states/nd/dhs/ccap/eligibility/child_age_limit.yaml index 17c8c1f6668..2135d7b97e2 100644 --- a/policyengine_us/parameters/gov/states/nd/dhs/ccap/eligibility/child_age_limit.yaml +++ b/policyengine_us/parameters/gov/states/nd/dhs/ccap/eligibility/child_age_limit.yaml @@ -1,12 +1,10 @@ description: North Dakota limits eligibility to children below this age under the Child Care Assistance Program. # Children are eligible through the month of their 13th birthday (under 13). -# Effective April 1, 2026, eligibility runs through the month the child turns -# 12 (under 12), aligning to state law. The 2020 entry is placed at -# PolicyEngine's backdating floor. +# The 2020 entry is placed at PolicyEngine's backdating floor. A reported +# under-12 change effective April 1, 2026 was reverted pending a primary source. values: 2020-01-01: 13 - 2026-04-01: 12 metadata: unit: year @@ -15,5 +13,3 @@ metadata: reference: - title: North Dakota CCAP Policy Manual, Eligible Children 400-28-35-02 href: https://www.nd.gov/dhs/policymanuals/40028/40028.htm - - title: North Dakota HHS Child Care Assistance Program updates 2026 - href: https://www.hhs.nd.gov/ec-news/child-care-assistance-program-ccap-updates-2026 diff --git a/policyengine_us/parameters/gov/states/nd/dhs/ccap/income/child_earned_income_exclusion_age.yaml b/policyengine_us/parameters/gov/states/nd/dhs/ccap/income/child_earned_income_exclusion_age.yaml new file mode 100644 index 00000000000..4a778b59ab7 --- /dev/null +++ b/policyengine_us/parameters/gov/states/nd/dhs/ccap/income/child_earned_income_exclusion_age.yaml @@ -0,0 +1,13 @@ +description: North Dakota excludes the earned income of children under this age from countable income under the Child Care Assistance Program. +values: + 2020-01-01: 19 + +metadata: + unit: year + period: year + label: North Dakota CCAP child earned income exclusion age + reference: + - title: North Dakota CCAP Policy Manual, Definition of Income 400-28-65-10-05 + href: https://www.nd.gov/dhs/policymanuals/40028/40028.htm + - title: NDAC 75-02-01.3-08(12) + href: https://ndlegis.gov/prod/acdata/pdf/75-02-01.3.pdf diff --git a/policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/infant_toddler_bonus_min_weekly_hours.yaml b/policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/infant_toddler_bonus_min_weekly_hours.yaml deleted file mode 100644 index 04ea1ae6109..00000000000 --- a/policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/infant_toddler_bonus_min_weekly_hours.yaml +++ /dev/null @@ -1,18 +0,0 @@ -description: North Dakota requires an infant or toddler to attend at least this many child care hours per week to qualify the provider for the infant/toddler bonus under the Child Care Assistance Program. - -# The bonus requires attendance of 40 or more hours per month. PolicyEngine -# tracks weekly child care hours, so the monthly threshold is approximated as -# 40 hours per month divided by the manual's 4.3 weeks-per-month conversion -# factor (400-28-70-05), giving roughly 9.3 hours per week. -values: - 2026-01-01: 9.3 - -metadata: - unit: hour - period: week - label: North Dakota CCAP infant/toddler bonus minimum weekly hours - reference: - - title: North Dakota HHS Child Care Assistance Program updates 2026 - href: https://www.hhs.nd.gov/ec-news/child-care-assistance-program-ccap-updates-2026 - - title: North Dakota CCAP Policy Manual, Income Conversion 400-28-70-05 - href: https://www.nd.gov/dhs/policymanuals/40028/40028.htm diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_eligible_child.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_eligible_child.yaml index 7d80a5bcd62..22e11f29745 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_eligible_child.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_eligible_child.yaml @@ -1,9 +1,7 @@ # Tests for nd_ccap_eligible_child (Person, MONTH, bool). # A child is eligible if under the age limit AND a US citizen or lawful # permanent resident (400-28-35-02, 400-28-50-25). The general age limit is -# 13 (under 13) through 2026-03-31 and 12 (under 12) from 2026-04-01; the -# special-needs / court-order limit is 19. Because test periods must be the -# first month or a whole year, the 12-limit era is exercised at 2027-01. +# 13 (under 13) across all years; the special-needs / court-order limit is 19. - name: Case 1, citizen infant, eligible. period: 2026-01 @@ -44,7 +42,7 @@ output: nd_ccap_eligible_child: false -- name: Case 4, age 12 eff 2027 (under-12 era), at the 12 limit, ineligible. +- name: Case 4, citizen age 12 in a later year, still under the 13 limit, eligible. period: 2027-01 input: people: @@ -55,20 +53,21 @@ members: [person1] state_code: ND output: - nd_ccap_eligible_child: false + # The under-13 limit applies in all years, so a 12-year-old stays eligible. + nd_ccap_eligible_child: true -- name: Case 5, age 11 eff 2027 (under-12 era), under the 12 limit, eligible. +- name: Case 5, citizen age 13 in a later year, at the 13 limit, ineligible. period: 2027-01 input: people: person1: - age: 11 + age: 13 households: household: members: [person1] state_code: ND output: - nd_ccap_eligible_child: true + nd_ccap_eligible_child: false - name: Case 6, disabled age 15, under the 19 special-needs limit, eligible. period: 2026-01 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/income/nd_ccap_countable_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/income/nd_ccap_countable_income.yaml index d4d764636dc..451b29f017a 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/income/nd_ccap_countable_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/income/nd_ccap_countable_income.yaml @@ -1,9 +1,9 @@ # Tests for nd_ccap_countable_income (SPMUnit, MONTH, USD). # Countable income is gross countable income minus the earned income of -# household members under 18 and minus the court-ordered child/spousal support -# deduction, floored at zero (400-28-65-15, 400-28-65-30). Annual person-level -# income inputs are read with the bare monthly period, so Core auto-divides -# them to monthly amounts. +# household members under 19 and minus the court-ordered child/spousal support +# deduction, floored at zero (400-28-65-10-05, 400-28-65-15, 400-28-65-30). +# Annual person-level income inputs are read with the bare monthly period, so +# Core auto-divides them to monthly amounts. - name: Case 1, single earner, gross equals countable. period: 2026-01 @@ -108,3 +108,47 @@ state_code: ND output: nd_ccap_countable_income: 0 + +- name: Case 6, an 18-year-old child's earned income is excluded. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + employment_income: 36_000 # 3,000/mo + person2: + age: 18 + employment_income: 6_000 # 500/mo, excluded as a child under 19 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: ND + output: + # 3,000 + 500 - 500 (18-year-old's earnings excluded under age 19) = 3,000 + nd_ccap_countable_income: 3_000 + +- name: Case 7, a 19-year-old child's earned income is counted. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + employment_income: 36_000 # 3,000/mo + person2: + age: 19 + employment_income: 6_000 # 500/mo, counted at age 19 and over + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: ND + output: + # 3,000 + 500 (19-year-old's earnings counted) = 3,500 + nd_ccap_countable_income: 3_500 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/integration.yaml index 21a49fbfa8d..c2accdeeea1 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/integration.yaml @@ -84,6 +84,7 @@ person2: age: 0.5 nd_ccap_provider_type: CENTER + nd_ccap_provider_qris_step: STEP_2 # unlocks the infant bonus (step 2+) childcare_hours_per_week: 40 spm_units: spm_unit: @@ -99,7 +100,8 @@ nd_ccap_eligible: true # ratio 0.799 > 40% -> copay capped at 7%: 6,200 * 0.07 = 434. # CENTER infant full-time 1,240; expenses 2,000 capped at 1,240. - # base = 1,240 - 434 = 806; infant bonus 200; nd_ccap = 1,006. + # base = 1,240 - 434 = 806. Step 2 carries no QRIS bonus in 2026 but + # unlocks the infant bonus 200; nd_ccap = 806 + 200 = 1,006. nd_ccap: 1_006 - name: Case 4, family at or below 30% SMI has the co-payment waived. @@ -113,6 +115,7 @@ person2: age: 0.5 nd_ccap_provider_type: CENTER + nd_ccap_provider_qris_step: STEP_2 # unlocks the infant bonus (step 2+) childcare_hours_per_week: 40 spm_units: spm_unit: @@ -141,6 +144,7 @@ person2: age: 0.5 nd_ccap_provider_type: CENTER + nd_ccap_provider_qris_step: STEP_2 # unlocks the infant bonus (step 2+) childcare_hours_per_week: 40 spm_units: spm_unit: @@ -304,6 +308,7 @@ person2: age: 0.5 nd_ccap_provider_type: CENTER + nd_ccap_provider_qris_step: STEP_2 # unlocks the infant bonus (step 2+) childcare_hours_per_week: 40 spm_units: spm_unit: @@ -389,14 +394,17 @@ person3: age: 0.5 nd_ccap_provider_type: CENTER + nd_ccap_provider_qris_step: STEP_2 # unlocks the infant bonus (step 2+) childcare_hours_per_week: 40 person4: age: 1.0 nd_ccap_provider_type: CENTER + nd_ccap_provider_qris_step: STEP_2 # unlocks the infant bonus (step 2+) childcare_hours_per_week: 40 person5: age: 2.0 nd_ccap_provider_type: CENTER + nd_ccap_provider_qris_step: STEP_2 # unlocks the toddler bonus (step 2+) childcare_hours_per_week: 40 person6: age: 6.0 @@ -427,8 +435,9 @@ # (no "not in care" flag), so the pooled cap is 3,604 + 3 * 416 = 4,852; # expenses 10,000 do not bind. base = 4,852 - 0 = 4,852. nd_ccap_base_subsidy: 4_852 - # infant/toddler bonuses: 200 + 200 + 115 = 515; no QRIS step set. - # nd_ccap = 4,852 + 515 = 5,367. + # all three children in care are at a step-2 provider, which unlocks the + # infant/toddler bonuses (200 + 200 + 115 = 515) but carries no QRIS step + # bonus in 2026. nd_ccap = 4,852 + 515 = 5,367. nd_ccap: 5_367 - name: Case 15, disabled infant at a QRIS step-4 center stacks special needs and bonuses. diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/nd_ccap.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/nd_ccap.yaml new file mode 100644 index 00000000000..b5c2c082dd3 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/nd_ccap.yaml @@ -0,0 +1,90 @@ +# Tests for nd_ccap (SPMUnit, MONTH, USD), the top-level North Dakota CCAP +# benefit. nd_ccap = base subsidy + QRIS step bonus + infant/toddler bonus, where +# the base subsidy is min(state max rate, billed expenses) - co-payment floored +# at zero and the two provider bonuses are additive (not capped at expenses and +# not reduced by the co-payment) (400-28-100-05, 400-28-100-30). defined_for = +# nd_ccap_eligible, so each case is a working parent with an eligible child in +# North Dakota. Rates are the statewide flat monthly maximums effective +# 2026-01-01; Center infant full-time = 1,240. + +- name: Case 1, infant in a step-3 center, base subsidy plus both bonuses. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + employment_income: 30_000 # 2,500/mo + person2: + age: 0.5 + nd_ccap_provider_type: CENTER + nd_ccap_provider_qris_step: STEP_3 + childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_pre_subsidy_childcare_expenses: 24_000 # 2,000/mo + households: + household: + members: [person1, person2] + state_code: ND + output: + # copay: ratio 0.322 -> 6% band; 2,500 * 0.06 = 150. + # base = min(1,240, 2,000) - 150 = 1,090. + # QRIS step 3 (2026) bonus 1,240 * 0.05 = 62; infant bonus 200. + # nd_ccap = 1,090 + 62 + 200 = 1,352. + nd_ccap: 1_352 + +- name: Case 2, infant at a step-2 center with the co-payment waived. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + employment_income: 24_000 # 2,000/mo, copay waived (<= 30% SMI) + person2: + age: 0.5 + nd_ccap_provider_type: CENTER + nd_ccap_provider_qris_step: STEP_2 + childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_pre_subsidy_childcare_expenses: 24_000 # 2,000/mo + households: + household: + members: [person1, person2] + state_code: ND + output: + # base = min(1,240, 2,000) - 0 = 1,240. + # QRIS step 2 carries no bonus in 2026; infant bonus 200. + # nd_ccap = 1,240 + 0 + 200 = 1,440. + nd_ccap: 1_440 + +- name: Case 3, zero billed expenses, base subsidy is zero but the bonuses still pay. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + employment_income: 24_000 # 2,000/mo, copay waived (<= 30% SMI) + person2: + age: 0.5 + nd_ccap_provider_type: CENTER + nd_ccap_provider_qris_step: STEP_3 + childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_pre_subsidy_childcare_expenses: 0 # no billed expenses + households: + household: + members: [person1, person2] + state_code: ND + output: + # base = min(1,240, 0) - 0 = 0 (the expense cap zeroes it). + # the bonuses are NOT capped at expenses: QRIS step 3 (2026) 1,240 * 0.05 = + # 62; infant bonus 200. nd_ccap = 0 + 62 + 200 = 262. + nd_ccap: 262 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_infant_toddler_bonus.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_infant_toddler_bonus.yaml index 815f8ea710f..b744448749a 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_infant_toddler_bonus.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_infant_toddler_bonus.yaml @@ -1,12 +1,13 @@ # Tests for nd_ccap_infant_toddler_bonus (Person, MONTH, USD). # A flat per-child provider payment of $200 for an infant or $115 for a toddler -# attending 40 or more hours per month, paid to providers other than approved -# relatives (400-28-100-30; HHS 2026 update). Effective 2026-01-01. The 40 -# hours/month condition is approximated as 9.3 child care hours per week. -# Preschool and school-age children receive no bonus. defined_for = -# nd_ccap_eligible_child. +# at a provider rated QRIS step 2 or higher, paid to providers other than +# approved relatives (400-28-100-30; HHS 2026 update). Effective 2026-01-01. +# The HHS source states the bonus "applies to all infants and toddlers in the +# provider's care," so there is no minimum-hours condition. Preschool and +# school-age children receive no bonus. Unrated and step-1 providers receive no +# bonus. defined_for = nd_ccap_eligible_child. -- name: Case 1, infant attending 40+ hours, $200 bonus. +- name: Case 1, infant at a step-2 provider, $200 bonus. period: 2026-01 absolute_error_margin: 0.01 input: @@ -14,6 +15,7 @@ person1: age: 0.5 nd_ccap_provider_type: CENTER + nd_ccap_provider_qris_step: STEP_2 childcare_hours_per_week: 40 households: household: @@ -22,7 +24,7 @@ output: nd_ccap_infant_toddler_bonus: 200 -- name: Case 2, toddler attending 40+ hours, $115 bonus. +- name: Case 2, toddler at a step-3 provider, $115 bonus. period: 2026-01 absolute_error_margin: 0.01 input: @@ -30,6 +32,7 @@ person1: age: 2 nd_ccap_provider_type: CENTER + nd_ccap_provider_qris_step: STEP_3 childcare_hours_per_week: 40 households: household: @@ -38,7 +41,7 @@ output: nd_ccap_infant_toddler_bonus: 115 -- name: Case 3, infant below the weekly-hours threshold, no bonus. +- name: Case 3, infant at a step-4 provider with low hours still gets the bonus. period: 2026-01 absolute_error_margin: 0.01 input: @@ -46,15 +49,34 @@ person1: age: 0.5 nd_ccap_provider_type: CENTER - childcare_hours_per_week: 9.0 # below the 9.3 approximation of 40 hours/month + nd_ccap_provider_qris_step: STEP_4 + childcare_hours_per_week: 5 # the bonus has no minimum-hours condition households: household: members: [person1] state_code: ND output: + nd_ccap_infant_toddler_bonus: 200 + +- name: Case 4, infant at a step-1 provider, no bonus. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 0.5 + nd_ccap_provider_type: CENTER + nd_ccap_provider_qris_step: STEP_1 + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: ND + output: + # Step 1 fails the step-2-or-higher provider gate. nd_ccap_infant_toddler_bonus: 0 -- name: Case 4, infant at exactly the weekly-hours threshold, $200 bonus. +- name: Case 5, infant at an unrated provider, no bonus. period: 2026-01 absolute_error_margin: 0.01 input: @@ -62,15 +84,16 @@ person1: age: 0.5 nd_ccap_provider_type: CENTER - childcare_hours_per_week: 9.3 # at the threshold + childcare_hours_per_week: 40 households: household: members: [person1] state_code: ND output: - nd_ccap_infant_toddler_bonus: 200 + # An unrated provider fails the step-2-or-higher provider gate. + nd_ccap_infant_toddler_bonus: 0 -- name: Case 5, infant at an approved-relative provider, no bonus. +- name: Case 6, infant at a step-2 approved-relative provider, no bonus. period: 2026-01 absolute_error_margin: 0.01 input: @@ -78,6 +101,7 @@ person1: age: 0.5 nd_ccap_provider_type: APPROVED_RELATIVE + nd_ccap_provider_qris_step: STEP_2 childcare_hours_per_week: 40 households: household: @@ -87,7 +111,7 @@ # Approved relatives are not eligible for the licensed-provider bonus. nd_ccap_infant_toddler_bonus: 0 -- name: Case 6, preschool child, no bonus. +- name: Case 7, preschool child at a step-3 provider, no bonus. period: 2026-01 absolute_error_margin: 0.01 input: @@ -95,6 +119,7 @@ person1: age: 4 nd_ccap_provider_type: CENTER + nd_ccap_provider_qris_step: STEP_3 childcare_hours_per_week: 40 households: household: @@ -103,7 +128,7 @@ output: nd_ccap_infant_toddler_bonus: 0 -- name: Case 7, school-age child, no bonus. +- name: Case 8, school-age child at a step-3 provider, no bonus. period: 2026-01 absolute_error_margin: 0.01 input: @@ -112,6 +137,7 @@ age: 8 is_in_k12_school: true nd_ccap_provider_type: CENTER + nd_ccap_provider_qris_step: STEP_3 childcare_hours_per_week: 40 households: household: diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_parent_in_eligible_activity.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_parent_in_eligible_activity.py index a4d1971b879..8cf044bdcc8 100644 --- a/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_parent_in_eligible_activity.py +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_parent_in_eligible_activity.py @@ -16,9 +16,13 @@ def formula(person, period, parameters): # activity is documented through proof of income (400-28-55-05). A # parent qualifies with positive wages, nonzero self-employment income # (a business loss still evidences active self-employment), full-time - # student status, TANF enrollment, or a disability (incapacity). We do - # not capture not-yet-paid new employment or active job search at the - # moment. + # student status, or TANF enrollment. A disabled caretaker also + # establishes a child care need: in a one-caretaker household child + # care is allowed when the caretaker is disabled, and in a + # two-caretaker household when one caretaker is in an activity and the + # other is disabled (Incapacity of a Caretaker/Child 400-28-35-15). + # We do not capture not-yet-paid new employment or active job search at + # the moment. has_earnings = (person("employment_income", period) > 0) | ( person("self_employment_income", period) != 0 ) diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/income/nd_ccap_countable_income.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/income/nd_ccap_countable_income.py index 4cebda9564d..6c7091b62f8 100644 --- a/policyengine_us/variables/gov/states/nd/dhs/ccap/income/nd_ccap_countable_income.py +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/income/nd_ccap_countable_income.py @@ -11,11 +11,12 @@ class nd_ccap_countable_income(Variable): reference = "https://www.nd.gov/dhs/policymanuals/40028/40028.htm" def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.nd.dhs.ccap.income gross_income = spm_unit("nd_ccap_gross_income", period) - # The earned income of household members under age 18 is excluded - # (400-28-65-15 #6). + # The earned income of children under age 19 is excluded + # (400-28-65-10-05; 400-28-65-15 #6; NDAC 75-02-01.3-08(12)). person = spm_unit.members - is_minor = person("age", period.this_year) < 18 + is_minor = person("age", period.this_year) < p.child_earned_income_exclusion_age minor_earned_income = spm_unit.sum( is_minor * add(person, period, ["employment_income", "self_employment_income"]) diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_infant_toddler_bonus.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_infant_toddler_bonus.py index fc1bd87ce2a..7af29ba6d5c 100644 --- a/policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_infant_toddler_bonus.py +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_infant_toddler_bonus.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.nd.dhs.ccap.rates.nd_ccap_provider_qris_step import ( + NDCCAPProviderQRISStep, +) from policyengine_us.variables.gov.states.nd.dhs.ccap.rates.nd_ccap_provider_type import ( NDCCAPProviderType, ) @@ -11,26 +14,29 @@ class nd_ccap_infant_toddler_bonus(Variable): label = "North Dakota CCAP infant/toddler bonus per child" definition_period = MONTH defined_for = "nd_ccap_eligible_child" - reference = "https://www.nd.gov/dhs/policymanuals/40028/40028.htm" + reference = ( + "https://www.hhs.nd.gov/ec-news/child-care-assistance-program-ccap-updates-2026" + ) def formula(person, period, parameters): p = parameters(period).gov.states.nd.dhs.ccap.rates - # The infant/toddler bonus is a flat per-child provider payment for - # children attending 40 or more hours per month, paid to North Dakota - # licensed, tribally licensed, or military licensed providers - # (400-28-100-30). The bonus is additive: it is not capped at the - # family's billed expenses and not reduced by the co-payment. The - # parameter is zero for preschool and school-age children, so the age - # filter is handled by the lookup. + # The infant/toddler bonus is a flat per-child provider payment paid to + # licensed, tribally licensed, or military licensed providers rated at + # QRIS step 2 or higher, for all infants and toddlers in care + # (400-28-100-30; 2026 CCAP updates). The bonus is additive: it is not + # capped at the family's billed expenses and not reduced by the + # co-payment. The parameter is zero for preschool and school-age + # children, so the age filter is handled by the lookup. age_group = person("nd_ccap_age_group", period) bonus_amount = p.infant_toddler_bonus[age_group] - # PolicyEngine tracks weekly child care hours, so the 40-hours-per-month - # attendance condition is approximated against a weekly threshold. - hours = person("childcare_hours_per_week", period.this_year) - meets_hours = hours >= p.infant_toddler_bonus_min_weekly_hours - # We do not track the provider's licensure jurisdiction, so we assume - # the licensure condition is met for all providers other than approved - # relatives. + qris_step = person("nd_ccap_provider_qris_step", period) + step_2_or_higher = ( + (qris_step == NDCCAPProviderQRISStep.STEP_2) + | (qris_step == NDCCAPProviderQRISStep.STEP_3) + | (qris_step == NDCCAPProviderQRISStep.STEP_4) + ) + # Approved relatives are not licensed providers and do not receive the + # licensed-provider bonus. provider_type = person("nd_ccap_provider_type", period) - eligible_provider = provider_type != NDCCAPProviderType.APPROVED_RELATIVE - return bonus_amount * meets_hours * eligible_provider + is_licensed_provider = provider_type != NDCCAPProviderType.APPROVED_RELATIVE + return bonus_amount * step_2_or_higher * is_licensed_provider From 2487efd19061bfeac2f137fe6a3ff278b1c18d73 Mon Sep 17 00:00:00 2001 From: Ziming Date: Mon, 22 Jun 2026 00:17:35 -0400 Subject: [PATCH 4/6] Review-fix round 2 polish: remove stale comment, document not-modeled limitations - Remove stale '40 hours/month' comment from infant_toddler_bonus.yaml (no hours gate after R1) - Document child-age grace-month and CCAP Workforce Benefit waiver as not-modeled Co-Authored-By: Claude Opus 4.8 (1M context) --- .../gov/states/nd/dhs/ccap/rates/infant_toddler_bonus.yaml | 6 +++--- .../nd/dhs/ccap/eligibility/nd_ccap_eligible_child.py | 4 ++++ .../nd/dhs/ccap/eligibility/nd_ccap_income_eligible.py | 4 ++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/infant_toddler_bonus.yaml b/policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/infant_toddler_bonus.yaml index dfd223cdc2d..47f305ef7d5 100644 --- a/policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/infant_toddler_bonus.yaml +++ b/policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/infant_toddler_bonus.yaml @@ -1,9 +1,9 @@ description: North Dakota pays providers this additional monthly amount per infant or toddler under the Child Care Assistance Program. # Infant/toddler bonus, paid to North Dakota licensed, tribally licensed, or -# military licensed providers as a separate payment on top of the service-month -# subsidy, for children attending 40 or more hours per month. Effective -# January 1, 2026. Preschool and school-age children receive no bonus. +# military licensed providers rated at QRIS step 2 or higher, as a separate +# payment on top of the service-month subsidy. Effective January 1, 2026. +# Preschool and school-age children receive no bonus. metadata: period: month diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_eligible_child.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_eligible_child.py index 1eea30031e7..32afcb5f570 100644 --- a/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_eligible_child.py +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_eligible_child.py @@ -21,6 +21,10 @@ def formula(person, period, parameters): # court-order pathway is not tracked at the moment (400-28-35-02). is_disabled = person("is_disabled", period.this_year) age_limit = where(is_disabled, p.disabled_child_age_limit, p.child_age_limit) + # A child who turns 13 mid-eligibility-period stays eligible through the + # next review (up to age 14) under the grandfather/grace rule. We don't + # track the application/review month at the moment, so we use a flat + # age < 13 cutoff instead (400-28-35-02). age_eligible = age < age_limit # The child (not the caretaker) must be a United States citizen or an # alien lawfully admitted for permanent residence (400-28-50-25). This diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_income_eligible.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_income_eligible.py index 52dda864958..a2235d409be 100644 --- a/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_income_eligible.py +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_income_eligible.py @@ -22,4 +22,8 @@ def formula(spm_unit, period, parameters): initial_limit = monthly_smi * p.initial_smi_rate continuing_limit = monthly_smi * p.continuing_smi_rate income_limit = where(enrolled, continuing_limit, initial_limit) + # The CCAP Workforce Benefit waives the income limit (and copay) for + # eligible early-childhood workforce families. We don't track which + # families qualify for the Workforce Benefit at the moment, so this + # waiver is not modeled. return countable_income <= income_limit From 0322a2cb4218deb124df75fccc5af99bb2aca54a Mon Sep 17 00:00:00 2001 From: Ziming Date: Fri, 26 Jun 2026 21:15:28 -0400 Subject: [PATCH 5/6] fix --- .../states/nd/dhs/ccap/copay/max_rate.yaml | 11 +- .../gov/states/nd/dhs/ccap/copay/rate.yaml | 134 ++++++++-- .../dhs/ccap/copay/waiver_smi_threshold.yaml | 8 +- .../states/nd/dhs/ccap/rates/full_time.yaml | 32 ++- .../states/nd/dhs/ccap/rates/part_time.yaml | 30 ++- .../nd/dhs/ccap/copay/nd_ccap_copay.yaml | 239 ++++++++++++++---- .../nd_ccap_activity_eligible.yaml | 21 ++ .../dhs/ccap/rates/nd_ccap_base_subsidy.yaml | 18 +- .../states/nd/dhs/ccap/copay/nd_ccap_copay.py | 11 +- .../eligibility/nd_ccap_activity_eligible.py | 7 +- .../nd/dhs/ccap/rates/nd_ccap_base_subsidy.py | 5 + 11 files changed, 421 insertions(+), 95 deletions(-) diff --git a/policyengine_us/parameters/gov/states/nd/dhs/ccap/copay/max_rate.yaml b/policyengine_us/parameters/gov/states/nd/dhs/ccap/copay/max_rate.yaml index c7549b70cb8..4e2137ca39b 100644 --- a/policyengine_us/parameters/gov/states/nd/dhs/ccap/copay/max_rate.yaml +++ b/policyengine_us/parameters/gov/states/nd/dhs/ccap/copay/max_rate.yaml @@ -1,13 +1,18 @@ description: North Dakota caps the family monthly co-payment at this share of countable monthly income under the Child Care Assistance Program. + +# Federal family-share ceiling under 45 CFR 98.45(k); the current HHS co-pay +# worksheet schedule tops out at a 6% band rate, so this 7% cap does not bind. +# Dated to the current co-pay matrix (2024-07-01, ML #3827); earlier periods are +# approximated by back-extrapolation. values: - 2022-10-01: 0.07 + 2024-07-01: 0.07 metadata: unit: /1 period: year label: North Dakota CCAP family co-payment maximum rate reference: - - title: DN 241 (10-2022) Sliding Fee Schedule, Child Care Assistance Program - href: https://ndlegis.gov/assembly/68-2023/testimony/SHUMSER-2190-20230118-13895-F-HOGAN_KATHY.pdf - title: 45 CFR 98.45(k) href: https://www.ecfr.gov/current/title-45/section-98.45#p-98.45(k) + - title: North Dakota HHS Individual Worksheet for Calculating Co-pay + href: https://app.powerbigov.us/view?r=eyJrIjoiOGI5NDY4NzctM2QzOS00NWY4LWJjNTAtMDA5N2IxMmE5NTMyIiwidCI6IjJkZWEwNDY0LWRhNTEtNGE4OC1iYWUyLWIzZGI5NGJjMGM1NCJ9 diff --git a/policyengine_us/parameters/gov/states/nd/dhs/ccap/copay/rate.yaml b/policyengine_us/parameters/gov/states/nd/dhs/ccap/copay/rate.yaml index ff2a04a98a2..bc0a2c5c69c 100644 --- a/policyengine_us/parameters/gov/states/nd/dhs/ccap/copay/rate.yaml +++ b/policyengine_us/parameters/gov/states/nd/dhs/ccap/copay/rate.yaml @@ -1,37 +1,131 @@ -description: North Dakota sets the family monthly co-payment as this share of countable monthly income, by income as a share of the state median income, under the Child Care Assistance Program. - -# PENDING validation against current Power BI worksheet. North Dakota does not -# publish a static co-pay percentage schedule; the family monthly co-pay is -# computed through the HHS Power BI "Worksheet for Calculating Copay". The -# bracket structure below recovers the schedule documented in DN 241 (10-2022): -# the co-pay is waived at or below 30% SMI, then rises through the sliding-fee -# levels to a 7% ceiling above 40% SMI. Replace these percentages once the -# current Power BI values are confirmed. Brackets are keyed on income as a share -# of the state median income; each "above X%" threshold is shifted by +0.0001 so -# that income exactly at a band's ceiling stays in the lower band. +description: North Dakota sets the family monthly co-payment as this share of the monthly state median income, by the family's income as a share of the state median income, under the Child Care Assistance Program. +# HHS "Individual Worksheet for Calculating Co-pay" (ND publishes no static +# table). Each amount = band rate x the band's lower SMI bound, so co-pay = +# amount x monthly SMI. Band rate: 2% (30-50%), 4% (50-60%), 6% (60%+); waived +# at or below 30% SMI. Effective 2024-07-01 (ML #3827); pre-2024-07 not modeled. brackets: - threshold: - 2022-10-01: 0 + 2024-07-01: 0 + amount: + 2024-07-01: 0 # at or below 30%: waived + - threshold: + 2024-07-01: 0.3 + amount: + 2024-07-01: 0.006 # 30-32%: 2% x 0.3 + - threshold: + 2024-07-01: 0.32 + amount: + 2024-07-01: 0.0064 # 32-34% + - threshold: + 2024-07-01: 0.34 + amount: + 2024-07-01: 0.0068 # 34-36% + - threshold: + 2024-07-01: 0.36 + amount: + 2024-07-01: 0.0072 # 36-38% + - threshold: + 2024-07-01: 0.38 + amount: + 2024-07-01: 0.0076 # 38-40% + - threshold: + 2024-07-01: 0.4 + amount: + 2024-07-01: 0.008 # 40-42%: 2% x 0.4 + - threshold: + 2024-07-01: 0.42 + amount: + 2024-07-01: 0.0084 # 42-44% + - threshold: + 2024-07-01: 0.44 + amount: + 2024-07-01: 0.0088 # 44-46% + - threshold: + 2024-07-01: 0.46 + amount: + 2024-07-01: 0.0092 # 46-48% + - threshold: + 2024-07-01: 0.48 + amount: + 2024-07-01: 0.0096 # 48-50% + - threshold: + 2024-07-01: 0.5 + amount: + 2024-07-01: 0.02 # 50-52%: 4% x 0.5 + - threshold: + 2024-07-01: 0.52 + amount: + 2024-07-01: 0.0208 # 52-54% + - threshold: + 2024-07-01: 0.54 + amount: + 2024-07-01: 0.0216 # 54-56% + - threshold: + 2024-07-01: 0.56 + amount: + 2024-07-01: 0.0224 # 56-58% + - threshold: + 2024-07-01: 0.58 + amount: + 2024-07-01: 0.0232 # 58-60% + - threshold: + 2024-07-01: 0.6 + amount: + 2024-07-01: 0.036 # 60-63%: 6% x 0.6 (3-point band) + - threshold: + 2024-07-01: 0.63 + amount: + 2024-07-01: 0.0378 # 63-65%: 6% x 0.63 + - threshold: + 2024-07-01: 0.65 + amount: + 2024-07-01: 0.039 # 65-67% + - threshold: + 2024-07-01: 0.67 + amount: + 2024-07-01: 0.0402 # 67-69% + - threshold: + 2024-07-01: 0.69 + amount: + 2024-07-01: 0.0414 # 69-71% + - threshold: + 2024-07-01: 0.71 + amount: + 2024-07-01: 0.0426 # 71-73% + - threshold: + 2024-07-01: 0.73 + amount: + 2024-07-01: 0.0438 # 73-75% + - threshold: + 2024-07-01: 0.75 + amount: + 2024-07-01: 0.045 # 75-77% (enrolled) + - threshold: + 2024-07-01: 0.77 + amount: + 2024-07-01: 0.0462 # 77-79% (enrolled) + - threshold: + 2024-07-01: 0.79 amount: - 2022-10-01: 0 # at or below 30% SMI: waived + 2024-07-01: 0.0474 # 79-81% (enrolled) - threshold: - 2022-10-01: 0.3001 # above 30% up to 40% SMI + 2024-07-01: 0.81 amount: - 2022-10-01: 0.06 + 2024-07-01: 0.0486 # 81-83% (enrolled) - threshold: - 2022-10-01: 0.4001 # above 40% SMI (capped at 7%) + 2024-07-01: 0.83 amount: - 2022-10-01: 0.07 + 2024-07-01: 0.0498 # 83-85% (enrolled) metadata: type: single_amount threshold_unit: /1 amount_unit: /1 period: year - label: North Dakota CCAP family co-payment rate by SMI share + label: North Dakota CCAP family co-payment share of SMI by income band reference: - title: North Dakota CCAP Policy Manual, Co-pay Determination 400-28-90-10 href: https://www.nd.gov/dhs/policymanuals/40028/40028.htm - - title: DN 241 (10-2022) Sliding Fee Schedule, Child Care Assistance Program - href: https://ndlegis.gov/assembly/68-2023/testimony/SHUMSER-2190-20230118-13895-F-HOGAN_KATHY.pdf + - title: North Dakota HHS Individual Worksheet for Calculating Co-pay + href: https://app.powerbigov.us/view?r=eyJrIjoiOGI5NDY4NzctM2QzOS00NWY4LWJjNTAtMDA5N2IxMmE5NTMyIiwidCI6IjJkZWEwNDY0LWRhNTEtNGE4OC1iYWUyLWIzZGI5NGJjMGM1NCJ9 diff --git a/policyengine_us/parameters/gov/states/nd/dhs/ccap/copay/waiver_smi_threshold.yaml b/policyengine_us/parameters/gov/states/nd/dhs/ccap/copay/waiver_smi_threshold.yaml index e29df499ff4..80327e09a4b 100644 --- a/policyengine_us/parameters/gov/states/nd/dhs/ccap/copay/waiver_smi_threshold.yaml +++ b/policyengine_us/parameters/gov/states/nd/dhs/ccap/copay/waiver_smi_threshold.yaml @@ -1,6 +1,10 @@ description: North Dakota waives the family co-payment for families with income at or below this share of the state median income under the Child Care Assistance Program. + +# Dated to the current co-pay matrix (2024-07-01, ML #3827), which incorporates +# this waiver. The standalone 30% SMI waiver was first added 2023-07-01 (ML +# #3732); earlier periods are approximated by back-extrapolation. values: - 2022-10-01: 0.3 + 2024-07-01: 0.3 metadata: unit: /1 @@ -9,3 +13,5 @@ metadata: reference: - title: North Dakota CCAP Policy Manual, Co-pay Waived 400-28-90-20 href: https://www.nd.gov/dhs/policymanuals/40028/40028.htm + - title: North Dakota HHS Individual Worksheet for Calculating Co-pay + href: https://app.powerbigov.us/view?r=eyJrIjoiOGI5NDY4NzctM2QzOS00NWY4LWJjNTAtMDA5N2IxMmE5NTMyIiwidCI6IjJkZWEwNDY0LWRhNTEtNGE4OC1iYWUyLWIzZGI5NGJjMGM1NCJ9 diff --git a/policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/full_time.yaml b/policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/full_time.yaml index 0067c229c44..e755fee4738 100644 --- a/policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/full_time.yaml +++ b/policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/full_time.yaml @@ -1,9 +1,15 @@ description: North Dakota provides these full-time monthly maximum reimbursement rates by provider type and age group under the Child Care Assistance Program. -# Statewide flat full-time (25+ hours/week) monthly maximums effective -# January 1, 2026, from the 2025 Market Rate Survey. Infant and toddler rates -# are set at the 75th percentile of market rate; preschool and school-age rates -# at the 50th percentile. +# Statewide flat full-time (25+ hours/week) monthly maximums. +# +# 2022-10-01: DN 241 (10-2022) Sliding Fee Schedule, "State Provider Rates" +# (Allowable Monthly Maximums for Full Time Care, Category One). Listed amounts +# are rounded down to whole dollars per the schedule note "State provider rates +# will be rounded down when the rate is not a full dollar amount". +# +# 2026-01-01: 2025 Market Rate Survey. Infant and toddler rates are set at the +# 75th percentile of market rate; preschool and school-age rates at the 50th +# percentile. metadata: period: month @@ -15,42 +21,60 @@ metadata: reference: - title: North Dakota CCAP Policy Manual, Basis for Allowable Child Care Rate of Payments 400-28-100-30 href: https://www.nd.gov/dhs/policymanuals/40028/40028.htm + - title: DN 241 (10-2022) Sliding Fee Schedule, Child Care Assistance Program + href: https://ndlegis.gov/assembly/68-2023/testimony/SHUMSER-2190-20230118-13895-F-HOGAN_KATHY.pdf - title: North Dakota HHS Child Care Assistance Program provider rates href: https://www.hhs.nd.gov/human-services/providers/ccap CENTER: INFANT: + 2022-10-01: 913 2026-01-01: 1_240 TODDLER: + 2022-10-01: 888 2026-01-01: 1_124 PRESCHOOL: + 2022-10-01: 811 2026-01-01: 940 SCHOOL_AGE: + 2022-10-01: 760 2026-01-01: 800 LICENSED_FAMILY: INFANT: + 2022-10-01: 700 2026-01-01: 900 TODDLER: + 2022-10-01: 700 2026-01-01: 880 PRESCHOOL: + 2022-10-01: 680 2026-01-01: 740 SCHOOL_AGE: + 2022-10-01: 660 2026-01-01: 700 SELF_DECLARED_TRIBAL: INFANT: + 2022-10-01: 462 2026-01-01: 646 TODDLER: + 2022-10-01: 429 2026-01-01: 600 PRESCHOOL: + 2022-10-01: 420 2026-01-01: 531 SCHOOL_AGE: + 2022-10-01: 407 2026-01-01: 529 APPROVED_RELATIVE: INFANT: + 2022-10-01: 367 2026-01-01: 422 TODDLER: + 2022-10-01: 346 2026-01-01: 398 PRESCHOOL: + 2022-10-01: 338 2026-01-01: 351 SCHOOL_AGE: + 2022-10-01: 325 2026-01-01: 348 diff --git a/policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/part_time.yaml b/policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/part_time.yaml index 71320755f51..5a274630bc6 100644 --- a/policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/part_time.yaml +++ b/policyengine_us/parameters/gov/states/nd/dhs/ccap/rates/part_time.yaml @@ -1,8 +1,14 @@ description: North Dakota provides these part-time monthly maximum reimbursement rates by provider type and age group under the Child Care Assistance Program. -# Statewide flat part-time (less than 25 hours/week) monthly maximums effective -# January 1, 2026, from the 2025 Market Rate Survey. All part-time rates are set -# at the 50th percentile of market rate. +# Statewide flat part-time (less than 25 hours/week) monthly maximums. +# +# 2022-10-01: DN 241 (10-2022) Sliding Fee Schedule, "State Provider Rates" +# (Allowable Monthly Maximums for Part-Time Care, Category Two). Listed amounts +# are rounded down to whole dollars per the schedule note "State provider rates +# will be rounded down when the rate is not a full dollar amount". +# +# 2026-01-01: 2025 Market Rate Survey. All part-time rates are set at the 50th +# percentile of market rate. metadata: period: month @@ -14,42 +20,60 @@ metadata: reference: - title: North Dakota CCAP Policy Manual, Basis for Allowable Child Care Rate of Payments 400-28-100-30 href: https://www.nd.gov/dhs/policymanuals/40028/40028.htm + - title: DN 241 (10-2022) Sliding Fee Schedule, Child Care Assistance Program + href: https://ndlegis.gov/assembly/68-2023/testimony/SHUMSER-2190-20230118-13895-F-HOGAN_KATHY.pdf - title: North Dakota HHS Child Care Assistance Program provider rates href: https://www.hhs.nd.gov/human-services/providers/ccap CENTER: INFANT: + 2022-10-01: 900 2026-01-01: 546 TODDLER: + 2022-10-01: 824 2026-01-01: 529 PRESCHOOL: + 2022-10-01: 565 2026-01-01: 489 SCHOOL_AGE: + 2022-10-01: 535 2026-01-01: 416 LICENSED_FAMILY: INFANT: + 2022-10-01: 700 2026-01-01: 416 TODDLER: + 2022-10-01: 700 2026-01-01: 393 PRESCHOOL: + 2022-10-01: 615 2026-01-01: 385 SCHOOL_AGE: + 2022-10-01: 540 2026-01-01: 364 SELF_DECLARED_TRIBAL: INFANT: + 2022-10-01: 277 2026-01-01: 284 TODDLER: + 2022-10-01: 257 2026-01-01: 283 PRESCHOOL: + 2022-10-01: 252 2026-01-01: 276 SCHOOL_AGE: + 2022-10-01: 244 2026-01-01: 275 APPROVED_RELATIVE: INFANT: + 2022-10-01: 220 2026-01-01: 186 TODDLER: + 2022-10-01: 207 2026-01-01: 187 PRESCHOOL: + 2022-10-01: 202 2026-01-01: 182 SCHOOL_AGE: + 2022-10-01: 195 2026-01-01: 181 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/copay/nd_ccap_copay.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/copay/nd_ccap_copay.yaml index 78fdc1e67ae..6b5973c305d 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/copay/nd_ccap_copay.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/copay/nd_ccap_copay.yaml @@ -1,16 +1,23 @@ # Tests for nd_ccap_copay (SPMUnit, MONTH, USD). -# The family monthly co-payment is a share of countable monthly income by -# income-to-SMI band, capped at 7% of income, and waived at or below 30% SMI or -# for TANF recipients (400-28-90-10/-20; DN 241 10-2022). The bracket -# percentages are PENDING validation against the HHS Power BI worksheet (the -# copay/rate.yaml parameter); when those values change, update these expected -# copays together. -# 2-person ND monthly SMI for 2026 = $7,762.97 (30% = $2,328.89, 40% = $3,105.19). +# The family monthly co-payment is a flat amount per SMI band from the HHS +# "Individual Worksheet for Calculating Co-pay": copay = share(income/SMI) x +# monthly SMI, where the band share = band rate (2% for 30-50%, 4% for 50-60%, +# 6% for 60%+) x the band's lower SMI bound. Waived at or below 30% SMI or for +# TANF recipients (400-28-90-10/-20). The rate structure was validated against +# the worksheet for household sizes 1-8 on 2026-06-26. +# +# Period is 2025-01: at that period PolicyEngine's hhs_smi equals the real +# published FFY2025 SMI exactly (the uprating projection only applies after the +# last published value, 2025-10-01), so these cases use real-world SMI rather +# than the uprated calendar-2026 projection. +# ND 4-person annual SMI FFY2025 = $123,092. Monthly SMI by size: +# 2-person = 123,092 * 0.68 / 12 = $6,975.21 (30% = $2,092.56) +# 3-person = 123,092 * 0.84 / 12 = $8,616.44 # Countable income is set directly and the unit is made eligible so copay # (defined_for = nd_ccap_eligible) is computed. - name: Case 1, income at or below 30% SMI, co-payment waived. - period: 2026-01 + period: 2025-01 absolute_error_margin: 0.01 input: people: @@ -21,7 +28,7 @@ spm_units: spm_unit: members: [person1, person2] - nd_ccap_countable_income: 2_000 # ratio 0.2576, at or below 30% SMI + nd_ccap_countable_income: 1_883 # ratio 0.2700, at or below 30% SMI nd_ccap_income_eligible: true is_ccdf_asset_eligible: true nd_ccap_activity_eligible: true @@ -32,8 +39,8 @@ output: nd_ccap_copay: 0 -- name: Case 2, income exactly at 30% SMI, co-payment waived (at or below). - period: 2026-01 +- name: Case 2, income just below 30% SMI, still waived. + period: 2025-01 absolute_error_margin: 0.01 input: people: @@ -44,7 +51,7 @@ spm_units: spm_unit: members: [person1, person2] - nd_ccap_countable_income: 2_328.89 # exactly 30% SMI + nd_ccap_countable_income: 2_058 # ratio 0.2950, at or below 30% SMI nd_ccap_income_eligible: true is_ccdf_asset_eligible: true nd_ccap_activity_eligible: true @@ -55,8 +62,8 @@ output: nd_ccap_copay: 0 -- name: Case 3, income in the 30-40% SMI band, 6% of income. - period: 2026-01 +- name: Case 3, income just above 30% SMI, 30-32% band (2% tier). + period: 2025-01 absolute_error_margin: 0.01 input: people: @@ -67,7 +74,7 @@ spm_units: spm_unit: members: [person1, person2] - nd_ccap_countable_income: 2_717.04 # ratio 0.35 + nd_ccap_countable_income: 2_170 # ratio 0.3111, band 30-32% nd_ccap_income_eligible: true is_ccdf_asset_eligible: true nd_ccap_activity_eligible: true @@ -76,11 +83,11 @@ members: [person1, person2] state_code: ND output: - # 2,717.04 * 0.06 = 163.02 - nd_ccap_copay: 163.02 + # share 0.0060 * 6,975.21 = 41.85 + nd_ccap_copay: 41.85 -- name: Case 4, income above 40% SMI, capped at 7% of income. - period: 2026-01 +- name: Case 4, income in the 44-46% band (2% tier). + period: 2025-01 absolute_error_margin: 0.01 input: people: @@ -91,7 +98,7 @@ spm_units: spm_unit: members: [person1, person2] - nd_ccap_countable_income: 3_881.49 # ratio 0.50 + nd_ccap_countable_income: 3_150 # ratio 0.4516, band 44-46% nd_ccap_income_eligible: true is_ccdf_asset_eligible: true nd_ccap_activity_eligible: true @@ -100,11 +107,11 @@ members: [person1, person2] state_code: ND output: - # 3,881.49 * 0.07 = 271.70 - nd_ccap_copay: 271.70 + # share 0.0088 * 6,975.21 = 61.38 + nd_ccap_copay: 61.38 -- name: Case 5, TANF recipient, co-payment waived regardless of income. - period: 2026-01 +- name: Case 5, income just below 50% SMI, 48-50% band (still 2% tier). + period: 2025-01 absolute_error_margin: 0.01 input: people: @@ -115,20 +122,20 @@ spm_units: spm_unit: members: [person1, person2] - nd_ccap_countable_income: 5_000 # well above 40% SMI + nd_ccap_countable_income: 3_450 # ratio 0.4946, band 48-50% nd_ccap_income_eligible: true is_ccdf_asset_eligible: true nd_ccap_activity_eligible: true - is_tanf_enrolled: true households: household: members: [person1, person2] state_code: ND output: - nd_ccap_copay: 0 + # share 0.0096 * 6,975.21 = 66.96 + nd_ccap_copay: 66.96 -- name: Case 6, negative countable income produces no co-payment. - period: 2026-01 +- name: Case 6, income just above 50% SMI, 50-52% band (4% tier begins). + period: 2025-01 absolute_error_margin: 0.01 input: people: @@ -139,7 +146,7 @@ spm_units: spm_unit: members: [person1, person2] - nd_ccap_countable_income: -5_000 # floored at zero in the formula + nd_ccap_countable_income: 3_560 # ratio 0.5104, band 50-52% nd_ccap_income_eligible: true is_ccdf_asset_eligible: true nd_ccap_activity_eligible: true @@ -148,10 +155,11 @@ members: [person1, person2] state_code: ND output: - nd_ccap_copay: 0 + # share 0.0200 * 6,975.21 = 139.50 (4% tier; up from 2% below 50%) + nd_ccap_copay: 139.50 -- name: Case 7, zero countable income, co-payment waived. - period: 2026-01 +- name: Case 7, income in the 56-58% band (4% tier). + period: 2025-01 absolute_error_margin: 0.01 input: people: @@ -162,7 +170,7 @@ spm_units: spm_unit: members: [person1, person2] - nd_ccap_countable_income: 0 # ratio 0, well below 30% SMI + nd_ccap_countable_income: 3_980 # ratio 0.5706, band 56-58% nd_ccap_income_eligible: true is_ccdf_asset_eligible: true nd_ccap_activity_eligible: true @@ -171,10 +179,11 @@ members: [person1, person2] state_code: ND output: - nd_ccap_copay: 0 + # share 0.0224 * 6,975.21 = 156.24 + nd_ccap_copay: 156.24 -- name: Case 8, income just above 30% SMI but at the band ceiling, still waived. - period: 2026-01 +- name: Case 8, income just above 60% SMI, 60-62% band (6% tier begins). + period: 2025-01 absolute_error_margin: 0.01 input: people: @@ -185,9 +194,7 @@ spm_units: spm_unit: members: [person1, person2] - # ratio 0.3001 sits exactly on the 6% band threshold; the bracket - # applies 6% only strictly above 0.3001, so this lands in the 0% band. - nd_ccap_countable_income: 2_329.6672 + nd_ccap_countable_income: 4_260 # ratio 0.6107, band 60-62% nd_ccap_income_eligible: true is_ccdf_asset_eligible: true nd_ccap_activity_eligible: true @@ -196,10 +203,11 @@ members: [person1, person2] state_code: ND output: - nd_ccap_copay: 0 + # share 0.0360 * 6,975.21 = 251.11 (6% tier; up from 4% below 60%) + nd_ccap_copay: 251.11 -- name: Case 9, income at exactly 40% SMI, still in the 6% band (not yet 7%). - period: 2026-01 +- name: Case 9, income in the 73-75% band (top of the applicant range, 6% tier). + period: 2025-01 absolute_error_margin: 0.01 input: people: @@ -210,7 +218,7 @@ spm_units: spm_unit: members: [person1, person2] - nd_ccap_countable_income: 3_105.1882 # exactly 40% SMI, ratio 0.40 + nd_ccap_countable_income: 5_170 # ratio 0.7412, band 73-75% nd_ccap_income_eligible: true is_ccdf_asset_eligible: true nd_ccap_activity_eligible: true @@ -219,11 +227,11 @@ members: [person1, person2] state_code: ND output: - # ratio 0.40 < 0.4001, so still the 6% band: 3,105.1882 * 0.06 = 186.31. - nd_ccap_copay: 186.31 + # share 0.0438 * 6,975.21 = 305.51 + nd_ccap_copay: 305.51 -- name: Case 10, income at the 40% band ceiling, still 6% (7% applies only above 40.01%). - period: 2026-01 +- name: Case 10, TANF recipient, co-payment waived regardless of income. + period: 2025-01 absolute_error_margin: 0.01 input: people: @@ -234,16 +242,141 @@ spm_units: spm_unit: members: [person1, person2] - # ratio 0.4001 sits exactly on the 7% band threshold; the bracket - # applies 7% only strictly above 0.4001, so this stays in the 6% band. - nd_ccap_countable_income: 3_105.9644 + nd_ccap_countable_income: 4_000 # well above 30% SMI nd_ccap_income_eligible: true is_ccdf_asset_eligible: true nd_ccap_activity_eligible: true + is_tanf_enrolled: true households: household: members: [person1, person2] state_code: ND output: - # 3,105.9644 * 0.06 = 186.36 (the 7% cap does not yet bind). - nd_ccap_copay: 186.36 + nd_ccap_copay: 0 + +- name: Case 11, negative countable income produces no co-payment. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + person2: + age: 3 + spm_units: + spm_unit: + members: [person1, person2] + nd_ccap_countable_income: -5_000 # floored at zero in the formula + nd_ccap_income_eligible: true + is_ccdf_asset_eligible: true + nd_ccap_activity_eligible: true + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_copay: 0 + +- name: Case 12, 3-person at 32.96% SMI, 32-34% band (2% tier). + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + person2: + age: 3 + person3: + age: 1 + spm_units: + spm_unit: + members: [person1, person2, person3] + nd_ccap_countable_income: 2_840 # ratio 0.3296, band 32-34% + nd_ccap_income_eligible: true + is_ccdf_asset_eligible: true + nd_ccap_activity_eligible: true + households: + household: + members: [person1, person2, person3] + state_code: ND + output: + # share 0.0064 * 8,616.44 = 55.15 + nd_ccap_copay: 55.15 + +- name: Case 13, 3-person at 53.04% SMI, 52-54% band (4% tier). + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + person2: + age: 3 + person3: + age: 1 + spm_units: + spm_unit: + members: [person1, person2, person3] + nd_ccap_countable_income: 4_570 # ratio 0.5304, band 52-54% + nd_ccap_income_eligible: true + is_ccdf_asset_eligible: true + nd_ccap_activity_eligible: true + households: + household: + members: [person1, person2, person3] + state_code: ND + output: + # share 0.0208 * 8,616.44 = 179.22 + nd_ccap_copay: 179.22 + +- name: Case 14, 3-person at 61.05% SMI, 60-62% band (6% tier). + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + person2: + age: 3 + person3: + age: 1 + spm_units: + spm_unit: + members: [person1, person2, person3] + nd_ccap_countable_income: 5_260 # ratio 0.6105, band 60-62% + nd_ccap_income_eligible: true + is_ccdf_asset_eligible: true + nd_ccap_activity_eligible: true + households: + household: + members: [person1, person2, person3] + state_code: ND + output: + # share 0.0360 * 8,616.44 = 310.19 + nd_ccap_copay: 310.19 + +- name: Case 15, 3-person enrolled at 80.08% SMI, 79-81% band (6% tier). + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + person2: + age: 3 + person3: + age: 1 + spm_units: + spm_unit: + members: [person1, person2, person3] + nd_ccap_countable_income: 6_900 # ratio 0.8008, band 79-81% (enrolled range) + nd_ccap_income_eligible: true + is_ccdf_asset_eligible: true + nd_ccap_activity_eligible: true + households: + household: + members: [person1, person2, person3] + state_code: ND + output: + # share 0.0474 * 8,616.44 = 408.42 + nd_ccap_copay: 408.42 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_activity_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_activity_eligible.yaml index 5cb288d51f8..fec407a9059 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_activity_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_activity_eligible.yaml @@ -111,3 +111,24 @@ state_code: ND output: nd_ccap_activity_eligible: false + +- name: Case 6, caretaker not individually active but meets the CCDF activity test, eligible via fallback. + period: 2026-01 + input: + people: + person1: + age: 30 # no modeled earnings, school, TANF, or disability + is_tax_unit_head_or_spouse: true + person2: + age: 3 + is_tax_unit_head_or_spouse: false + spm_units: + spm_unit: + members: [person1, person2] + meets_ccdf_activity_test: true # e.g. job search or education/training + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_ccap_activity_eligible: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_base_subsidy.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_base_subsidy.yaml index 8cf31958ea7..754ee388a81 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_base_subsidy.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_base_subsidy.yaml @@ -13,7 +13,7 @@ people: person1: age: 30 - employment_income: 48_000 # 4,000/mo, ratio 0.515 -> copay 280 + employment_income: 48_000 # 4,000/mo, ratio 0.543 -> copay 159.01 person2: age: 0.5 nd_ccap_provider_type: CENTER @@ -27,9 +27,10 @@ members: [person1, person2] state_code: ND output: - # rate 1,240 < expenses 2,000, so capped at 1,240; copay 280. - # base = 1,240 - 280 = 960. - nd_ccap_base_subsidy: 960 + # rate 1,240 < expenses 2,000, so capped at 1,240; copay 159.01 (HHS + # worksheet share for the 54-56% SMI band x monthly SMI). + # base = 1,240 - 159.01 = 1,080.99. + nd_ccap_base_subsidy: 1_080.99 - name: Case 2, expense-limited base, co-payment waived. period: 2026-01 @@ -63,7 +64,7 @@ people: person1: age: 30 - employment_income: 60_000 # 5,000/mo, ratio 0.644 -> copay 350 + employment_income: 72_000 # 6,000/mo, ratio 0.815 -> copay 357.78 person2: age: 8 is_in_k12_school: true @@ -73,12 +74,13 @@ spm_unit: members: [person1, person2] spm_unit_pre_subsidy_childcare_expenses: 24_000 # 2,000/mo - is_nd_ccap_enrolled: true # 5,000/mo passes the 85% tier + is_nd_ccap_enrolled: true # 6,000/mo passes the 85% tier households: household: members: [person1, person2] state_code: ND output: - # approved-relative school-age full-time rate 348; copay 5,000 * 0.07 = 350. - # base = max(min(2,000, 348) - 350, 0) = max(-2, 0) = 0. + # approved-relative school-age full-time rate 348; copay 357.78 (81-83% SMI + # band share x monthly SMI) exceeds the capped rate. + # base = max(min(2,000, 348) - 357.78, 0) = max(-9.78, 0) = 0. nd_ccap_base_subsidy: 0 diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_copay.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_copay.py index 0d5414a9c21..ecf4ca3cf01 100644 --- a/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_copay.py +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/copay/nd_ccap_copay.py @@ -19,8 +19,15 @@ def formula(spm_unit, period, parameters): # auto-divides it to a monthly value. monthly_smi = spm_unit("hhs_smi", period) income_to_smi = where(monthly_smi > 0, countable_income / monthly_smi, 0) - copay_rate = p.rate.calc(income_to_smi) - copay = min_(countable_income * copay_rate, countable_income * p.max_rate) + # The HHS co-pay worksheet charges a flat amount per SMI band equal to + # the band's co-pay share of monthly SMI (rate x band floor), so the + # co-pay depends on the family's SMI band rather than its exact income. + copay_share = p.rate.calc(income_to_smi) + copay = copay_share * monthly_smi + # Defensive federal family-share ceiling (45 CFR 98.45(k)): the co-pay + # never exceeds max_rate of countable income. This does not bind under + # the current schedule (the band rate tops out at 6%). + copay = min_(copay, p.max_rate * countable_income) # The co-payment is waived for families at or below 30% of the state # median income and for TANF recipients (400-28-90-20). Diversion and # Crossroads recipients are also waived but are not tracked at the diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_activity_eligible.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_activity_eligible.py index 0f669701ed1..080c50a68d8 100644 --- a/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_activity_eligible.py +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/eligibility/nd_ccap_activity_eligible.py @@ -19,4 +19,9 @@ def formula(spm_unit, period, parameters): in_eligible_activity = person("nd_ccap_parent_in_eligible_activity", period) has_head_or_spouse = spm_unit.sum(is_head_or_spouse) >= 1 all_covered = spm_unit.sum(is_head_or_spouse & ~in_eligible_activity) == 0 - return has_head_or_spouse & all_covered + modeled_eligible = has_head_or_spouse & all_covered + # Fall back to the CCDF activity-test input for approved activities not + # individually modeled (job search, education or training programs, + # SNAP E&T, or a temporary leave from work or school; 400-28-55-05). + meets_ccdf = spm_unit("meets_ccdf_activity_test", period.this_year) + return modeled_eligible | meets_ccdf diff --git a/policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_base_subsidy.py b/policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_base_subsidy.py index f3e1c47b72f..26c4b68a821 100644 --- a/policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_base_subsidy.py +++ b/policyengine_us/variables/gov/states/nd/dhs/ccap/rates/nd_ccap_base_subsidy.py @@ -18,6 +18,11 @@ def formula(spm_unit, period, parameters): # expense cap. The expense cap pools the per-child maximum rates across # the unit (the IN / RI / MA convention), because billed expenses are a # single SPM-unit input. + # Registration fees (annual enrollment fees CCAP pays directly to + # C/E/K/M/F/G/H providers, up to $150 per child per calendar year, + # 400-28-130-15-15) are not modeled: they are a separate provider + # payment outside the monthly care subsidy, and we don't track whether a + # provider charges a registration fee or its amount at the moment. maximum_monthly_rate = add(spm_unit, period, ["nd_ccap_state_max_rate"]) pre_subsidy_childcare_expenses = spm_unit( "spm_unit_pre_subsidy_childcare_expenses", period From 9a29028b594d0913cb2ea4e5c3b6583d9f3d4cda Mon Sep 17 00:00:00 2001 From: Ziming Date: Sat, 27 Jun 2026 18:32:33 -0400 Subject: [PATCH 6/6] Fix ND CCAP tests for the validated copay schedule and FFY2026 SMI - Recalibrate nd_ccap_income_eligible incomes to the real FFY2026 SMI (the published value, now in gov/hhs/smi/amount.yaml upstream). - Update integration/nd_ccap copay-cascade expecteds at 2026-01. - Update QRIS 2025-era bonus expecteds for the 2022 DN 241 provider rates. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../eligibility/nd_ccap_income_eligible.yaml | 18 +++---- .../gov/states/nd/dhs/ccap/integration.yaml | 53 ++++++++++--------- .../gov/states/nd/dhs/ccap/nd_ccap.yaml | 8 +-- .../ccap/rates/nd_ccap_qris_step_bonus.yaml | 21 ++++---- 4 files changed, 51 insertions(+), 49 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_income_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_income_eligible.yaml index 4c43ad80bd6..640bd12232a 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_income_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/eligibility/nd_ccap_income_eligible.yaml @@ -3,8 +3,8 @@ # the monthly state median income; enrolled recipients (is_nd_ccap_enrolled = # true) are tested against 85% (400-28-25-15). Countable income is set # directly to isolate the two-tier comparison. -# 2-person ND monthly SMI for 2026 (uprated) = $7,762.97/mo: -# 75% = $5,822.23/mo, 85% = $6,598.53/mo. +# 2-person ND monthly SMI for 2026 = $7,361.62/mo: +# 75% = $5,521.22/mo, 85% = $6,257.38/mo. # nd_ccap_countable_income is a MONTH variable, so values are monthly. - name: Case 1, applicant just below 75% SMI, income eligible. @@ -18,7 +18,7 @@ spm_units: spm_unit: members: [person1, person2] - nd_ccap_countable_income: 5_800 # below 5,822.23 (75% SMI) + nd_ccap_countable_income: 5_500 # below 5,521.22 (75% SMI) is_nd_ccap_enrolled: false households: household: @@ -38,7 +38,7 @@ spm_units: spm_unit: members: [person1, person2] - nd_ccap_countable_income: 5_900 # above 5,822.23 (75% SMI) + nd_ccap_countable_income: 5_900 # above 5,521.22 (75% SMI) is_nd_ccap_enrolled: false households: household: @@ -58,7 +58,7 @@ spm_units: spm_unit: members: [person1, person2] - nd_ccap_countable_income: 5_900 # above 75% but below 6,598.53 (85% SMI) + nd_ccap_countable_income: 5_900 # above 75% but below 6,257.38 (85% SMI) is_nd_ccap_enrolled: true households: household: @@ -78,7 +78,7 @@ spm_units: spm_unit: members: [person1, person2] - nd_ccap_countable_income: 6_700 # above 6,598.53 (85% SMI) + nd_ccap_countable_income: 6_700 # above 6,257.38 (85% SMI) is_nd_ccap_enrolled: true households: household: @@ -98,7 +98,7 @@ spm_units: spm_unit: members: [person1, person2] - nd_ccap_countable_income: 5_822.22 # at 75% SMI (5,822.23) + nd_ccap_countable_income: 5_521.21 # at 75% SMI (5,521.22) is_nd_ccap_enrolled: false households: household: @@ -118,7 +118,7 @@ spm_units: spm_unit: members: [person1, person2] - nd_ccap_countable_income: 6_598.525 # exactly 85% SMI; <= passes + nd_ccap_countable_income: 6_257.37 # at 85% SMI (6,257.38); <= passes is_nd_ccap_enrolled: true households: household: @@ -138,7 +138,7 @@ spm_units: spm_unit: members: [person1, person2] - nd_ccap_countable_income: 6_598.53 # just above 85% SMI (6,598.525) + nd_ccap_countable_income: 6_598.53 # above 85% SMI (6,257.38) is_nd_ccap_enrolled: true households: household: diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/integration.yaml index c2accdeeea1..07fa56297e7 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/integration.yaml @@ -6,11 +6,11 @@ # child_care_subsidies total. # # All monetary rates are the statewide flat monthly maximums effective -# 2026-01-01. 2-person ND monthly SMI for 2026 = $7,762.97 (75% = $5,822.23, -# 85% = $6,598.53, 30% = $2,328.89, 40% = $3,105.19). The co-pay bracket -# percentages (6% in the 30-40% band, 7% above 40%) are PENDING validation -# against the HHS Power BI worksheet; update these expectations together with -# the copay/rate.yaml parameter. +# 2026-01-01. 2-person ND monthly SMI for 2026 = $7,361.62 (75% = $5,521.22, +# 85% = $6,257.38, 30% = $2,208.49, 40% = $2,944.65). The co-pay follows the +# HHS worksheet band schedule: a flat amount per 2-point SMI band equal to the +# band rate (2% in 30-50% SMI, 4% in 50-60%, 6% in 60-85%) times the band's +# lower SMI bound times monthly SMI; waived at or below 30% SMI. - name: Case 1, single working parent, infant in a step-3 center, positive subsidy. period: 2026-01 @@ -35,16 +35,17 @@ state_code: ND output: nd_ccap_eligible: true - # countable income 2,500/mo; ratio 0.322 -> 6% band; copay 2,500 * 0.06 = 150. + # countable income 2,500/mo; ratio 0.340 -> 32-34% SMI band, share 0.0064; + # copay 0.0064 * 7,361.62 = 47.11. # CENTER infant full-time rate 1,240; expenses 2,000 capped at 1,240. - # base = 1,240 - 150 = 1,090. + # base = 1,240 - 47.11 = 1,192.89. # QRIS step 3 (2026) bonus 1,240 * 0.05 = 62; infant bonus 200. - # nd_ccap = 1,090 + 62 + 200 = 1,352. - nd_ccap: 1_352 + # nd_ccap = 1,192.89 + 62 + 200 = 1,454.89. + nd_ccap: 1_454.89 # nd_child_care_subsidies is the YEAR aggregator (adds nd_ccap); read at a - # month period it reports the monthly value 1,352, confirming nd_ccap flows - # into the federal child_care_subsidies total. - nd_child_care_subsidies: 1_352 + # month period it reports the monthly value 1,454.89, confirming nd_ccap + # flows into the federal child_care_subsidies total. + nd_child_care_subsidies: 1_454.89 - name: Case 2, applicant over 75% SMI is ineligible but the same family enrolled qualifies. period: 2026-01 @@ -68,7 +69,7 @@ members: [person1, person2] state_code: ND output: - # 6,200/mo > 75% SMI (5,822.23), so an applicant fails the income test. + # 6,200/mo > 75% SMI (5,521.22), so an applicant fails the income test. nd_ccap_income_eligible: false nd_ccap_eligible: false nd_ccap: 0 @@ -80,7 +81,7 @@ people: person1: age: 30 - employment_income: 74_400 # 6,200/mo, below 85% SMI (6,598.53) + employment_income: 74_400 # 6,200/mo, below 85% SMI (6,257.38) person2: age: 0.5 nd_ccap_provider_type: CENTER @@ -98,11 +99,11 @@ output: nd_ccap_income_eligible: true nd_ccap_eligible: true - # ratio 0.799 > 40% -> copay capped at 7%: 6,200 * 0.07 = 434. - # CENTER infant full-time 1,240; expenses 2,000 capped at 1,240. - # base = 1,240 - 434 = 806. Step 2 carries no QRIS bonus in 2026 but - # unlocks the infant bonus 200; nd_ccap = 806 + 200 = 1,006. - nd_ccap: 1_006 + # ratio 0.842 -> 83-85% SMI band, share 0.0498; copay 0.0498 * 7,361.62 = + # 366.61. CENTER infant full-time 1,240; expenses 2,000 capped at 1,240. + # base = 1,240 - 366.61 = 873.39. Step 2 carries no QRIS bonus in 2026 but + # unlocks the infant bonus 200; nd_ccap = 873.39 + 200 = 1,073.39. + nd_ccap: 1_073.39 - name: Case 4, family at or below 30% SMI has the co-payment waived. period: 2026-01 @@ -111,7 +112,7 @@ people: person1: age: 30 - employment_income: 24_000 # 2,000/mo, ratio 0.258 at or below 30% SMI + employment_income: 24_000 # 2,000/mo, ratio 0.272 at or below 30% SMI person2: age: 0.5 nd_ccap_provider_type: CENTER @@ -133,14 +134,14 @@ nd_ccap_base_subsidy: 600 nd_ccap: 800 -- name: Case 5, family above 40% SMI is capped at a 7% co-payment. +- name: Case 5, family in the 50-60% SMI band pays a worksheet co-payment. period: 2026-01 absolute_error_margin: 0.01 input: people: person1: age: 30 - employment_income: 48_000 # 4,000/mo, ratio 0.515 above 40% SMI + employment_income: 48_000 # 4,000/mo, ratio 0.543 in the 54-56% band person2: age: 0.5 nd_ccap_provider_type: CENTER @@ -156,10 +157,10 @@ state_code: ND output: nd_ccap_eligible: true - # copay capped at 7%: 4,000 * 0.07 = 280. - nd_ccap_copay: 280 - # base = 1,240 - 280 = 960; infant bonus 200; nd_ccap = 1,160. - nd_ccap: 1_160 + # ratio 0.543 -> 54-56% SMI band, share 0.0216; copay 0.0216 * 7,361.62 = 159.01. + nd_ccap_copay: 159.01 + # base = 1,240 - 159.01 = 1,080.99; infant bonus 200; nd_ccap = 1,280.99. + nd_ccap: 1_280.99 - name: Case 6, special-needs +10% raises the rate ceiling above the base case. period: 2026-01 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/nd_ccap.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/nd_ccap.yaml index b5c2c082dd3..09778f17dbb 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/nd_ccap.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/nd_ccap.yaml @@ -29,11 +29,11 @@ members: [person1, person2] state_code: ND output: - # copay: ratio 0.322 -> 6% band; 2,500 * 0.06 = 150. - # base = min(1,240, 2,000) - 150 = 1,090. + # copay: ratio 0.340 -> 32-34% SMI band, share 0.0064; 0.0064 * 7,361.62 = 47.11. + # base = min(1,240, 2,000) - 47.11 = 1,192.89. # QRIS step 3 (2026) bonus 1,240 * 0.05 = 62; infant bonus 200. - # nd_ccap = 1,090 + 62 + 200 = 1,352. - nd_ccap: 1_352 + # nd_ccap = 1,192.89 + 62 + 200 = 1,454.89. + nd_ccap: 1_454.89 - name: Case 2, infant at a step-2 center with the co-payment waived. period: 2026-01 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_qris_step_bonus.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_qris_step_bonus.yaml index 893f5bcd4be..e3c0d40d5c9 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_qris_step_bonus.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/ccap/rates/nd_ccap_qris_step_bonus.yaml @@ -4,10 +4,11 @@ # times the quality-step bonus rate (400-28-100-30). It is date-varying: # 2023-06-01 era: step 2 = +5%, step 3 = +10%, step 4 = +15% # 2026-01-01 era: step 2 = +0%, step 3 = +5%, step 4 = +10% -# The 2026 monthly rate table backfills to the 2025 era (the pre-2026 rate -# schedule is not separately available), so the 2025 cases use the same base -# rate but the earlier bonus percentages. Center infant full-time base = 1,240; -# part-time base = 546. defined_for = nd_ccap_eligible_child. +# The base state maximum rate is also date-varying: the 2022-10-01 DN 241 +# schedule applies through 2025 (Center infant full-time = 913, part-time = 900) +# and the 2026-01-01 schedule applies from 2026 (Center infant full-time = 1,240, +# part-time = 546). So the 2025 cases pair the 2023-06-01 bonus percentages +# (+5/+10/+15) with the 2022 base rate. defined_for = nd_ccap_eligible_child. # --- 2026 era (step 2 eliminated; step 3 +5%, step 4 +10%) --- @@ -132,8 +133,8 @@ members: [person1] state_code: ND output: - # 1,240 * 0.05 = 62 - nd_ccap_qris_step_bonus: 62 + # 2022 base 913 * 0.05 = 45.65 + nd_ccap_qris_step_bonus: 45.65 - name: Case 8, step 3 in 2025 is +10% of the base rate. period: 2025-01 @@ -150,8 +151,8 @@ members: [person1] state_code: ND output: - # 1,240 * 0.10 = 124 - nd_ccap_qris_step_bonus: 124 + # 2022 base 913 * 0.10 = 91.30 + nd_ccap_qris_step_bonus: 91.30 - name: Case 9, step 4 in 2025 is +15% of the base rate. period: 2025-01 @@ -168,5 +169,5 @@ members: [person1] state_code: ND output: - # 1,240 * 0.15 = 186 - nd_ccap_qris_step_bonus: 186 + # 2022 base 913 * 0.15 = 136.95 + nd_ccap_qris_step_bonus: 136.95