From 2367186987c65019e185c9f1ffa1919e65b52513 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 7 Nov 2026 10:47:30 -0500 Subject: [PATCH 1/2] Require a need for care in North Carolina's child care program Under 10A NCAC 10 .0905 and Chapter 5 of the Subsidized Child Care Assistance policy manual, North Carolina pays for care only when both parents or responsible adults in the household need it, through work, education or training, or an inability to provide care, or when the child needs it for developmental needs or Child Protective Services. nc_scca_entry_eligible had no such test, so a family received a subsidy whether or not anyone in it worked or studied. Add nc_scca_need_eligible and require it in nc_scca_entry_eligible. Work First enrollment, child disability and protective services stand in for the agency's records, and meets_ccdf_activity_test covers job search, absences, part-time study and Child Welfare Services. Part of #9289. Co-Authored-By: Claude Opus 5.5 --- changelog.d/nc-scca-need-test.fixed.md | 1 + .../scca/need/earned_income_sources.yaml | 15 + .../nc/ncdhhs/scca/court_supervision.yaml | 1 + .../scca/entry/nc_scca_entry_eligible.yaml | 21 + .../scca/entry/nc_scca_need_eligible.yaml | 495 ++++++++++++++++++ .../states/nc/ncdhhs/scca/integration.yaml | 2 + .../ncdhhs/scca/nc_scca_child_eligible.yaml | 1 + .../ncdhhs/scca/nc_scca_maximum_payment.yaml | 3 + .../tests/test_nc_scca_need_properties.py | 168 ++++++ .../scca/entry/nc_scca_entry_eligible.py | 8 +- .../scca/entry/nc_scca_need_eligible.py | 75 +++ 11 files changed, 788 insertions(+), 2 deletions(-) create mode 100644 changelog.d/nc-scca-need-test.fixed.md create mode 100644 policyengine_us/parameters/gov/states/nc/ncdhhs/scca/need/earned_income_sources.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/nc/ncdhhs/scca/entry/nc_scca_need_eligible.yaml create mode 100644 policyengine_us/tests/test_nc_scca_need_properties.py create mode 100644 policyengine_us/variables/gov/states/nc/ncdhhs/scca/entry/nc_scca_need_eligible.py diff --git a/changelog.d/nc-scca-need-test.fixed.md b/changelog.d/nc-scca-need-test.fixed.md new file mode 100644 index 00000000000..765cb44d8ab --- /dev/null +++ b/changelog.d/nc-scca-need-test.fixed.md @@ -0,0 +1 @@ +Require a need for care in North Carolina's Subsidized Child Care Assistance program under 10A NCAC 10 .0905. diff --git a/policyengine_us/parameters/gov/states/nc/ncdhhs/scca/need/earned_income_sources.yaml b/policyengine_us/parameters/gov/states/nc/ncdhhs/scca/need/earned_income_sources.yaml new file mode 100644 index 00000000000..609d98530e3 --- /dev/null +++ b/policyengine_us/parameters/gov/states/nc/ncdhhs/scca/need/earned_income_sources.yaml @@ -0,0 +1,15 @@ +description: North Carolina counts these income sources as employment or self-employment under the Subsidized Child Care Assistance need for care. +values: + 2019-03-01: + - employment_income + - self_employment_income + - sstb_self_employment_income + - farm_operations_income + +metadata: + unit: list + period: year + label: North Carolina SCCA need for care earned income sources + reference: + - title: Subsidized Child Care Assistance Program Policy Manual, Chapter 5, section III.B, child care to support employment + href: https://policies.ncdhhs.gov/wp-content/uploads/Chapter-5-CN-25-3-Establishing-Need-and-Plan-of-Care.pdf#page=5 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nc/ncdhhs/scca/court_supervision.yaml b/policyengine_us/tests/policy/baseline/gov/states/nc/ncdhhs/scca/court_supervision.yaml index b5c222070f9..69ee7263f13 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nc/ncdhhs/scca/court_supervision.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nc/ncdhhs/scca/court_supervision.yaml @@ -112,6 +112,7 @@ spm_units: spm_unit: members: [person1, person2, person3] + nc_scca_need_eligible: true nc_scca_countable_income: 2_150 spm_unit_pre_subsidy_childcare_expenses: 120_000 households: diff --git a/policyengine_us/tests/policy/baseline/gov/states/nc/ncdhhs/scca/entry/nc_scca_entry_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/nc/ncdhhs/scca/entry/nc_scca_entry_eligible.yaml index 359214ba0ea..61e73f04544 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nc/ncdhhs/scca/entry/nc_scca_entry_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nc/ncdhhs/scca/entry/nc_scca_entry_eligible.yaml @@ -9,6 +9,7 @@ spm_units: spm_unit: members: [person1, child1] + nc_scca_need_eligible: true households: household: members: [person1, child1] @@ -47,6 +48,7 @@ spm_units: spm_unit: members: [person1, child1, child2] + nc_scca_need_eligible: true households: household: members: [person1, child1, child2] @@ -71,3 +73,22 @@ state_code: SC output: nc_scca_entry_eligible: false + +- name: Case 5, an eligible child without a need for care, not entry eligible. + period: 2024-01 + input: + people: + person1: + nc_scca_child_eligible: false + child1: + nc_scca_child_eligible: true + spm_units: + spm_unit: + members: [person1, child1] + nc_scca_need_eligible: false + households: + household: + members: [person1, child1] + state_code: NC + output: + nc_scca_entry_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/nc/ncdhhs/scca/entry/nc_scca_need_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/nc/ncdhhs/scca/entry/nc_scca_need_eligible.yaml new file mode 100644 index 00000000000..62a43b9b002 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nc/ncdhhs/scca/entry/nc_scca_need_eligible.yaml @@ -0,0 +1,495 @@ +# 10A NCAC 10 .0905 and Subsidized Child Care Assistance Program Policy Manual, Chapter 5. +- name: Case 1, single parent with wages has a need for care. + period: 2026-01 + input: + people: + parent: + age: 30 + employment_income: 20_000 + child: + age: 3 + spm_units: + spm_unit: + members: [parent, child] + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: NC + output: + nc_scca_need_eligible: true + +- name: Case 2, single parent with no activity has no need for care. + period: 2026-01 + input: + people: + parent: + age: 30 + child: + age: 3 + spm_units: + spm_unit: + members: [parent, child] + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: NC + output: + nc_scca_need_eligible: false + +- name: Case 3, part-time wages below minimum wage still count, since Chapter 5 III.B sets no floor. + period: 2026-01 + input: + people: + parent: + age: 30 + employment_income: 2_000 + weekly_hours_worked_before_lsr: 40 + weeks_worked: 52 + child: + age: 3 + spm_units: + spm_unit: + members: [parent, child] + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: NC + output: + nc_scca_need_eligible: true + +- name: Case 4, self-employment income meets Chapter 5 III.B.4. + period: 2026-01 + input: + people: + parent: + age: 30 + self_employment_income: 15_000 + child: + age: 3 + spm_units: + spm_unit: + members: [parent, child] + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: NC + output: + nc_scca_need_eligible: true + +- name: Case 5, a business loss with no other activity does not. + period: 2026-01 + input: + people: + parent: + age: 30 + self_employment_income: -5_000 + child: + age: 3 + spm_units: + spm_unit: + members: [parent, child] + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: NC + output: + nc_scca_need_eligible: false + +- name: Case 6, a business loss does not erase wages. + period: 2026-01 + input: + people: + parent: + age: 30 + employment_income: 20_000 + self_employment_income: -30_000 + child: + age: 3 + spm_units: + spm_unit: + members: [parent, child] + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: NC + output: + nc_scca_need_eligible: true + +- name: Case 7, a full-time college student has a need under .0905(f). + period: 2026-01 + input: + people: + parent: + age: 30 + is_full_time_college_student: true + child: + age: 3 + spm_units: + spm_unit: + members: [parent, child] + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: NC + output: + nc_scca_need_eligible: true + +- name: Case 8, two parents who both work have a need under Chapter 5 III.A. + period: 2026-01 + input: + people: + parent1: + age: 30 + employment_income: 20_000 + parent2: + age: 30 + employment_income: 20_000 + child: + age: 3 + spm_units: + spm_unit: + members: [parent1, parent2, child] + tax_units: + tax_unit: + members: [parent1, parent2, child] + households: + household: + members: [parent1, parent2, child] + state_code: NC + output: + nc_scca_need_eligible: true + +- name: Case 9, one parent working and the other in school have a need under Chapter 5 III.A. + period: 2026-01 + input: + people: + parent1: + age: 30 + employment_income: 20_000 + parent2: + age: 30 + is_full_time_college_student: true + child: + age: 3 + spm_units: + spm_unit: + members: [parent1, parent2, child] + tax_units: + tax_unit: + members: [parent1, parent2, child] + households: + household: + members: [parent1, parent2, child] + state_code: NC + output: + nc_scca_need_eligible: true + +- name: Case 10, one parent working and the other at home and able to provide care have no need. + period: 2026-01 + input: + people: + parent1: + age: 30 + employment_income: 20_000 + parent2: + age: 30 + child: + age: 3 + spm_units: + spm_unit: + members: [parent1, parent2, child] + tax_units: + tax_unit: + members: [parent1, parent2, child] + households: + household: + members: [parent1, parent2, child] + state_code: NC + output: + nc_scca_need_eligible: false + +- name: Case 11, one parent working and the other disabled have a need under Chapter 5 III.A. + period: 2026-01 + input: + people: + parent1: + age: 30 + employment_income: 20_000 + parent2: + age: 30 + is_disabled: true + child: + age: 3 + spm_units: + spm_unit: + members: [parent1, parent2, child] + tax_units: + tax_unit: + members: [parent1, parent2, child] + households: + household: + members: [parent1, parent2, child] + state_code: NC + output: + nc_scca_need_eligible: true + +- name: Case 12, a single parent unable to work or provide care has a need under .0905(b). + period: 2026-01 + input: + people: + parent: + age: 30 + is_disabled: true + child: + age: 3 + spm_units: + spm_unit: + members: [parent, child] + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: NC + output: + nc_scca_need_eligible: true + +- name: Case 13, an incarcerated parent is out of the household, so the working parent has a need. + period: 2026-01 + input: + people: + parent1: + age: 30 + employment_income: 20_000 + parent2: + age: 30 + is_incarcerated: true + child: + age: 3 + spm_units: + spm_unit: + members: [parent1, parent2, child] + tax_units: + tax_unit: + members: [parent1, parent2, child] + households: + household: + members: [parent1, parent2, child] + state_code: NC + output: + nc_scca_need_eligible: true + +- name: Case 14, a retired grandparent who files separately does not block the working parent's need. + period: 2026-01 + input: + people: + grandparent: + age: 70 + parent: + age: 30 + employment_income: 20_000 + child: + age: 3 + spm_units: + spm_unit: + members: [grandparent, parent, child] + tax_units: + grandparent_tax_unit: + members: [grandparent] + parent_tax_unit: + members: [parent, child] + households: + household: + members: [grandparent, parent, child] + state_code: NC + output: + nc_scca_need_eligible: true + +- name: Case 15, a working grandparent who files separately does not supply the parent's need. + period: 2026-01 + input: + people: + grandparent: + age: 60 + employment_income: 50_000 + parent: + age: 30 + child: + age: 3 + spm_units: + spm_unit: + members: [grandparent, parent, child] + tax_units: + grandparent_tax_unit: + members: [grandparent] + parent_tax_unit: + members: [parent, child] + households: + household: + members: [grandparent, parent, child] + state_code: NC + output: + nc_scca_need_eligible: false + +- name: Case 16, Work First enrollment meets Chapter 5 III.B.5. + period: 2026-01 + input: + people: + parent: + age: 30 + child: + age: 3 + spm_units: + spm_unit: + members: [parent, child] + is_tanf_enrolled: true + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: NC + output: + nc_scca_need_eligible: true + +- name: Case 17, a disabled eligible child has a developmental need under Chapter 5 III.D. + period: 2026-01 + input: + people: + parent: + age: 30 + child: + age: 3 + is_disabled: true + spm_units: + spm_unit: + members: [parent, child] + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: NC + output: + nc_scca_need_eligible: true + +- name: Case 18, an eligible child receiving Child Protective Services has a need under Chapter 5 III.E. + period: 2026-01 + input: + people: + parent: + age: 30 + child: + age: 3 + receives_or_needs_protective_services: true + spm_units: + spm_unit: + members: [parent, child] + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: NC + output: + nc_scca_need_eligible: true + +- name: Case 19, the activity input covers job search and other needs the model does not track. + period: 2026-01 + input: + people: + parent: + age: 30 + child: + age: 3 + spm_units: + spm_unit: + members: [parent, child] + meets_ccdf_activity_test: true + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: NC + output: + nc_scca_need_eligible: true + +- name: Case 20, a working parent with an eligible child receives the subsidy through the full pipeline. + period: 2026-01 + input: + people: + parent: + age: 30 + employment_income: 30_000 + child: + age: 3 + spm_units: + spm_unit: + members: [parent, child] + spm_unit_pre_subsidy_childcare_expenses: 12_000 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: NC + county_str: WAKE_COUNTY_NC + output: + nc_scca_need_eligible: true + nc_scca_entry_eligible: true + # $1,000 a month less the 10% fee on $2,500, which the $1,256 market rate leaves uncapped. + nc_scca: 750 + +- name: Case 21, the same family with the same income from a pension receives nothing. + period: 2026-01 + input: + people: + parent: + age: 30 + pension_income: 30_000 + child: + age: 3 + spm_units: + spm_unit: + members: [parent, child] + spm_unit_pre_subsidy_childcare_expenses: 12_000 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: NC + county_str: WAKE_COUNTY_NC + output: + nc_scca_need_eligible: false + nc_scca_entry_eligible: false + nc_scca: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nc/ncdhhs/scca/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/nc/ncdhhs/scca/integration.yaml index f72d862bdd9..1269041a0d1 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nc/ncdhhs/scca/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nc/ncdhhs/scca/integration.yaml @@ -122,6 +122,7 @@ spm_unit: nc_scca_countable_income: 5_199 members: [person1, person2, child1, child2] + nc_scca_need_eligible: true spm_unit_pre_subsidy_childcare_expenses: 120_000 households: household: @@ -154,6 +155,7 @@ spm_unit: nc_scca_countable_income: 5_199 members: [person1, person2, child1, child2] + nc_scca_need_eligible: true spm_unit_pre_subsidy_childcare_expenses: 120_000 households: household: diff --git a/policyengine_us/tests/policy/baseline/gov/states/nc/ncdhhs/scca/nc_scca_child_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/nc/ncdhhs/scca/nc_scca_child_eligible.yaml index 5ba72c53b0b..4e001ab3603 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nc/ncdhhs/scca/nc_scca_child_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nc/ncdhhs/scca/nc_scca_child_eligible.yaml @@ -38,6 +38,7 @@ spm_units: spm_unit: members: [person1, child1, child2] + nc_scca_need_eligible: true # 2026 FPG for 3 = 27_320/yr = 2_276.67/mo. # 133% = 3_027.97 < 4_500 (age 7 fails); 200% = 4_553.33 > 4_500 (age 4 passes). nc_scca_countable_income: 4_500 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nc/ncdhhs/scca/nc_scca_maximum_payment.yaml b/policyengine_us/tests/policy/baseline/gov/states/nc/ncdhhs/scca/nc_scca_maximum_payment.yaml index 0efd7c92c10..8c030c1badb 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nc/ncdhhs/scca/nc_scca_maximum_payment.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nc/ncdhhs/scca/nc_scca_maximum_payment.yaml @@ -20,6 +20,7 @@ spm_unit: nc_scca_countable_income: 51_99 members: [person1, person2, child1, child2] + nc_scca_need_eligible: true households: household: members: [person1, person2, child1, child2] @@ -96,6 +97,7 @@ # 2026 FPG for 3 = 2_276.67/mo; 133% = 3_027.97, 200% = 4_553.33. nc_scca_countable_income: 4_500 members: [person1, child1, child2] + nc_scca_need_eligible: true households: household: members: [person1, child1, child2] @@ -132,6 +134,7 @@ # 2026 FPG for 4 = 2_750/mo; 133% = 3_657.50, 200% = 5_500. nc_scca_countable_income: 4_125 members: [person1, child1, child2, child3] + nc_scca_need_eligible: true households: household: members: [person1, child1, child2, child3] diff --git a/policyengine_us/tests/test_nc_scca_need_properties.py b/policyengine_us/tests/test_nc_scca_need_properties.py new file mode 100644 index 00000000000..7c4f023da1b --- /dev/null +++ b/policyengine_us/tests/test_nc_scca_need_properties.py @@ -0,0 +1,168 @@ +"""Invariants of North Carolina's need for child care. + +10A NCAC 10 .0905 and Chapter 5 of the Subsidized Child Care Assistance Program +Policy Manual say which families have a need. + +1. Without the other paths, a family has a need when each parent or responsible + adult works, studies, or can't work and provide care (.0905(a), (b) and (f); + Chapter 5 III.A). Any positive earnings count. +2. No family without a need receives a subsidy: nc_scca_entry_eligible implies + nc_scca_need_eligible, and nc_scca is zero without it. +3. Giving an adult earnings never removes the need. +4. An adult who files separately and claims no eligible child doesn't change the + answer, whatever that adult earns. +5. The other paths each give a need to a family with no activity (Chapter 5 III.B.5, + III.D and III.E, and the activity input). + +The YAML cases pin single families. These generate earnings and family makeups, +which YAML can't express. Tests of the need itself fix the child as eligible, so +an income above the program's limit can't empty the test. +""" + +import pytest +from hypothesis import example, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +YEAR = 2026 +MONTH = "2026-01" + +adults = st.fixed_dictionaries( + { + "wages": st.integers(min_value=0, max_value=60_000), + "business": st.integers(min_value=-30_000, max_value=60_000), + "student": st.booleans(), + "disabled": st.booleans(), + } +) + +IDLE = {"wages": 0, "business": 0, "student": False, "disabled": False} +WORKING = {"wages": 20_000, "business": 0, "student": False, "disabled": False} +ELIGIBLE_CHILD = {"nc_scca_child_eligible": {MONTH: True}} + + +def person_input(adult): + return { + "age": {YEAR: 30}, + "employment_income": {YEAR: adult["wages"]}, + "self_employment_income": {YEAR: adult["business"]}, + "is_full_time_college_student": {YEAR: adult["student"]}, + "is_disabled": {YEAR: adult["disabled"]}, + } + + +def simulate(adults, spm_unit=None, other_adult=None, child=ELIGIBLE_CHILD): + names = [f"adult{index}" for index in range(len(adults))] + people = {name: person_input(item) for name, item in zip(names, adults)} + people["child"] = {"age": {YEAR: 3}, **child} + family = names + ["child"] + tax_units = {"claiming": {"members": family}} + members = list(family) + if other_adult is not None: + people["other_adult"] = {"age": {YEAR: 60}, **other_adult} + tax_units["other"] = {"members": ["other_adult"]} + members.append("other_adult") + situation = { + "people": people, + "tax_units": tax_units, + "spm_units": { + "spm_unit": { + "members": members, + "spm_unit_pre_subsidy_childcare_expenses": {YEAR: 12_000}, + **(spm_unit or {}), + } + }, + "families": {"family": {"members": members}}, + "marital_units": {name: {"members": [name]} for name in people}, + "households": { + "household": { + "members": members, + "state_name": {YEAR: "NC"}, + "county_str": {YEAR: "WAKE_COUNTY_NC"}, + } + }, + } + return Simulation(situation=situation) + + +def has_need(simulation): + return bool(simulation.calculate("nc_scca_need_eligible", MONTH)[0]) + + +def adult_has_need(adult): + earns = adult["wages"] > 0 or adult["business"] > 0 + return earns or adult["student"] or adult["disabled"] + + +@settings(max_examples=25, deadline=None) +@given(family=st.lists(adults, min_size=1, max_size=2)) +# One adult with no activity, alone and beside a working adult; a single adult who +# can't work or provide care; and a business loss beside wages. +@example(family=[IDLE]) +@example(family=[WORKING, IDLE]) +@example(family=[{**IDLE, "disabled": True}]) +@example(family=[{**WORKING, "business": -30_000}]) +def test_need_follows_the_rule(family): + simulation = simulate(family) + assert has_need(simulation) == all(adult_has_need(item) for item in family) + + +@settings(max_examples=15, deadline=None) +@given(family=st.lists(adults, min_size=1, max_size=2)) +def test_no_family_without_a_need_receives_a_subsidy(family): + # This test runs the child's own age and income tests. + simulation = simulate(family, child={}) + entry_eligible = bool(simulation.calculate("nc_scca_entry_eligible", MONTH)[0]) + subsidy = float(simulation.calculate("nc_scca", MONTH)[0]) + if entry_eligible: + assert has_need(simulation) + if not entry_eligible: + assert subsidy == 0 + if not has_need(simulation): + assert not entry_eligible + assert subsidy >= 0 + + +@settings(max_examples=15, deadline=None) +@given( + family=st.lists(adults, min_size=1, max_size=2), + raise_by=st.integers(min_value=1, max_value=40_000), +) +def test_earnings_never_remove_the_need(family, raise_by): + before = has_need(simulate(family)) + raised = [{**family[0], "wages": family[0]["wages"] + raise_by}] + family[1:] + assert has_need(simulate(raised)) >= before + + +@settings(max_examples=15, deadline=None) +@given( + family=st.lists(adults, min_size=1, max_size=2), + other_earnings=st.integers(min_value=0, max_value=60_000), +) +def test_a_separate_filer_does_not_change_the_answer(family, other_earnings): + alone = has_need(simulate(family)) + beside = simulate(family, other_adult={"employment_income": {YEAR: other_earnings}}) + assert has_need(beside) == alone + + +def test_a_family_with_no_activity_has_no_need_without_another_path(): + assert not has_need(simulate([IDLE])) + + +@pytest.mark.parametrize( + "path", + [ + {"spm_unit": {"is_tanf_enrolled": {MONTH: True}}}, + {"spm_unit": {"meets_ccdf_activity_test": {YEAR: True}}}, + {"child": {**ELIGIBLE_CHILD, "is_disabled": {YEAR: True}}}, + { + "child": { + **ELIGIBLE_CHILD, + "receives_or_needs_protective_services": {YEAR: True}, + } + }, + ], +) +def test_each_other_path_gives_a_family_with_no_activity_a_need(path): + assert has_need(simulate([IDLE], **path)) diff --git a/policyengine_us/variables/gov/states/nc/ncdhhs/scca/entry/nc_scca_entry_eligible.py b/policyengine_us/variables/gov/states/nc/ncdhhs/scca/entry/nc_scca_entry_eligible.py index 10d84548f59..d7b34f38117 100644 --- a/policyengine_us/variables/gov/states/nc/ncdhhs/scca/entry/nc_scca_entry_eligible.py +++ b/policyengine_us/variables/gov/states/nc/ncdhhs/scca/entry/nc_scca_entry_eligible.py @@ -7,9 +7,13 @@ class nc_scca_entry_eligible(Variable): label = ( "North Carolina entry eligibility for Subsidized Child Care Assistance Program" ) - reference = "https://policies.ncdhhs.gov/wp-content/uploads/FINAL-Chapter-7-Family-definition-and-determining-income-eligibility-08-05-24.pdf#page=2" + reference = ( + "https://policies.ncdhhs.gov/wp-content/uploads/FINAL-Chapter-7-Family-definition-and-determining-income-eligibility-08-05-24.pdf#page=2", + "https://policies.ncdhhs.gov/wp-content/uploads/Chapter-5-CN-25-3-Establishing-Need-and-Plan-of-Care.pdf#page=2", + ) definition_period = MONTH defined_for = StateCode.NC def formula(spm_unit, period, parameters): - return add(spm_unit, period, ["nc_scca_child_eligible"]) > 0 + has_eligible_child = add(spm_unit, period, ["nc_scca_child_eligible"]) > 0 + return has_eligible_child & spm_unit("nc_scca_need_eligible", period) diff --git a/policyengine_us/variables/gov/states/nc/ncdhhs/scca/entry/nc_scca_need_eligible.py b/policyengine_us/variables/gov/states/nc/ncdhhs/scca/entry/nc_scca_need_eligible.py new file mode 100644 index 00000000000..84327ec8e4a --- /dev/null +++ b/policyengine_us/variables/gov/states/nc/ncdhhs/scca/entry/nc_scca_need_eligible.py @@ -0,0 +1,75 @@ +from policyengine_us.model_api import * + + +class nc_scca_need_eligible(Variable): + value_type = bool + entity = SPMUnit + label = "Meets the North Carolina Subsidized Child Care Assistance need for care" + definition_period = MONTH + defined_for = StateCode.NC + reference = ( + # 10A NCAC 10 .0905: employment, training, and recipients unable to work + # or train and incapable of providing care. + "http://reports.oah.state.nc.us/ncac/title%2010a%20-%20health%20and%20human%20services/chapter%2010%20-%20subsidized%20child%20care/10a%20ncac%2010%20.0905.pdf", + # Policy Manual Chapter 5, section II: the five need categories. + "https://policies.ncdhhs.gov/wp-content/uploads/Chapter-5-CN-25-3-Establishing-Need-and-Plan-of-Care.pdf#page=2", + # Section III.A: both parents or responsible adults must have a need. + "https://policies.ncdhhs.gov/wp-content/uploads/Chapter-5-CN-25-3-Establishing-Need-and-Plan-of-Care.pdf#page=4", + # Section III.B.5: Work First. + "https://policies.ncdhhs.gov/wp-content/uploads/Chapter-5-CN-25-3-Establishing-Need-and-Plan-of-Care.pdf#page=8", + # Section III.C: education. + "https://policies.ncdhhs.gov/wp-content/uploads/Chapter-5-CN-25-3-Establishing-Need-and-Plan-of-Care.pdf#page=9", + # Section III.D: developmental needs of the child. + "https://policies.ncdhhs.gov/wp-content/uploads/Chapter-5-CN-25-3-Establishing-Need-and-Plan-of-Care.pdf#page=12", + # Section III.E: Child Protective Services. + "https://policies.ncdhhs.gov/wp-content/uploads/Chapter-5-CN-25-3-Establishing-Need-and-Plan-of-Care.pdf#page=14", + ) + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.nc.ncdhhs.scca.need + person = spm_unit.members + year = period.this_year + # Section III.B sets no minimum hours for employment, and a self-employed + # person short of minimum wage gets fewer authorized hours under III.B.4, + # still with a need for care. Any positive earnings count. + employed = np.any( + [person(source, year) > 0 for source in p.earned_income_sources], + axis=0, + ) + # .0905(f) covers high school, basic education and post-secondary or skills + # training. The model can't see the 20-month limit or graduate study. + student = person("is_full_time_student", year) + # .0905(b) pays for recipients unable to work or train and incapable of + # providing care, including a recipient with no other adult at home. + # Disability stands in for the agency's determination. + unable_to_provide_care = person("is_disabled", year) | person( + "is_incapable_of_self_care", year + ) + has_need = employed | student | unable_to_provide_care + # Section III.A requires a need of both parents or responsible adults in the + # household, and another adult in the home doesn't block care, so the filers + # who claim an eligible child stand in for them. An incarcerated adult isn't + # in the household. + eligible_child = person("nc_scca_child_eligible", period) + claims_eligible_child = person.tax_unit.sum(eligible_child) > 0 + responsible_adult = ( + person("is_tax_unit_head_or_spouse", year) + & claims_eligible_child + & ~person("is_incarcerated", period) + ) + adults_have_need = spm_unit.sum(responsible_adult & ~has_need) == 0 + # Section III.B.5 serves Work First participants through their approved + # employment and training plan. Enrollment stands in for that plan. + work_first = spm_unit("is_tanf_enrolled", period) + # Section III.D serves a child whose development is delayed or at risk of + # delay, and section III.E a child receiving Child Protective Services. + # Disability stands in for the documented delay. + child_need = person("is_disabled", year) | person( + "receives_or_needs_protective_services", year + ) + has_child_need = spm_unit.any(eligible_child & child_need) + # This input covers job search under .0905(d), absences under .0905(c) and + # (e), part-time study, an elderly recipient under .0905(b)(6), for which + # the rule sets no age, and Child Welfare Services under section III.F. + other_need = spm_unit("meets_ccdf_activity_test", year) + return adults_have_need | work_first | has_child_need | other_need From 9cd97f594a9c92b27fe6e62202323212042c93ac Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 7 Nov 2026 11:11:53 -0500 Subject: [PATCH 2/2] Cite section III for the other-adult rule in North Carolina's need test Co-Authored-By: Claude Opus 5.5 --- .../states/nc/ncdhhs/scca/entry/nc_scca_need_eligible.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/policyengine_us/variables/gov/states/nc/ncdhhs/scca/entry/nc_scca_need_eligible.py b/policyengine_us/variables/gov/states/nc/ncdhhs/scca/entry/nc_scca_need_eligible.py index 84327ec8e4a..9e2954e073a 100644 --- a/policyengine_us/variables/gov/states/nc/ncdhhs/scca/entry/nc_scca_need_eligible.py +++ b/policyengine_us/variables/gov/states/nc/ncdhhs/scca/entry/nc_scca_need_eligible.py @@ -46,10 +46,11 @@ def formula(spm_unit, period, parameters): "is_incapable_of_self_care", year ) has_need = employed | student | unable_to_provide_care - # Section III.A requires a need of both parents or responsible adults in the - # household, and another adult in the home doesn't block care, so the filers - # who claim an eligible child stand in for them. An incarcerated adult isn't - # in the household. + # Sections III and III.A require a need of both parents or responsible + # adults in the household. Under section III, another adult in the home + # blocks care only when willing and able to provide it, which the model + # can't see, so the filers who claim an eligible child stand in for the + # responsible adults. An incarcerated adult isn't in the household. eligible_child = person("nc_scca_child_eligible", period) claims_eligible_child = person.tax_unit.sum(eligible_child) > 0 responsible_adult = (