From 645ef24b62473070c6eca040b7835b4d4390c81b Mon Sep 17 00:00:00 2001 From: Ziming Date: Fri, 10 Jul 2026 17:11:45 -0400 Subject: [PATCH 1/7] Start South Dakota CCAP implementation Co-Authored-By: Claude Fable 5 From 66a874a04dfc81f835b90b4ad1701bcec495a5af Mon Sep 17 00:00:00 2001 From: Ziming Date: Fri, 10 Jul 2026 18:55:36 -0400 Subject: [PATCH 2/7] Implement South Dakota Child Care Assistance (CCA) (ref #8998) Co-Authored-By: Claude Fable 5 --- .../hhs/ccdf/child_care_subsidy_programs.yaml | 1 + .../dss/cca/age_group/infant_max_months.yaml | 13 + .../cca/age_group/preschool_max_months.yaml | 15 + .../cca/age_group/school_age_min_months.yaml | 15 + .../dss/cca/age_group/toddler_max_months.yaml | 14 + .../sd/dss/cca/copay/fpl_threshold.yaml | 17 + .../gov/states/sd/dss/cca/copay/max_rate.yaml | 15 + .../gov/states/sd/dss/cca/copay/rate.yaml | 17 + .../dss/cca/eligibility/child_age_limit.yaml | 15 + .../eligibility/special_needs_age_limit.yaml | 15 + .../special_needs_student_age_limit.yaml | 15 + .../child_earned_income_exclusion_age.yaml | 15 + .../cca/income/earned_income_disregard.yaml | 14 + .../states/sd/dss/cca/income/fpl_limit.yaml | 16 + .../states/sd/dss/cca/income/smi_limit.yaml | 15 + .../gov/states/sd/dss/cca/income/sources.yaml | 38 +++ .../cca/rates/special_needs_multiplier.yaml | 14 + .../sd/dss/cca/rates/suburban_counties.yaml | 26 ++ .../sd/dss/cca/rates/urban_counties.yaml | 15 + .../sd/dss/cca/rates/uses_weekly_rates.yaml | 17 + .../dss/cca/rates/weekly/family_day_care.yaml | 101 ++++++ .../sd/dss/cca/rates/weekly/informal.yaml | 20 ++ .../dss/cca/rates/weekly/licensed_center.yaml | 101 ++++++ .../time_category/full_time_min_hours.yaml | 13 + .../time_category/part_time_min_hours.yaml | 14 + .../states/sd/dss/cca/copay/sd_cca_copay.yaml | 176 +++++++++++ .../eligibility/sd_cca_activity_eligible.yaml | 170 ++++++++++ .../dss/cca/eligibility/sd_cca_eligible.yaml | 171 ++++++++++ .../eligibility/sd_cca_eligible_child.yaml | 195 ++++++++++++ .../eligibility/sd_cca_income_eligible.yaml | 233 ++++++++++++++ .../cca/income/sd_cca_countable_income.yaml | 251 +++++++++++++++ .../gov/states/sd/dss/cca/integration.yaml | 294 ++++++++++++++++++ .../sd/dss/cca/rates/sd_cca_age_group.yaml | 185 +++++++++++ .../sd/dss/cca/rates/sd_cca_region.yaml | 117 +++++++ .../dss/cca/rates/sd_cca_time_category.yaml | 103 ++++++ .../sd/dss/cca/rates/sd_cca_weekly_rate.yaml | 273 ++++++++++++++++ .../gov/states/sd/dss/cca/sd_cca.yaml | 207 ++++++++++++ .../states/sd/dss/cca/copay/sd_cca_copay.py | 38 +++ .../eligibility/sd_cca_activity_eligible.py | 28 ++ .../sd/dss/cca/eligibility/sd_cca_eligible.py | 19 ++ .../cca/eligibility/sd_cca_eligible_child.py | 38 +++ .../cca/eligibility/sd_cca_income_eligible.py | 27 ++ .../sd_cca_parent_in_eligible_activity.py | 27 ++ .../dss/cca/income/sd_cca_countable_income.py | 36 +++ .../sd/dss/cca/income/sd_cca_gross_income.py | 14 + .../states/sd/dss/cca/is_sd_cca_enrolled.py | 12 + .../sd/dss/cca/rates/sd_cca_age_group.py | 43 +++ .../sd/dss/cca/rates/sd_cca_provider_type.py | 18 ++ .../states/sd/dss/cca/rates/sd_cca_region.py | 34 ++ .../sd/dss/cca/rates/sd_cca_time_category.py | 32 ++ .../sd/dss/cca/rates/sd_cca_weekly_rate.py | 46 +++ .../variables/gov/states/sd/dss/cca/sd_cca.py | 38 +++ .../sd/dss/cca/sd_child_care_subsidies.py | 11 + 53 files changed, 3407 insertions(+) create mode 100644 policyengine_us/parameters/gov/states/sd/dss/cca/age_group/infant_max_months.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/cca/age_group/preschool_max_months.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/cca/age_group/school_age_min_months.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/cca/age_group/toddler_max_months.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/cca/copay/fpl_threshold.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/cca/copay/max_rate.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/cca/copay/rate.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/cca/eligibility/child_age_limit.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/cca/eligibility/special_needs_age_limit.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/cca/eligibility/special_needs_student_age_limit.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/cca/income/child_earned_income_exclusion_age.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/cca/income/earned_income_disregard.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/cca/income/fpl_limit.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/cca/income/smi_limit.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/cca/income/sources.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/cca/rates/special_needs_multiplier.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/cca/rates/suburban_counties.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/cca/rates/urban_counties.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/cca/rates/uses_weekly_rates.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/cca/rates/weekly/family_day_care.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/cca/rates/weekly/informal.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/cca/rates/weekly/licensed_center.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/cca/time_category/full_time_min_hours.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/cca/time_category/part_time_min_hours.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/copay/sd_cca_copay.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_activity_eligible.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_eligible.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_eligible_child.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_income_eligible.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/income/sd_cca_countable_income.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/integration.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/rates/sd_cca_age_group.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/rates/sd_cca_region.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/rates/sd_cca_time_category.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/rates/sd_cca_weekly_rate.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/sd_cca.yaml create mode 100644 policyengine_us/variables/gov/states/sd/dss/cca/copay/sd_cca_copay.py create mode 100644 policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_activity_eligible.py create mode 100644 policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_eligible.py create mode 100644 policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_eligible_child.py create mode 100644 policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_income_eligible.py create mode 100644 policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_parent_in_eligible_activity.py create mode 100644 policyengine_us/variables/gov/states/sd/dss/cca/income/sd_cca_countable_income.py create mode 100644 policyengine_us/variables/gov/states/sd/dss/cca/income/sd_cca_gross_income.py create mode 100644 policyengine_us/variables/gov/states/sd/dss/cca/is_sd_cca_enrolled.py create mode 100644 policyengine_us/variables/gov/states/sd/dss/cca/rates/sd_cca_age_group.py create mode 100644 policyengine_us/variables/gov/states/sd/dss/cca/rates/sd_cca_provider_type.py create mode 100644 policyengine_us/variables/gov/states/sd/dss/cca/rates/sd_cca_region.py create mode 100644 policyengine_us/variables/gov/states/sd/dss/cca/rates/sd_cca_time_category.py create mode 100644 policyengine_us/variables/gov/states/sd/dss/cca/rates/sd_cca_weekly_rate.py create mode 100644 policyengine_us/variables/gov/states/sd/dss/cca/sd_cca.py create mode 100644 policyengine_us/variables/gov/states/sd/dss/cca/sd_child_care_subsidies.py diff --git a/policyengine_us/parameters/gov/hhs/ccdf/child_care_subsidy_programs.yaml b/policyengine_us/parameters/gov/hhs/ccdf/child_care_subsidy_programs.yaml index acd03170470..87bb7254a4f 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 @@ -39,6 +39,7 @@ values: - 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 + - sd_child_care_subsidies # South Dakota Child Care Assistance (CCA) metadata: unit: list diff --git a/policyengine_us/parameters/gov/states/sd/dss/cca/age_group/infant_max_months.yaml b/policyengine_us/parameters/gov/states/sd/dss/cca/age_group/infant_max_months.yaml new file mode 100644 index 00000000000..368acfdfe6d --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/cca/age_group/infant_max_months.yaml @@ -0,0 +1,13 @@ +description: South Dakota classifies children below this age in months as infants for reimbursement rates under the Child Care Assistance program. + +# The rate-table "Infant" column covers children under 12 months. +values: + 2026-08-03: 12 + +metadata: + unit: month + period: year + label: South Dakota CCA infant maximum age in months + reference: + - title: South Dakota Child Care Assistance Rates, effective 8/3/2026 + href: https://dss.sd.gov/docs/childcare/assistance/CCA_Weekly_Reimbursement_Rates.pdf diff --git a/policyengine_us/parameters/gov/states/sd/dss/cca/age_group/preschool_max_months.yaml b/policyengine_us/parameters/gov/states/sd/dss/cca/age_group/preschool_max_months.yaml new file mode 100644 index 00000000000..197f189dc0f --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/cca/age_group/preschool_max_months.yaml @@ -0,0 +1,15 @@ +description: South Dakota classifies children below this age in months as preschoolers for reimbursement rates under the Child Care Assistance program. + +# The rate-table "Preschool" column covers children 36 through 71 months (under +# 72 months). Children 60 through 71 months who are enrolled in school are +# instead classified as school age (see school_age_min_months.yaml). +values: + 2026-08-03: 72 + +metadata: + unit: month + period: year + label: South Dakota CCA preschool maximum age in months + reference: + - title: South Dakota Child Care Assistance Rates, effective 8/3/2026 + href: https://dss.sd.gov/docs/childcare/assistance/CCA_Weekly_Reimbursement_Rates.pdf diff --git a/policyengine_us/parameters/gov/states/sd/dss/cca/age_group/school_age_min_months.yaml b/policyengine_us/parameters/gov/states/sd/dss/cca/age_group/school_age_min_months.yaml new file mode 100644 index 00000000000..ecb69dc518b --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/cca/age_group/school_age_min_months.yaml @@ -0,0 +1,15 @@ +description: South Dakota classifies school-enrolled children at or above this age in months as school age for reimbursement rates under the Child Care Assistance program. + +# The rate-table "School Age" column covers children 60 or more months. A child +# 60 through 71 months old is classified as school age only when enrolled in +# school; otherwise the child remains a preschooler through 71 months. +values: + 2026-08-03: 60 + +metadata: + unit: month + period: year + label: South Dakota CCA school age minimum age in months + reference: + - title: South Dakota Child Care Assistance Rates, effective 8/3/2026 + href: https://dss.sd.gov/docs/childcare/assistance/CCA_Weekly_Reimbursement_Rates.pdf diff --git a/policyengine_us/parameters/gov/states/sd/dss/cca/age_group/toddler_max_months.yaml b/policyengine_us/parameters/gov/states/sd/dss/cca/age_group/toddler_max_months.yaml new file mode 100644 index 00000000000..755a2e77fcb --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/cca/age_group/toddler_max_months.yaml @@ -0,0 +1,14 @@ +description: South Dakota classifies children below this age in months as toddlers for reimbursement rates under the Child Care Assistance program. + +# The rate-table "Toddler" column covers children 12 through 35 months (under 36 +# months). +values: + 2026-08-03: 36 + +metadata: + unit: month + period: year + label: South Dakota CCA toddler maximum age in months + reference: + - title: South Dakota Child Care Assistance Rates, effective 8/3/2026 + href: https://dss.sd.gov/docs/childcare/assistance/CCA_Weekly_Reimbursement_Rates.pdf diff --git a/policyengine_us/parameters/gov/states/sd/dss/cca/copay/fpl_threshold.yaml b/policyengine_us/parameters/gov/states/sd/dss/cca/copay/fpl_threshold.yaml new file mode 100644 index 00000000000..2efcdcb006c --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/cca/copay/fpl_threshold.yaml @@ -0,0 +1,17 @@ +description: South Dakota sets the family co-payment income threshold at this multiple of the federal poverty guideline under the Child Care Assistance program. + +# Households with gross monthly countable income at or below 170% of the federal +# poverty level have no co-payment; above 170%, the co-payment is a share of +# income in excess of this threshold (Subsidy Manual, Section 9). +values: + 2026-08-03: 1.7 + +metadata: + unit: /1 + period: year + label: South Dakota CCA co-payment federal poverty guideline threshold + reference: + - title: South Dakota Child Care Assistance Subsidy Manual, Section 9 Recipient Co-payment and Length of Eligibility + href: https://dss.sd.gov/docs/childcare/assistance/Subsidy_Manual.pdf#page=21 + - title: South Dakota 2026 Child Care Subsidy Co-Payments (Sliding Fee Scale) + href: https://dss.sd.gov/docs/childcare/assistance/Sliding_Fee_Scale.pdf diff --git a/policyengine_us/parameters/gov/states/sd/dss/cca/copay/max_rate.yaml b/policyengine_us/parameters/gov/states/sd/dss/cca/copay/max_rate.yaml new file mode 100644 index 00000000000..6f80ade0de4 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/cca/copay/max_rate.yaml @@ -0,0 +1,15 @@ +description: South Dakota caps the family co-payment at this share of gross monthly income under the Child Care Assistance program. + +# In no case may the co-payment exceed 12% of gross monthly countable income; +# co-payments above this ceiling are reduced to exactly 12% (Subsidy Manual, +# Section 9). +values: + 2026-08-03: 0.12 + +metadata: + unit: /1 + period: year + label: South Dakota CCA co-payment maximum rate + reference: + - title: South Dakota Child Care Assistance Subsidy Manual, Section 9 Recipient Co-payment and Length of Eligibility + href: https://dss.sd.gov/docs/childcare/assistance/Subsidy_Manual.pdf#page=21 diff --git a/policyengine_us/parameters/gov/states/sd/dss/cca/copay/rate.yaml b/policyengine_us/parameters/gov/states/sd/dss/cca/copay/rate.yaml new file mode 100644 index 00000000000..42833e7b7bf --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/cca/copay/rate.yaml @@ -0,0 +1,17 @@ +description: South Dakota sets the family co-payment as this share of countable income above the co-payment threshold under the Child Care Assistance program. + +# The co-payment equals gross monthly countable income in excess of 170% of the +# federal poverty level multiplied by 5% (a 95% reduction) (Subsidy Manual, +# Section 9). +values: + 2026-08-03: 0.05 + +metadata: + unit: /1 + period: year + label: South Dakota CCA co-payment rate + reference: + - title: South Dakota Child Care Assistance Subsidy Manual, Section 9 Recipient Co-payment and Length of Eligibility + href: https://dss.sd.gov/docs/childcare/assistance/Subsidy_Manual.pdf#page=21 + - title: South Dakota 2026 Child Care Subsidy Co-Payments (Sliding Fee Scale) + href: https://dss.sd.gov/docs/childcare/assistance/Sliding_Fee_Scale.pdf diff --git a/policyengine_us/parameters/gov/states/sd/dss/cca/eligibility/child_age_limit.yaml b/policyengine_us/parameters/gov/states/sd/dss/cca/eligibility/child_age_limit.yaml new file mode 100644 index 00000000000..a7cbacde3c3 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/cca/eligibility/child_age_limit.yaml @@ -0,0 +1,15 @@ +description: South Dakota limits eligibility to children below this age under the Child Care Assistance program. + +# A child is eligible if under age 13, or turned 13 during the current +# eligibility period (Subsidy Manual, Section 3). Dated to the modeled program +# start (weekly rate era, 2026-08-03). +values: + 2026-08-03: 13 + +metadata: + unit: year + period: year + label: South Dakota CCA child age limit + reference: + - title: South Dakota Child Care Assistance Subsidy Manual, Section 3 Eligibility Criteria + href: https://dss.sd.gov/docs/childcare/assistance/Subsidy_Manual.pdf#page=9 diff --git a/policyengine_us/parameters/gov/states/sd/dss/cca/eligibility/special_needs_age_limit.yaml b/policyengine_us/parameters/gov/states/sd/dss/cca/eligibility/special_needs_age_limit.yaml new file mode 100644 index 00000000000..562ba426f09 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/cca/eligibility/special_needs_age_limit.yaml @@ -0,0 +1,15 @@ +description: South Dakota limits eligibility to special-needs children below this age under the Child Care Assistance program. + +# A child physically or mentally incapable of self-care is eligible if under age +# 18; the limit extends to the school-enrolled age limit when the child is +# enrolled in school and expected to graduate (Subsidy Manual, Section 3). +values: + 2026-08-03: 18 + +metadata: + unit: year + period: year + label: South Dakota CCA special-needs child age limit + reference: + - title: South Dakota Child Care Assistance Subsidy Manual, Section 3 Eligibility Criteria + href: https://dss.sd.gov/docs/childcare/assistance/Subsidy_Manual.pdf#page=9 diff --git a/policyengine_us/parameters/gov/states/sd/dss/cca/eligibility/special_needs_student_age_limit.yaml b/policyengine_us/parameters/gov/states/sd/dss/cca/eligibility/special_needs_student_age_limit.yaml new file mode 100644 index 00000000000..4530b3f893b --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/cca/eligibility/special_needs_student_age_limit.yaml @@ -0,0 +1,15 @@ +description: South Dakota limits eligibility to school-enrolled special-needs children below this age under the Child Care Assistance program. + +# A child incapable of self-care who is enrolled in school and expected to +# graduate is eligible through age 18 (under 19) rather than the standard +# special-needs limit of under 18 (Subsidy Manual, Section 3). +values: + 2026-08-03: 19 + +metadata: + unit: year + period: year + label: South Dakota CCA school-enrolled special-needs child age limit + reference: + - title: South Dakota Child Care Assistance Subsidy Manual, Section 3 Eligibility Criteria + href: https://dss.sd.gov/docs/childcare/assistance/Subsidy_Manual.pdf#page=9 diff --git a/policyengine_us/parameters/gov/states/sd/dss/cca/income/child_earned_income_exclusion_age.yaml b/policyengine_us/parameters/gov/states/sd/dss/cca/income/child_earned_income_exclusion_age.yaml new file mode 100644 index 00000000000..288868f0056 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/cca/income/child_earned_income_exclusion_age.yaml @@ -0,0 +1,15 @@ +description: South Dakota excludes the earned income of children below this age from countable income under the Child Care Assistance program. + +# Earned income is counted only when received by an applicant or other countable +# adult household member, so the earned income of children under 18 is excluded +# (Subsidy Manual, Section 4 Income Types). +values: + 2026-08-03: 18 + +metadata: + unit: year + period: year + label: South Dakota CCA child earned income exclusion age + reference: + - title: South Dakota Child Care Assistance Subsidy Manual, Section 4 Income Types + href: https://dss.sd.gov/docs/childcare/assistance/Subsidy_Manual.pdf#page=10 diff --git a/policyengine_us/parameters/gov/states/sd/dss/cca/income/earned_income_disregard.yaml b/policyengine_us/parameters/gov/states/sd/dss/cca/income/earned_income_disregard.yaml new file mode 100644 index 00000000000..37d386bb58e --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/cca/income/earned_income_disregard.yaml @@ -0,0 +1,14 @@ +description: South Dakota excludes this share of earned income from countable income under the Child Care Assistance program. + +# A 4% standard deduction is allowed from all earned and self-employment income +# before determining gross monthly countable income (Subsidy Manual, Section 5). +values: + 2026-08-03: 0.04 + +metadata: + unit: /1 + period: year + label: South Dakota CCA earned income disregard + reference: + - title: South Dakota Child Care Assistance Subsidy Manual, Section 5 Calculating Earned Income from Employment + href: https://dss.sd.gov/docs/childcare/assistance/Subsidy_Manual.pdf#page=12 diff --git a/policyengine_us/parameters/gov/states/sd/dss/cca/income/fpl_limit.yaml b/policyengine_us/parameters/gov/states/sd/dss/cca/income/fpl_limit.yaml new file mode 100644 index 00000000000..a2735829481 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/cca/income/fpl_limit.yaml @@ -0,0 +1,16 @@ +description: South Dakota limits countable income to this multiple of the federal poverty guideline under the Child Care Assistance program. + +# Households with gross monthly countable income exceeding 209% of the federal +# poverty level are not eligible (Subsidy Manual, Section 3). This is the initial +# income ceiling; continuing recipients use the state median income ceiling in +# smi_limit.yaml. +values: + 2026-08-03: 2.09 + +metadata: + unit: /1 + period: year + label: South Dakota CCA federal poverty guideline income limit + reference: + - title: South Dakota Child Care Assistance Subsidy Manual, Section 3 Eligibility Criteria + href: https://dss.sd.gov/docs/childcare/assistance/Subsidy_Manual.pdf#page=8 diff --git a/policyengine_us/parameters/gov/states/sd/dss/cca/income/smi_limit.yaml b/policyengine_us/parameters/gov/states/sd/dss/cca/income/smi_limit.yaml new file mode 100644 index 00000000000..87038383a35 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/cca/income/smi_limit.yaml @@ -0,0 +1,15 @@ +description: South Dakota limits countable income to this share of the state median income under the Child Care Assistance program. + +# A continuing recipient whose income exceeds 209% of the federal poverty level +# but not 85% of the state median income may remain eligible under the phase-out +# (Subsidy Manual, Section 3 Phase Out; Section 10 Continuous Eligibility). +values: + 2026-08-03: 0.85 + +metadata: + unit: /1 + period: year + label: South Dakota CCA state median income limit + reference: + - title: South Dakota Child Care Assistance Subsidy Manual, Section 3 Eligibility Criteria Phase Out + href: https://dss.sd.gov/docs/childcare/assistance/Subsidy_Manual.pdf#page=9 diff --git a/policyengine_us/parameters/gov/states/sd/dss/cca/income/sources.yaml b/policyengine_us/parameters/gov/states/sd/dss/cca/income/sources.yaml new file mode 100644 index 00000000000..9a949126aab --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/cca/income/sources.yaml @@ -0,0 +1,38 @@ +description: South Dakota counts these income sources as countable income under the Child Care Assistance program. +values: + 2026-08-03: + # Earned income (wages, salary, commissions, tips, bonuses, fees, and + # self-employment net of allowable business expenses). + - employment_income + - self_employment_income + # Unearned income (Subsidy Manual, Section 4). + - rental_income # "lease income" + - social_security # "Social Security (SSA)"; already excludes SSI + - unemployment_compensation + - workers_compensation + - veterans_benefits # "VA benefits"; VA disability is non-countable + - pension_income # "pensions" and "retirement" + - alimony_income + - child_support_received # "child support" and "back child support" + - interest_income # "interest income" + - dividend_income # "investment earnings" + - capital_gains # Section 6: "Capital gains and interest shall be + # added into the total income." + + # ---------------------------------------------------------------------- + # NON-COUNTABLE income (Subsidy Manual, Section 4), not listed above: + # student financial aid (grants, loans, scholarships), GI Bill, utility + # allowance, foster care payments, SSI disability, VA disability, Earned + # Income Tax Credits, and IRS tax refunds. + # + # TANF cash assistance is countable per the manual but is omitted here to + # avoid the Child Care Assistance to Temporary Assistance for Needy Families + # circular dependency through the dependent care deduction. + +metadata: + unit: list + period: year + label: South Dakota CCA countable income sources + reference: + - title: South Dakota Child Care Assistance Subsidy Manual, Section 4 Income Types + href: https://dss.sd.gov/docs/childcare/assistance/Subsidy_Manual.pdf#page=10 diff --git a/policyengine_us/parameters/gov/states/sd/dss/cca/rates/special_needs_multiplier.yaml b/policyengine_us/parameters/gov/states/sd/dss/cca/rates/special_needs_multiplier.yaml new file mode 100644 index 00000000000..54f3fab4e3b --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/cca/rates/special_needs_multiplier.yaml @@ -0,0 +1,14 @@ +description: South Dakota multiplies the base reimbursement rate by this factor for children with special needs under the Child Care Assistance program. + +# A provider caring for a child with special needs may receive the base rate +# plus an additional 75% of that rate (base rate x 1.75). +values: + 2026-08-03: 1.75 + +metadata: + unit: /1 + period: year + label: South Dakota CCA special needs rate multiplier + reference: + - title: South Dakota Child Care Assistance Rates, effective 8/3/2026 (Special Needs) + href: https://dss.sd.gov/docs/childcare/assistance/CCA_Weekly_Reimbursement_Rates.pdf diff --git a/policyengine_us/parameters/gov/states/sd/dss/cca/rates/suburban_counties.yaml b/policyengine_us/parameters/gov/states/sd/dss/cca/rates/suburban_counties.yaml new file mode 100644 index 00000000000..42632cf12c5 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/cca/rates/suburban_counties.yaml @@ -0,0 +1,26 @@ +description: South Dakota assigns these counties to the suburban provider rate region under the Child Care Assistance program. +values: + 2026-08-03: + - BEADLE_COUNTY_SD + - BROOKINGS_COUNTY_SD + - BROWN_COUNTY_SD + - CLAY_COUNTY_SD + - CODINGTON_COUNTY_SD + - CUSTER_COUNTY_SD + - DAVISON_COUNTY_SD + - HUGHES_COUNTY_SD + - LAKE_COUNTY_SD + - LAWRENCE_COUNTY_SD + - MCCOOK_COUNTY_SD + - STANLEY_COUNTY_SD + - TURNER_COUNTY_SD + - UNION_COUNTY_SD + - YANKTON_COUNTY_SD + +metadata: + unit: list + period: year + label: South Dakota CCA suburban region counties + reference: + - title: South Dakota Provider Rate Regions, effective August 3, 2026 + href: https://dss.sd.gov/docs/childcare/assistance/Provider_Rate_Regions.pdf diff --git a/policyengine_us/parameters/gov/states/sd/dss/cca/rates/urban_counties.yaml b/policyengine_us/parameters/gov/states/sd/dss/cca/rates/urban_counties.yaml new file mode 100644 index 00000000000..6dcbaed3ca4 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/cca/rates/urban_counties.yaml @@ -0,0 +1,15 @@ +description: South Dakota assigns these counties to the urban provider rate region under the Child Care Assistance program. +values: + 2026-08-03: + - LINCOLN_COUNTY_SD + - MEADE_COUNTY_SD + - MINNEHAHA_COUNTY_SD + - PENNINGTON_COUNTY_SD + +metadata: + unit: list + period: year + label: South Dakota CCA urban region counties + reference: + - title: South Dakota Provider Rate Regions, effective August 3, 2026 + href: https://dss.sd.gov/docs/childcare/assistance/Provider_Rate_Regions.pdf diff --git a/policyengine_us/parameters/gov/states/sd/dss/cca/rates/uses_weekly_rates.yaml b/policyengine_us/parameters/gov/states/sd/dss/cca/rates/uses_weekly_rates.yaml new file mode 100644 index 00000000000..f56d973d350 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/cca/rates/uses_weekly_rates.yaml @@ -0,0 +1,17 @@ +description: South Dakota uses this indicator to determine whether the weekly reimbursement rate schedule applies under the Child Care Assistance program. + +# The weekly region-based rate schedule takes effect August 3, 2026, replacing +# the earlier hourly county-based schedule. Only the weekly era is modeled; this +# indicator gates the benefit off for prior periods (the hourly era is a +# separate follow-up implementation). +values: + 2024-07-01: false + 2026-08-03: true + +metadata: + unit: bool + period: year + label: South Dakota CCA weekly rate schedule in effect + reference: + - title: South Dakota Child Care Assistance Rates, effective 8/3/2026 + href: https://dss.sd.gov/docs/childcare/assistance/CCA_Weekly_Reimbursement_Rates.pdf diff --git a/policyengine_us/parameters/gov/states/sd/dss/cca/rates/weekly/family_day_care.yaml b/policyengine_us/parameters/gov/states/sd/dss/cca/rates/weekly/family_day_care.yaml new file mode 100644 index 00000000000..be3d3d2eb80 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/cca/rates/weekly/family_day_care.yaml @@ -0,0 +1,101 @@ +description: South Dakota provides these weekly maximum reimbursement rates for family day care providers under the Child Care Assistance program. + +metadata: + period: week + unit: currency-USD + label: South Dakota CCA family day care weekly rates + breakdown: + - sd_cca_region + - sd_cca_age_group + - sd_cca_time_category + reference: + - title: South Dakota Child Care Assistance Rates, effective 8/3/2026 (Family Day Care) + href: https://dss.sd.gov/docs/childcare/assistance/CCA_Weekly_Reimbursement_Rates.pdf + +URBAN: + INFANT: + FULL_TIME: + 2026-08-03: 180 + PART_TIME: + 2026-08-03: 90 + LIMITED_TIME: + 2026-08-03: 45 + TODDLER: + FULL_TIME: + 2026-08-03: 177.5 + PART_TIME: + 2026-08-03: 88.75 + LIMITED_TIME: + 2026-08-03: 44.5 + PRESCHOOL: + FULL_TIME: + 2026-08-03: 175 + PART_TIME: + 2026-08-03: 87.5 + LIMITED_TIME: + 2026-08-03: 43.75 + SCHOOL_AGE: + FULL_TIME: + 2026-08-03: 170 + PART_TIME: + 2026-08-03: 85 + LIMITED_TIME: + 2026-08-03: 42.5 +SUBURBAN: + INFANT: + FULL_TIME: + 2026-08-03: 153 + PART_TIME: + 2026-08-03: 76.5 + LIMITED_TIME: + 2026-08-03: 38.25 + TODDLER: + FULL_TIME: + 2026-08-03: 150 + PART_TIME: + 2026-08-03: 75 + LIMITED_TIME: + 2026-08-03: 37.5 + PRESCHOOL: + FULL_TIME: + 2026-08-03: 150 + PART_TIME: + 2026-08-03: 75 + LIMITED_TIME: + 2026-08-03: 37.5 + SCHOOL_AGE: + FULL_TIME: + 2026-08-03: 145 + PART_TIME: + 2026-08-03: 72.5 + LIMITED_TIME: + 2026-08-03: 36.25 +RURAL: + INFANT: + FULL_TIME: + 2026-08-03: 140 + PART_TIME: + 2026-08-03: 70 + LIMITED_TIME: + 2026-08-03: 35 + TODDLER: + FULL_TIME: + 2026-08-03: 145 + PART_TIME: + 2026-08-03: 72.5 + LIMITED_TIME: + 2026-08-03: 36.25 + PRESCHOOL: + FULL_TIME: + 2026-08-03: 135 + PART_TIME: + 2026-08-03: 67.5 + LIMITED_TIME: + 2026-08-03: 33.75 + SCHOOL_AGE: + FULL_TIME: + 2026-08-03: 140 + PART_TIME: + 2026-08-03: 70 + LIMITED_TIME: + 2026-08-03: 35 diff --git a/policyengine_us/parameters/gov/states/sd/dss/cca/rates/weekly/informal.yaml b/policyengine_us/parameters/gov/states/sd/dss/cca/rates/weekly/informal.yaml new file mode 100644 index 00000000000..019d2d2686b --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/cca/rates/weekly/informal.yaml @@ -0,0 +1,20 @@ +description: South Dakota provides these weekly maximum reimbursement rates for informal, in-home, and relative providers under the Child Care Assistance program. + +# Informal, in-home, and relative provider rates are flat statewide: they do not +# vary by region or age group, only by level of service. +metadata: + period: week + unit: currency-USD + label: South Dakota CCA informal provider weekly rates + breakdown: + - sd_cca_time_category + reference: + - title: South Dakota Child Care Assistance Rates, effective 8/3/2026 (Informal, In-home, relative) + href: https://dss.sd.gov/docs/childcare/assistance/CCA_Weekly_Reimbursement_Rates.pdf + +FULL_TIME: + 2026-08-03: 116.25 +PART_TIME: + 2026-08-03: 58.25 +LIMITED_TIME: + 2026-08-03: 29 diff --git a/policyengine_us/parameters/gov/states/sd/dss/cca/rates/weekly/licensed_center.yaml b/policyengine_us/parameters/gov/states/sd/dss/cca/rates/weekly/licensed_center.yaml new file mode 100644 index 00000000000..ae879268a01 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/cca/rates/weekly/licensed_center.yaml @@ -0,0 +1,101 @@ +description: South Dakota provides these weekly maximum reimbursement rates for licensed centers and school age programs under the Child Care Assistance program. + +metadata: + period: week + unit: currency-USD + label: South Dakota CCA licensed center weekly rates + breakdown: + - sd_cca_region + - sd_cca_age_group + - sd_cca_time_category + reference: + - title: South Dakota Child Care Assistance Rates, effective 8/3/2026 (Licensed Center or School Age Program) + href: https://dss.sd.gov/docs/childcare/assistance/CCA_Weekly_Reimbursement_Rates.pdf + +URBAN: + INFANT: + FULL_TIME: + 2026-08-03: 276 + PART_TIME: + 2026-08-03: 138 + LIMITED_TIME: + 2026-08-03: 69 + TODDLER: + FULL_TIME: + 2026-08-03: 275 + PART_TIME: + 2026-08-03: 137.5 + LIMITED_TIME: + 2026-08-03: 68.75 + PRESCHOOL: + FULL_TIME: + 2026-08-03: 250 + PART_TIME: + 2026-08-03: 125 + LIMITED_TIME: + 2026-08-03: 62.5 + SCHOOL_AGE: + FULL_TIME: + 2026-08-03: 185 + PART_TIME: + 2026-08-03: 92.5 + LIMITED_TIME: + 2026-08-03: 46.25 +SUBURBAN: + INFANT: + FULL_TIME: + 2026-08-03: 225 + PART_TIME: + 2026-08-03: 112.5 + LIMITED_TIME: + 2026-08-03: 56.25 + TODDLER: + FULL_TIME: + 2026-08-03: 212.5 + PART_TIME: + 2026-08-03: 106.25 + LIMITED_TIME: + 2026-08-03: 53.25 + PRESCHOOL: + FULL_TIME: + 2026-08-03: 203 + PART_TIME: + 2026-08-03: 101.5 + LIMITED_TIME: + 2026-08-03: 50.75 + SCHOOL_AGE: + FULL_TIME: + 2026-08-03: 170 + PART_TIME: + 2026-08-03: 85 + LIMITED_TIME: + 2026-08-03: 42.5 +RURAL: + INFANT: + FULL_TIME: + 2026-08-03: 175 + PART_TIME: + 2026-08-03: 87.5 + LIMITED_TIME: + 2026-08-03: 43.75 + TODDLER: + FULL_TIME: + 2026-08-03: 169 + PART_TIME: + 2026-08-03: 84.5 + LIMITED_TIME: + 2026-08-03: 42.25 + PRESCHOOL: + FULL_TIME: + 2026-08-03: 165 + PART_TIME: + 2026-08-03: 82.5 + LIMITED_TIME: + 2026-08-03: 41.25 + SCHOOL_AGE: + FULL_TIME: + 2026-08-03: 150 + PART_TIME: + 2026-08-03: 75 + LIMITED_TIME: + 2026-08-03: 37.5 diff --git a/policyengine_us/parameters/gov/states/sd/dss/cca/time_category/full_time_min_hours.yaml b/policyengine_us/parameters/gov/states/sd/dss/cca/time_category/full_time_min_hours.yaml new file mode 100644 index 00000000000..6dfe7655997 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/cca/time_category/full_time_min_hours.yaml @@ -0,0 +1,13 @@ +description: South 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 service is 28 or more hours per week. +values: + 2026-08-03: 28 + +metadata: + unit: hour + period: week + label: South Dakota CCA full-time minimum weekly hours + reference: + - title: South Dakota Child Care Assistance Rates, effective 8/3/2026 + href: https://dss.sd.gov/docs/childcare/assistance/CCA_Weekly_Reimbursement_Rates.pdf diff --git a/policyengine_us/parameters/gov/states/sd/dss/cca/time_category/part_time_min_hours.yaml b/policyengine_us/parameters/gov/states/sd/dss/cca/time_category/part_time_min_hours.yaml new file mode 100644 index 00000000000..50eb0bf1ea1 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/cca/time_category/part_time_min_hours.yaml @@ -0,0 +1,14 @@ +description: South Dakota classifies child care of at least this many hours per week as part-time under the Child Care Assistance program. + +# Part-time level of service is 16 to 27 hours per week; limited-time care is 1 +# to 15 hours per week. +values: + 2026-08-03: 16 + +metadata: + unit: hour + period: week + label: South Dakota CCA part-time minimum weekly hours + reference: + - title: South Dakota Child Care Assistance Rates, effective 8/3/2026 + href: https://dss.sd.gov/docs/childcare/assistance/CCA_Weekly_Reimbursement_Rates.pdf diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/copay/sd_cca_copay.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/copay/sd_cca_copay.yaml new file mode 100644 index 00000000000..c81dc8201c9 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/copay/sd_cca_copay.yaml @@ -0,0 +1,176 @@ +# Tests for sd_cca_copay (SPMUnit, MONTH, USD). +# The family co-payment is (monthly countable income - 170% of the monthly +# federal poverty guideline) x 5%, floored at zero, capped at 12% of countable +# income, and waived entirely for TANF recipients and for foster/protective- +# services children (Sliding Fee Scale; Subsidy Manual Section 9). The 12% +# ceiling is defensive: with a 5% rate on income above the threshold it cannot +# bind, so it is not exercised here (this mirrors the North Dakota co-pay +# ceiling, which also does not bind). +# Each household is otherwise eligible. spm_unit_fpg is pinned as an annual +# input (24,000/yr -> 2,000/mo; 170% = 3,400/mo) so the threshold is exact; +# countable income equals 96% of earned income after the 4% disregard. + +- name: Case 1, income below 170% FPL, no co-payment. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income: 36_000 # 3,000/mo gross; countable 2,880 + person2: + age: 1 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_fpg: 24_000 + meets_ccdf_activity_test: true + households: + household: + members: [person1, person2] + state_code: SD + output: + # countable 2,880 < 3,400 (170% FPL), so the co-payment is zero. + sd_cca_copay: 0 + +- name: Case 2, income above 170% FPL, positive co-payment. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income: 47_500 # 3,958.33/mo gross; countable 3,800 + person2: + age: 1 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_fpg: 24_000 + meets_ccdf_activity_test: true + households: + household: + members: [person1, person2] + state_code: SD + output: + # (3,800 - 3,400) x 5% = 20.00. 12% cap (0.12 x 3,800 = 456) not binding. + sd_cca_copay: 20 + +- name: Case 3, TANF recipient, co-payment waived. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income: 47_500 # would otherwise owe a 20 co-payment + person2: + age: 1 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_fpg: 24_000 + meets_ccdf_activity_test: true + is_tanf_enrolled: true + households: + household: + members: [person1, person2] + state_code: SD + output: + sd_cca_copay: 0 + +- name: Case 4, foster child in the unit, co-payment waived. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income: 47_500 # would otherwise owe a 20 co-payment + person2: + age: 1 + is_in_foster_care: true + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_fpg: 24_000 + meets_ccdf_activity_test: true + households: + household: + members: [person1, person2] + state_code: SD + output: + sd_cca_copay: 0 + +- name: Case 5, countable income exactly at 170% FPL, no co-payment. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income: 42_500 # 3,541.67/mo gross; countable exactly 3,400 + person2: + age: 1 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_fpg: 24_000 + meets_ccdf_activity_test: true + households: + household: + members: [person1, person2] + state_code: SD + output: + # countable 3,400 = 170% FPL, so income above the threshold is 0: copay 0. + sd_cca_copay: 0 + +- name: Case 6, self-employment loss, countable income floored, no co-payment. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + self_employment_income: -120_000 # -10,000/mo + person2: + age: 1 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_fpg: 24_000 + meets_ccdf_activity_test: true + households: + household: + members: [person1, person2] + state_code: SD + output: + # countable income floors at 0, which is below the threshold: copay 0. + sd_cca_copay: 0 + +- name: Case 7, enrolled high earner, co-payment is 5% of excess and below the 12% cap. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income: 100_000 # 8,333.33/mo gross; countable 8,000 + person2: + age: 1 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_fpg: 24_000 # 170% FPL = 3,400/mo + hhs_smi: 120_000 # 10,000/mo; 85% = 8,500/mo continuing ceiling + is_sd_cca_enrolled: true # continuing recipient stays eligible at 8,000 + meets_ccdf_activity_test: true + households: + household: + members: [person1, person2] + state_code: SD + output: + # copay = (8,000 - 3,400) x 5% = 230. The 12% cap (0.12 x 8,000 = 960) + # never binds: with a 5% marginal rate the copay share of income cannot + # reach 12%. + sd_cca_copay: 230 diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_activity_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_activity_eligible.yaml new file mode 100644 index 00000000000..fb1aadd913b --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_activity_eligible.yaml @@ -0,0 +1,170 @@ +# Tests for sd_cca_activity_eligible (SPMUnit, MONTH, bool). +# The work/activity requirement (each caretaker employed 80+ hrs/month, or in +# school/training; ARSD 67:47:01:03.01, Subsidy Manual Sections 5-6) is not +# tracked at the hour level, so eligibility falls back to the federal CCDF +# activity-test input meets_ccdf_activity_test (a YEAR variable read with +# period.this_year). + +- name: Case 1, activity test met, activity eligible. + period: 2027-01 + input: + people: + person1: + age: 30 + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + meets_ccdf_activity_test: true + households: + household: + members: [person1, person2] + state_code: SD + output: + sd_cca_activity_eligible: true + +- name: Case 2, activity test not met, not activity eligible. + period: 2027-01 + input: + people: + person1: + age: 30 + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + meets_ccdf_activity_test: false + households: + household: + members: [person1, person2] + state_code: SD + output: + sd_cca_activity_eligible: false + +# The cases below exercise the modeled activity path +# (sd_cca_parent_in_eligible_activity), which requires every caretaker to be in +# an approved activity, with the CCDF fallback turned off. +- name: Case 3, single employed parent, activity eligible via the modeled path. + period: 2027-01 + input: + people: + person1: + age: 30 + employment_income: 24_000 # employed caretaker + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + meets_ccdf_activity_test: false # not relying on the CCDF fallback + households: + household: + members: [person1, person2] + state_code: SD + output: + # The sole caretaker is employed, so every caretaker is in an approved + # activity even though the CCDF activity-test input is false. + sd_cca_activity_eligible: true + +- name: Case 4, single parent with a self-employment loss, activity eligible. + period: 2027-01 + input: + people: + person1: + age: 30 + self_employment_income: -12_000 # a business loss still evidences activity + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + meets_ccdf_activity_test: false + households: + household: + members: [person1, person2] + state_code: SD + output: + sd_cca_activity_eligible: true + +- name: Case 5, single full-time student parent, activity eligible. + period: 2027-01 + input: + people: + person1: + age: 30 + is_full_time_student: true # school enrollment is an approved activity + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + meets_ccdf_activity_test: false + households: + household: + members: [person1, person2] + state_code: SD + output: + sd_cca_activity_eligible: true + +- name: Case 6, two married caretakers both employed, activity eligible. + period: 2027-01 + input: + people: + person1: + age: 30 + employment_income: 24_000 + is_tax_unit_head: true + person2: + age: 30 + employment_income: 24_000 + is_tax_unit_spouse: true + marital_units: + marital_unit: + members: [person1, person2] + tax_units: + tax_unit: + members: [person1, person2] + spm_units: + spm_unit: + members: [person1, person2] + meets_ccdf_activity_test: false + households: + household: + members: [person1, person2] + state_code: SD + output: + sd_cca_activity_eligible: true + +- name: Case 7, two married caretakers with one not in an activity, ineligible. + period: 2027-01 + input: + people: + person1: + age: 30 + employment_income: 24_000 # employed + is_tax_unit_head: true + person2: + age: 30 + employment_income: 0 # available caretaker, not in any activity + is_full_time_student: false + is_tax_unit_spouse: true + marital_units: + marital_unit: + members: [person1, person2] + tax_units: + tax_unit: + members: [person1, person2] + spm_units: + spm_unit: + members: [person1, person2] + meets_ccdf_activity_test: false + households: + household: + members: [person1, person2] + state_code: SD + output: + # Every caretaker must be in an approved activity; the available (non-active) + # spouse makes the unit ineligible under the modeled path. + sd_cca_activity_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_eligible.yaml new file mode 100644 index 00000000000..414e1bab133 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_eligible.yaml @@ -0,0 +1,171 @@ +# Tests for sd_cca_eligible (SPMUnit, MONTH, bool). +# A unit is eligible when it has at least one eligible child AND is income +# eligible AND asset eligible AND activity eligible (ARSD 67:47:01:03, :05; +# Subsidy Manual Section 3). Each case below meets every condition except the +# one under test. + +- name: Case 1, all conditions met, eligible. + period: 2027-01 + input: + people: + person1: + age: 30 + employment_income: 24_000 # 2,000/mo, well below the income limit + person2: + age: 1 # citizen infant, eligible child + spm_units: + spm_unit: + members: [person1, person2] + meets_ccdf_activity_test: true + households: + household: + members: [person1, person2] + state_code: SD + output: + sd_cca_eligible: true + +- name: Case 2, no eligible child, ineligible. + period: 2027-01 + input: + people: + person1: + age: 30 + employment_income: 24_000 + person2: + age: 13 # over the age-13 limit, not an eligible child + is_in_k12_school: true + spm_units: + spm_unit: + members: [person1, person2] + meets_ccdf_activity_test: true + households: + household: + members: [person1, person2] + state_code: SD + output: + sd_cca_eligible: false + +- name: Case 3, income too high, ineligible. + period: 2027-01 + input: + people: + person1: + age: 30 + employment_income: 240_000 # 20,000/mo, far above the income limit + person2: + age: 1 + spm_units: + spm_unit: + members: [person1, person2] + meets_ccdf_activity_test: true + is_sd_cca_enrolled: false + households: + household: + members: [person1, person2] + state_code: SD + output: + sd_cca_eligible: false + +- name: Case 4, assets above the CCDF limit, ineligible. + period: 2027-01 + input: + people: + person1: + age: 30 + employment_income: 24_000 + person2: + age: 1 + spm_units: + spm_unit: + members: [person1, person2] + meets_ccdf_activity_test: true + spm_unit_assets: 2_000_000 # above the $1,000,000 CCDF asset limit + households: + household: + members: [person1, person2] + state_code: SD + output: + sd_cca_eligible: false + +- name: Case 5, activity test not met, ineligible. + period: 2027-01 + input: + people: + person1: + age: 30 # no earnings, not in an activity + person2: + age: 1 + spm_units: + spm_unit: + members: [person1, person2] + meets_ccdf_activity_test: false + households: + household: + members: [person1, person2] + state_code: SD + output: + sd_cca_eligible: false + +- name: Case 6, zero-income family with an eligible child, eligible. + period: 2027-01 + input: + people: + person1: + age: 30 # activity met via the CCDF activity test + person2: + age: 1 # citizen infant, eligible child + spm_units: + spm_unit: + members: [person1, person2] + meets_ccdf_activity_test: true + households: + household: + members: [person1, person2] + state_code: SD + output: + sd_cca_eligible: true + +- name: Case 7, enrolled family in the phase-out band, eligible. + period: 2027-01 + input: + people: + person1: + age: 30 + employment_income: 60_000 # 5,000/mo gross; countable 4,800 + person2: + age: 1 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_fpg: 24_000 # 209% FPL = 4,180/mo + hhs_smi: 84_000 # 85% SMI = 5,950/mo + is_sd_cca_enrolled: true # continuing recipient + meets_ccdf_activity_test: true + households: + household: + members: [person1, person2] + state_code: SD + output: + # countable 4,800 exceeds the 4,180 applicant ceiling but is below the + # 5,950 continuing ceiling, so the enrolled family remains eligible. + sd_cca_eligible: true + +- name: Case 8, single adult with no child, ineligible. + period: 2027-01 + input: + people: + person1: + age: 30 + employment_income: 24_000 + spm_units: + spm_unit: + members: [person1] + meets_ccdf_activity_test: true + households: + household: + members: [person1] + state_code: SD + output: + # No eligible child in the unit, so the family is ineligible regardless of + # income and activity. + sd_cca_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_eligible_child.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_eligible_child.yaml new file mode 100644 index 00000000000..60a935ec920 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_eligible_child.yaml @@ -0,0 +1,195 @@ +# Tests for sd_cca_eligible_child (Person, MONTH, bool). +# A child is eligible if under the applicable age limit AND a US citizen or +# lawful permanent resident (ARSD 67:47:01:02, :03; Subsidy Manual Section 3): +# general limit: under 13 +# special needs (disabled), not in school: under 18 +# special needs (disabled), enrolled in school: under 19 +# is_in_k12_school imputes true for ages 5-17, so the disabled cases set it +# explicitly to isolate the school vs non-school age limit. + +- name: Case 1, citizen infant, eligible. + period: 2027-01 + input: + people: + person1: + age: 1 + households: + household: + members: [person1] + state_code: SD + output: + sd_cca_eligible_child: true + +- name: Case 2, citizen age 12, under the 13 limit, eligible. + period: 2027-01 + input: + people: + person1: + age: 12 + is_in_k12_school: true + households: + household: + members: [person1] + state_code: SD + output: + sd_cca_eligible_child: true + +- name: Case 3, citizen age 13, at the 13 limit, ineligible. + period: 2027-01 + input: + people: + person1: + age: 13 + is_in_k12_school: true + households: + household: + members: [person1] + state_code: SD + output: + sd_cca_eligible_child: false + +- name: Case 4, non-disabled age 14, over the general limit, ineligible. + period: 2027-01 + input: + people: + person1: + age: 14 + is_in_k12_school: true + households: + household: + members: [person1] + state_code: SD + output: + sd_cca_eligible_child: false + +- name: Case 5, disabled age 17 not in school, under the 18 limit, eligible. + period: 2027-01 + input: + people: + person1: + age: 17 + is_disabled: true + is_in_k12_school: false + households: + household: + members: [person1] + state_code: SD + output: + sd_cca_eligible_child: true + +- name: Case 6, disabled age 18 not in school, at the 18 limit, ineligible. + period: 2027-01 + input: + people: + person1: + age: 18 + is_disabled: true + is_in_k12_school: false + households: + household: + members: [person1] + state_code: SD + output: + sd_cca_eligible_child: false + +- name: Case 7, disabled age 18 in school, under the 19 limit, eligible. + period: 2027-01 + input: + people: + person1: + age: 18 + is_disabled: true + is_in_k12_school: true + households: + household: + members: [person1] + state_code: SD + output: + sd_cca_eligible_child: true + +- name: Case 8, disabled age 19 in school, at the 19 limit, ineligible. + period: 2027-01 + input: + people: + person1: + age: 19 + is_disabled: true + is_in_k12_school: true + households: + household: + members: [person1] + state_code: SD + output: + sd_cca_eligible_child: false + +- name: Case 9, lawful permanent resident infant, eligible. + period: 2027-01 + input: + people: + person1: + age: 1 + immigration_status: LEGAL_PERMANENT_RESIDENT + households: + household: + members: [person1] + state_code: SD + output: + sd_cca_eligible_child: true + +- name: Case 10, undocumented infant, immigration-ineligible. + period: 2027-01 + input: + people: + person1: + age: 1 + immigration_status: UNDOCUMENTED + households: + household: + members: [person1] + state_code: SD + output: + sd_cca_eligible_child: false + +- name: Case 11, refugee infant, immigration-ineligible (narrower than federal CCDF). + period: 2027-01 + input: + people: + person1: + age: 1 + immigration_status: REFUGEE + households: + household: + members: [person1] + state_code: SD + output: + sd_cca_eligible_child: false + +- name: Case 12, newborn citizen, eligible (minimum age). + period: 2027-01 + input: + people: + person1: + age: 0 + households: + household: + members: [person1] + state_code: SD + output: + sd_cca_eligible_child: true + +- name: Case 13, disabled age 13 not in school, eligible (disability extends past 13). + period: 2027-01 + input: + people: + person1: + age: 13 + is_disabled: true + is_in_k12_school: false + households: + household: + members: [person1] + state_code: SD + output: + # Age 13 exceeds the general under-13 limit, but the special-needs limit + # (under 18, not in school) keeps the child eligible (contrast Case 3). + sd_cca_eligible_child: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_income_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_income_eligible.yaml new file mode 100644 index 00000000000..76fa8350cb7 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_income_eligible.yaml @@ -0,0 +1,233 @@ +# Tests for sd_cca_income_eligible (SPMUnit, MONTH, bool). +# Initial applicants (is_sd_cca_enrolled = false) must have adjusted monthly +# income at or below the lesser of 209% of the federal poverty guideline and +# 85% of the state median income; continuing recipients (is_sd_cca_enrolled = +# true) are held only to the 85% SMI ceiling (ARSD 67:47:01:05; Subsidy Manual +# Section 3). spm_unit_fpg and hhs_smi are pinned as annual inputs so the +# thresholds do not depend on future uprating; read with the bare monthly +# period they auto-divide to monthly. sd_cca_countable_income is set directly +# to isolate the two-tier comparison. +# Pinned: spm_unit_fpg 24,000/yr -> 2,000/mo; 209% = 4,180/mo. +# hhs_smi 84,000/yr -> 7,000/mo; 85% = 5,950/mo. + +- name: Case 1, applicant below 209% FPL, income eligible. + period: 2027-01 + input: + people: + person1: + age: 30 + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_fpg: 24_000 + hhs_smi: 84_000 + sd_cca_countable_income: 4_000 # below 4,180 (209% FPL) + is_sd_cca_enrolled: false + households: + household: + members: [person1, person2] + state_code: SD + output: + sd_cca_income_eligible: true + +- name: Case 2, applicant above 209% FPL, not income eligible as applicant. + period: 2027-01 + input: + people: + person1: + age: 30 + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_fpg: 24_000 + hhs_smi: 84_000 + sd_cca_countable_income: 4_300 # above 4,180 (209% FPL) + is_sd_cca_enrolled: false + households: + household: + members: [person1, person2] + state_code: SD + output: + sd_cca_income_eligible: false + +- name: Case 3, enrolled family at the same 4,300 income, eligible under the 85% SMI tier. + period: 2027-01 + input: + people: + person1: + age: 30 + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_fpg: 24_000 + hhs_smi: 84_000 + sd_cca_countable_income: 4_300 # above 209% FPL but below 5,950 (85% SMI) + is_sd_cca_enrolled: true + households: + household: + members: [person1, person2] + state_code: SD + output: + sd_cca_income_eligible: true + +- name: Case 4, enrolled family above 85% SMI, not income eligible. + period: 2027-01 + input: + people: + person1: + age: 30 + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_fpg: 24_000 + hhs_smi: 84_000 + sd_cca_countable_income: 6_000 # above 5,950 (85% SMI) + is_sd_cca_enrolled: true + households: + household: + members: [person1, person2] + state_code: SD + output: + sd_cca_income_eligible: false + +- name: Case 5, applicant exactly at 209% FPL, income eligible (at or below). + period: 2027-01 + input: + people: + person1: + age: 30 + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_fpg: 24_000 + hhs_smi: 84_000 + sd_cca_countable_income: 4_180 # exactly 209% FPL + is_sd_cca_enrolled: false + households: + household: + members: [person1, person2] + state_code: SD + output: + sd_cca_income_eligible: true + +- name: Case 6, enrolled family exactly at 85% SMI, income eligible (at or below). + period: 2027-01 + input: + people: + person1: + age: 30 + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_fpg: 24_000 + hhs_smi: 84_000 + sd_cca_countable_income: 5_950 # exactly 85% SMI + is_sd_cca_enrolled: true + households: + household: + members: [person1, person2] + state_code: SD + output: + sd_cca_income_eligible: true + +- name: Case 7, applicant where 85% SMI is the binding (lesser) ceiling. + period: 2027-01 + input: + people: + person1: + age: 30 + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_fpg: 48_000 # 4,000/mo; 209% = 8,360/mo + hhs_smi: 60_000 # 5,000/mo; 85% = 4,250/mo (the lesser limit) + sd_cca_countable_income: 4_300 # below 209% FPL but above 85% SMI + is_sd_cca_enrolled: false + households: + household: + members: [person1, person2] + state_code: SD + output: + # The applicant ceiling is the lesser of the two, so 85% SMI (4,250) binds + # and 4,300 fails even though it is well below 209% FPL (8,360). + sd_cca_income_eligible: false + +- name: Case 8, applicant one dollar above 209% FPL, ineligible. + period: 2027-01 + input: + people: + person1: + age: 30 + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_fpg: 24_000 + hhs_smi: 84_000 + sd_cca_countable_income: 4_181 # 1 above the 4,180 applicant ceiling + is_sd_cca_enrolled: false + households: + household: + members: [person1, person2] + state_code: SD + output: + sd_cca_income_eligible: false + +- name: Case 9, enrolled family one dollar above 85% SMI, ineligible. + period: 2027-01 + input: + people: + person1: + age: 30 + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_fpg: 24_000 + hhs_smi: 84_000 + sd_cca_countable_income: 5_951 # 1 above the 5,950 continuing ceiling + is_sd_cca_enrolled: true + households: + household: + members: [person1, person2] + state_code: SD + output: + sd_cca_income_eligible: false + +- name: Case 10, applicant with zero income, income eligible. + period: 2027-01 + input: + people: + person1: + age: 30 + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_fpg: 24_000 + hhs_smi: 84_000 + sd_cca_countable_income: 0 + is_sd_cca_enrolled: false + households: + household: + members: [person1, person2] + state_code: SD + output: + sd_cca_income_eligible: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/income/sd_cca_countable_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/income/sd_cca_countable_income.yaml new file mode 100644 index 00000000000..9cb545905dc --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/income/sd_cca_countable_income.yaml @@ -0,0 +1,251 @@ +# Tests for sd_cca_countable_income (SPMUnit, MONTH, USD). +# Countable income is gross countable income, reduced by a 4% disregard on +# earned income, minus child support paid and minus the earned income of +# children under 18, floored at zero (ARSD 67:47:01:05, :05.01; Subsidy Manual +# Sections 4-6). 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, 4% earned income disregard. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income: 60_000 # 5,000/mo + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: SD + output: + # 5,000 - 4% x 5,000 (200) = 4,800 + sd_cca_countable_income: 4_800 + +- name: Case 2, unearned income gets no disregard. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + social_security: 24_000 # 2,000/mo, unearned + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: SD + output: + # The 4% disregard applies only to earned income, so Social Security is + # counted in full: 2,000. + sd_cca_countable_income: 2_000 + +- name: Case 3, child support paid is deducted. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income: 60_000 # 5,000/mo + child_support_expense: 12_000 # 1,000/mo paid out + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: SD + output: + # 5,000 - 200 (4% disregard) - 1,000 (child support paid) = 3,800 + sd_cca_countable_income: 3_800 + +- name: Case 4, a young child's earned income is excluded. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 40 + social_security: 36_000 # 3,000/mo, unearned (no disregard) + person2: + age: 10 + employment_income: 12_000 # 1,000/mo, excluded as a child under 18 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: SD + output: + # 3,000 (parent Social Security) + 1,000 (child earnings) - 1,000 + # (child earnings excluded) = 3,000. No earned income is counted, so the + # 4% disregard does not apply. + sd_cca_countable_income: 3_000 + +- name: Case 5, an 18-year-old's earned income is counted. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 45 + social_security: 36_000 # 3,000/mo, unearned + person2: + age: 18 + employment_income: 12_000 # 1,000/mo, counted at age 18 and over + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: SD + output: + # 3,000 + 1,000 - 4% x 1,000 (40, disregard on the counted earnings) = 3,960 + sd_cca_countable_income: 3_960 + +- name: Case 6, negative self-employment income is floored at zero. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + self_employment_income: -120_000 # -10,000/mo + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: SD + output: + sd_cca_countable_income: 0 + +- name: Case 7, capital gains and interest are counted. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + capital_gains: 12_000 # 1,000/mo + interest_income: 6_000 # 500/mo + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: SD + output: + # South Dakota counts capital gains and interest as unearned income (no + # disregard): 1,000 + 500 = 1,500. + sd_cca_countable_income: 1_500 + +- name: Case 8, no income, zero countable income. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: SD + output: + sd_cca_countable_income: 0 + +- name: Case 9, self-employment loss offsets unearned income, floored at zero. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + self_employment_income: -36_000 # -3,000/mo loss + social_security: 24_000 # 2,000/mo unearned + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: SD + output: + # gross = -3,000 (loss) + 2,000 (Social Security) = -1,000; floored at 0. + # The 4% disregard applies to max(earned, 0) = 0. + sd_cca_countable_income: 0 + +- name: Case 10, child support paid exceeds income, floored at zero. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income: 24_000 # 2,000/mo + child_support_expense: 36_000 # 3,000/mo paid out + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: SD + output: + # 2,000 - 80 (4% disregard) - 3,000 (child support) = -1,080; floored at 0. + sd_cca_countable_income: 0 + +- name: Case 11, two children's earned income both excluded. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 40 + social_security: 36_000 # 3,000/mo unearned + person2: + age: 10 + employment_income: 12_000 # 1,000/mo, excluded as a child under 18 + person3: + age: 8 + employment_income: 6_000 # 500/mo, excluded as a child under 18 + spm_units: + spm_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: SD + output: + # 3,000 (parent Social Security) + 1,500 (children's earnings) - 1,500 + # (both children's earnings excluded) = 3,000. No adult earned income, so + # the 4% disregard does not apply. + sd_cca_countable_income: 3_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/integration.yaml new file mode 100644 index 00000000000..d35c4b2c635 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/integration.yaml @@ -0,0 +1,294 @@ +# Integration tests for South Dakota Child Care Assistance (CCA). +# End-to-end scenarios verifying the eligibility chain, the region/age/level-of- +# service rate lookup, the weekly-to-monthly conversion (weekly x 52/12), the +# co-payment, the special-needs +75% rate, and the flow into sd_cca and the +# federal child_care_subsidies total. All cases run in 2027, inside the weekly +# rate era (effective 2026-08-03). Where a positive co-payment is exercised, +# spm_unit_fpg is pinned (24,000/yr -> 2,000/mo; 170% FPL = 3,400/mo) so the +# threshold does not depend on future uprating; countable income equals 96% of +# earned income after the 4% disregard. + +- name: Case 1, single parent, infant in an urban licensed center, positive co-payment. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income: 47_500 # 3,958.33/mo gross; countable 3,800 + person2: + age: 0.5 # infant + sd_cca_provider_type: LICENSED_CENTER + childcare_hours_per_week: 40 # full time + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_fpg: 24_000 # pins 170% FPL to 3,400/mo + meets_ccdf_activity_test: true + spm_unit_pre_subsidy_childcare_expenses: 24_000 # 2,000/mo, above the rate + households: + household: + members: [person1, person2] + state_code: SD + county_str: MINNEHAHA_COUNTY_SD # urban + output: + sd_cca_region: URBAN + sd_cca_income_eligible: true + sd_cca_eligible: true + # urban licensed center full-time infant 276/wk; parent (not an eligible + # child) contributes 0. + sd_cca_weekly_rate: [0, 276] + # copay = (3,800 - 3,400) x 5% = 20. + sd_cca_copay: 20 + # rate 276 x 52/12 = 1,196; capped at expenses (2,000); benefit = 1,196 - 20 = 1,176. + sd_cca: 1_176 + sd_child_care_subsidies: 1_176 + # sd_cca is registered as a federal child care subsidy program. + child_care_subsidies: 1_176 + +- name: Case 2, rural family day care, low income, no co-payment. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 28 + employment_income: 24_000 # 2,000/mo; countable 1,920, below 170% FPL + person2: + age: 0.5 # infant + sd_cca_provider_type: FAMILY_DAY_CARE + childcare_hours_per_week: 40 # full time + spm_units: + spm_unit: + members: [person1, person2] + meets_ccdf_activity_test: true + spm_unit_pre_subsidy_childcare_expenses: 24_000 # 2,000/mo, above the rate + households: + household: + members: [person1, person2] + state_code: SD + county_str: AURORA_COUNTY_SD # rural + output: + sd_cca_region: RURAL + sd_cca_eligible: true + sd_cca_copay: 0 + # rural family day care full-time infant 140/wk x 52/12 = 606.67; copay 0. + sd_cca: 606.67 + +- name: Case 3, two children in suburban family day care, rates summed. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 32 + employment_income: 24_000 # copay 0 + person2: + age: 0.5 # infant, full time + sd_cca_provider_type: FAMILY_DAY_CARE + childcare_hours_per_week: 40 + person3: + age: 4 # preschool, full time + sd_cca_provider_type: FAMILY_DAY_CARE + childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1, person2, person3] + meets_ccdf_activity_test: true + spm_unit_pre_subsidy_childcare_expenses: 36_000 # 3,000/mo, above the summed rate + households: + household: + members: [person1, person2, person3] + state_code: SD + county_str: BROOKINGS_COUNTY_SD # suburban + output: + sd_cca_region: SUBURBAN + sd_cca_eligible: true + # suburban family day care full-time: infant 153/wk + preschool 150/wk = 303/wk. + # 303 x 52/12 = 1,313; copay 0. + sd_cca: 1_313 + +- name: Case 4, special-needs infant, urban family day care, base plus 75%. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income: 24_000 # copay 0 + person2: + age: 0.5 # infant + is_disabled: true + sd_cca_provider_type: FAMILY_DAY_CARE + childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1, person2] + meets_ccdf_activity_test: true + spm_unit_pre_subsidy_childcare_expenses: 24_000 # 2,000/mo + households: + household: + members: [person1, person2] + state_code: SD + county_str: MINNEHAHA_COUNTY_SD # urban + output: + sd_cca_eligible: true + # urban family day care full-time infant 180/wk x 1.75 (special needs) = 315/wk. + sd_cca_weekly_rate: [0, 315] + # 315 x 52/12 = 1,365; capped at expenses (2,000); copay 0. + sd_cca: 1_365 + +- name: Case 5, income too high, applicant ineligible. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income: 240_000 # 20,000/mo, above the income limit + person2: + age: 0.5 + sd_cca_provider_type: FAMILY_DAY_CARE + childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1, person2] + meets_ccdf_activity_test: true + is_sd_cca_enrolled: false + spm_unit_pre_subsidy_childcare_expenses: 24_000 + households: + household: + members: [person1, person2] + state_code: SD + county_str: MINNEHAHA_COUNTY_SD + output: + sd_cca_income_eligible: false + sd_cca_eligible: false + sd_cca: 0 + sd_child_care_subsidies: 0 + +- name: Case 6, TANF recipient, co-payment waived. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income: 47_500 # countable 3,800, would otherwise owe a copay + person2: + age: 0.5 # infant + sd_cca_provider_type: FAMILY_DAY_CARE + childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_fpg: 24_000 + meets_ccdf_activity_test: true + is_tanf_enrolled: true + spm_unit_pre_subsidy_childcare_expenses: 24_000 + households: + household: + members: [person1, person2] + state_code: SD + county_str: MINNEHAHA_COUNTY_SD + output: + sd_cca_eligible: true + # copay waived for TANF recipients. + sd_cca_copay: 0 + # urban family day care full-time infant 180/wk x 52/12 = 780; copay 0. + sd_cca: 780 + +- name: Case 7, foster child, co-payment waived. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income: 47_500 # countable 3,800, would otherwise owe a copay + person2: + age: 0.5 # infant in foster care + is_in_foster_care: true + sd_cca_provider_type: FAMILY_DAY_CARE + childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_fpg: 24_000 + meets_ccdf_activity_test: true + spm_unit_pre_subsidy_childcare_expenses: 24_000 + households: + household: + members: [person1, person2] + state_code: SD + county_str: MINNEHAHA_COUNTY_SD + output: + sd_cca_eligible: true + # copay waived for foster/protective-services children. + sd_cca_copay: 0 + sd_cca: 780 + +- name: Case 8, enrolled family in the phase-out band, benefit net of co-payment. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income: 60_000 # 5,000/mo gross; countable 4,800 + person2: + age: 0.5 # infant + sd_cca_provider_type: FAMILY_DAY_CARE + childcare_hours_per_week: 40 # full time + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_fpg: 24_000 # 209% FPL = 4,180/mo; 170% FPL = 3,400/mo + hhs_smi: 84_000 # 7,000/mo; 85% = 5,950/mo continuing ceiling + is_sd_cca_enrolled: true # continuing recipient + meets_ccdf_activity_test: true + spm_unit_pre_subsidy_childcare_expenses: 24_000 # 2,000/mo, above the rate + households: + household: + members: [person1, person2] + state_code: SD + county_str: MINNEHAHA_COUNTY_SD # urban + output: + # countable 4,800 exceeds the 4,180 applicant ceiling but is below the + # 5,950 continuing ceiling, so the enrolled family stays eligible. + sd_cca_income_eligible: true + sd_cca_eligible: true + # copay = (4,800 - 3,400) x 5% = 70. + sd_cca_copay: 70 + # urban family day care full-time infant 180/wk x 52/12 = 780 monthly cap; + # expenses 2,000 higher, capped at 780; benefit = 780 - 70 = 710. + sd_cca: 710 + sd_child_care_subsidies: 710 + +- name: Case 9, zero-income family, full rate benefit, no co-payment. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 # activity met through the CCDF activity test (e.g., training) + person2: + age: 0.5 # infant + sd_cca_provider_type: FAMILY_DAY_CARE + childcare_hours_per_week: 40 # full time + spm_units: + spm_unit: + members: [person1, person2] + meets_ccdf_activity_test: true + spm_unit_pre_subsidy_childcare_expenses: 24_000 # 2,000/mo, above the rate + households: + household: + members: [person1, person2] + state_code: SD + county_str: MINNEHAHA_COUNTY_SD # urban + output: + sd_cca_income_eligible: true + sd_cca_eligible: true + sd_cca_copay: 0 # countable income 0 is below the 170% FPL threshold + # urban family day care full-time infant 180/wk x 52/12 = 780; copay 0. + sd_cca: 780 diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/rates/sd_cca_age_group.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/rates/sd_cca_age_group.yaml new file mode 100644 index 00000000000..17313c244d4 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/rates/sd_cca_age_group.yaml @@ -0,0 +1,185 @@ +# Tests for sd_cca_age_group (Person, MONTH, Enum). +# Rate-table age bands from age in months (age * 12): +# INFANT: under 12 months +# TODDLER: 12 through 35 months (under 36) +# PRESCHOOL: 36 through 71 months, unless enrolled in school at 60+ months +# SCHOOL_AGE: 60+ months and in K-12 school, or 72+ months (default) +# The 60-71 month overlap resolves on is_in_k12_school. is_in_k12_school +# imputes true for ages 5-17, so the "60-71 months not in school" cases set +# is_in_k12_school explicitly to false. + +- name: Case 1, 6 months old, infant. + period: 2027-01 + input: + people: + person1: + age: 0.5 # 6 months + households: + household: + members: [person1] + state_code: SD + output: + sd_cca_age_group: INFANT + +- name: Case 2, 11 months old, still infant. + period: 2027-01 + input: + people: + person1: + age: 0.9166 # 11 months, under the 12-month infant cutoff + households: + household: + members: [person1] + state_code: SD + output: + sd_cca_age_group: INFANT + +- name: Case 3, exactly 12 months, switches to toddler. + period: 2027-01 + input: + people: + person1: + age: 1.0 # 12 months, infant cutoff is under 12 so this is toddler + households: + household: + members: [person1] + state_code: SD + output: + sd_cca_age_group: TODDLER + +- name: Case 4, 35 months, still toddler. + period: 2027-01 + input: + people: + person1: + age: 2.9166 # 35 months, under the 36-month toddler cutoff + households: + household: + members: [person1] + state_code: SD + output: + sd_cca_age_group: TODDLER + +- name: Case 5, exactly 36 months, switches to preschool. + period: 2027-01 + input: + people: + person1: + age: 3.0 # 36 months, toddler cutoff is under 36 so this is preschool + households: + household: + members: [person1] + state_code: SD + output: + sd_cca_age_group: PRESCHOOL + +- name: Case 6, 4 years old, preschool (not yet school age). + period: 2027-01 + input: + people: + person1: + age: 4.0 # under 5, so not imputed in school + households: + household: + members: [person1] + state_code: SD + output: + sd_cca_age_group: PRESCHOOL + +- name: Case 7, exactly 60 months in school, school age. + period: 2027-01 + input: + people: + person1: + age: 5.0 # 60 months; age 5 is imputed in K-12 school + households: + household: + members: [person1] + state_code: SD + output: + sd_cca_age_group: SCHOOL_AGE + +- name: Case 8, exactly 60 months not in school, preschool. + period: 2027-01 + input: + people: + person1: + age: 5.0 # 60 months + is_in_k12_school: false # override the age-5+ school imputation + households: + household: + members: [person1] + state_code: SD + output: + # Within the 60-71 month overlap, a child not in school stays preschool. + sd_cca_age_group: PRESCHOOL + +- name: Case 9, 71 months in school, school age. + period: 2027-01 + input: + people: + person1: + age: 5.9166 # 71 months, in the overlap and imputed in school + households: + household: + members: [person1] + state_code: SD + output: + sd_cca_age_group: SCHOOL_AGE + +- name: Case 10, 71 months not in school, preschool. + period: 2027-01 + input: + people: + person1: + age: 5.9166 # 71 months + is_in_k12_school: false + households: + household: + members: [person1] + state_code: SD + output: + sd_cca_age_group: PRESCHOOL + +- name: Case 11, 72 months not in school, school age by default. + period: 2027-01 + input: + people: + person1: + age: 6.0 # 72 months + is_in_k12_school: false + households: + household: + members: [person1] + state_code: SD + output: + # At 72+ months the child is school age regardless of school enrollment, + # since preschool tops out at 71 months. + sd_cca_age_group: SCHOOL_AGE + +- name: Case 12, 10 years old in school, school age. + period: 2027-01 + input: + people: + person1: + age: 10.0 + is_in_k12_school: true + households: + household: + members: [person1] + state_code: SD + output: + sd_cca_age_group: SCHOOL_AGE + +- name: Case 13, newborn, infant (minimum age). + period: 2027-01 + input: + people: + person1: + age: 0 # 0 months + households: + household: + members: [person1] + state_code: SD + output: + sd_cca_age_group: INFANT diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/rates/sd_cca_region.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/rates/sd_cca_region.yaml new file mode 100644 index 00000000000..95df60dda4a --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/rates/sd_cca_region.yaml @@ -0,0 +1,117 @@ +# Tests for sd_cca_region (Household, MONTH, Enum). +# The provider rate region is Urban (4 counties: Lincoln, Meade, Minnehaha, +# Pennington), Suburban (15 named counties), or Rural (all remaining counties, +# the default), effective 2026-08-03. Tests run in 2027 (fully inside the +# weekly rate era). county_str is a YEAR string read in the monthly formula. + +- name: Case 1, Minnehaha County is urban. + period: 2027-01 + input: + people: + person1: + age: 30 + households: + household: + members: [person1] + state_code: SD + county_str: MINNEHAHA_COUNTY_SD + output: + sd_cca_region: URBAN + +- name: Case 2, Pennington County is urban. + period: 2027-01 + input: + people: + person1: + age: 30 + households: + household: + members: [person1] + state_code: SD + county_str: PENNINGTON_COUNTY_SD + output: + sd_cca_region: URBAN + +- name: Case 3, Brookings County is suburban. + period: 2027-01 + input: + people: + person1: + age: 30 + households: + household: + members: [person1] + state_code: SD + county_str: BROOKINGS_COUNTY_SD + output: + sd_cca_region: SUBURBAN + +- name: Case 4, Yankton County is suburban. + period: 2027-01 + input: + people: + person1: + age: 30 + households: + household: + members: [person1] + state_code: SD + county_str: YANKTON_COUNTY_SD + output: + sd_cca_region: SUBURBAN + +- name: Case 5, Aurora County is rural (default region). + period: 2027-01 + input: + people: + person1: + age: 30 + households: + household: + members: [person1] + state_code: SD + county_str: AURORA_COUNTY_SD + output: + sd_cca_region: RURAL + +- name: Case 6, Butte County is rural (default region). + period: 2027-01 + input: + people: + person1: + age: 30 + households: + household: + members: [person1] + state_code: SD + county_str: BUTTE_COUNTY_SD + output: + sd_cca_region: RURAL + +- name: Case 7, Lincoln County is urban. + period: 2027-01 + input: + people: + person1: + age: 30 + households: + household: + members: [person1] + state_code: SD + county_str: LINCOLN_COUNTY_SD + output: + sd_cca_region: URBAN + +- name: Case 8, Meade County is urban. + period: 2027-01 + input: + people: + person1: + age: 30 + households: + household: + members: [person1] + state_code: SD + county_str: MEADE_COUNTY_SD + output: + sd_cca_region: URBAN diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/rates/sd_cca_time_category.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/rates/sd_cca_time_category.yaml new file mode 100644 index 00000000000..83faddb1efd --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/rates/sd_cca_time_category.yaml @@ -0,0 +1,103 @@ +# Tests for sd_cca_time_category (Person, MONTH, Enum). +# Level of service by weekly child care hours: Full-Time 28+ hrs/wk; +# Part-Time 16 to 27 hrs/wk; Limited-Time 1 to 15 hrs/wk (default). +# childcare_hours_per_week is a YEAR variable read with period.this_year, so +# the weekly-hours value passes through directly. + +- name: Case 1, 40 hours per week, full time. + period: 2027-01 + input: + people: + person1: + age: 3 + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: SD + output: + sd_cca_time_category: FULL_TIME + +- name: Case 2, exactly 28 hours per week, full time (at the threshold). + period: 2027-01 + input: + people: + person1: + age: 3 + childcare_hours_per_week: 28 + households: + household: + members: [person1] + state_code: SD + output: + sd_cca_time_category: FULL_TIME + +- name: Case 3, 27 hours per week, part time (just below full time). + period: 2027-01 + input: + people: + person1: + age: 3 + childcare_hours_per_week: 27 + households: + household: + members: [person1] + state_code: SD + output: + sd_cca_time_category: PART_TIME + +- name: Case 4, exactly 16 hours per week, part time (at the threshold). + period: 2027-01 + input: + people: + person1: + age: 3 + childcare_hours_per_week: 16 + households: + household: + members: [person1] + state_code: SD + output: + sd_cca_time_category: PART_TIME + +- name: Case 5, 15 hours per week, limited time (just below part time). + period: 2027-01 + input: + people: + person1: + age: 3 + childcare_hours_per_week: 15 + households: + household: + members: [person1] + state_code: SD + output: + sd_cca_time_category: LIMITED_TIME + +- name: Case 6, 5 hours per week, limited time. + period: 2027-01 + input: + people: + person1: + age: 3 + childcare_hours_per_week: 5 + households: + household: + members: [person1] + state_code: SD + output: + sd_cca_time_category: LIMITED_TIME + +- name: Case 7, zero hours per week, limited time (default). + period: 2027-01 + input: + people: + person1: + age: 3 + childcare_hours_per_week: 0 + households: + household: + members: [person1] + state_code: SD + output: + sd_cca_time_category: LIMITED_TIME diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/rates/sd_cca_weekly_rate.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/rates/sd_cca_weekly_rate.yaml new file mode 100644 index 00000000000..6ae32180f03 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/rates/sd_cca_weekly_rate.yaml @@ -0,0 +1,273 @@ +# Tests for sd_cca_weekly_rate (Person, MONTH, USD per week), effective +# 2026-08-03 (tested in 2027, inside the weekly rate era). The weekly maximum +# is keyed on region x provider category x age band x level of service, with +# informal/in-home/relative rates flat statewide (region and age moot). A child +# with special needs receives the base rate plus an additional 75% (x 1.75). +# Each case sets county_str (region), sd_cca_provider_type, age (age band), and +# childcare_hours_per_week (level of service). Values are transcribed from the +# CCA Weekly Reimbursement Rates schedule. + +# ---- Family day care ---- +- name: Case 1, urban family day care, full time infant. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 0.5 # infant + sd_cca_provider_type: FAMILY_DAY_CARE + childcare_hours_per_week: 40 # full time + households: + household: + members: [person1] + state_code: SD + county_str: MINNEHAHA_COUNTY_SD # urban + output: + sd_cca_weekly_rate: 180 + +- name: Case 2, urban family day care, part time toddler. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 1.5 # 18 months, toddler + sd_cca_provider_type: FAMILY_DAY_CARE + childcare_hours_per_week: 20 # part time + households: + household: + members: [person1] + state_code: SD + county_str: MINNEHAHA_COUNTY_SD + output: + sd_cca_weekly_rate: 88.75 + +- name: Case 3, urban family day care, limited time preschool. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 4 # preschool (under 5, not in school) + sd_cca_provider_type: FAMILY_DAY_CARE + childcare_hours_per_week: 10 # limited time + households: + household: + members: [person1] + state_code: SD + county_str: MINNEHAHA_COUNTY_SD + output: + sd_cca_weekly_rate: 43.75 + +- name: Case 4, urban family day care, full time school age. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 8 # school age (in K-12 by imputation) + sd_cca_provider_type: FAMILY_DAY_CARE + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: SD + county_str: MINNEHAHA_COUNTY_SD + output: + sd_cca_weekly_rate: 170 + +- name: Case 5, suburban family day care, full time infant. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 0.5 + sd_cca_provider_type: FAMILY_DAY_CARE + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: SD + county_str: BROOKINGS_COUNTY_SD # suburban + output: + sd_cca_weekly_rate: 153 + +- name: Case 6, rural family day care, full time toddler. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 1.5 + sd_cca_provider_type: FAMILY_DAY_CARE + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: SD + county_str: AURORA_COUNTY_SD # rural + output: + sd_cca_weekly_rate: 145 + +# ---- Licensed center or school age program ---- +- name: Case 7, urban licensed center, full time infant. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 0.5 + sd_cca_provider_type: LICENSED_CENTER + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: SD + county_str: MINNEHAHA_COUNTY_SD + output: + sd_cca_weekly_rate: 276 + +- name: Case 8, suburban licensed center, full time preschool. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 4 + sd_cca_provider_type: LICENSED_CENTER + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: SD + county_str: BROOKINGS_COUNTY_SD + output: + sd_cca_weekly_rate: 203 + +- name: Case 9, rural licensed center, full time school age. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 8 + sd_cca_provider_type: LICENSED_CENTER + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: SD + county_str: AURORA_COUNTY_SD + output: + sd_cca_weekly_rate: 150 + +# ---- Informal / in-home / relative (flat statewide by level of service) ---- +- name: Case 10, informal provider, full time (flat statewide). + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 0.5 + sd_cca_provider_type: INFORMAL + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: SD + county_str: AURORA_COUNTY_SD + output: + sd_cca_weekly_rate: 116.25 + +- name: Case 11, informal provider, part time (flat statewide). + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 0.5 + sd_cca_provider_type: INFORMAL + childcare_hours_per_week: 20 + households: + household: + members: [person1] + state_code: SD + county_str: AURORA_COUNTY_SD + output: + sd_cca_weekly_rate: 58.25 + +- name: Case 12, informal provider, limited time (flat statewide). + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 0.5 + sd_cca_provider_type: INFORMAL + childcare_hours_per_week: 10 + households: + household: + members: [person1] + state_code: SD + county_str: AURORA_COUNTY_SD + output: + sd_cca_weekly_rate: 29 + +# ---- Special needs (base rate x 1.75) ---- +- name: Case 13, special-needs child, urban family day care full time infant, base plus 75%. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 0.5 + is_disabled: true + sd_cca_provider_type: FAMILY_DAY_CARE + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: SD + county_str: MINNEHAHA_COUNTY_SD + output: + # base 180 x 1.75 = 315 + sd_cca_weekly_rate: 315 + +- name: Case 14, special-needs school-age child, rural licensed center full time, base plus 75%. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 8 # school age (in K-12 by imputation) + is_disabled: true + sd_cca_provider_type: LICENSED_CENTER + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: SD + county_str: AURORA_COUNTY_SD # rural + output: + # rural licensed center full-time school age 150 x 1.75 = 262.5 + sd_cca_weekly_rate: 262.5 + +- name: Case 15, age-ineligible child (age 15, non-disabled), no rate. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 15 # over the age-13 limit and not disabled, not an eligible child + sd_cca_provider_type: LICENSED_CENTER + childcare_hours_per_week: 40 + households: + household: + members: [person1] + state_code: SD + county_str: MINNEHAHA_COUNTY_SD + output: + # sd_cca_weekly_rate is defined_for sd_cca_eligible_child, so an + # age-ineligible child receives no rate. + sd_cca_weekly_rate: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/sd_cca.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/sd_cca.yaml new file mode 100644 index 00000000000..9d4858e1736 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/sd_cca.yaml @@ -0,0 +1,207 @@ +# Tests for sd_cca (SPMUnit, MONTH, USD), the South Dakota Child Care +# Assistance benefit. The benefit is the lesser of the summed per-child weekly +# maximum rates (converted to a monthly amount with WEEKS_IN_YEAR / +# MONTHS_IN_YEAR = 52/12 = 4.3333) and the family's billed child care expenses, +# minus the monthly co-payment, floored at zero (ARSD 67:47:01:16). There are +# no separate additive bonuses. sd_child_care_subsidies is the YEAR aggregator +# (adds sd_cca); read at a month period it reports the monthly value, and +# sd_cca is registered in the federal child_care_subsidies total. +# Urban family day care full-time infant weekly rate 180 -> 180 x 52/12 = 780. + +- name: Case 1, rate-limited benefit, co-payment zero. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income: 24_000 # 2,000/mo; countable 1,920, below 170% FPL + person2: + age: 0.5 # infant + sd_cca_provider_type: FAMILY_DAY_CARE + childcare_hours_per_week: 40 # full time + spm_units: + spm_unit: + members: [person1, person2] + meets_ccdf_activity_test: true + spm_unit_pre_subsidy_childcare_expenses: 24_000 # 2,000/mo, above the rate + households: + household: + members: [person1, person2] + state_code: SD + county_str: MINNEHAHA_COUNTY_SD # urban + output: + # rate 780 < expenses 2,000, capped at 780; copay 0. benefit = 780. + sd_cca: 780 + sd_child_care_subsidies: 780 + # sd_cca is registered as a federal child care subsidy program. + child_care_subsidies: 780 + +- name: Case 2, expense-limited benefit, co-payment zero. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income: 24_000 + person2: + age: 0.5 + sd_cca_provider_type: FAMILY_DAY_CARE + childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1, person2] + meets_ccdf_activity_test: true + spm_unit_pre_subsidy_childcare_expenses: 3_600 # 300/mo, below the rate + households: + household: + members: [person1, person2] + state_code: SD + county_str: MINNEHAHA_COUNTY_SD + output: + # expenses 300 < rate 780, capped at 300; copay 0. benefit = 300. + sd_cca: 300 + +- name: Case 3, co-payment subtracted from the capped rate. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income: 47_500 # countable 3,800, above 170% FPL + person2: + age: 0.5 + sd_cca_provider_type: FAMILY_DAY_CARE + childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_fpg: 24_000 # pins 170% FPL to 3,400/mo + meets_ccdf_activity_test: true + spm_unit_pre_subsidy_childcare_expenses: 24_000 # 2,000/mo + households: + household: + members: [person1, person2] + state_code: SD + county_str: MINNEHAHA_COUNTY_SD + output: + # rate 780 capped below expenses; copay (3,800 - 3,400) x 5% = 20. + # benefit = 780 - 20 = 760. + sd_cca: 760 + +- name: Case 4, two eligible children, per-child rates summed. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income: 24_000 # copay 0 + person2: + age: 0.5 # infant + sd_cca_provider_type: FAMILY_DAY_CARE + childcare_hours_per_week: 40 + person3: + age: 0.8 # infant + sd_cca_provider_type: FAMILY_DAY_CARE + childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1, person2, person3] + meets_ccdf_activity_test: true + spm_unit_pre_subsidy_childcare_expenses: 36_000 # 3,000/mo, above the summed rate + households: + household: + members: [person1, person2, person3] + state_code: SD + county_str: MINNEHAHA_COUNTY_SD + output: + # two infants at 780/mo each = 1,560; expenses 3,000 higher; copay 0. + sd_cca: 1_560 + +- name: Case 5, ineligible family, no benefit. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income: 240_000 # 20,000/mo, above the income limit + person2: + age: 0.5 + sd_cca_provider_type: FAMILY_DAY_CARE + childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1, person2] + meets_ccdf_activity_test: true + spm_unit_pre_subsidy_childcare_expenses: 24_000 + households: + household: + members: [person1, person2] + state_code: SD + county_str: MINNEHAHA_COUNTY_SD + output: + sd_cca: 0 + sd_child_care_subsidies: 0 + +- name: Case 6, zero child care expenses, no benefit. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income: 24_000 # copay 0 + person2: + age: 0.5 # infant + sd_cca_provider_type: FAMILY_DAY_CARE + childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1, person2] + meets_ccdf_activity_test: true + spm_unit_pre_subsidy_childcare_expenses: 0 # no billed expenses + households: + household: + members: [person1, person2] + state_code: SD + county_str: MINNEHAHA_COUNTY_SD + output: + # capped expenses = min(0, rate) = 0; benefit floors at 0. + sd_cca: 0 + +- name: Case 7, co-payment exceeds capped expenses, benefit floors at zero. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income: 100_000 # 8,333.33/mo gross; countable 8,000 + person2: + age: 8 # school age + sd_cca_provider_type: LICENSED_CENTER + childcare_hours_per_week: 40 # full time + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_fpg: 24_000 # 170% FPL = 3,400/mo + hhs_smi: 120_000 # 10,000/mo; 85% = 8,500/mo continuing ceiling + is_sd_cca_enrolled: true # continuing recipient, 85% SMI tier + meets_ccdf_activity_test: true + spm_unit_pre_subsidy_childcare_expenses: 1_200 # 100/mo billed + households: + household: + members: [person1, person2] + state_code: SD + county_str: AURORA_COUNTY_SD # rural + output: + sd_cca_eligible: true # countable 8,000 <= 8,500 (85% SMI, enrolled) + # copay = (8,000 - 3,400) x 5% = 230. + sd_cca_copay: 230 + # rural licensed center full-time school age 150/wk x 52/12 = 650 monthly + # cap; expenses 100 bind, capped at 100; benefit = max(100 - 230, 0) = 0. + sd_cca: 0 diff --git a/policyengine_us/variables/gov/states/sd/dss/cca/copay/sd_cca_copay.py b/policyengine_us/variables/gov/states/sd/dss/cca/copay/sd_cca_copay.py new file mode 100644 index 00000000000..f97aea704dd --- /dev/null +++ b/policyengine_us/variables/gov/states/sd/dss/cca/copay/sd_cca_copay.py @@ -0,0 +1,38 @@ +from policyengine_us.model_api import * + + +class sd_cca_copay(Variable): + value_type = float + entity = SPMUnit + unit = USD + label = "South Dakota CCA family co-payment" + definition_period = MONTH + defined_for = "sd_cca_eligible" + reference = ( + "https://dss.sd.gov/docs/childcare/assistance/Subsidy_Manual.pdf#page=21", + "https://dss.sd.gov/docs/childcare/assistance/Sliding_Fee_Scale.pdf", + ) + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.sd.dss.cca.copay + # Income is floored at zero so a self-employment loss cannot produce a + # negative co-payment. + countable_income = max_(spm_unit("sd_cca_countable_income", period), 0) + # spm_unit_fpg is an annual dollar amount; the bare monthly period + # auto-divides it to a monthly value. + monthly_fpg = spm_unit("spm_unit_fpg", period) + # The co-payment is 5% of countable income above 170% of the federal + # poverty level, and zero at or below that threshold. + income_above_threshold = max_( + countable_income - p.fpl_threshold * monthly_fpg, 0 + ) + copay = income_above_threshold * p.rate + # The co-payment never exceeds 12% of countable income. + copay = min_(copay, p.max_rate * countable_income) + # The co-payment is waived for families enrolled in the Temporary + # Assistance for Needy Families program and for foster or + # protective-services children. + is_tanf_enrolled = spm_unit("is_tanf_enrolled", period) + has_foster_child = add(spm_unit, period, ["is_in_foster_care"]) > 0 + waived = is_tanf_enrolled | has_foster_child + return where(waived, 0, copay) diff --git a/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_activity_eligible.py b/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_activity_eligible.py new file mode 100644 index 00000000000..0c6e1fe9de2 --- /dev/null +++ b/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_activity_eligible.py @@ -0,0 +1,28 @@ +from policyengine_us.model_api import * + + +class sd_cca_activity_eligible(Variable): + value_type = bool + entity = SPMUnit + label = "South Dakota CCA activity eligible" + definition_period = MONTH + defined_for = StateCode.SD + reference = ( + "https://dss.sd.gov/docs/childcare/assistance/Subsidy_Manual.pdf#page=12" + ) + + def formula(spm_unit, period, parameters): + # Every caretaker (head and spouse) must be in an approved activity + # (employment, self-employment, or school). Requiring every caretaker + # to be active also serves as the proxy for the availability rule: 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("sd_cca_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 + 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). + meets_ccdf = spm_unit("meets_ccdf_activity_test", period.this_year) + return modeled_eligible | meets_ccdf diff --git a/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_eligible.py b/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_eligible.py new file mode 100644 index 00000000000..7346eacbea0 --- /dev/null +++ b/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_eligible.py @@ -0,0 +1,19 @@ +from policyengine_us.model_api import * + + +class sd_cca_eligible(Variable): + value_type = bool + entity = SPMUnit + label = "South Dakota CCA eligible" + definition_period = MONTH + defined_for = StateCode.SD + reference = "https://dss.sd.gov/docs/childcare/assistance/Subsidy_Manual.pdf#page=8" + + def formula(spm_unit, period, parameters): + has_eligible_child = add(spm_unit, period, ["sd_cca_eligible_child"]) > 0 + income_eligible = spm_unit("sd_cca_income_eligible", period) + # The $1,000,000 self-certified asset limit matches the federal CCDF + # limit, so we reuse is_ccdf_asset_eligible. + asset_eligible = spm_unit("is_ccdf_asset_eligible", period) + activity_eligible = spm_unit("sd_cca_activity_eligible", period) + return has_eligible_child & income_eligible & asset_eligible & activity_eligible diff --git a/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_eligible_child.py b/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_eligible_child.py new file mode 100644 index 00000000000..c87a6643109 --- /dev/null +++ b/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_eligible_child.py @@ -0,0 +1,38 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.household.demographic.person.immigration_status import ( + ImmigrationStatus, +) + + +class sd_cca_eligible_child(Variable): + value_type = bool + entity = Person + label = "South Dakota CCA eligible child" + definition_period = MONTH + defined_for = StateCode.SD + reference = "https://dss.sd.gov/docs/childcare/assistance/Subsidy_Manual.pdf#page=9" + + def formula(person, period, parameters): + p = parameters(period).gov.states.sd.dss.cca.eligibility + age = person("age", period.this_year) + # A child under 13 is eligible. A child physically or mentally incapable + # of self-care is eligible through age 17 (under 18), extended through + # age 18 (under 19) when enrolled in school and expected to graduate. + # is_disabled proxies the incapacity determination; the + # court-supervision pathway is not modeled. + is_disabled = person("is_disabled", period.this_year) + is_in_school = person("is_in_k12_school", period.this_year) + special_needs_age_limit = where( + is_in_school, + p.special_needs_student_age_limit, + p.special_needs_age_limit, + ) + age_limit = where(is_disabled, special_needs_age_limit, p.child_age_limit) + age_eligible = age < age_limit + # The child must be a United States citizen or a lawful permanent + # resident, which is narrower than the federal CCDF immigration test. + 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/sd/dss/cca/eligibility/sd_cca_income_eligible.py b/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_income_eligible.py new file mode 100644 index 00000000000..c758016c7c8 --- /dev/null +++ b/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_income_eligible.py @@ -0,0 +1,27 @@ +from policyengine_us.model_api import * + + +class sd_cca_income_eligible(Variable): + value_type = bool + entity = SPMUnit + label = "South Dakota CCA income eligible" + definition_period = MONTH + defined_for = StateCode.SD + reference = "https://dss.sd.gov/docs/childcare/assistance/Subsidy_Manual.pdf#page=8" + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.sd.dss.cca.income + countable_income = spm_unit("sd_cca_countable_income", period) + enrolled = spm_unit("is_sd_cca_enrolled", period) + # spm_unit_fpg and hhs_smi are annual dollar amounts, so reading them + # with the bare monthly period auto-divides them to monthly values. + monthly_fpg = spm_unit("spm_unit_fpg", period) + monthly_smi = spm_unit("hhs_smi", period) + smi_limit = monthly_smi * p.smi_limit + # Initial applicants must be at or below the lesser of 209% of the + # federal poverty level and 85% of the state median income. Continuing + # recipients remain eligible up to 85% of the state median income under + # the graduated phase-out. + initial_limit = min_(monthly_fpg * p.fpl_limit, smi_limit) + income_limit = where(enrolled, smi_limit, initial_limit) + return countable_income <= income_limit diff --git a/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_parent_in_eligible_activity.py b/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_parent_in_eligible_activity.py new file mode 100644 index 00000000000..9e441faa93c --- /dev/null +++ b/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_parent_in_eligible_activity.py @@ -0,0 +1,27 @@ +from policyengine_us.model_api import * + + +class sd_cca_parent_in_eligible_activity(Variable): + value_type = bool + entity = Person + label = "South Dakota CCA parent in an eligible activity" + definition_period = MONTH + defined_for = StateCode.SD + reference = ( + "https://dss.sd.gov/docs/childcare/assistance/Subsidy_Manual.pdf#page=12" + ) + + def formula(person, period, parameters): + # Approved activities are employment, self-employment, and school or + # training enrollment. A parent qualifies with positive wages, nonzero + # self-employment income (a business loss still evidences active + # self-employment), full-time student status, or enrollment in the + # Temporary Assistance for Needy Families program. The exact hour and + # credit thresholds are not modeled because activity hours are not + # tracked. + has_earnings = (person("employment_income", period) > 0) | ( + person("self_employment_income", period) != 0 + ) + is_student = person("is_full_time_student", period.this_year) + is_tanf_enrolled = person.spm_unit("is_tanf_enrolled", period) + return has_earnings | is_student | is_tanf_enrolled diff --git a/policyengine_us/variables/gov/states/sd/dss/cca/income/sd_cca_countable_income.py b/policyengine_us/variables/gov/states/sd/dss/cca/income/sd_cca_countable_income.py new file mode 100644 index 00000000000..5919dce1c4e --- /dev/null +++ b/policyengine_us/variables/gov/states/sd/dss/cca/income/sd_cca_countable_income.py @@ -0,0 +1,36 @@ +from policyengine_us.model_api import * + + +class sd_cca_countable_income(Variable): + value_type = float + entity = SPMUnit + unit = USD + label = "South Dakota CCA countable income" + definition_period = MONTH + defined_for = StateCode.SD + reference = ( + "https://dss.sd.gov/docs/childcare/assistance/Subsidy_Manual.pdf#page=10" + ) + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.sd.dss.cca.income + gross_income = spm_unit("sd_cca_gross_income", period) + person = spm_unit.members + person_earned = add( + person, period, ["employment_income", "self_employment_income"] + ) + is_child = person("age", period.this_year) < p.child_earned_income_exclusion_age + # The earned income of children is excluded entirely. + child_earned_income = spm_unit.sum(is_child * person_earned) + # A 4% standard deduction applies to the remaining (adult) earned income. + adult_earned_income = spm_unit.sum(~is_child * person_earned) + earned_disregard = max_(adult_earned_income, 0) * p.earned_income_disregard + # Court-ordered child support paid out is deducted. + child_support_expense = add(spm_unit, period, ["child_support_expense"]) + return max_( + gross_income + - child_earned_income + - earned_disregard + - child_support_expense, + 0, + ) diff --git a/policyengine_us/variables/gov/states/sd/dss/cca/income/sd_cca_gross_income.py b/policyengine_us/variables/gov/states/sd/dss/cca/income/sd_cca_gross_income.py new file mode 100644 index 00000000000..e0b359625cc --- /dev/null +++ b/policyengine_us/variables/gov/states/sd/dss/cca/income/sd_cca_gross_income.py @@ -0,0 +1,14 @@ +from policyengine_us.model_api import * + + +class sd_cca_gross_income(Variable): + value_type = float + entity = SPMUnit + unit = USD + label = "South Dakota CCA gross countable income" + definition_period = MONTH + defined_for = StateCode.SD + reference = ( + "https://dss.sd.gov/docs/childcare/assistance/Subsidy_Manual.pdf#page=10" + ) + adds = "gov.states.sd.dss.cca.income.sources" diff --git a/policyengine_us/variables/gov/states/sd/dss/cca/is_sd_cca_enrolled.py b/policyengine_us/variables/gov/states/sd/dss/cca/is_sd_cca_enrolled.py new file mode 100644 index 00000000000..fb86f631e62 --- /dev/null +++ b/policyengine_us/variables/gov/states/sd/dss/cca/is_sd_cca_enrolled.py @@ -0,0 +1,12 @@ +from policyengine_us.model_api import * + + +class is_sd_cca_enrolled(Variable): + value_type = bool + entity = SPMUnit + definition_period = MONTH + label = "Enrolled in South Dakota CCA" + defined_for = StateCode.SD + reference = ( + "https://dss.sd.gov/docs/childcare/assistance/Subsidy_Manual.pdf#page=21" + ) diff --git a/policyengine_us/variables/gov/states/sd/dss/cca/rates/sd_cca_age_group.py b/policyengine_us/variables/gov/states/sd/dss/cca/rates/sd_cca_age_group.py new file mode 100644 index 00000000000..514e199e659 --- /dev/null +++ b/policyengine_us/variables/gov/states/sd/dss/cca/rates/sd_cca_age_group.py @@ -0,0 +1,43 @@ +from policyengine_us.model_api import * + + +class SDCCAAgeGroup(Enum): + INFANT = "Infant" + TODDLER = "Toddler" + PRESCHOOL = "Preschool" + SCHOOL_AGE = "School age" + + +class sd_cca_age_group(Variable): + value_type = Enum + entity = Person + possible_values = SDCCAAgeGroup + default_value = SDCCAAgeGroup.SCHOOL_AGE + definition_period = MONTH + defined_for = StateCode.SD + label = "South Dakota CCA rate-table age group" + reference = "https://dss.sd.gov/docs/childcare/assistance/CCA_Weekly_Reimbursement_Rates.pdf" + + def formula(person, period, parameters): + p = parameters(period).gov.states.sd.dss.cca.age_group + # Infant is under 12 months, toddler 12 through 35 months, and preschool + # 36 through 71 months. A child 60 through 71 months old who is enrolled + # in school is classified as school age rather than preschool. + age_months = person("age", period.this_year) * MONTHS_IN_YEAR + is_in_school = person("is_in_k12_school", period.this_year) + school_age_by_enrollment = ( + age_months >= p.school_age_min_months + ) & is_in_school + return select( + [ + age_months < p.infant_max_months, + age_months < p.toddler_max_months, + (age_months < p.preschool_max_months) & ~school_age_by_enrollment, + ], + [ + SDCCAAgeGroup.INFANT, + SDCCAAgeGroup.TODDLER, + SDCCAAgeGroup.PRESCHOOL, + ], + default=SDCCAAgeGroup.SCHOOL_AGE, + ) diff --git a/policyengine_us/variables/gov/states/sd/dss/cca/rates/sd_cca_provider_type.py b/policyengine_us/variables/gov/states/sd/dss/cca/rates/sd_cca_provider_type.py new file mode 100644 index 00000000000..c027dc41a04 --- /dev/null +++ b/policyengine_us/variables/gov/states/sd/dss/cca/rates/sd_cca_provider_type.py @@ -0,0 +1,18 @@ +from policyengine_us.model_api import * + + +class SDCCAProviderType(Enum): + FAMILY_DAY_CARE = "Family day care" + LICENSED_CENTER = "Licensed center or school age program" + INFORMAL = "Informal, in-home, or relative" + + +class sd_cca_provider_type(Variable): + value_type = Enum + entity = Person + possible_values = SDCCAProviderType + default_value = SDCCAProviderType.LICENSED_CENTER + definition_period = MONTH + defined_for = StateCode.SD + label = "South Dakota CCA child care provider type" + reference = "https://dss.sd.gov/docs/childcare/assistance/CCA_Weekly_Reimbursement_Rates.pdf" diff --git a/policyengine_us/variables/gov/states/sd/dss/cca/rates/sd_cca_region.py b/policyengine_us/variables/gov/states/sd/dss/cca/rates/sd_cca_region.py new file mode 100644 index 00000000000..e4e15f00f6f --- /dev/null +++ b/policyengine_us/variables/gov/states/sd/dss/cca/rates/sd_cca_region.py @@ -0,0 +1,34 @@ +from policyengine_us.model_api import * + + +class SDCCARegion(Enum): + URBAN = "Urban" + SUBURBAN = "Suburban" + RURAL = "Rural" + + +class sd_cca_region(Variable): + value_type = Enum + entity = Household + possible_values = SDCCARegion + # The 46 counties that are neither urban nor suburban (and any non-South + # Dakota county) fall through to the rural region. This default is + # load-bearing because defined_for does not short-circuit vectorized + # formula execution. + default_value = SDCCARegion.RURAL + definition_period = MONTH + defined_for = StateCode.SD + label = "South Dakota CCA provider rate region" + reference = "https://dss.sd.gov/docs/childcare/assistance/Provider_Rate_Regions.pdf" + + def formula(household, period, parameters): + county = household("county_str", period) + p = parameters(period).gov.states.sd.dss.cca.rates + return select( + [ + np.isin(county, p.urban_counties), + np.isin(county, p.suburban_counties), + ], + [SDCCARegion.URBAN, SDCCARegion.SUBURBAN], + default=SDCCARegion.RURAL, + ) diff --git a/policyengine_us/variables/gov/states/sd/dss/cca/rates/sd_cca_time_category.py b/policyengine_us/variables/gov/states/sd/dss/cca/rates/sd_cca_time_category.py new file mode 100644 index 00000000000..1b5a7f9858e --- /dev/null +++ b/policyengine_us/variables/gov/states/sd/dss/cca/rates/sd_cca_time_category.py @@ -0,0 +1,32 @@ +from policyengine_us.model_api import * + + +class SDCCATimeCategory(Enum): + FULL_TIME = "Full time" + PART_TIME = "Part time" + LIMITED_TIME = "Limited time" + + +class sd_cca_time_category(Variable): + value_type = Enum + entity = Person + possible_values = SDCCATimeCategory + default_value = SDCCATimeCategory.LIMITED_TIME + definition_period = MONTH + defined_for = StateCode.SD + label = "South Dakota CCA level of service" + reference = "https://dss.sd.gov/docs/childcare/assistance/CCA_Weekly_Reimbursement_Rates.pdf" + + def formula(person, period, parameters): + p = parameters(period).gov.states.sd.dss.cca.time_category + # Full-time care is 28 or more hours per week, part-time is 16 through + # 27 hours, and limited-time is 1 through 15 hours. + hours = person("childcare_hours_per_week", period.this_year) + return select( + [ + hours >= p.full_time_min_hours, + hours >= p.part_time_min_hours, + ], + [SDCCATimeCategory.FULL_TIME, SDCCATimeCategory.PART_TIME], + default=SDCCATimeCategory.LIMITED_TIME, + ) diff --git a/policyengine_us/variables/gov/states/sd/dss/cca/rates/sd_cca_weekly_rate.py b/policyengine_us/variables/gov/states/sd/dss/cca/rates/sd_cca_weekly_rate.py new file mode 100644 index 00000000000..f1abc794511 --- /dev/null +++ b/policyengine_us/variables/gov/states/sd/dss/cca/rates/sd_cca_weekly_rate.py @@ -0,0 +1,46 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.sd.dss.cca.rates.sd_cca_provider_type import ( + SDCCAProviderType, +) + + +class sd_cca_weekly_rate(Variable): + value_type = float + entity = Person + unit = USD + label = "South Dakota CCA weekly maximum reimbursement rate per child" + definition_period = MONTH + defined_for = "sd_cca_eligible_child" + reference = "https://dss.sd.gov/docs/childcare/assistance/CCA_Weekly_Reimbursement_Rates.pdf" + + def formula(person, period, parameters): + p = parameters(period).gov.states.sd.dss.cca.rates + region = person.household("sd_cca_region", period) + age_group = person("sd_cca_age_group", period) + time_category = person("sd_cca_time_category", period) + provider_type = person("sd_cca_provider_type", period) + family_day_care_rate = p.weekly.family_day_care[region][age_group][ + time_category + ] + licensed_center_rate = p.weekly.licensed_center[region][age_group][ + time_category + ] + # Informal, in-home, and relative rates are flat statewide (no region or + # age variation), so they are keyed only by level of service. + informal_rate = p.weekly.informal[time_category] + base_rate = select( + [ + provider_type == SDCCAProviderType.FAMILY_DAY_CARE, + provider_type == SDCCAProviderType.LICENSED_CENTER, + provider_type == SDCCAProviderType.INFORMAL, + ], + [family_day_care_rate, licensed_center_rate, informal_rate], + default=licensed_center_rate, + ) + # A provider caring for a child with special needs may receive the base + # rate plus an additional 75% of that rate. is_disabled proxies the + # special-needs determination. The multiplier raises the rate ceiling, + # so it folds inside the expense cap applied in sd_cca. + is_disabled = person("is_disabled", period.this_year) + special_needs_multiplier = where(is_disabled, p.special_needs_multiplier, 1) + return base_rate * special_needs_multiplier diff --git a/policyengine_us/variables/gov/states/sd/dss/cca/sd_cca.py b/policyengine_us/variables/gov/states/sd/dss/cca/sd_cca.py new file mode 100644 index 00000000000..c1569a1d639 --- /dev/null +++ b/policyengine_us/variables/gov/states/sd/dss/cca/sd_cca.py @@ -0,0 +1,38 @@ +from policyengine_us.model_api import * + + +class sd_cca(Variable): + value_type = float + entity = SPMUnit + unit = USD + label = "South Dakota Child Care Assistance benefit amount" + definition_period = MONTH + defined_for = "sd_cca_eligible" + reference = ( + "https://dss.sd.gov/docs/childcare/assistance/Subsidy_Manual.pdf#page=21" + ) + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.sd.dss.cca.rates + # Only the weekly region-based rate schedule (effective 2026-08-03) is + # modeled; the earlier hourly county-based schedule is deferred to a + # follow-up implementation, so no benefit is paid before that date. + if not p.uses_weekly_rates: + return 0 + # The benefit is the lesser of the summed per-child weekly maximum rates + # (converted to a monthly amount) and the family's billed child care + # expenses, minus the monthly co-payment, floored at zero. The + # special-needs multiplier is already folded into sd_cca_weekly_rate, so + # it sits inside the expense cap. Billed expenses are a single SPM-unit + # input, so the per-child maximum rates are pooled across the unit. + person = spm_unit.members + is_eligible_child = person("sd_cca_eligible_child", period) + weekly_rate = person("sd_cca_weekly_rate", period) + weekly_to_monthly = WEEKS_IN_YEAR / MONTHS_IN_YEAR + max_reimbursement = ( + spm_unit.sum(weekly_rate * is_eligible_child) * weekly_to_monthly + ) + actual_expenses = spm_unit("spm_unit_pre_subsidy_childcare_expenses", period) + capped_expenses = min_(actual_expenses, max_reimbursement) + copay = spm_unit("sd_cca_copay", period) + return max_(capped_expenses - copay, 0) diff --git a/policyengine_us/variables/gov/states/sd/dss/cca/sd_child_care_subsidies.py b/policyengine_us/variables/gov/states/sd/dss/cca/sd_child_care_subsidies.py new file mode 100644 index 00000000000..ab2f24995b0 --- /dev/null +++ b/policyengine_us/variables/gov/states/sd/dss/cca/sd_child_care_subsidies.py @@ -0,0 +1,11 @@ +from policyengine_us.model_api import * + + +class sd_child_care_subsidies(Variable): + value_type = float + entity = SPMUnit + label = "South Dakota child care subsidies" + unit = USD + definition_period = YEAR + defined_for = StateCode.SD + adds = ["sd_cca"] From b5afb4c14f5a5a3804b1d75be7c496e641a1012f Mon Sep 17 00:00:00 2001 From: Ziming Date: Fri, 10 Jul 2026 18:57:52 -0400 Subject: [PATCH 3/7] Add changelog fragment and register SD CCA in programs.yaml Co-Authored-By: Claude Fable 5 --- changelog.d/sd-ccap.added.md | 1 + policyengine_us/programs.yaml | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 changelog.d/sd-ccap.added.md diff --git a/changelog.d/sd-ccap.added.md b/changelog.d/sd-ccap.added.md new file mode 100644 index 00000000000..781e2238d6d --- /dev/null +++ b/changelog.d/sd-ccap.added.md @@ -0,0 +1 @@ +South Dakota Child Care Assistance (CCA) program. diff --git a/policyengine_us/programs.yaml b/policyengine_us/programs.yaml index a5114d95d5e..5afa7c9b9df 100644 --- a/policyengine_us/programs.yaml +++ b/policyengine_us/programs.yaml @@ -460,7 +460,7 @@ programs: category: Benefits agency: HHS status: partial - coverage: AK, AR, AL, AZ, CA, CO, CT, DE, DC, FL, GA, HI, IA, ID, IL, IN, KS, KY, LA, MA, MD, ME, MI, MN, MO, MS, MT, NC, ND, NE, NH, NJ, NM, NV, PA, RI, SC, TX, VA, VT, WA, WV + coverage: AK, AR, AL, AZ, CA, CO, CT, DE, DC, FL, GA, HI, IA, ID, IL, IN, KS, KY, LA, MA, MD, ME, MI, MN, MO, MS, MT, NC, ND, NE, NH, NJ, NM, NV, PA, RI, SC, SD, TX, VA, VT, WA, WV state_implementations: - state: AK status: complete @@ -702,6 +702,12 @@ programs: full_name: New Mexico Child Care Assistance Program variable: nm_ccap parameter_prefix: gov.states.nm.ececd.ccap + - state: SD + status: in_progress + name: South Dakota CCA + full_name: South Dakota Child Care Assistance + variable: sd_cca + parameter_prefix: gov.states.sd.dss.cca - id: head_start name: Head Start From 03aa19b9d4d7eb9686efadb3d9327e0d27eaca58 Mon Sep 17 00:00:00 2001 From: Ziming Date: Fri, 10 Jul 2026 22:55:55 -0400 Subject: [PATCH 4/7] =?UTF-8?q?Review-fix=20round=201:=20floor=20self-empl?= =?UTF-8?q?oyment=20losses=20at=20zero=20per=20person=20(Manual=20=C2=A76)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- .../gov/states/sd/dss/cca/income/sources.yaml | 4 +++- .../cca/income/sd_cca_countable_income.yaml | 10 ++++---- .../dss/cca/income/sd_cca_countable_income.py | 7 +++++- .../income/sd_cca_self_employment_income.py | 23 +++++++++++++++++++ 4 files changed, 38 insertions(+), 6 deletions(-) create mode 100644 policyengine_us/variables/gov/states/sd/dss/cca/income/sd_cca_self_employment_income.py diff --git a/policyengine_us/parameters/gov/states/sd/dss/cca/income/sources.yaml b/policyengine_us/parameters/gov/states/sd/dss/cca/income/sources.yaml index 9a949126aab..00080580a51 100644 --- a/policyengine_us/parameters/gov/states/sd/dss/cca/income/sources.yaml +++ b/policyengine_us/parameters/gov/states/sd/dss/cca/income/sources.yaml @@ -4,7 +4,9 @@ values: # Earned income (wages, salary, commissions, tips, bonuses, fees, and # self-employment net of allowable business expenses). - employment_income - - self_employment_income + # Section 6: a self-employment loss is counted as $0 and may not offset + # other income, so we sum the per-person floored figure. + - sd_cca_self_employment_income # Unearned income (Subsidy Manual, Section 4). - rental_income # "lease income" - social_security # "Social Security (SSA)"; already excludes SSI diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/income/sd_cca_countable_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/income/sd_cca_countable_income.yaml index 9cb545905dc..21e87bb26d1 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/income/sd_cca_countable_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/income/sd_cca_countable_income.yaml @@ -178,7 +178,7 @@ output: sd_cca_countable_income: 0 -- name: Case 9, self-employment loss offsets unearned income, floored at zero. +- name: Case 9, self-employment loss does not offset unearned income. period: 2027-01 absolute_error_margin: 0.1 input: @@ -197,9 +197,11 @@ members: [person1, person2] state_code: SD output: - # gross = -3,000 (loss) + 2,000 (Social Security) = -1,000; floored at 0. - # The 4% disregard applies to max(earned, 0) = 0. - sd_cca_countable_income: 0 + # Per Subsidy Manual Section 6, a self-employment loss is counted as $0 per + # person and may not offset other income, so it does not reduce the 2,000 + # Social Security. gross = 0 (loss floored) + 2,000 = 2,000. Earned income + # (the floored self-employment) is 0, so the 4% disregard is 0. + sd_cca_countable_income: 2_000 - name: Case 10, child support paid exceeds income, floored at zero. period: 2027-01 diff --git a/policyengine_us/variables/gov/states/sd/dss/cca/income/sd_cca_countable_income.py b/policyengine_us/variables/gov/states/sd/dss/cca/income/sd_cca_countable_income.py index 5919dce1c4e..3d0c5ec08ae 100644 --- a/policyengine_us/variables/gov/states/sd/dss/cca/income/sd_cca_countable_income.py +++ b/policyengine_us/variables/gov/states/sd/dss/cca/income/sd_cca_countable_income.py @@ -16,8 +16,13 @@ def formula(spm_unit, period, parameters): p = parameters(period).gov.states.sd.dss.cca.income gross_income = spm_unit("sd_cca_gross_income", period) person = spm_unit.members + # Self-employment income is floored at zero per person (Section 6), so + # the child exclusion and 4% disregard operate on the same figure that + # entered gross income. person_earned = add( - person, period, ["employment_income", "self_employment_income"] + person, + period, + ["employment_income", "sd_cca_self_employment_income"], ) is_child = person("age", period.this_year) < p.child_earned_income_exclusion_age # The earned income of children is excluded entirely. diff --git a/policyengine_us/variables/gov/states/sd/dss/cca/income/sd_cca_self_employment_income.py b/policyengine_us/variables/gov/states/sd/dss/cca/income/sd_cca_self_employment_income.py new file mode 100644 index 00000000000..57e52268fa7 --- /dev/null +++ b/policyengine_us/variables/gov/states/sd/dss/cca/income/sd_cca_self_employment_income.py @@ -0,0 +1,23 @@ +from policyengine_us.model_api import * + + +class sd_cca_self_employment_income(Variable): + value_type = float + entity = Person + unit = USD + label = "South Dakota CCA countable self-employment income" + definition_period = YEAR + defined_for = StateCode.SD + reference = ( + "https://dss.sd.gov/docs/childcare/assistance/Subsidy_Manual.pdf#page=14" + ) + + def formula(person, period, parameters): + # Subsidy Manual, Section 6: "Self-employment showing a loss shall be + # counted as $0.00. A loss may not be deducted from other forms of + # income the household may have." We floor self-employment income at + # zero per person so a loss cannot offset other income. PolicyEngine + # carries a single aggregate self-employment figure per person, so the + # Manual's per-business separation (a loss from one business will not + # offset another) is not modeled. + return max_(person("self_employment_income", period), 0) From 7edd3d10d7a30e935963be9eb56e96cfd03d8545 Mon Sep 17 00:00:00 2001 From: Ziming Date: Mon, 13 Jul 2026 15:27:32 -0400 Subject: [PATCH 5/7] Review-fix round 2: work thresholds, per-child reason for care, copay waiver, care-hours mask - Enforce the activity work thresholds: employed caretakers need 80+ hours/month at minimum-wage-equivalent pay (Manual Sections 3, 5); self-employed need 20+ hours/week earning at least 20x the federal minimum wage, with losses counting as zero (Manual Section 6) - Add sd_cca_reason_for_care_eligible (Person), mirroring the federal is_ccdf_reason_for_care_eligible pattern: protective services is an independent per-child reason for care (ARSD 67:47:01:03), embedded in sd_cca_eligible_child; sd_cca_eligible drops the unit-level activity conjunct - Waive the co-payment for protective-services children (Sliding Fee Scale), not just TANF and foster cases - Exclude children with zero care hours from the reimbursement cap (the limited-time rate tier starts at one hour per week) Co-Authored-By: Claude Fable 5 --- .../cca/work_requirement/monthly_hours.yaml | 18 +++ .../self_employed_weekly_hours.yaml | 17 +++ .../states/sd/dss/cca/copay/sd_cca_copay.yaml | 25 ++++ .../eligibility/sd_cca_activity_eligible.yaml | 127 ++++++++++++++++-- .../dss/cca/eligibility/sd_cca_eligible.yaml | 33 ++++- .../eligibility/sd_cca_eligible_child.yaml | 105 ++++++++++++++- .../sd_cca_reason_for_care_eligible.yaml | 71 ++++++++++ .../gov/states/sd/dss/cca/sd_cca.yaml | 67 ++++++++- .../states/sd/dss/cca/copay/sd_cca_copay.py | 12 +- .../eligibility/sd_cca_activity_eligible.py | 5 +- .../sd/dss/cca/eligibility/sd_cca_eligible.py | 5 +- .../cca/eligibility/sd_cca_eligible_child.py | 11 +- .../sd_cca_parent_in_eligible_activity.py | 43 ++++-- .../sd_cca_reason_for_care_eligible.py | 24 ++++ .../variables/gov/states/sd/dss/cca/sd_cca.py | 5 +- 15 files changed, 530 insertions(+), 38 deletions(-) create mode 100644 policyengine_us/parameters/gov/states/sd/dss/cca/work_requirement/monthly_hours.yaml create mode 100644 policyengine_us/parameters/gov/states/sd/dss/cca/work_requirement/self_employed_weekly_hours.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_reason_for_care_eligible.yaml create mode 100644 policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_reason_for_care_eligible.py diff --git a/policyengine_us/parameters/gov/states/sd/dss/cca/work_requirement/monthly_hours.yaml b/policyengine_us/parameters/gov/states/sd/dss/cca/work_requirement/monthly_hours.yaml new file mode 100644 index 00000000000..eb0e87db829 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/cca/work_requirement/monthly_hours.yaml @@ -0,0 +1,18 @@ +description: South Dakota requires each employed caretaker to work at least this many hours per month under the Child Care Assistance program. + +# The applicant must work at least 80 hours per month at a salary equivalent +# to the federal minimum wage; in two-parent households each parent must meet +# the requirement (Subsidy Manual, Section 3). Dated to the modeled program +# start (weekly rate era, 2026-08-03). +values: + 2026-08-03: 80 + +metadata: + unit: hour + period: month + label: South Dakota CCA employed caretaker monthly work hours requirement + reference: + - title: South Dakota Child Care Assistance Subsidy Manual, Section 3 Eligibility Criteria + href: https://dss.sd.gov/docs/childcare/assistance/Subsidy_Manual.pdf#page=8 + - title: ARSD 67:47:01:03.01 Time requirements for work and education + href: https://sdlegislature.gov/Rules/Administrative/67:47:01:03.01 diff --git a/policyengine_us/parameters/gov/states/sd/dss/cca/work_requirement/self_employed_weekly_hours.yaml b/policyengine_us/parameters/gov/states/sd/dss/cca/work_requirement/self_employed_weekly_hours.yaml new file mode 100644 index 00000000000..8a1b4cc2a9f --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/cca/work_requirement/self_employed_weekly_hours.yaml @@ -0,0 +1,17 @@ +description: South Dakota requires each self-employed caretaker to work at least this many hours per week under the Child Care Assistance program. + +# Self-employed persons must work at least 20 hours a week and receive weekly +# earnings equal to the federal minimum wage multiplied by 20 hours (Subsidy +# Manual, Section 6). This threshold appears only in the Subsidy Manual, not +# in ARSD 67:47:01. Dated to the modeled program start (weekly rate era, +# 2026-08-03). +values: + 2026-08-03: 20 + +metadata: + unit: hour + period: week + label: South Dakota CCA self-employed caretaker weekly work hours requirement + reference: + - title: South Dakota Child Care Assistance Subsidy Manual, Section 6 Calculating Income from Self-employment + href: https://dss.sd.gov/docs/childcare/assistance/Subsidy_Manual.pdf#page=13 diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/copay/sd_cca_copay.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/copay/sd_cca_copay.yaml index c81dc8201c9..70123e332c5 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/copay/sd_cca_copay.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/copay/sd_cca_copay.yaml @@ -174,3 +174,28 @@ # never binds: with a 5% marginal rate the copay share of income cannot # reach 12%. sd_cca_copay: 230 + +- name: Case 8, protective-services child in the unit, co-payment waived. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income: 47_500 # would otherwise owe a 20 co-payment + person2: + age: 1 + receives_or_needs_protective_services: true + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_fpg: 24_000 + meets_ccdf_activity_test: true + households: + household: + members: [person1, person2] + state_code: SD + output: + # The Sliding Fee Scale waives the co-payment for children determined to + # be in need of protective services. + sd_cca_copay: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_activity_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_activity_eligible.yaml index fb1aadd913b..a974219b3f9 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_activity_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_activity_eligible.yaml @@ -1,9 +1,15 @@ # Tests for sd_cca_activity_eligible (SPMUnit, MONTH, bool). -# The work/activity requirement (each caretaker employed 80+ hrs/month, or in -# school/training; ARSD 67:47:01:03.01, Subsidy Manual Sections 5-6) is not -# tracked at the hour level, so eligibility falls back to the federal CCDF -# activity-test input meets_ccdf_activity_test (a YEAR variable read with -# period.this_year). +# Each caretaker must be in an approved activity (ARSD 67:47:01:03, :03.01; +# Subsidy Manual Sections 3, 5-6): employed at least 80 hours per month at a +# salary equivalent to the federal minimum wage ($7.25/hr), or self-employed +# at least 20 hours per week with weekly earnings of at least +# 20 x $7.25 = $145 (a loss counts as zero), or a full-time student, or +# enrolled in TANF. Activities not modeled (job search, other training) fall +# back to the federal CCDF activity-test input meets_ccdf_activity_test. +# Protective services is handled per child in sd_cca_reason_for_care_eligible, +# not here. +# weekly_hours_worked defaults to 40/week = 173.33/month, so the default +# minimum-wage-equivalence bar is 173.33 x 7.25 = 1,256.67/month. - name: Case 1, activity test met, activity eligible. period: 2027-01 @@ -52,7 +58,8 @@ people: person1: age: 30 - employment_income: 24_000 # employed caretaker + employment_income: 24_000 # 2,000/mo + weekly_hours_worked: 40 # 173.33 hrs/mo, above the 80-hour minimum person2: age: 4 spm_units: @@ -64,17 +71,18 @@ members: [person1, person2] state_code: SD output: - # The sole caretaker is employed, so every caretaker is in an approved - # activity even though the CCDF activity-test input is false. + # 173.33 hrs/mo >= 80 and 2,000 >= 173.33 x 7.25 = 1,256.67, so the sole + # caretaker meets the employment requirement even though the CCDF + # activity-test input is false. sd_cca_activity_eligible: true -- name: Case 4, single parent with a self-employment loss, activity eligible. +- name: Case 4, single parent with a self-employment loss, ineligible. period: 2027-01 input: people: person1: age: 30 - self_employment_income: -12_000 # a business loss still evidences activity + self_employment_income: -12_000 # -1,000/mo person2: age: 4 spm_units: @@ -86,7 +94,10 @@ members: [person1, person2] state_code: SD output: - sd_cca_activity_eligible: true + # A loss counts as zero self-employment earnings (Manual Section 6), which + # falls below the gainful-employment floor of 20 x 7.25 = 145/week, so + # the caretaker is not in an approved activity. + sd_cca_activity_eligible: false - name: Case 5, single full-time student parent, activity eligible. period: 2027-01 @@ -168,3 +179,97 @@ # Every caretaker must be in an approved activity; the available (non-active) # spouse makes the unit ineligible under the modeled path. sd_cca_activity_eligible: false + +- name: Case 8, employed parent below the 80-hour monthly minimum, ineligible. + period: 2027-01 + input: + people: + person1: + age: 30 + employment_income: 12_000 # 1,000/mo + weekly_hours_worked: 15 # 65 hrs/mo, below the 80-hour minimum + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + meets_ccdf_activity_test: false + households: + household: + members: [person1, person2] + state_code: SD + output: + # 15 hrs/wk x 52/12 = 65 hrs/mo < 80, so the employment requirement fails + # even though the implied wage (1,000 / 65 = 15.38/hr) is above the + # federal minimum. + sd_cca_activity_eligible: false + +- name: Case 9, employed parent below the minimum-wage equivalence, ineligible. + period: 2027-01 + input: + people: + person1: + age: 30 + employment_income: 6_000 # 500/mo + weekly_hours_worked: 40 # 173.33 hrs/mo + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + meets_ccdf_activity_test: false + households: + household: + members: [person1, person2] + state_code: SD + output: + # 500/mo over 173.33 hrs/mo implies 2.88/hr, below the 7.25 federal + # minimum wage (500 < 173.33 x 7.25 = 1,256.67), so the employment + # requirement fails (Manual Section 5, FLSA rule). + sd_cca_activity_eligible: false + +- name: Case 10, self-employed parent meeting both gainful-employment prongs, eligible. + period: 2027-01 + input: + people: + person1: + age: 30 + self_employment_income: 12_000 # 1,000/mo = 230.77/wk + weekly_hours_worked: 25 # above the 20-hour weekly minimum + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + meets_ccdf_activity_test: false + households: + household: + members: [person1, person2] + state_code: SD + output: + # 25 hrs/wk >= 20 and weekly earnings 230.77 >= 20 x 7.25 = 145, so the + # self-employment requirement is met. + sd_cca_activity_eligible: true + +- name: Case 11, protective-services child does not satisfy the caretaker activity test. + period: 2027-01 + input: + people: + person1: + age: 30 + weekly_hours_worked: 0 # not in any activity + person2: + age: 4 + receives_or_needs_protective_services: true + spm_units: + spm_unit: + members: [person1, person2] + meets_ccdf_activity_test: false + households: + household: + members: [person1, person2] + state_code: SD + output: + # This variable tests caretaker activity only; the protective-services + # pathway is applied per child in sd_cca_reason_for_care_eligible. + sd_cca_activity_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_eligible.yaml index 414e1bab133..a74ba8e1040 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_eligible.yaml @@ -1,8 +1,10 @@ # Tests for sd_cca_eligible (SPMUnit, MONTH, bool). # A unit is eligible when it has at least one eligible child AND is income -# eligible AND asset eligible AND activity eligible (ARSD 67:47:01:03, :05; -# Subsidy Manual Section 3). Each case below meets every condition except the -# one under test. +# eligible AND asset eligible (ARSD 67:47:01:03, :05; Subsidy Manual +# Section 3). The activity requirement is embedded per child: an eligible +# child must have a reason for care (caretaker activity or protective +# services) via sd_cca_reason_for_care_eligible. Each case below meets every +# condition except the one under test. - name: Case 1, all conditions met, eligible. period: 2027-01 @@ -104,6 +106,8 @@ members: [person1, person2] state_code: SD output: + # With no caretaker activity and no protective services, the child has no + # reason for care, so the unit has no eligible child. sd_cca_eligible: false - name: Case 6, zero-income family with an eligible child, eligible. @@ -169,3 +173,26 @@ # No eligible child in the unit, so the family is ineligible regardless of # income and activity. sd_cca_eligible: false + +- name: Case 9, protective-services child with an inactive caretaker, eligible. + period: 2027-01 + input: + people: + person1: + age: 30 + weekly_hours_worked: 0 # not in any activity + person2: + age: 1 + receives_or_needs_protective_services: true + spm_units: + spm_unit: + members: [person1, person2] + meets_ccdf_activity_test: false + households: + household: + members: [person1, person2] + state_code: SD + output: + # Protective services gives the child a reason for care + # (ARSD 67:47:01:03), so the unit is eligible without caretaker activity. + sd_cca_eligible: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_eligible_child.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_eligible_child.yaml index 60a935ec920..f20ac5c4e4e 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_eligible_child.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_eligible_child.yaml @@ -1,11 +1,15 @@ # Tests for sd_cca_eligible_child (Person, MONTH, bool). # A child is eligible if under the applicable age limit AND a US citizen or -# lawful permanent resident (ARSD 67:47:01:02, :03; Subsidy Manual Section 3): +# lawful permanent resident AND has a reason for care (every caretaker in an +# approved activity, or the child receives protective services) +# (ARSD 67:47:01:02, :03; Subsidy Manual Section 3): # general limit: under 13 # special needs (disabled), not in school: under 18 # special needs (disabled), enrolled in school: under 19 # is_in_k12_school imputes true for ages 5-17, so the disabled cases set it -# explicitly to isolate the school vs non-school age limit. +# explicitly to isolate the school vs non-school age limit. Each case sets +# meets_ccdf_activity_test true so the age and immigration dimensions are +# isolated; Cases 14-15 exercise the reason-for-care dimension. - name: Case 1, citizen infant, eligible. period: 2027-01 @@ -13,6 +17,10 @@ people: person1: age: 1 + spm_units: + spm_unit: + members: [person1] + meets_ccdf_activity_test: true households: household: members: [person1] @@ -27,6 +35,10 @@ person1: age: 12 is_in_k12_school: true + spm_units: + spm_unit: + members: [person1] + meets_ccdf_activity_test: true households: household: members: [person1] @@ -41,6 +53,10 @@ person1: age: 13 is_in_k12_school: true + spm_units: + spm_unit: + members: [person1] + meets_ccdf_activity_test: true households: household: members: [person1] @@ -55,6 +71,10 @@ person1: age: 14 is_in_k12_school: true + spm_units: + spm_unit: + members: [person1] + meets_ccdf_activity_test: true households: household: members: [person1] @@ -70,6 +90,10 @@ age: 17 is_disabled: true is_in_k12_school: false + spm_units: + spm_unit: + members: [person1] + meets_ccdf_activity_test: true households: household: members: [person1] @@ -85,6 +109,10 @@ age: 18 is_disabled: true is_in_k12_school: false + spm_units: + spm_unit: + members: [person1] + meets_ccdf_activity_test: true households: household: members: [person1] @@ -100,6 +128,10 @@ age: 18 is_disabled: true is_in_k12_school: true + spm_units: + spm_unit: + members: [person1] + meets_ccdf_activity_test: true households: household: members: [person1] @@ -115,6 +147,10 @@ age: 19 is_disabled: true is_in_k12_school: true + spm_units: + spm_unit: + members: [person1] + meets_ccdf_activity_test: true households: household: members: [person1] @@ -129,6 +165,10 @@ person1: age: 1 immigration_status: LEGAL_PERMANENT_RESIDENT + spm_units: + spm_unit: + members: [person1] + meets_ccdf_activity_test: true households: household: members: [person1] @@ -143,6 +183,10 @@ person1: age: 1 immigration_status: UNDOCUMENTED + spm_units: + spm_unit: + members: [person1] + meets_ccdf_activity_test: true households: household: members: [person1] @@ -157,6 +201,10 @@ person1: age: 1 immigration_status: REFUGEE + spm_units: + spm_unit: + members: [person1] + meets_ccdf_activity_test: true households: household: members: [person1] @@ -170,6 +218,10 @@ people: person1: age: 0 + spm_units: + spm_unit: + members: [person1] + meets_ccdf_activity_test: true households: household: members: [person1] @@ -185,6 +237,10 @@ age: 13 is_disabled: true is_in_k12_school: false + spm_units: + spm_unit: + members: [person1] + meets_ccdf_activity_test: true households: household: members: [person1] @@ -193,3 +249,48 @@ # Age 13 exceeds the general under-13 limit, but the special-needs limit # (under 18, not in school) keeps the child eligible (contrast Case 3). sd_cca_eligible_child: true + +- name: Case 14, protective-services infant with an inactive caretaker, eligible. + period: 2027-01 + input: + people: + person1: + age: 30 + weekly_hours_worked: 0 # not in any activity + person2: + age: 1 + receives_or_needs_protective_services: true + spm_units: + spm_unit: + members: [person1, person2] + meets_ccdf_activity_test: false + households: + household: + members: [person1, person2] + state_code: SD + output: + # Protective services is an independent reason for care + # (ARSD 67:47:01:03), so the child is eligible without caretaker activity. + sd_cca_eligible_child: [false, true] + +- name: Case 15, infant with an inactive caretaker and no protective services, ineligible. + period: 2027-01 + input: + people: + person1: + age: 30 + weekly_hours_worked: 0 # not in any activity + person2: + age: 1 + spm_units: + spm_unit: + members: [person1, person2] + meets_ccdf_activity_test: false + households: + household: + members: [person1, person2] + state_code: SD + output: + # Age and immigration eligible, but no caretaker activity and no + # protective services: the child has no reason for care. + sd_cca_eligible_child: [false, false] diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_reason_for_care_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_reason_for_care_eligible.yaml new file mode 100644 index 00000000000..c6a908d54d1 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_reason_for_care_eligible.yaml @@ -0,0 +1,71 @@ +# Tests for sd_cca_reason_for_care_eligible (Person, MONTH, bool). +# A child has a reason for care when every caretaker is in an approved +# activity (sd_cca_activity_eligible) or when the child receives or needs +# protective services (ARSD 67:47:01:03). This mirrors the federal +# is_ccdf_reason_for_care_eligible pattern. + +- name: Case 1, caretaker activity met, every member has a reason for care. + period: 2027-01 + input: + people: + person1: + age: 30 + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + meets_ccdf_activity_test: true + households: + household: + members: [person1, person2] + state_code: SD + output: + # The unit-level activity result projects to every member. + sd_cca_reason_for_care_eligible: [true, true] + +- name: Case 2, inactive caretaker, only the protective-services child qualifies. + period: 2027-01 + input: + people: + person1: + age: 30 + weekly_hours_worked: 0 # not in any activity + person2: + age: 4 + receives_or_needs_protective_services: true + person3: + age: 2 # sibling without protective services + spm_units: + spm_unit: + members: [person1, person2, person3] + meets_ccdf_activity_test: false + households: + household: + members: [person1, person2, person3] + state_code: SD + output: + # Protective services is an independent, per-child reason for care: the + # non-protective sibling does not qualify through the protected child. + sd_cca_reason_for_care_eligible: [false, true, false] + +- name: Case 3, employed caretaker via the modeled activity path, child qualifies. + period: 2027-01 + input: + people: + person1: + age: 30 + employment_income: 24_000 # 2,000/mo + weekly_hours_worked: 40 # 173.33 hrs/mo, above the 80-hour minimum + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + meets_ccdf_activity_test: false + households: + household: + members: [person1, person2] + state_code: SD + output: + sd_cca_reason_for_care_eligible: [true, true] diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/sd_cca.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/sd_cca.yaml index 9d4858e1736..2ab5093cc60 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/sd_cca.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/sd_cca.yaml @@ -2,7 +2,9 @@ # Assistance benefit. The benefit is the lesser of the summed per-child weekly # maximum rates (converted to a monthly amount with WEEKS_IN_YEAR / # MONTHS_IN_YEAR = 52/12 = 4.3333) and the family's billed child care expenses, -# minus the monthly co-payment, floored at zero (ARSD 67:47:01:16). There are +# minus the monthly co-payment, floored at zero (ARSD 67:47:01:16). Only +# eligible children with positive care hours contribute to the rate cap (the +# limited-time rate tier starts at one hour per week). There are # no separate additive bonuses. sd_child_care_subsidies is the YEAR aggregator # (adds sd_cca); read at a month period it reports the monthly value, and # sd_cca is registered in the federal child_care_subsidies total. @@ -205,3 +207,66 @@ # rural licensed center full-time school age 150/wk x 52/12 = 650 monthly # cap; expenses 100 bind, capped at 100; benefit = max(100 - 230, 0) = 0. sd_cca: 0 + +- name: Case 8, child with no care hours, no reimbursement despite billed expenses. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income: 24_000 # copay 0 + person2: + age: 0.5 # infant + sd_cca_provider_type: FAMILY_DAY_CARE + childcare_hours_per_week: 0 # not in care + spm_units: + spm_unit: + members: [person1, person2] + meets_ccdf_activity_test: true + spm_unit_pre_subsidy_childcare_expenses: 24_000 # 2,000/mo billed + households: + household: + members: [person1, person2] + state_code: SD + county_str: MINNEHAHA_COUNTY_SD + output: + # The limited-time rate tier starts at one hour of care per week, so a + # child with zero care hours is excluded from the reimbursement cap: + # capped expenses = min(2,000, 0) = 0 and no benefit is paid. + sd_cca: 0 + +- name: Case 9, protective-services child covered, non-protective sibling excluded. + period: 2027-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + weekly_hours_worked: 0 # not in any activity + person2: + age: 0.5 # infant receiving protective services + receives_or_needs_protective_services: true + sd_cca_provider_type: FAMILY_DAY_CARE + childcare_hours_per_week: 40 + person3: + age: 0.8 # infant sibling without protective services + sd_cca_provider_type: FAMILY_DAY_CARE + childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1, person2, person3] + meets_ccdf_activity_test: false + spm_unit_pre_subsidy_childcare_expenses: 36_000 # 3,000/mo billed + households: + household: + members: [person1, person2, person3] + state_code: SD + county_str: MINNEHAHA_COUNTY_SD # urban + output: + # With no caretaker activity, only the protective-services infant has a + # reason for care (ARSD 67:47:01:03): one urban family day care full-time + # infant rate 180/wk x 52/12 = 780/mo, not 1,560 for both children. + # Expenses 3,000 exceed the cap; the co-payment is waived for + # protective-services children. Benefit = 780. + sd_cca: 780 diff --git a/policyengine_us/variables/gov/states/sd/dss/cca/copay/sd_cca_copay.py b/policyengine_us/variables/gov/states/sd/dss/cca/copay/sd_cca_copay.py index f97aea704dd..0c5e041661a 100644 --- a/policyengine_us/variables/gov/states/sd/dss/cca/copay/sd_cca_copay.py +++ b/policyengine_us/variables/gov/states/sd/dss/cca/copay/sd_cca_copay.py @@ -31,8 +31,16 @@ def formula(spm_unit, period, parameters): copay = min_(copay, p.max_rate * countable_income) # The co-payment is waived for families enrolled in the Temporary # Assistance for Needy Families program and for foster or - # protective-services children. + # protective-services children (Sliding Fee Scale). is_tanf_enrolled = spm_unit("is_tanf_enrolled", period) has_foster_child = add(spm_unit, period, ["is_in_foster_care"]) > 0 - waived = is_tanf_enrolled | has_foster_child + has_protective_services_child = ( + add( + spm_unit, + period.this_year, + ["receives_or_needs_protective_services"], + ) + > 0 + ) + waived = is_tanf_enrolled | has_foster_child | has_protective_services_child return where(waived, 0, copay) diff --git a/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_activity_eligible.py b/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_activity_eligible.py index 0c6e1fe9de2..83fbedd4ea1 100644 --- a/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_activity_eligible.py +++ b/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_activity_eligible.py @@ -8,7 +8,8 @@ class sd_cca_activity_eligible(Variable): definition_period = MONTH defined_for = StateCode.SD reference = ( - "https://dss.sd.gov/docs/childcare/assistance/Subsidy_Manual.pdf#page=12" + "https://dss.sd.gov/docs/childcare/assistance/Subsidy_Manual.pdf#page=8", + "https://sdlegislature.gov/Rules/Administrative/67:47:01:03", ) def formula(spm_unit, period, parameters): @@ -24,5 +25,7 @@ def formula(spm_unit, period, parameters): 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). + # Protective services is handled per child in + # sd_cca_reason_for_care_eligible, not here. meets_ccdf = spm_unit("meets_ccdf_activity_test", period.this_year) return modeled_eligible | meets_ccdf diff --git a/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_eligible.py b/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_eligible.py index 7346eacbea0..22a78f5cf4c 100644 --- a/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_eligible.py +++ b/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_eligible.py @@ -10,10 +10,11 @@ class sd_cca_eligible(Variable): reference = "https://dss.sd.gov/docs/childcare/assistance/Subsidy_Manual.pdf#page=8" def formula(spm_unit, period, parameters): + # The activity requirement is embedded per child through + # sd_cca_reason_for_care_eligible inside sd_cca_eligible_child. has_eligible_child = add(spm_unit, period, ["sd_cca_eligible_child"]) > 0 income_eligible = spm_unit("sd_cca_income_eligible", period) # The $1,000,000 self-certified asset limit matches the federal CCDF # limit, so we reuse is_ccdf_asset_eligible. asset_eligible = spm_unit("is_ccdf_asset_eligible", period) - activity_eligible = spm_unit("sd_cca_activity_eligible", period) - return has_eligible_child & income_eligible & asset_eligible & activity_eligible + return has_eligible_child & income_eligible & asset_eligible diff --git a/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_eligible_child.py b/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_eligible_child.py index c87a6643109..635e664f1de 100644 --- a/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_eligible_child.py +++ b/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_eligible_child.py @@ -10,7 +10,10 @@ class sd_cca_eligible_child(Variable): label = "South Dakota CCA eligible child" definition_period = MONTH defined_for = StateCode.SD - reference = "https://dss.sd.gov/docs/childcare/assistance/Subsidy_Manual.pdf#page=9" + reference = ( + "https://dss.sd.gov/docs/childcare/assistance/Subsidy_Manual.pdf#page=9", + "https://sdlegislature.gov/Rules/Administrative/67:47:01:03", + ) def formula(person, period, parameters): p = parameters(period).gov.states.sd.dss.cca.eligibility @@ -35,4 +38,8 @@ def formula(person, period, parameters): immigration_eligible = (immigration_status == ImmigrationStatus.CITIZEN) | ( immigration_status == ImmigrationStatus.LEGAL_PERMANENT_RESIDENT ) - return age_eligible & immigration_eligible + # The child needs a reason for care: every caretaker in an approved + # activity, or the child receives protective services + # (ARSD 67:47:01:03). + reason_for_care_eligible = person("sd_cca_reason_for_care_eligible", period) + return age_eligible & immigration_eligible & reason_for_care_eligible diff --git a/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_parent_in_eligible_activity.py b/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_parent_in_eligible_activity.py index 9e441faa93c..3e2d654082b 100644 --- a/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_parent_in_eligible_activity.py +++ b/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_parent_in_eligible_activity.py @@ -7,21 +7,38 @@ class sd_cca_parent_in_eligible_activity(Variable): label = "South Dakota CCA parent in an eligible activity" definition_period = MONTH defined_for = StateCode.SD - reference = ( - "https://dss.sd.gov/docs/childcare/assistance/Subsidy_Manual.pdf#page=12" - ) + reference = "https://dss.sd.gov/docs/childcare/assistance/Subsidy_Manual.pdf#page=8" def formula(person, period, parameters): - # Approved activities are employment, self-employment, and school or - # training enrollment. A parent qualifies with positive wages, nonzero - # self-employment income (a business loss still evidences active - # self-employment), full-time student status, or enrollment in the - # Temporary Assistance for Needy Families program. The exact hour and - # credit thresholds are not modeled because activity hours are not - # tracked. - has_earnings = (person("employment_income", period) > 0) | ( - person("self_employment_income", period) != 0 + p = parameters(period).gov.states.sd.dss.cca.work_requirement + min_wage = parameters(period).gov.dol.minimum_wage + weekly_hours = person("weekly_hours_worked", period.this_year) + weeks_per_month = WEEKS_IN_YEAR / MONTHS_IN_YEAR + monthly_hours = weekly_hours * weeks_per_month + # An employed caretaker must work at least 80 hours per month at a + # salary equivalent to the federal minimum wage (Manual Sections 3 + # and 5). + employment_income = person("employment_income", period) + meets_employment_requirement = (monthly_hours >= p.monthly_hours) & ( + employment_income >= monthly_hours * min_wage ) + # A self-employed caretaker must work at least 20 hours per week and + # receive weekly earnings of at least the federal minimum wage times + # that hour minimum; a business loss counts as zero earnings (Manual + # Section 6). + weekly_self_employment_income = ( + person("self_employment_income", period) / weeks_per_month + ) + meets_self_employment_requirement = ( + weekly_hours >= p.self_employed_weekly_hours + ) & (weekly_self_employment_income >= p.self_employed_weekly_hours * min_wage) + # Full-time students meet the 12-semester-credit-hour minimum; the + # combined 80-hour work-plus-school path is not separately modeled. is_student = person("is_full_time_student", period.this_year) is_tanf_enrolled = person.spm_unit("is_tanf_enrolled", period) - return has_earnings | is_student | is_tanf_enrolled + return ( + meets_employment_requirement + | meets_self_employment_requirement + | is_student + | is_tanf_enrolled + ) diff --git a/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_reason_for_care_eligible.py b/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_reason_for_care_eligible.py new file mode 100644 index 00000000000..852d9c94bff --- /dev/null +++ b/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_reason_for_care_eligible.py @@ -0,0 +1,24 @@ +from policyengine_us.model_api import * + + +class sd_cca_reason_for_care_eligible(Variable): + value_type = bool + entity = Person + label = "South Dakota CCA reason-for-care eligible" + definition_period = MONTH + defined_for = StateCode.SD + reference = ( + "https://dss.sd.gov/docs/childcare/assistance/Subsidy_Manual.pdf#page=8", + "https://sdlegislature.gov/Rules/Administrative/67:47:01:03", + ) + + def formula(person, period, parameters): + # A child needs care when every caretaker is in an approved activity + # or when the child receives or needs protective services + # (ARSD 67:47:01:03), mirroring the federal + # is_ccdf_reason_for_care_eligible pattern. + parent_activity_eligible = person.spm_unit("sd_cca_activity_eligible", period) + protective_services = person( + "receives_or_needs_protective_services", period.this_year + ) + return parent_activity_eligible | protective_services diff --git a/policyengine_us/variables/gov/states/sd/dss/cca/sd_cca.py b/policyengine_us/variables/gov/states/sd/dss/cca/sd_cca.py index c1569a1d639..1867a7cd76e 100644 --- a/policyengine_us/variables/gov/states/sd/dss/cca/sd_cca.py +++ b/policyengine_us/variables/gov/states/sd/dss/cca/sd_cca.py @@ -27,10 +27,13 @@ def formula(spm_unit, period, parameters): # input, so the per-child maximum rates are pooled across the unit. person = spm_unit.members is_eligible_child = person("sd_cca_eligible_child", period) + # The rate schedule's limited-time tier starts at one hour of care per + # week, so a child with no care hours generates no reimbursement. + in_care = person("childcare_hours_per_week", period.this_year) > 0 weekly_rate = person("sd_cca_weekly_rate", period) weekly_to_monthly = WEEKS_IN_YEAR / MONTHS_IN_YEAR max_reimbursement = ( - spm_unit.sum(weekly_rate * is_eligible_child) * weekly_to_monthly + spm_unit.sum(weekly_rate * is_eligible_child * in_care) * weekly_to_monthly ) actual_expenses = spm_unit("spm_unit_pre_subsidy_childcare_expenses", period) capped_expenses = min_(actual_expenses, max_reimbursement) From 98aee82f422d5cfbe2c6a8816b00bacb2213c623 Mon Sep 17 00:00:00 2001 From: Ziming Date: Mon, 13 Jul 2026 17:39:42 -0400 Subject: [PATCH 6/7] minor --- .../cca/work_requirement/monthly_hours.yaml | 18 +++-- .../self_employed_weekly_hours.yaml | 17 ---- .../eligibility/sd_cca_activity_eligible.yaml | 70 +++++++++++++---- .../sd/dss/cca/rates/sd_cca_weekly_rate.yaml | 78 +++++++++++++++++-- .../sd_cca_parent_in_eligible_activity.py | 25 +++--- 5 files changed, 150 insertions(+), 58 deletions(-) delete mode 100644 policyengine_us/parameters/gov/states/sd/dss/cca/work_requirement/self_employed_weekly_hours.yaml diff --git a/policyengine_us/parameters/gov/states/sd/dss/cca/work_requirement/monthly_hours.yaml b/policyengine_us/parameters/gov/states/sd/dss/cca/work_requirement/monthly_hours.yaml index eb0e87db829..f06247fdea3 100644 --- a/policyengine_us/parameters/gov/states/sd/dss/cca/work_requirement/monthly_hours.yaml +++ b/policyengine_us/parameters/gov/states/sd/dss/cca/work_requirement/monthly_hours.yaml @@ -1,18 +1,20 @@ -description: South Dakota requires each employed caretaker to work at least this many hours per month under the Child Care Assistance program. +description: South Dakota requires each employed or self-employed caretaker to establish at least this many work hours per month under the Child Care Assistance program. -# The applicant must work at least 80 hours per month at a salary equivalent -# to the federal minimum wage; in two-parent households each parent must meet -# the requirement (Subsidy Manual, Section 3). Dated to the modeled program -# start (weekly rate era, 2026-08-03). +# The applicant must establish at least 80 work hours per month; employed and +# self-employed applicants must also have earnings equivalent to the federal +# minimum wage. Dated to the modeled program start (weekly rate era, +# 2026-08-03). values: 2026-08-03: 80 metadata: unit: hour period: month - label: South Dakota CCA employed caretaker monthly work hours requirement + label: South Dakota CCA caretaker monthly work hours requirement reference: - - title: South Dakota Child Care Assistance Subsidy Manual, Section 3 Eligibility Criteria - href: https://dss.sd.gov/docs/childcare/assistance/Subsidy_Manual.pdf#page=8 + - title: South Dakota BEES Child Care Assistance Policy Manual, Section 7.1 Minimum Work Hours/Wage + href: https://dss.sd.gov/docs/childcare/assistance/BEES_CCA_Policy_Manual.pdf#page=35 + - title: South Dakota BEES Child Care Assistance Policy Manual, Section 8.3.1 Earned Income + href: https://dss.sd.gov/docs/childcare/assistance/BEES_CCA_Policy_Manual.pdf#page=37 - title: ARSD 67:47:01:03.01 Time requirements for work and education href: https://sdlegislature.gov/Rules/Administrative/67:47:01:03.01 diff --git a/policyengine_us/parameters/gov/states/sd/dss/cca/work_requirement/self_employed_weekly_hours.yaml b/policyengine_us/parameters/gov/states/sd/dss/cca/work_requirement/self_employed_weekly_hours.yaml deleted file mode 100644 index 8a1b4cc2a9f..00000000000 --- a/policyengine_us/parameters/gov/states/sd/dss/cca/work_requirement/self_employed_weekly_hours.yaml +++ /dev/null @@ -1,17 +0,0 @@ -description: South Dakota requires each self-employed caretaker to work at least this many hours per week under the Child Care Assistance program. - -# Self-employed persons must work at least 20 hours a week and receive weekly -# earnings equal to the federal minimum wage multiplied by 20 hours (Subsidy -# Manual, Section 6). This threshold appears only in the Subsidy Manual, not -# in ARSD 67:47:01. Dated to the modeled program start (weekly rate era, -# 2026-08-03). -values: - 2026-08-03: 20 - -metadata: - unit: hour - period: week - label: South Dakota CCA self-employed caretaker weekly work hours requirement - reference: - - title: South Dakota Child Care Assistance Subsidy Manual, Section 6 Calculating Income from Self-employment - href: https://dss.sd.gov/docs/childcare/assistance/Subsidy_Manual.pdf#page=13 diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_activity_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_activity_eligible.yaml index a974219b3f9..da2baa1b116 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_activity_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_activity_eligible.yaml @@ -1,15 +1,15 @@ # Tests for sd_cca_activity_eligible (SPMUnit, MONTH, bool). # Each caretaker must be in an approved activity (ARSD 67:47:01:03, :03.01; -# Subsidy Manual Sections 3, 5-6): employed at least 80 hours per month at a +# BEES Manual Sections 7.1 and 8.3.1): employed at least 80 hours per month at a # salary equivalent to the federal minimum wage ($7.25/hr), or self-employed -# at least 20 hours per week with weekly earnings of at least -# 20 x $7.25 = $145 (a loss counts as zero), or a full-time student, or +# with at least 80 reported monthly hours and monthly net earnings of at least +# 80 x $7.25 = $580 (a loss counts as zero), or a full-time student, or # enrolled in TANF. Activities not modeled (job search, other training) fall # back to the federal CCDF activity-test input meets_ccdf_activity_test. # Protective services is handled per child in sd_cca_reason_for_care_eligible, # not here. # weekly_hours_worked defaults to 40/week = 173.33/month, so the default -# minimum-wage-equivalence bar is 173.33 x 7.25 = 1,256.67/month. +# employed minimum-wage-equivalence bar is 173.33 x 7.25 = 1,256.67/month. - name: Case 1, activity test met, activity eligible. period: 2027-01 @@ -94,9 +94,9 @@ members: [person1, person2] state_code: SD output: - # A loss counts as zero self-employment earnings (Manual Section 6), which - # falls below the gainful-employment floor of 20 x 7.25 = 145/week, so - # the caretaker is not in an approved activity. + # A loss counts as zero self-employment earnings (BEES Manual Section + # 8.3.1), which falls below the monthly $580 minimum-wage floor, so the + # caretaker is not in an approved activity. sd_cca_activity_eligible: false - name: Case 5, single full-time student parent, activity eligible. @@ -225,17 +225,17 @@ output: # 500/mo over 173.33 hrs/mo implies 2.88/hr, below the 7.25 federal # minimum wage (500 < 173.33 x 7.25 = 1,256.67), so the employment - # requirement fails (Manual Section 5, FLSA rule). + # requirement fails (BEES Manual Section 7.1). sd_cca_activity_eligible: false -- name: Case 10, self-employed parent meeting both gainful-employment prongs, eligible. +- name: Case 10, self-employed parent meeting both monthly work prongs, eligible. period: 2027-01 input: people: person1: age: 30 - self_employment_income: 12_000 # 1,000/mo = 230.77/wk - weekly_hours_worked: 25 # above the 20-hour weekly minimum + self_employment_income: 6_960 # 580/mo = 80 x 7.25 + weekly_hours_worked: 20 # 86.67 hrs/mo, above the 80-hour minimum person2: age: 4 spm_units: @@ -247,11 +247,53 @@ members: [person1, person2] state_code: SD output: - # 25 hrs/wk >= 20 and weekly earnings 230.77 >= 20 x 7.25 = 145, so the - # self-employment requirement is met. + # Reported hours exceed 80/month and monthly earnings equal the $580 + # minimum-wage floor, so the self-employment requirement is met. sd_cca_activity_eligible: true -- name: Case 11, protective-services child does not satisfy the caretaker activity test. +- name: Case 11, self-employed parent below 80 monthly hours, ineligible. + period: 2027-01 + input: + people: + person1: + age: 30 + self_employment_income: 12_000 # earnings are high enough + weekly_hours_worked: 18 # 78 hrs/mo, below the 80-hour minimum + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + meets_ccdf_activity_test: false + households: + household: + members: [person1, person2] + state_code: SD + output: + sd_cca_activity_eligible: false + +- name: Case 12, self-employed parent below minimum-wage earnings, ineligible. + period: 2027-01 + input: + people: + person1: + age: 30 + self_employment_income: 6_948 # 579/mo, below the $580 floor + weekly_hours_worked: 20 # 86.67 hrs/mo, above the hours minimum + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + meets_ccdf_activity_test: false + households: + household: + members: [person1, person2] + state_code: SD + output: + sd_cca_activity_eligible: false + +- name: Case 13, protective-services child does not satisfy the caretaker activity test. period: 2027-01 input: people: diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/rates/sd_cca_weekly_rate.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/rates/sd_cca_weekly_rate.yaml index 6ae32180f03..0783b2e94d0 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/rates/sd_cca_weekly_rate.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/rates/sd_cca_weekly_rate.yaml @@ -3,9 +3,11 @@ # is keyed on region x provider category x age band x level of service, with # informal/in-home/relative rates flat statewide (region and age moot). A child # with special needs receives the base rate plus an additional 75% (x 1.75). -# Each case sets county_str (region), sd_cca_provider_type, age (age band), and -# childcare_hours_per_week (level of service). Values are transcribed from the -# CCA Weekly Reimbursement Rates schedule. +# Each case establishes an approved activity and sets county_str (region), +# sd_cca_provider_type, age (age band), and childcare_hours_per_week (level of +# service). Values are transcribed from the CCA Weekly Reimbursement Rates +# schedule. The final case keeps activity eligibility true to isolate the +# child-age guard. # ---- Family day care ---- - name: Case 1, urban family day care, full time infant. @@ -17,6 +19,10 @@ age: 0.5 # infant sd_cca_provider_type: FAMILY_DAY_CARE childcare_hours_per_week: 40 # full time + spm_units: + spm_unit: + members: [person1] + meets_ccdf_activity_test: true households: household: members: [person1] @@ -34,6 +40,10 @@ age: 1.5 # 18 months, toddler sd_cca_provider_type: FAMILY_DAY_CARE childcare_hours_per_week: 20 # part time + spm_units: + spm_unit: + members: [person1] + meets_ccdf_activity_test: true households: household: members: [person1] @@ -51,6 +61,10 @@ age: 4 # preschool (under 5, not in school) sd_cca_provider_type: FAMILY_DAY_CARE childcare_hours_per_week: 10 # limited time + spm_units: + spm_unit: + members: [person1] + meets_ccdf_activity_test: true households: household: members: [person1] @@ -68,6 +82,10 @@ age: 8 # school age (in K-12 by imputation) sd_cca_provider_type: FAMILY_DAY_CARE childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1] + meets_ccdf_activity_test: true households: household: members: [person1] @@ -85,6 +103,10 @@ age: 0.5 sd_cca_provider_type: FAMILY_DAY_CARE childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1] + meets_ccdf_activity_test: true households: household: members: [person1] @@ -102,6 +124,10 @@ age: 1.5 sd_cca_provider_type: FAMILY_DAY_CARE childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1] + meets_ccdf_activity_test: true households: household: members: [person1] @@ -120,6 +146,10 @@ age: 0.5 sd_cca_provider_type: LICENSED_CENTER childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1] + meets_ccdf_activity_test: true households: household: members: [person1] @@ -137,6 +167,10 @@ age: 4 sd_cca_provider_type: LICENSED_CENTER childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1] + meets_ccdf_activity_test: true households: household: members: [person1] @@ -154,6 +188,10 @@ age: 8 sd_cca_provider_type: LICENSED_CENTER childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1] + meets_ccdf_activity_test: true households: household: members: [person1] @@ -172,6 +210,10 @@ age: 0.5 sd_cca_provider_type: INFORMAL childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1] + meets_ccdf_activity_test: true households: household: members: [person1] @@ -189,6 +231,10 @@ age: 0.5 sd_cca_provider_type: INFORMAL childcare_hours_per_week: 20 + spm_units: + spm_unit: + members: [person1] + meets_ccdf_activity_test: true households: household: members: [person1] @@ -206,6 +252,10 @@ age: 0.5 sd_cca_provider_type: INFORMAL childcare_hours_per_week: 10 + spm_units: + spm_unit: + members: [person1] + meets_ccdf_activity_test: true households: household: members: [person1] @@ -225,6 +275,10 @@ is_disabled: true sd_cca_provider_type: FAMILY_DAY_CARE childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1] + meets_ccdf_activity_test: true households: household: members: [person1] @@ -244,6 +298,10 @@ is_disabled: true sd_cca_provider_type: LICENSED_CENTER childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1] + meets_ccdf_activity_test: true households: household: members: [person1] @@ -253,21 +311,27 @@ # rural licensed center full-time school age 150 x 1.75 = 262.5 sd_cca_weekly_rate: 262.5 -- name: Case 15, age-ineligible child (age 15, non-disabled), no rate. +- name: Case 15, adult with an approved activity is age-ineligible and receives no rate. period: 2027-01 absolute_error_margin: 0.1 input: people: person1: - age: 15 # over the age-13 limit and not disabled, not an eligible child + age: 30 sd_cca_provider_type: LICENSED_CENTER childcare_hours_per_week: 40 + spm_units: + spm_unit: + members: [person1] + meets_ccdf_activity_test: true households: household: members: [person1] state_code: SD county_str: MINNEHAHA_COUNTY_SD output: - # sd_cca_weekly_rate is defined_for sd_cca_eligible_child, so an - # age-ineligible child receives no rate. + # The activity test is met, but sd_cca_weekly_rate is defined_for + # sd_cca_eligible_child, so an adult receives no rate. + sd_cca_reason_for_care_eligible: true + sd_cca_eligible_child: false sd_cca_weekly_rate: 0 diff --git a/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_parent_in_eligible_activity.py b/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_parent_in_eligible_activity.py index 3e2d654082b..4718c641c53 100644 --- a/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_parent_in_eligible_activity.py +++ b/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_parent_in_eligible_activity.py @@ -7,7 +7,10 @@ class sd_cca_parent_in_eligible_activity(Variable): label = "South Dakota CCA parent in an eligible activity" definition_period = MONTH defined_for = StateCode.SD - reference = "https://dss.sd.gov/docs/childcare/assistance/Subsidy_Manual.pdf#page=8" + reference = ( + "https://dss.sd.gov/docs/childcare/assistance/BEES_CCA_Policy_Manual.pdf#page=35", + "https://dss.sd.gov/docs/childcare/assistance/BEES_CCA_Policy_Manual.pdf#page=37", + ) def formula(person, period, parameters): p = parameters(period).gov.states.sd.dss.cca.work_requirement @@ -16,22 +19,20 @@ def formula(person, period, parameters): weeks_per_month = WEEKS_IN_YEAR / MONTHS_IN_YEAR monthly_hours = weekly_hours * weeks_per_month # An employed caretaker must work at least 80 hours per month at a - # salary equivalent to the federal minimum wage (Manual Sections 3 - # and 5). + # salary equivalent to the federal minimum wage (BEES Manual Section + # 7.1). employment_income = person("employment_income", period) meets_employment_requirement = (monthly_hours >= p.monthly_hours) & ( employment_income >= monthly_hours * min_wage ) - # A self-employed caretaker must work at least 20 hours per week and - # receive weekly earnings of at least the federal minimum wage times - # that hour minimum; a business loss counts as zero earnings (Manual - # Section 6). - weekly_self_employment_income = ( - person("self_employment_income", period) / weeks_per_month + # A self-employed caretaker must establish at least 80 hours per month + # through both reported work hours and net earnings at the federal + # minimum wage; a business loss counts as zero earnings (BEES Manual + # Sections 7.1 and 8.3.1). + self_employment_income = person("self_employment_income", period) + meets_self_employment_requirement = (monthly_hours >= p.monthly_hours) & ( + self_employment_income >= p.monthly_hours * min_wage ) - meets_self_employment_requirement = ( - weekly_hours >= p.self_employed_weekly_hours - ) & (weekly_self_employment_income >= p.self_employed_weekly_hours * min_wage) # Full-time students meet the 12-semester-credit-hour minimum; the # combined 80-hour work-plus-school path is not separately modeled. is_student = person("is_full_time_student", period.this_year) From 2b68ccb436c52385f7dfa298a55303fecdb749f6 Mon Sep 17 00:00:00 2001 From: Ziming Date: Mon, 13 Jul 2026 18:39:02 -0400 Subject: [PATCH 7/7] Fix South Dakota CCA review findings --- .../sd/dss/cca/rates/uses_weekly_rates.yaml | 8 +- .../weekly_to_monthly_factor.yaml | 14 +++ .../states/sd/dss/cca/copay/sd_cca_copay.yaml | 77 ++++++++++++++- .../eligibility/sd_cca_activity_eligible.yaml | 95 ++++++++++++++++--- .../cca/income/sd_cca_countable_income.yaml | 57 ++++++++++- .../states/sd/dss/cca/copay/sd_cca_copay.py | 34 ++++--- .../sd_cca_parent_in_eligible_activity.py | 12 ++- .../dss/cca/income/sd_cca_countable_income.py | 25 +++-- .../variables/gov/states/sd/dss/cca/sd_cca.py | 4 +- 9 files changed, 273 insertions(+), 53 deletions(-) create mode 100644 policyengine_us/parameters/gov/states/sd/dss/cca/work_requirement/weekly_to_monthly_factor.yaml diff --git a/policyengine_us/parameters/gov/states/sd/dss/cca/rates/uses_weekly_rates.yaml b/policyengine_us/parameters/gov/states/sd/dss/cca/rates/uses_weekly_rates.yaml index f56d973d350..1c41fc79a40 100644 --- a/policyengine_us/parameters/gov/states/sd/dss/cca/rates/uses_weekly_rates.yaml +++ b/policyengine_us/parameters/gov/states/sd/dss/cca/rates/uses_weekly_rates.yaml @@ -1,12 +1,12 @@ description: South Dakota uses this indicator to determine whether the weekly reimbursement rate schedule applies under the Child Care Assistance program. # The weekly region-based rate schedule takes effect August 3, 2026, replacing -# the earlier hourly county-based schedule. Only the weekly era is modeled; this -# indicator gates the benefit off for prior periods (the hourly era is a -# separate follow-up implementation). +# the earlier hourly county-based schedule. Because the benefit is modeled +# monthly, August 1 is used as the monthly approximation of that effective +# date. Only the weekly era is modeled; the hourly era is a separate follow-up. values: 2024-07-01: false - 2026-08-03: true + 2026-08-01: true metadata: unit: bool diff --git a/policyengine_us/parameters/gov/states/sd/dss/cca/work_requirement/weekly_to_monthly_factor.yaml b/policyengine_us/parameters/gov/states/sd/dss/cca/work_requirement/weekly_to_monthly_factor.yaml new file mode 100644 index 00000000000..241f8e6abc1 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sd/dss/cca/work_requirement/weekly_to_monthly_factor.yaml @@ -0,0 +1,14 @@ +description: South Dakota multiplies weekly work hours by this factor when determining monthly work hours under the Child Care Assistance program. + +# BEES Section 10.1.1 requires weekly hours to be multiplied by 4.3 and the +# result to be rounded up to the next whole hour. +values: + 2026-08-01: 4.3 + +metadata: + unit: /1 + period: year + label: South Dakota CCA weekly-to-monthly work hours factor + reference: + - title: South Dakota BEES Child Care Assistance Policy Manual, Section 10.1.1 Converting Work Hours + href: https://dss.sd.gov/docs/childcare/assistance/BEES_CCA_Policy_Manual.pdf#page=44 diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/copay/sd_cca_copay.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/copay/sd_cca_copay.yaml index 70123e332c5..0e4ede185b1 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/copay/sd_cca_copay.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/copay/sd_cca_copay.yaml @@ -1,8 +1,9 @@ # Tests for sd_cca_copay (SPMUnit, MONTH, USD). # The family co-payment is (monthly countable income - 170% of the monthly # federal poverty guideline) x 5%, floored at zero, capped at 12% of countable -# income, and waived entirely for TANF recipients and for foster/protective- -# services children (Sliding Fee Scale; Subsidy Manual Section 9). The 12% +# income, then rounded down to a whole dollar. TANF waives the family amount; +# foster/protective-services waivers apply only to the eligible covered child +# (Sliding Fee Scale; BEES Section 9). The 12% # ceiling is defensive: with a 5% rate on income above the threshold it cannot # bind, so it is not exercised here (this mirrors the North Dakota co-pay # ceiling, which also does not bind). @@ -199,3 +200,75 @@ # The Sliding Fee Scale waives the co-payment for children determined to # be in need of protective services. sd_cca_copay: 0 + +- name: Case 9, foster child waiver does not erase an ordinary child's copay. + period: 2027-01 + input: + people: + person1: + age: 30 + employment_income: 47_500 + person2: + age: 1 + person3: + age: 2 + is_in_foster_care: true + spm_units: + spm_unit: + members: [person1, person2, person3] + spm_unit_fpg: 24_000 + meets_ccdf_activity_test: true + households: + household: + members: [person1, person2, person3] + state_code: SD + output: + # BEES Section 9.3 keeps the full family copay on the ordinary child. + sd_cca_copay: 20 + +- name: Case 10, protective waiver does not erase an ordinary child's copay. + period: 2027-01 + input: + people: + person1: + age: 30 + employment_income: 47_500 + person2: + age: 1 + person3: + age: 2 + receives_or_needs_protective_services: true + spm_units: + spm_unit: + members: [person1, person2, person3] + spm_unit_fpg: 24_000 + meets_ccdf_activity_test: true + households: + household: + members: [person1, person2, person3] + state_code: SD + output: + sd_cca_copay: 20 + +- name: Case 11, calculated monthly copay rounds down to a whole dollar. + period: 2027-01 + input: + people: + person1: + age: 30 + employment_income: 47_512.50 + person2: + age: 1 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_fpg: 24_000 + meets_ccdf_activity_test: true + households: + household: + members: [person1, person2] + state_code: SD + output: + sd_cca_countable_income: 3_801 + # (3,801 - 3,400) x 5% = 20.05, rounded down under BEES Section 9.2. + sd_cca_copay: 20 diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_activity_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_activity_eligible.yaml index da2baa1b116..1bbe18cc70a 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_activity_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/eligibility/sd_cca_activity_eligible.yaml @@ -3,13 +3,14 @@ # BEES Manual Sections 7.1 and 8.3.1): employed at least 80 hours per month at a # salary equivalent to the federal minimum wage ($7.25/hr), or self-employed # with at least 80 reported monthly hours and monthly net earnings of at least -# 80 x $7.25 = $580 (a loss counts as zero), or a full-time student, or -# enrolled in TANF. Activities not modeled (job search, other training) fall -# back to the federal CCDF activity-test input meets_ccdf_activity_test. +# 80 x $7.25 = $580 (a loss counts as zero), or a full-time student. Activities +# not modeled (positive TANF activity, job search, other training) fall back to +# the federal CCDF activity-test input meets_ccdf_activity_test. TANF enrollment +# alone does not satisfy the activity requirement. # Protective services is handled per child in sd_cca_reason_for_care_eligible, # not here. -# weekly_hours_worked defaults to 40/week = 173.33/month, so the default -# employed minimum-wage-equivalence bar is 173.33 x 7.25 = 1,256.67/month. +# weekly_hours_worked defaults to 40/week = ceil(40 x 4.3) = 172/month, so the default +# employed minimum-wage-equivalence bar is 172 x 7.25 = 1,247/month. - name: Case 1, activity test met, activity eligible. period: 2027-01 @@ -59,7 +60,7 @@ person1: age: 30 employment_income: 24_000 # 2,000/mo - weekly_hours_worked: 40 # 173.33 hrs/mo, above the 80-hour minimum + weekly_hours_worked: 40 # ceil(40 x 4.3) = 172 hrs/mo person2: age: 4 spm_units: @@ -71,7 +72,7 @@ members: [person1, person2] state_code: SD output: - # 173.33 hrs/mo >= 80 and 2,000 >= 173.33 x 7.25 = 1,256.67, so the sole + # 172 hrs/mo >= 80 and 2,000 >= 172 x 7.25 = 1,247, so the sole # caretaker meets the employment requirement even though the CCDF # activity-test input is false. sd_cca_activity_eligible: true @@ -199,7 +200,7 @@ members: [person1, person2] state_code: SD output: - # 15 hrs/wk x 52/12 = 65 hrs/mo < 80, so the employment requirement fails + # ceil(15 hrs/wk x 4.3) = 65 hrs/mo < 80, so the employment requirement fails # even though the implied wage (1,000 / 65 = 15.38/hr) is above the # federal minimum. sd_cca_activity_eligible: false @@ -211,7 +212,7 @@ person1: age: 30 employment_income: 6_000 # 500/mo - weekly_hours_worked: 40 # 173.33 hrs/mo + weekly_hours_worked: 40 # ceil(40 x 4.3) = 172 hrs/mo person2: age: 4 spm_units: @@ -223,8 +224,8 @@ members: [person1, person2] state_code: SD output: - # 500/mo over 173.33 hrs/mo implies 2.88/hr, below the 7.25 federal - # minimum wage (500 < 173.33 x 7.25 = 1,256.67), so the employment + # 500/mo over 172 hrs/mo implies 2.91/hr, below the 7.25 federal minimum + # wage (500 < 172 x 7.25 = 1,247), so the employment # requirement fails (BEES Manual Section 7.1). sd_cca_activity_eligible: false @@ -235,7 +236,7 @@ person1: age: 30 self_employment_income: 6_960 # 580/mo = 80 x 7.25 - weekly_hours_worked: 20 # 86.67 hrs/mo, above the 80-hour minimum + weekly_hours_worked: 20 # ceil(20 x 4.3) = 86 hrs/mo person2: age: 4 spm_units: @@ -258,7 +259,7 @@ person1: age: 30 self_employment_income: 12_000 # earnings are high enough - weekly_hours_worked: 18 # 78 hrs/mo, below the 80-hour minimum + weekly_hours_worked: 18 # ceil(18 x 4.3) = 78 hrs/mo person2: age: 4 spm_units: @@ -279,7 +280,7 @@ person1: age: 30 self_employment_income: 6_948 # 579/mo, below the $580 floor - weekly_hours_worked: 20 # 86.67 hrs/mo, above the hours minimum + weekly_hours_worked: 20 # ceil(20 x 4.3) = 86 hrs/mo person2: age: 4 spm_units: @@ -315,3 +316,69 @@ # This variable tests caretaker activity only; the protective-services # pathway is applied per child in sd_cca_reason_for_care_eligible. sd_cca_activity_eligible: false + +- name: Case 14, TANF enrollment without TANF activity is ineligible. + period: 2027-01 + input: + people: + person1: + age: 30 + weekly_hours_worked: 0 + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + is_tanf_enrolled: true + meets_ccdf_activity_test: false + households: + household: + members: [person1, person2] + state_code: SD + output: + # Enrollment alone does not establish positive TANF activity hours. + sd_cca_activity_eligible: false + +- name: Case 15, weekly work hours use the 4.3 factor and round up. + period: 2027-01 + input: + people: + person1: + age: 30 + employment_income: 12_000 + weekly_hours_worked: 18.4 + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + meets_ccdf_activity_test: false + households: + household: + members: [person1, person2] + state_code: SD + output: + # ceil(18.4 x 4.3) = 80, and 1,000/month exceeds 80 x 7.25. + sd_cca_activity_eligible: true + +- name: Case 16, weekly work hours just below the rounded boundary are ineligible. + period: 2027-01 + input: + people: + person1: + age: 30 + employment_income: 12_000 + weekly_hours_worked: 18.3 + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + meets_ccdf_activity_test: false + households: + household: + members: [person1, person2] + state_code: SD + output: + # ceil(18.3 x 4.3) = 79, below the 80-hour requirement. + sd_cca_activity_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/income/sd_cca_countable_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/income/sd_cca_countable_income.yaml index 21e87bb26d1..30510bbab1d 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/income/sd_cca_countable_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sd/dss/cca/income/sd_cca_countable_income.yaml @@ -1,9 +1,10 @@ # Tests for sd_cca_countable_income (SPMUnit, MONTH, USD). # Countable income is gross countable income, reduced by a 4% disregard on # earned income, minus child support paid and minus the earned income of -# children under 18, floored at zero (ARSD 67:47:01:05, :05.01; Subsidy Manual -# Sections 4-6). Annual person-level income inputs are read with the bare -# monthly period, so Core auto-divides them to monthly amounts. +# children under 18 who are not the tax-unit head or spouse, floored at zero +# (ARSD 67:47:01:05, :05.01; BEES Section 8.3.1). 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, 4% earned income disregard. period: 2027-01 @@ -251,3 +252,53 @@ # (both children's earnings excluded) = 3,000. No adult earned income, so # the 4% disregard does not apply. sd_cca_countable_income: 3_000 + +- name: Case 12, a minor tax-unit head's earned income is counted. + period: 2027-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 17 + employment_income: 12_000 # 1,000/mo + is_tax_unit_head: true + person2: + age: 1 + tax_units: + tax_unit: + members: [person1, person2] + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: SD + output: + # The minor applicant's 1,000 earnings count, less the 4% disregard. + sd_cca_countable_income: 960 + +- name: Case 13, a minor who is not a tax-unit head or spouse has earnings excluded. + period: 2027-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + social_security: 36_000 # 3,000/mo unearned + is_tax_unit_head: true + person2: + age: 17 + employment_income: 12_000 # 1,000/mo excluded + tax_units: + tax_unit: + members: [person1, person2] + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: SD + output: + sd_cca_countable_income: 3_000 diff --git a/policyengine_us/variables/gov/states/sd/dss/cca/copay/sd_cca_copay.py b/policyengine_us/variables/gov/states/sd/dss/cca/copay/sd_cca_copay.py index 0c5e041661a..81e97ff3c95 100644 --- a/policyengine_us/variables/gov/states/sd/dss/cca/copay/sd_cca_copay.py +++ b/policyengine_us/variables/gov/states/sd/dss/cca/copay/sd_cca_copay.py @@ -9,7 +9,7 @@ class sd_cca_copay(Variable): definition_period = MONTH defined_for = "sd_cca_eligible" reference = ( - "https://dss.sd.gov/docs/childcare/assistance/Subsidy_Manual.pdf#page=21", + "https://dss.sd.gov/docs/childcare/assistance/BEES_CCA_Policy_Manual.pdf#page=42", "https://dss.sd.gov/docs/childcare/assistance/Sliding_Fee_Scale.pdf", ) @@ -29,18 +29,26 @@ def formula(spm_unit, period, parameters): copay = income_above_threshold * p.rate # The co-payment never exceeds 12% of countable income. copay = min_(copay, p.max_rate * countable_income) - # The co-payment is waived for families enrolled in the Temporary - # Assistance for Needy Families program and for foster or - # protective-services children (Sliding Fee Scale). + # BEES Section 9.2 rounds the monthly co-payment down to a whole dollar. + # The small tolerance prevents exact whole-dollar results represented + # infinitesimally below the integer from flooring to the prior dollar. + copay = np.floor(copay + 1e-4) + # TANF enrollment waives the family co-payment. Foster and protective- + # services waivers are child-specific: the family amount remains when + # any ordinary eligible child is covered, and is zero only when all + # eligible covered children qualify for a child-based waiver. is_tanf_enrolled = spm_unit("is_tanf_enrolled", period) - has_foster_child = add(spm_unit, period, ["is_in_foster_care"]) > 0 - has_protective_services_child = ( - add( - spm_unit, - period.this_year, - ["receives_or_needs_protective_services"], - ) - > 0 + person = spm_unit.members + is_eligible_child = person("sd_cca_eligible_child", period) + receives_child_based_waiver = person("is_in_foster_care", period) | person( + "receives_or_needs_protective_services", period.this_year ) - waived = is_tanf_enrolled | has_foster_child | has_protective_services_child + has_waived_eligible_child = ( + spm_unit.sum(is_eligible_child & receives_child_based_waiver) > 0 + ) + has_non_waived_eligible_child = ( + spm_unit.sum(is_eligible_child & ~receives_child_based_waiver) > 0 + ) + child_based_waiver = has_waived_eligible_child & ~has_non_waived_eligible_child + waived = is_tanf_enrolled | child_based_waiver return where(waived, 0, copay) diff --git a/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_parent_in_eligible_activity.py b/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_parent_in_eligible_activity.py index 4718c641c53..4895337d693 100644 --- a/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_parent_in_eligible_activity.py +++ b/policyengine_us/variables/gov/states/sd/dss/cca/eligibility/sd_cca_parent_in_eligible_activity.py @@ -16,8 +16,9 @@ def formula(person, period, parameters): p = parameters(period).gov.states.sd.dss.cca.work_requirement min_wage = parameters(period).gov.dol.minimum_wage weekly_hours = person("weekly_hours_worked", period.this_year) - weeks_per_month = WEEKS_IN_YEAR / MONTHS_IN_YEAR - monthly_hours = weekly_hours * weeks_per_month + # BEES Section 10.1.1 converts weekly hours with a 4.3 factor and + # rounds the result up to the next whole monthly hour. + monthly_hours = np.ceil(weekly_hours * p.weekly_to_monthly_factor) # An employed caretaker must work at least 80 hours per month at a # salary equivalent to the federal minimum wage (BEES Manual Section # 7.1). @@ -34,12 +35,13 @@ def formula(person, period, parameters): self_employment_income >= p.monthly_hours * min_wage ) # Full-time students meet the 12-semester-credit-hour minimum; the - # combined 80-hour work-plus-school path is not separately modeled. + # combined 80-hour work-plus-school path and positive TANF activity + # hours are represented through the CCDF activity-test fallback. + # TANF enrollment alone does not qualify a caretaker because deferred + # recipients can have no TANF activity hours. is_student = person("is_full_time_student", period.this_year) - is_tanf_enrolled = person.spm_unit("is_tanf_enrolled", period) return ( meets_employment_requirement | meets_self_employment_requirement | is_student - | is_tanf_enrolled ) diff --git a/policyengine_us/variables/gov/states/sd/dss/cca/income/sd_cca_countable_income.py b/policyengine_us/variables/gov/states/sd/dss/cca/income/sd_cca_countable_income.py index 3d0c5ec08ae..933ae62c1f0 100644 --- a/policyengine_us/variables/gov/states/sd/dss/cca/income/sd_cca_countable_income.py +++ b/policyengine_us/variables/gov/states/sd/dss/cca/income/sd_cca_countable_income.py @@ -8,9 +8,7 @@ class sd_cca_countable_income(Variable): label = "South Dakota CCA countable income" definition_period = MONTH defined_for = StateCode.SD - reference = ( - "https://dss.sd.gov/docs/childcare/assistance/Subsidy_Manual.pdf#page=10" - ) + reference = "https://dss.sd.gov/docs/childcare/assistance/BEES_CCA_Policy_Manual.pdf#page=37" def formula(spm_unit, period, parameters): p = parameters(period).gov.states.sd.dss.cca.income @@ -24,17 +22,24 @@ def formula(spm_unit, period, parameters): period, ["employment_income", "sd_cca_self_employment_income"], ) - is_child = person("age", period.this_year) < p.child_earned_income_exclusion_age - # The earned income of children is excluded entirely. - child_earned_income = spm_unit.sum(is_child * person_earned) - # A 4% standard deduction applies to the remaining (adult) earned income. - adult_earned_income = spm_unit.sum(~is_child * person_earned) - earned_disregard = max_(adult_earned_income, 0) * p.earned_income_disregard + age = person("age", period.this_year) + is_head_or_spouse = person("is_tax_unit_head_or_spouse", period.this_year) + # BEES Section 8.3.1 excludes earnings of a child only when the child is + # not the Primary Applicant or Primary Partner. Tax-unit head/spouse is + # the available proxy, so a minor applicant parent's earnings count. + is_excluded_minor = ( + age < p.child_earned_income_exclusion_age + ) & ~is_head_or_spouse + excluded_minor_earned_income = spm_unit.sum(is_excluded_minor * person_earned) + # A 4% standard deduction applies to all remaining earned income, + # including the earnings of a minor tax-unit head or spouse. + counted_earned_income = spm_unit.sum(~is_excluded_minor * person_earned) + earned_disregard = max_(counted_earned_income, 0) * p.earned_income_disregard # Court-ordered child support paid out is deducted. child_support_expense = add(spm_unit, period, ["child_support_expense"]) return max_( gross_income - - child_earned_income + - excluded_minor_earned_income - earned_disregard - child_support_expense, 0, diff --git a/policyengine_us/variables/gov/states/sd/dss/cca/sd_cca.py b/policyengine_us/variables/gov/states/sd/dss/cca/sd_cca.py index 1867a7cd76e..55c6cbc4277 100644 --- a/policyengine_us/variables/gov/states/sd/dss/cca/sd_cca.py +++ b/policyengine_us/variables/gov/states/sd/dss/cca/sd_cca.py @@ -15,8 +15,8 @@ class sd_cca(Variable): def formula(spm_unit, period, parameters): p = parameters(period).gov.states.sd.dss.cca.rates # Only the weekly region-based rate schedule (effective 2026-08-03) is - # modeled; the earlier hourly county-based schedule is deferred to a - # follow-up implementation, so no benefit is paid before that date. + # modeled. The monthly model starts it in August 2026; the earlier + # hourly county-based schedule is deferred to a follow-up implementation. if not p.uses_weekly_rates: return 0 # The benefit is the lesser of the summed per-child weekly maximum rates