diff --git a/changelog_entry.yaml b/changelog_entry.yaml index e69de29bb2d..bf26a998a40 100644 --- a/changelog_entry.yaml +++ b/changelog_entry.yaml @@ -0,0 +1,4 @@ +- bump: minor + changes: + added: + - Implemented North Dakota Temporary Assistance for Needy Families (TANF) program diff --git a/policyengine_us/parameters/gov/states/nd/dhs/tanf/benefit/max_children.yaml b/policyengine_us/parameters/gov/states/nd/dhs/tanf/benefit/max_children.yaml new file mode 100644 index 00000000000..89add576ab3 --- /dev/null +++ b/policyengine_us/parameters/gov/states/nd/dhs/tanf/benefit/max_children.yaml @@ -0,0 +1,11 @@ +description: North Dakota limits the number of children at this amount for standard of need lookup under the Temporary Assistance for Needy Families program. +values: + 2015-10-01: 10 + +metadata: + unit: person + period: month + label: North Dakota TANF max children for standard of need + reference: + - title: North Dakota Policy Manual 400-19-110-05 Standard of Need Chart + href: https://www.nd.gov/dhs/policymanuals/40019/400_19_110_05.htm diff --git a/policyengine_us/parameters/gov/states/nd/dhs/tanf/benefit/standard_of_need.yaml b/policyengine_us/parameters/gov/states/nd/dhs/tanf/benefit/standard_of_need.yaml new file mode 100644 index 00000000000..b9f1e6b02cc --- /dev/null +++ b/policyengine_us/parameters/gov/states/nd/dhs/tanf/benefit/standard_of_need.yaml @@ -0,0 +1,190 @@ +description: North Dakota provides this amount as the standard of need under the Temporary Assistance for Needy Families program. +metadata: + label: North Dakota TANF standard of need + unit: currency-USD + period: month + breakdown: + - range(1, 3) + - range(1, 11) + breakdown_label: + - Number of caretakers + - Number of children + reference: + - title: North Dakota Policy Manual 400-19-110-05 Standard of Need Chart + href: https://www.nd.gov/dhs/policymanuals/40019/400_19_110_05.htm + - title: North Dakota Policy Manual 400-19-110-05 Standard of Need Chart (2024-10-01) + href: https://www.nd.gov/dhs/policymanuals/40019/Archive%20Documents/2025/Release%2025.1/400_19_110_05.htm + - title: North Dakota Policy Manual 400-19-110-05 Standard of Need Chart (2023-08-01) + href: https://www.nd.gov/dhs/policymanuals/40019/Archive%20Documents/2024%20-%20ML%203869/400_19_110_05.htm + - title: North Dakota Policy Manual 400-19-110-05 Standard of Need Chart (2015-10-01) + href: https://www.nd.gov/dhs/policymanuals/40019/Archive%20Documents/2023%20-%20ML%203740/400_19_110_05.htm + - title: N.D. Admin Code 75-02-01.2-35(1) - Authority for Standard of Need + href: https://www.law.cornell.edu/regulations/north-dakota/N-D-A-C-75-02-1.2-35 + +0: + 0: + 2015-10-01: 0 + 1: + 2015-10-01: 166 + 2023-08-01: 332 + 2024-10-01: 349 + 2025-10-01: 366 + 2: + 2015-10-01: 243 + 2023-08-01: 486 + 2024-10-01: 510 + 2025-10-01: 536 + 3: + 2015-10-01: 316 + 2023-08-01: 632 + 2024-10-01: 664 + 2025-10-01: 697 + 4: + 2015-10-01: 393 + 2023-08-01: 786 + 2024-10-01: 825 + 2025-10-01: 866 + 5: + 2015-10-01: 466 + 2023-08-01: 932 + 2024-10-01: 979 + 2025-10-01: 1_028 + 6: + 2015-10-01: 543 + 2023-08-01: 1_086 + 2024-10-01: 1_140 + 2025-10-01: 1_197 + 7: + 2015-10-01: 617 + 2023-08-01: 1_234 + 2024-10-01: 1_296 + 2025-10-01: 1_361 + 8: + 2015-10-01: 693 + 2023-08-01: 1_386 + 2024-10-01: 1_455 + 2025-10-01: 1_528 + 9: + 2015-10-01: 767 + 2023-08-01: 1_534 + 2024-10-01: 1_611 + 2025-10-01: 1_692 + 10: + 2015-10-01: 843 + 2023-08-01: 1_686 + 2024-10-01: 1_770 + 2025-10-01: 1_859 + +1: + 0: + 2015-10-01: 237 + 2023-08-01: 474 + 2024-10-01: 498 + 2025-10-01: 523 + 1: + 2015-10-01: 335 + 2023-08-01: 670 + 2024-10-01: 704 + 2025-10-01: 739 + 2: + 2015-10-01: 436 + 2023-08-01: 872 + 2024-10-01: 916 + 2025-10-01: 962 + 3: + 2015-10-01: 533 + 2023-08-01: 1_066 + 2024-10-01: 1_119 + 2025-10-01: 1_175 + 4: + 2015-10-01: 632 + 2023-08-01: 1_264 + 2024-10-01: 1_327 + 2025-10-01: 1_393 + 5: + 2015-10-01: 731 + 2023-08-01: 1_462 + 2024-10-01: 1_535 + 2025-10-01: 1_612 + 6: + 2015-10-01: 830 + 2023-08-01: 1_660 + 2024-10-01: 1_743 + 2025-10-01: 1_830 + 7: + 2015-10-01: 929 + 2023-08-01: 1_858 + 2024-10-01: 1_951 + 2025-10-01: 2_049 + 8: + 2015-10-01: 1_028 + 2023-08-01: 2_056 + 2024-10-01: 2_159 + 2025-10-01: 2_267 + 9: + 2015-10-01: 1_127 + 2023-08-01: 2_254 + 2024-10-01: 2_367 + 2025-10-01: 2_485 + 10: + 2015-10-01: 1_225 + 2023-08-01: 2_450 + 2024-10-01: 2_573 + 2025-10-01: 2_702 + +2: + 0: + 2015-10-01: 335 + 2023-08-01: 670 + 2024-10-01: 704 + 2025-10-01: 739 + 1: + 2015-10-01: 436 + 2023-08-01: 872 + 2024-10-01: 916 + 2025-10-01: 962 + 2: + 2015-10-01: 533 + 2023-08-01: 1_066 + 2024-10-01: 1_119 + 2025-10-01: 1_175 + 3: + 2015-10-01: 632 + 2023-08-01: 1_264 + 2024-10-01: 1_327 + 2025-10-01: 1_393 + 4: + 2015-10-01: 731 + 2023-08-01: 1_462 + 2024-10-01: 1_535 + 2025-10-01: 1_612 + 5: + 2015-10-01: 830 + 2023-08-01: 1_660 + 2024-10-01: 1_743 + 2025-10-01: 1_830 + 6: + 2015-10-01: 929 + 2023-08-01: 1_858 + 2024-10-01: 1_951 + 2025-10-01: 2_049 + 7: + 2015-10-01: 1_028 + 2023-08-01: 2_056 + 2024-10-01: 2_159 + 2025-10-01: 2_267 + 8: + 2015-10-01: 1_127 + 2023-08-01: 2_254 + 2024-10-01: 2_367 + 2025-10-01: 2_485 + 9: + 2015-10-01: 1_225 + 2023-08-01: 2_450 + 2024-10-01: 2_573 + 2025-10-01: 2_702 + 10: + 2015-10-01: 1_325 + 2023-08-01: 2_650 + 2024-10-01: 2_783 + 2025-10-01: 2_922 diff --git a/policyengine_us/parameters/gov/states/nd/dhs/tanf/income/deductions/standard_employment_expense/minimum.yaml b/policyengine_us/parameters/gov/states/nd/dhs/tanf/income/deductions/standard_employment_expense/minimum.yaml new file mode 100644 index 00000000000..236ff71b089 --- /dev/null +++ b/policyengine_us/parameters/gov/states/nd/dhs/tanf/income/deductions/standard_employment_expense/minimum.yaml @@ -0,0 +1,14 @@ +description: North Dakota excludes at least this amount from gross earned income as a standard employment expense allowance under the Temporary Assistance for Needy Families program. + +values: + 1997-07-01: 180 + +metadata: + unit: currency-USD + period: month + label: North Dakota TANF standard employment expense minimum + reference: + - title: North Dakota Policy Manual 400-19-105-25 Employment Disregards + href: https://www.nd.gov/dhs/policymanuals/40019/400_19_105_25.htm + - title: N.D. Admin Code 75-02-01.2-51(2)(a) Disregard of Earned Income + href: https://www.law.cornell.edu/regulations/north-dakota/N-D-A-C-75-02-1.2-51 diff --git a/policyengine_us/parameters/gov/states/nd/dhs/tanf/income/deductions/standard_employment_expense/rate.yaml b/policyengine_us/parameters/gov/states/nd/dhs/tanf/income/deductions/standard_employment_expense/rate.yaml new file mode 100644 index 00000000000..6ea2ab68032 --- /dev/null +++ b/policyengine_us/parameters/gov/states/nd/dhs/tanf/income/deductions/standard_employment_expense/rate.yaml @@ -0,0 +1,14 @@ +description: North Dakota excludes this share of gross earned income as a standard employment expense allowance under the Temporary Assistance for Needy Families program. + +values: + 1997-07-01: 0.27 + +metadata: + unit: /1 + period: month + label: North Dakota TANF standard employment expense rate + reference: + - title: North Dakota Policy Manual 400-19-105-25 Employment Disregards + href: https://www.nd.gov/dhs/policymanuals/40019/400_19_105_25.htm + - title: N.D. Admin Code 75-02-01.2-51(2)(a) Disregard of Earned Income + href: https://www.law.cornell.edu/regulations/north-dakota/N-D-A-C-75-02-1.2-51 diff --git a/policyengine_us/parameters/gov/states/nd/dhs/tanf/income/deductions/time_limited_percentage/rate.yaml b/policyengine_us/parameters/gov/states/nd/dhs/tanf/income/deductions/time_limited_percentage/rate.yaml new file mode 100644 index 00000000000..7319f791326 --- /dev/null +++ b/policyengine_us/parameters/gov/states/nd/dhs/tanf/income/deductions/time_limited_percentage/rate.yaml @@ -0,0 +1,36 @@ +description: North Dakota excludes this share of earned income as a time limited percentage disregard under the Temporary Assistance for Needy Families program. +# NOTE: This uses calendar month as a proxy for participation months. +# The disregard cycle resets each January (month 1). +# In reality, TLP tracks cumulative participation months, but PolicyEngine +# currently lacks the infrastructure to track state over time. + +brackets: + - threshold: + 1997-07-01: 1 + amount: + 1997-07-01: 0.5 # Months 1-6: 50% + - threshold: + 1997-07-01: 7 + amount: + 1997-07-01: 0.35 # Months 7-9: 35% + - threshold: + 1997-07-01: 10 + amount: + 1997-07-01: 0.25 # Months 10-12: 25% + # NOTE: Threshold 13 commented out because calendar month proxy only goes 1-12. + # In actual ND TANF policy, TLP ends after 12 months of participation (0% disregard). + # - threshold: + # 1997-07-01: 13 + # amount: + # 1997-07-01: 0.0 # Months 13+: 0% + +metadata: + type: single_amount + threshold_unit: month + amount_unit: /1 + label: North Dakota TANF time limited percentage rate + reference: + - title: North Dakota Policy Manual 400-19-105-25 Employment Disregards + href: https://www.nd.gov/dhs/policymanuals/40019/400_19_105_25.htm + - title: N.D. Admin Code 75-02-01.2-51(2)(c)(1) Disregard of Earned Income + href: https://www.law.cornell.edu/regulations/north-dakota/N-D-A-C-75-02-1.2-51 diff --git a/policyengine_us/parameters/gov/states/nd/dhs/tanf/resources/limit/base.yaml b/policyengine_us/parameters/gov/states/nd/dhs/tanf/resources/limit/base.yaml new file mode 100644 index 00000000000..517e9d829b4 --- /dev/null +++ b/policyengine_us/parameters/gov/states/nd/dhs/tanf/resources/limit/base.yaml @@ -0,0 +1,14 @@ +description: North Dakota limits resources to this amount based on household size under the Temporary Assistance for Needy Families program. +metadata: + label: North Dakota TANF resource limit base + unit: currency-USD + period: month + reference: + - title: North Dakota Policy Manual 400-19-55-05-05 Asset Limits + href: https://www.nd.gov/dhs/policymanuals/40019/400_19_55_05_05.htm + - title: N.D. Admin Code 75-02-01.2-22 Asset Limits + href: https://www.law.cornell.edu/regulations/north-dakota/N-D-A-C-75-02-1.2-22 +1: + 1997-07-01: 3_000 +2: + 1997-07-01: 6_000 diff --git a/policyengine_us/parameters/gov/states/nd/dhs/tanf/resources/limit/increment.yaml b/policyengine_us/parameters/gov/states/nd/dhs/tanf/resources/limit/increment.yaml new file mode 100644 index 00000000000..b79ccb69c63 --- /dev/null +++ b/policyengine_us/parameters/gov/states/nd/dhs/tanf/resources/limit/increment.yaml @@ -0,0 +1,14 @@ +description: North Dakota increases the resource limit by this amount for each person beyond two under the Temporary Assistance for Needy Families program. + +values: + 1997-07-01: 25 + +metadata: + unit: currency-USD + period: month + label: North Dakota TANF resource limit per additional person + reference: + - title: North Dakota Policy Manual 400-19-55-05-05 Asset Limits + href: https://www.nd.gov/dhs/policymanuals/40019/400_19_55_05_05.htm + - title: N.D. Admin Code 75-02-01.2-22 Asset Limits + href: https://www.law.cornell.edu/regulations/north-dakota/N-D-A-C-75-02-1.2-22 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/tanf/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/tanf/integration.yaml new file mode 100644 index 00000000000..52656cd03f6 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/tanf/integration.yaml @@ -0,0 +1,965 @@ +# North Dakota TANF Integration Tests +# Tests the complete benefit calculation pipeline +# +# Key Parameters (2024-01): +# - Standard Employment Expense: max(27% of gross earned, $180) +# - Time Limited Percentage (TLP) disregard: 50% of remaining earned income +# - Standard of Need: 2D table lookup by caretakers × children +# - Income Eligibility: Countable income < Standard of Need +# +# Source: https://www.nd.gov/dhs/policymanuals/40019/400_19_110_05.htm + +- name: Case 1, family of 3 with no income receives full benefit. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + person2: + age: 8 + person3: + age: 5 + spm_units: + spm_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: ND + output: + # 1 caretaker, 2 children → Standard of Need = $962 + nd_tanf_standard_of_need: 962 + nd_tanf_countable_earned_income: 0 + nd_tanf_countable_unearned_income: 0 + nd_tanf_countable_income: 0 + nd_tanf_income_eligible: true + nd_tanf_eligible: true + # Benefit = $962 - $0 = $962 + nd_tanf: 962 + +- name: Case 2, family of 3 with moderate earned income. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + # Monthly: $1,500; Annual: $18,000 + employment_income_before_lsr: 18_000 + person2: + age: 10 + person3: + age: 7 + spm_units: + spm_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: ND + output: + # 1 caretaker, 2 children → Standard of Need = $962 + nd_tanf_standard_of_need: 962 + # Gross monthly earned income: $18,000 / 12 = $1,500 + # Standard Employment Expense: max($1,500 * 0.27, $180) = max($405, $180) = $405 + # After SEE: $1,500 - $405 = $1,095 + # TLP disregard (50%): $1,095 * 0.50 = $547.50 + # Countable earned: $1,095 - $547.50 = $547.50 + nd_tanf_countable_earned_income: 547.50 + nd_tanf_countable_unearned_income: 0 + nd_tanf_countable_income: 547.50 + # Income eligible: $547.50 < $962 = true + nd_tanf_income_eligible: true + nd_tanf_eligible: true + # Benefit = $962 - $547.50 = $414.50 + nd_tanf: 414.50 + +- name: Case 3, family of 3 with earned and unearned income. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 28 + # Monthly earned: $1,000; Annual: $12,000 + employment_income_before_lsr: 12_000 + # Monthly unearned: $200; Annual: $2,400 + social_security_dependents: 2_400 + person2: + age: 6 + person3: + age: 4 + spm_units: + spm_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: ND + output: + # 1 caretaker, 2 children → Standard of Need = $962 + nd_tanf_standard_of_need: 962 + # Gross monthly earned income: $12,000 / 12 = $1,000 + # Standard Employment Expense: max($1,000 * 0.27, $180) = max($270, $180) = $270 + # After SEE: $1,000 - $270 = $730 + # TLP disregard (50%): $730 * 0.50 = $365 + # Countable earned: $730 - $365 = $365 + nd_tanf_countable_earned_income: 365 + # Countable unearned: $2,400 / 12 = $200 + nd_tanf_countable_unearned_income: 200 + # Total countable: $365 + $200 = $565 + nd_tanf_countable_income: 565 + # Income eligible: $565 < $962 = true + nd_tanf_income_eligible: true + nd_tanf_eligible: true + # Benefit = $962 - $565 = $397 + nd_tanf: 397 + +- name: Case 4, low earned income applies minimum $180 deduction. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 25 + # Monthly: $500; Annual: $6,000 + employment_income_before_lsr: 6_000 + person2: + age: 3 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: ND + output: + # 1 caretaker, 1 child → Standard of Need = $739 + nd_tanf_standard_of_need: 739 + # Gross monthly earned income: $6,000 / 12 = $500 + # Standard Employment Expense: max($500 * 0.27, $180) = max($135, $180) = $180 + # After SEE: $500 - $180 = $320 + # TLP disregard (50%): $320 * 0.50 = $160 + # Countable earned: $320 - $160 = $160 + nd_tanf_countable_earned_income: 160 + nd_tanf_countable_unearned_income: 0 + nd_tanf_countable_income: 160 + nd_tanf_income_eligible: true + nd_tanf_eligible: true + # Benefit = $739 - $160 = $579 + nd_tanf: 579 + +- name: Case 5, income resulting in low benefit but above minimum. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 32 + # Monthly: $1,400; Annual: $16,800 + employment_income_before_lsr: 16_800 + person2: + age: 2 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: ND + output: + # 1 caretaker, 1 child → Standard of Need = $739 + nd_tanf_standard_of_need: 739 + # Monthly earned: $16,800 / 12 = $1,400 + # SEE: max($1,400 * 0.27, $180) = max($378, $180) = $378 + # After SEE: $1,400 - $378 = $1,022 + # TLP: $1,022 * 0.50 = $511 + # Countable earned: $1,022 - $511 = $511 + nd_tanf_countable_earned_income: 511 + nd_tanf_countable_income: 511 + # Income eligible: $511 < $739 = true + nd_tanf_income_eligible: true + nd_tanf_eligible: true + # Benefit = $739 - $511 = $228 + nd_tanf: 228 + +- name: Case 6, income over eligibility limit results in zero benefit. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + # Monthly: $3,000; Annual: $36,000 + employment_income_before_lsr: 36_000 + person2: + age: 12 + person3: + age: 9 + spm_units: + spm_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: ND + output: + # 1 caretaker, 2 children → Standard of Need = $962 + nd_tanf_standard_of_need: 962 + # Monthly earned: $36,000 / 12 = $3,000 + # SEE: max($3,000 * 0.27, $180) = max($810, $180) = $810 + # After SEE: $3,000 - $810 = $2,190 + # TLP: $2,190 * 0.50 = $1,095 + # Countable earned: $2,190 - $1,095 = $1,095 + nd_tanf_countable_earned_income: 1_095 + nd_tanf_countable_income: 1_095 + # Income eligible: $1,095 < $962 = false + nd_tanf_income_eligible: false + nd_tanf_eligible: false + nd_tanf: 0 + +- name: Case 7, pregnant woman with unearned income only. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 22 + is_pregnant: true + # Monthly unearned: $400; Annual: $4,800 + social_security_dependents: 4_800 + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + state_code: ND + output: + # 1 caretaker, 0 children → Standard of Need = $523 + nd_tanf_standard_of_need: 523 + # No earned income, so no deductions + nd_tanf_countable_earned_income: 0 + # Countable unearned: $4,800 / 12 = $400 + nd_tanf_countable_unearned_income: 400 + nd_tanf_countable_income: 400 + # Income eligible: $400 < $523 = true + nd_tanf_income_eligible: true + nd_tanf_eligible: true + # Benefit = $523 - $400 = $123 + nd_tanf: 123 + +- name: Case 8, large household with moderate income. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + # Monthly: $2,000; Annual: $24,000 + employment_income_before_lsr: 24_000 + person2: + age: 43 + person3: + age: 17 + person4: + age: 15 + person5: + age: 12 + person6: + age: 9 + person7: + age: 6 + person8: + age: 3 + spm_units: + spm_unit: + members: [person1, person2, person3, person4, person5, person6, person7, person8] + households: + household: + members: [person1, person2, person3, person4, person5, person6, person7, person8] + state_code: ND + output: + # 2 caretakers, 6 children → Standard of Need = $2,049 + nd_tanf_standard_of_need: 2_049 + # Monthly earned: $24,000 / 12 = $2,000 + # SEE: max($2,000 * 0.27, $180) = max($540, $180) = $540 + # After SEE: $2,000 - $540 = $1,460 + # TLP: $1,460 * 0.50 = $730 + # Countable earned: $1,460 - $730 = $730 + nd_tanf_countable_earned_income: 730 + nd_tanf_countable_unearned_income: 0 + nd_tanf_countable_income: 730 + # Income eligible: $730 < $2,049 = true + nd_tanf_income_eligible: true + # Resource limit for 8 persons: $6,000 + $25 * 6 = $6,150 + nd_tanf_resources_eligible: true + nd_tanf_eligible: true + # Benefit = $2,049 - $730 = $1,319 + nd_tanf: 1_319 + +- name: Case 9, ineligible due to resources exceeding limit. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + person2: + age: 8 + spm_units: + spm_unit: + members: [person1, person2] + # Resources over limit ($6,000 for 2 persons) + spm_unit_assets: 6_500 + households: + household: + members: [person1, person2] + state_code: ND + output: + # 1 caretaker, 1 child → Standard of Need = $739 + nd_tanf_standard_of_need: 739 + nd_tanf_countable_income: 0 + nd_tanf_income_eligible: true + # Resource limit for 2 persons: $6,000 + # $6,500 > $6,000 = false + nd_tanf_resources_eligible: false + nd_tanf_eligible: false + nd_tanf: 0 + +- name: Case 10, very low income where deductions exceed gross. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 26 + # Monthly: $100; Annual: $1,200 + employment_income_before_lsr: 1_200 + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: ND + output: + # 1 caretaker, 1 child → Standard of Need = $739 + nd_tanf_standard_of_need: 739 + # Monthly gross: $1,200 / 12 = $100 + # SEE: max($100 * 0.27, $180) = max($27, $180) = $180 + # After SEE: max($100 - $180, 0) = $0 (clipped) + # TLP: $0 * 0.50 = $0 + # Countable earned: max($100 - $180 - $0, 0) = $0 + nd_tanf_countable_earned_income: 0 + nd_tanf_countable_unearned_income: 0 + nd_tanf_countable_income: 0 + nd_tanf_income_eligible: true + nd_tanf_eligible: true + # Benefit = $739 - $0 = $739 + nd_tanf: 739 + +- name: Case 11, small benefit with high unearned income. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + # Unearned income: $732 * 12 = $8,784 + social_security_dependents: 8_784 + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: ND + output: + # 1 caretaker, 1 child → Standard of Need = $739 + nd_tanf_standard_of_need: 739 + nd_tanf_countable_earned_income: 0 + # Countable unearned: $8,784 / 12 = $732 + nd_tanf_countable_unearned_income: 732 + nd_tanf_countable_income: 732 + # Income eligible: $732 < $739 = true + nd_tanf_income_eligible: true + nd_tanf_eligible: true + # Benefit = $739 - $732 = $7 + nd_tanf: 7 + +- name: Case 12, very small benefit with high unearned income. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 28 + # Unearned: $735 * 12 = $8,820 + social_security_dependents: 8_820 + person2: + age: 3 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: ND + output: + # 1 caretaker, 1 child → Standard of Need = $739 + nd_tanf_standard_of_need: 739 + nd_tanf_countable_earned_income: 0 + # Countable unearned: $8,820 / 12 = $735 + nd_tanf_countable_unearned_income: 735 + nd_tanf_countable_income: 735 + # Income eligible: $735 < $739 = true + nd_tanf_income_eligible: true + nd_tanf_eligible: true + # Benefit = $739 - $735 = $4 + nd_tanf: 4 + +- name: Case 13, two working parents (multiple earners). + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + # Monthly: $1,000; Annual: $12,000 + employment_income_before_lsr: 12_000 + person2: + age: 33 + # Monthly: $800; Annual: $9,600 + employment_income_before_lsr: 9_600 + person3: + age: 10 + person4: + age: 7 + spm_units: + spm_unit: + members: [person1, person2, person3, person4] + households: + household: + members: [person1, person2, person3, person4] + state_code: ND + output: + # 2 caretakers, 2 children → Standard of Need = $1,175 + nd_tanf_standard_of_need: 1_175 + # Person 1: $1,000/month + # SEE: max($1,000 * 0.27, $180) = max($270, $180) = $270 + # After SEE: $1,000 - $270 = $730 + # TLP: $730 * 0.50 = $365 + # Countable: $730 - $365 = $365 + # Person 2: $800/month + # SEE: max($800 * 0.27, $180) = max($216, $180) = $216 + # After SEE: $800 - $216 = $584 + # TLP: $584 * 0.50 = $292 + # Countable: $584 - $292 = $292 + # Total countable earned: $365 + $292 = $657 + nd_tanf_countable_earned_income: 657 + nd_tanf_countable_income: 657 + nd_tanf_income_eligible: true + nd_tanf_eligible: true + # Benefit = $1,175 - $657 = $518 + nd_tanf: 518 + +- name: Case 14, child-only household (zero caretakers). + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 16 + person2: + age: 14 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: ND + output: + # 0 caretakers, 2 children → Standard of Need = $536 + nd_tanf_standard_of_need: 536 + nd_tanf_countable_earned_income: 0 + nd_tanf_countable_income: 0 + nd_tanf_income_eligible: true + nd_tanf_eligible: true + # Benefit = $536 - $0 = $536 + nd_tanf: 536 + +- name: Case 15, three adults caps caretakers at two. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + person2: + age: 42 + person3: + age: 20 + person4: + age: 12 + person5: + age: 8 + spm_units: + spm_unit: + members: [person1, person2, person3, person4, person5] + households: + household: + members: [person1, person2, person3, person4, person5] + state_code: ND + output: + # 3 adults in household, but caretakers capped at 2 + # 2 caretakers (capped), 2 children → Standard of Need = $1,175 + nd_tanf_standard_of_need: 1_175 + nd_tanf_countable_income: 0 + nd_tanf_income_eligible: true + nd_tanf_eligible: true + # Benefit = $1,175 - $0 = $1,175 + nd_tanf: 1_175 + +- name: Case 16, eleven children caps at ten. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + person2: + age: 17 + person3: + age: 16 + person4: + age: 15 + person5: + age: 14 + person6: + age: 13 + person7: + age: 11 + person8: + age: 9 + person9: + age: 7 + person10: + age: 5 + person11: + age: 3 + person12: + age: 1 + spm_units: + spm_unit: + members: [person1, person2, person3, person4, person5, person6, person7, person8, person9, person10, person11, person12] + households: + household: + members: [person1, person2, person3, person4, person5, person6, person7, person8, person9, person10, person11, person12] + state_code: ND + output: + # 1 caretaker, 11 children in household, but children capped at 10 + # 1 caretaker, 10 children (capped) → Standard of Need = $2,702 + nd_tanf_standard_of_need: 2_702 + nd_tanf_countable_income: 0 + nd_tanf_income_eligible: true + nd_tanf_eligible: true + # Benefit = $2,702 - $0 = $2,702 + nd_tanf: 2_702 + +- name: Case 17, two low-income earners each get $180 minimum SEE. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + # Monthly: $500; Annual: $6,000 + employment_income_before_lsr: 6_000 + person2: + age: 33 + # Monthly: $500; Annual: $6,000 + employment_income_before_lsr: 6_000 + person3: + age: 10 + person4: + age: 7 + spm_units: + spm_unit: + members: [person1, person2, person3, person4] + households: + household: + members: [person1, person2, person3, person4] + state_code: ND + output: + # 2 caretakers, 2 children → Standard of Need = $1,175 + nd_tanf_standard_of_need: 1_175 + # Per-person calculation (each earner gets own $180 minimum): + # Person 1: $500/month + # SEE: max($500 * 0.27, $180) = max($135, $180) = $180 + # After SEE: $500 - $180 = $320 + # TLP: $320 * 0.50 = $160 + # Countable: $320 - $160 = $160 + # Person 2: $500/month + # SEE: max($500 * 0.27, $180) = max($135, $180) = $180 + # After SEE: $500 - $180 = $320 + # TLP: $320 * 0.50 = $160 + # Countable: $320 - $160 = $160 + # Total countable earned: $160 + $160 = $320 + # Note: If calculated at household level, would incorrectly be: + # SEE: max($1,000 * 0.27, $180) = $270 + # After: $730, TLP: $365, Countable: $365 + nd_tanf_countable_earned_income: 320 + nd_tanf_countable_income: 320 + nd_tanf_income_eligible: true + nd_tanf_eligible: true + # Benefit = $1,175 - $320 = $855 + nd_tanf: 855 + +- name: Case 18, child-only household with minor's earned income. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 17 + # Minor works part-time: $400/month = $4,800/year + employment_income_before_lsr: 4_800 + person2: + age: 14 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: ND + output: + # 0 caretakers, 2 children → Standard of Need = $536 + nd_tanf_standard_of_need: 536 + # Person 1 (17 y/o): $400/month + # SEE: max($400 * 0.27, $180) = max($108, $180) = $180 + # After SEE: $400 - $180 = $220 + # TLP: $220 * 0.50 = $110 + # Countable: $220 - $110 = $110 + nd_tanf_countable_earned_income: 110 + nd_tanf_countable_income: 110 + # $110 < $536 = true + nd_tanf_income_eligible: true + nd_tanf_eligible: true + # Benefit = $536 - $110 = $426 + nd_tanf: 426 + +- name: Case 19, three earners with varying incomes. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + # High earner: $2,000/month = $24,000/year + employment_income_before_lsr: 24_000 + person2: + age: 43 + # Medium earner: $800/month = $9,600/year + employment_income_before_lsr: 9_600 + person3: + age: 20 + # Low earner: $300/month = $3,600/year (uses $180 minimum) + employment_income_before_lsr: 3_600 + person4: + age: 10 + person5: + age: 7 + spm_units: + spm_unit: + members: [person1, person2, person3, person4, person5] + households: + household: + members: [person1, person2, person3, person4, person5] + state_code: ND + output: + # 3 caretakers capped at 2, 2 children → Standard = $1,175 + nd_tanf_standard_of_need: 1_175 + # Person 1: $2,000/mo + # SEE: max($540, $180) = $540 + # After: $1,460, TLP: $730, Countable: $730 + # Person 2: $800/mo + # SEE: max($216, $180) = $216 + # After: $584, TLP: $292, Countable: $292 + # Person 3: $300/mo + # SEE: max($81, $180) = $180 + # After: $120, TLP: $60, Countable: $60 + # Total: $730 + $292 + $60 = $1,082 + nd_tanf_countable_earned_income: 1_082 + nd_tanf_countable_income: 1_082 + # $1,082 < $1,175 = true + nd_tanf_income_eligible: true + nd_tanf_eligible: true + # Benefit = $1,175 - $1,082 = $93 + nd_tanf: 93 + +- name: Case 20, one earner with high income plus one with $1 income. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + # $2,000/month = $24,000/year + employment_income_before_lsr: 24_000 + person2: + age: 33 + # $1/month = $12/year (tests minimum clip to zero) + employment_income_before_lsr: 12 + person3: + age: 8 + spm_units: + spm_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: ND + output: + # 2 caretakers, 1 child → Standard of Need = $962 + nd_tanf_standard_of_need: 962 + # Person 1: $2,000/mo + # SEE: max($540, $180) = $540 + # After: $1,460, TLP: $730, Countable: $730 + # Person 2: $1/mo + # SEE: max($0.27, $180) = $180 + # After: max($1 - $180, 0) = $0 (clipped) + # TLP: $0, Countable: $0 + # Total countable earned: $730 + $0 = $730 + nd_tanf_countable_earned_income: 730 + nd_tanf_countable_income: 730 + # $730 < $962 = true + nd_tanf_income_eligible: true + nd_tanf_eligible: true + # Benefit = $962 - $730 = $232 + nd_tanf: 232 + +- name: Case 21, benefit of exactly one dollar. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + # Unearned: $738 * 12 = $8,856/year + social_security_dependents: 8_856 + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: ND + output: + # 1 caretaker, 1 child → Standard = $739 + nd_tanf_standard_of_need: 739 + nd_tanf_countable_earned_income: 0 + # Countable unearned: $8,856 / 12 = $738 + nd_tanf_countable_unearned_income: 738 + nd_tanf_countable_income: 738 + # $738 < $739 = true + nd_tanf_income_eligible: true + nd_tanf_eligible: true + # Benefit = $739 - $738 = $1 + nd_tanf: 1 + +- name: Case 22, child support exclusion reduces countable income. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 28 + # Social security: $300/month = $3,600/year + social_security_dependents: 3_600 + # Child support: $200/month = $2,400/year (excluded) + child_support_received: 2_400 + person2: + age: 6 + person3: + age: 4 + spm_units: + spm_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: ND + output: + # 1 caretaker, 2 children → Standard of Need = $962 + nd_tanf_standard_of_need: 962 + nd_tanf_countable_earned_income: 0 + # Gross unearned: $300 + $200 = $500 + # Less child support: $500 - $200 = $300 + nd_tanf_countable_unearned_income: 300 + nd_tanf_countable_income: 300 + # $300 < $962 = true + nd_tanf_income_eligible: true + nd_tanf_eligible: true + # Benefit = $962 - $300 = $662 + nd_tanf: 662 + +- name: Case 23, child support as only income results in full benefit. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 25 + # Only child support: $400/month = $4,800/year (fully excluded) + child_support_received: 4_800 + person2: + age: 3 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: ND + output: + # 1 caretaker, 1 child → Standard of Need = $739 + nd_tanf_standard_of_need: 739 + nd_tanf_countable_earned_income: 0 + # Child support is fully excluded + nd_tanf_countable_unearned_income: 0 + nd_tanf_countable_income: 0 + nd_tanf_income_eligible: true + nd_tanf_eligible: true + # Benefit = $739 - $0 = $739 (full benefit) + nd_tanf: 739 + +- name: Case 24, maximum possible benefit (2 caretakers, 10 children). + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + person2: + age: 43 + person3: + age: 17 + person4: + age: 16 + person5: + age: 15 + person6: + age: 14 + person7: + age: 12 + person8: + age: 10 + person9: + age: 8 + person10: + age: 6 + person11: + age: 4 + person12: + age: 2 + spm_units: + spm_unit: + members: [person1, person2, person3, person4, person5, person6, person7, person8, person9, person10, person11, person12] + households: + household: + members: [person1, person2, person3, person4, person5, person6, person7, person8, person9, person10, person11, person12] + state_code: ND + output: + # 2 caretakers, 10 children (capped) → Maximum Standard of Need = $2,922 + nd_tanf_standard_of_need: 2_922 + nd_tanf_countable_earned_income: 0 + nd_tanf_countable_unearned_income: 0 + nd_tanf_countable_income: 0 + nd_tanf_income_eligible: true + nd_tanf_eligible: true + # Maximum benefit = $2,922 - $0 = $2,922 + nd_tanf: 2_922 + +- name: Case 25, self-employment income included in earned income. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + # Self-employment: $1,200/month = $14,400/year + self_employment_income_before_lsr: 14_400 + person2: + age: 8 + person3: + age: 5 + spm_units: + spm_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: ND + output: + # 1 caretaker, 2 children → Standard of Need = $962 + nd_tanf_standard_of_need: 962 + # Gross self-employment: $14,400 / 12 = $1,200/month + # SEE: max($1,200 * 0.27, $180) = max($324, $180) = $324 + # After SEE: $1,200 - $324 = $876 + # TLP: $876 * 0.50 = $438 + # Countable earned: $876 - $438 = $438 + nd_tanf_countable_earned_income: 438 + nd_tanf_countable_unearned_income: 0 + nd_tanf_countable_income: 438 + # Income eligible: $438 < $962 = true + nd_tanf_income_eligible: true + nd_tanf_eligible: true + # Benefit = $962 - $438 = $524 + nd_tanf: 524 + +- name: Case 26, mixed employment and self-employment income. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 32 + # Employment: $800/month = $9,600/year + employment_income_before_lsr: 9_600 + # Self-employment: $400/month = $4,800/year + self_employment_income_before_lsr: 4_800 + person2: + age: 6 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: ND + output: + # 1 caretaker, 1 child → Standard of Need = $739 + nd_tanf_standard_of_need: 739 + # Total gross earned: ($9,600 + $4,800) / 12 = $1,200/month + # SEE: max($1,200 * 0.27, $180) = max($324, $180) = $324 + # After SEE: $1,200 - $324 = $876 + # TLP: $876 * 0.50 = $438 + # Countable earned: $876 - $438 = $438 + nd_tanf_countable_earned_income: 438 + nd_tanf_countable_unearned_income: 0 + nd_tanf_countable_income: 438 + # Income eligible: $438 < $739 = true + nd_tanf_income_eligible: true + nd_tanf_eligible: true + # Benefit = $739 - $438 = $301 + nd_tanf: 301 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/tanf/nd_tanf.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/tanf/nd_tanf.yaml new file mode 100644 index 00000000000..702d2a29ce3 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/tanf/nd_tanf.yaml @@ -0,0 +1,131 @@ +# North Dakota TANF Benefit Amount Unit Tests +# +# Benefit = max(Standard of Need - Countable Income, 0) +# +# NOTE: Tests must include people and proper entity structure for +# defined_for = StateCode.ND to work correctly. + +- name: Case 1, not eligible receives zero. + period: 2024-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + spm_units: + spm_unit: + members: [person1] + nd_tanf_eligible: false + nd_tanf_standard_of_need: 1_000 + nd_tanf_countable_income: 0 + households: + household: + members: [person1] + state_code: ND + output: + nd_tanf: 0 + +- name: Case 2, eligible with zero income receives full benefit. + period: 2024-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + spm_units: + spm_unit: + members: [person1] + nd_tanf_eligible: true + nd_tanf_standard_of_need: 1_035.83 + nd_tanf_countable_income: 0 + households: + household: + members: [person1] + state_code: ND + output: + # Benefit = $1,035.83 - $0 = $1,035.83 + nd_tanf: 1_035.83 + +- name: Case 3, eligible with partial income receives reduced benefit. + period: 2024-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + spm_units: + spm_unit: + members: [person1] + nd_tanf_eligible: true + nd_tanf_standard_of_need: 850 + nd_tanf_countable_income: 400 + households: + household: + members: [person1] + state_code: ND + output: + # Benefit = $850 - $400 = $450 + nd_tanf: 450 + +- name: Case 4, eligible with income at standard receives zero. + period: 2024-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + spm_units: + spm_unit: + members: [person1] + nd_tanf_eligible: true + nd_tanf_standard_of_need: 850 + nd_tanf_countable_income: 850 + households: + household: + members: [person1] + state_code: ND + output: + # Benefit = $850 - $850 = $0 + nd_tanf: 0 + +- name: Case 5, eligible with income above standard receives zero. + period: 2024-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + spm_units: + spm_unit: + members: [person1] + nd_tanf_eligible: true + nd_tanf_standard_of_need: 850 + nd_tanf_countable_income: 900 + households: + household: + members: [person1] + state_code: ND + output: + # Benefit = max($850 - $900, 0) = $0 + nd_tanf: 0 + +- name: Case 6, small benefit is issued. + period: 2024-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + spm_units: + spm_unit: + members: [person1] + nd_tanf_eligible: true + nd_tanf_standard_of_need: 850 + nd_tanf_countable_income: 845 + households: + household: + members: [person1] + state_code: ND + output: + # Benefit = $850 - $845 = $5 + nd_tanf: 5 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/tanf/nd_tanf_countable_earned_income_person.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/tanf/nd_tanf_countable_earned_income_person.yaml new file mode 100644 index 00000000000..a3e9c63de76 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/tanf/nd_tanf_countable_earned_income_person.yaml @@ -0,0 +1,136 @@ +- name: Case 1, no earnings. + period: 2024-01 + input: + state_code: ND + tanf_gross_earned_income: 0 + output: + nd_tanf_countable_earned_income_person: 0 + +- name: Case 2, low earnings where $180 minimum applies. + period: 2024-01 + input: + state_code: ND + tanf_gross_earned_income: 500 + output: + # Standard expense: max(500 * 0.27, 180) = max(135, 180) = 180 + # Earned after expense: 500 - 180 = 320 + # TLP deduction: 320 * 0.5 = 160 + # Countable: 320 - 160 = 160 + nd_tanf_countable_earned_income_person: 160 + +- name: Case 3, high earnings where 27% exceeds $180. + period: 2024-01 + input: + state_code: ND + tanf_gross_earned_income: 1_000 + output: + # Standard expense: max(1000 * 0.27, 180) = max(270, 180) = 270 + # Earned after expense: 1000 - 270 = 730 + # TLP deduction: 730 * 0.5 = 365 + # Countable: 730 - 365 = 365 + nd_tanf_countable_earned_income_person: 365 + +- name: Case 4, earnings at breakpoint where 27% equals $180. + period: 2024-01 + absolute_error_margin: 0.01 + input: + state_code: ND + # Breakpoint: 180 / 0.27 = 666.67 + tanf_gross_earned_income: 666.67 + output: + # Standard expense: max(666.67 * 0.27, 180) = max(180, 180) = 180 + # Earned after expense: 666.67 - 180 = 486.67 + # TLP deduction: 486.67 * 0.5 = 243.335 + # Countable: 486.67 - 243.335 = 243.335 + nd_tanf_countable_earned_income_person: 243.34 + +- name: Case 5, very small earnings where deductions exceed gross. + period: 2024-01 + input: + state_code: ND + # $50/month - deductions will exceed gross + tanf_gross_earned_income: 50 + output: + # SEE: max($50 * 0.27, $180) = max($13.50, $180) = $180 + # After SEE: max($50 - $180, 0) = $0 (clipped to zero) + # TLP: $0 * 0.50 = $0 + # Countable: $0 + nd_tanf_countable_earned_income_person: 0 + +- name: Case 6, one dollar of earnings. + period: 2024-01 + input: + state_code: ND + tanf_gross_earned_income: 1 + output: + # SEE: max($1 * 0.27, $180) = max($0.27, $180) = $180 + # After SEE: max($1 - $180, 0) = $0 (clipped) + # TLP: $0 * 0.50 = $0 + # Countable: $0 + nd_tanf_countable_earned_income_person: 0 + +# SEE Breakpoint boundary tests +# Breakpoint where 27% equals $180: $180 / 0.27 = $666.67 +# Below breakpoint: $180 minimum applies +# Above breakpoint: 27% applies + +- name: Case 7, just below SEE breakpoint ($665). + period: 2024-01 + absolute_error_margin: 0.01 + input: + state_code: ND + tanf_gross_earned_income: 665 + output: + # SEE: max($665 * 0.27, $180) = max($179.55, $180) = $180 + # After SEE: $665 - $180 = $485 + # TLP: $485 * 0.50 = $242.50 + # Countable: $485 - $242.50 = $242.50 + nd_tanf_countable_earned_income_person: 242.50 + +- name: Case 8, just above SEE breakpoint ($667). + period: 2024-01 + absolute_error_margin: 0.01 + input: + state_code: ND + tanf_gross_earned_income: 667 + output: + # SEE: max($667 * 0.27, $180) = max($180.09, $180) = $180.09 + # After SEE: $667 - $180.09 = $486.91 + # TLP: $486.91 * 0.50 = $243.455 + # Countable: $486.91 - $243.455 = $243.455 + nd_tanf_countable_earned_income_person: 243.46 + +- name: Case 9, well above SEE breakpoint ($700). + period: 2024-01 + absolute_error_margin: 0.01 + input: + state_code: ND + tanf_gross_earned_income: 700 + output: + # SEE: max($700 * 0.27, $180) = max($189, $180) = $189 + # After SEE: $700 - $189 = $511 + # TLP: $511 * 0.50 = $255.50 + # Countable: $511 - $255.50 = $255.50 + nd_tanf_countable_earned_income_person: 255.50 + +# NOTE: TLP declining rate tests (35% for Jul-Sep, 25% for Oct-Dec) cannot be +# tested in YAML because only 2024-01 or 2024 periods are supported. +# The bracket pattern is verified via Python simulation. +# TLP rates: Months 1-6: 50%, Months 7-9: 35%, Months 10-12: 25% + +- name: Case 10, yearly period applies TLP rate per calendar month. + period: 2018 + absolute_error_margin: 0.01 + input: + state_code: ND + tanf_gross_earned_income: 12_000 # $1,000/month + output: + # Per month: $1,000 gross + # SEE: max($1,000 * 0.27, $180) = $270 + # After SEE: $1,000 - $270 = $730 + # TLP rates applied per calendar month: + # Jan-Jun (6 mo): 50% → $730 - $365 = $365/mo → $2,190 + # Jul-Sep (3 mo): 35% → $730 - $255.5 = $474.5/mo → $1,423.5 + # Oct-Dec (3 mo): 25% → $730 - $182.5 = $547.5/mo → $1,642.5 + # Yearly total: $2,190 + $1,423.5 + $1,642.5 = $5,256 + nd_tanf_countable_earned_income_person: 5_256 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/tanf/nd_tanf_countable_unearned_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/tanf/nd_tanf_countable_unearned_income.yaml new file mode 100644 index 00000000000..a2bd9b9235e --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/tanf/nd_tanf_countable_unearned_income.yaml @@ -0,0 +1,115 @@ +# North Dakota TANF Countable Unearned Income Unit Tests +# +# Child support received assigned to Child Support Division is excluded +# from countable unearned income per NDAC 75-02-01.2-19. +# +# Source: https://www.nd.gov/dhs/policymanuals/40019/400_19_55_25.htm + +- name: Case 1, unearned income with no child support. + period: 2024-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + # $300/month unearned = $3,600/year + social_security_dependents: 3_600 + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + state_code: ND + output: + # $300 monthly unearned, no exclusion + nd_tanf_countable_unearned_income: 300 + +- name: Case 2, child support is fully excluded from unearned income. + period: 2024-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + # $500/month unearned = $6,000/year + social_security_dependents: 6_000 + # $200/month child support = $2,400/year + child_support_received: 2_400 + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + state_code: ND + output: + # Gross unearned includes child support: $500 + $200 = $700 + # Less child support exclusion: $700 - $200 = $500 + nd_tanf_countable_unearned_income: 500 + +- name: Case 3, child support is only unearned income source. + period: 2024-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + # Only child support: $300/month = $3,600/year + child_support_received: 3_600 + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + state_code: ND + output: + # Child support is excluded, result is $0 + nd_tanf_countable_unearned_income: 0 + +- name: Case 4, multiple unearned income sources with child support. + period: 2024-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + # $200/month SS dependents = $2,400/year + social_security_dependents: 2_400 + # $150/month child support = $1,800/year + child_support_received: 1_800 + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + state_code: ND + output: + # Gross unearned: $200 + $150 = $350 + # Less child support: $350 - $150 = $200 + nd_tanf_countable_unearned_income: 200 + +- name: Case 5, large child support amount excluded. + period: 2024-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + # $100/month unearned = $1,200/year + social_security_dependents: 1_200 + # $500/month child support = $6,000/year + child_support_received: 6_000 + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + state_code: ND + output: + # Gross unearned: $100 + $500 = $600 + # Less child support: $600 - $500 = $100 + nd_tanf_countable_unearned_income: 100 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/tanf/nd_tanf_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/tanf/nd_tanf_eligible.yaml new file mode 100644 index 00000000000..6ad700e349b --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/tanf/nd_tanf_eligible.yaml @@ -0,0 +1,152 @@ +# North Dakota TANF Eligibility Unit Tests +# +# Eligibility requires: +# 1. Has a dependent child (demographic eligibility) +# 2. Income eligible +# 3. Resources eligible (if implemented) + +- name: Case 1, eligible with dependent child and income eligible. + period: 2024-01 + input: + people: + person1: + age: 30 + person2: + age: 8 + is_person_demographic_tanf_eligible: true + spm_units: + spm_unit: + members: [person1, person2] + nd_tanf_income_eligible: true + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_tanf_eligible: true + +- name: Case 2, not eligible due to income. + period: 2024-01 + input: + people: + person1: + age: 35 + person2: + age: 10 + is_person_demographic_tanf_eligible: true + spm_units: + spm_unit: + members: [person1, person2] + nd_tanf_income_eligible: false + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_tanf_eligible: false + +- name: Case 3, pregnant woman eligible. + period: 2024-01 + input: + people: + person1: + age: 22 + is_pregnant: true + spm_units: + spm_unit: + members: [person1] + nd_tanf_income_eligible: true + households: + household: + members: [person1] + state_code: ND + output: + nd_tanf_eligible: true + +- name: Case 4, no dependent child and not pregnant is not eligible. + period: 2024-01 + input: + people: + person1: + age: 25 + is_pregnant: false + spm_units: + spm_unit: + members: [person1] + nd_tanf_income_eligible: true + households: + household: + members: [person1] + state_code: ND + output: + # No dependent child and not pregnant + nd_tanf_eligible: false + +- name: Case 5, ineligible due to immigration status. + period: 2024-01 + input: + people: + person1: + age: 30 + is_citizen_or_legal_immigrant: false + person2: + age: 8 + is_citizen_or_legal_immigrant: false + spm_units: + spm_unit: + members: [person1, person2] + nd_tanf_income_eligible: true + nd_tanf_resources_eligible: true + households: + household: + members: [person1, person2] + state_code: ND + output: + # No one meets immigration requirements + nd_tanf_eligible: false + +- name: Case 6, mixed citizenship household still eligible. + period: 2024-01 + input: + people: + person1: + age: 32 + is_citizen_or_legal_immigrant: false + person2: + age: 10 + is_citizen_or_legal_immigrant: true + spm_units: + spm_unit: + members: [person1, person2] + nd_tanf_income_eligible: true + nd_tanf_resources_eligible: true + households: + household: + members: [person1, person2] + state_code: ND + output: + # At least one person (child) meets immigration requirements + nd_tanf_eligible: true + +- name: Case 7, legal permanent resident is eligible. + period: 2024-01 + input: + people: + person1: + age: 28 + immigration_status: LEGAL_PERMANENT_RESIDENT + person2: + age: 6 + immigration_status: CITIZEN + spm_units: + spm_unit: + members: [person1, person2] + nd_tanf_income_eligible: true + nd_tanf_resources_eligible: true + households: + household: + members: [person1, person2] + state_code: ND + output: + # Both parent (LPR) and child (citizen) meet immigration requirements + nd_tanf_eligible: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/tanf/nd_tanf_income_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/tanf/nd_tanf_income_eligible.yaml new file mode 100644 index 00000000000..f3c151fde0e --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/tanf/nd_tanf_income_eligible.yaml @@ -0,0 +1,103 @@ +# North Dakota TANF Income Eligibility Unit Tests +# +# Income eligible when: Countable Income < Standard of Need (strict less than) +# Standard of Need: 2D table lookup by caretakers × children +# Source: https://www.nd.gov/dhs/policymanuals/40019/400_19_110_05.htm +# +# NOTE: Tests must include people and proper entity structure for +# defined_for = StateCode.ND to work correctly. + +- name: Case 1, zero income is eligible. + period: 2024-01 + input: + people: + person1: + age: 30 + spm_units: + spm_unit: + members: [person1] + nd_tanf_countable_income: 0 + nd_tanf_standard_of_need: 850 + households: + household: + members: [person1] + state_code: ND + output: + # $0 < $850 = true + nd_tanf_income_eligible: true + +- name: Case 2, income one dollar below standard of need is eligible. + period: 2024-01 + input: + people: + person1: + age: 30 + spm_units: + spm_unit: + members: [person1] + nd_tanf_countable_income: 849 + nd_tanf_standard_of_need: 850 + households: + household: + members: [person1] + state_code: ND + output: + # $849 < $850 = true (just under threshold) + nd_tanf_income_eligible: true + +- name: Case 3, income exactly at standard of need is not eligible. + period: 2024-01 + input: + people: + person1: + age: 30 + spm_units: + spm_unit: + members: [person1] + nd_tanf_countable_income: 850 + nd_tanf_standard_of_need: 850 + households: + household: + members: [person1] + state_code: ND + output: + # $850 < $850 = false (at threshold - strict less than) + nd_tanf_income_eligible: false + +- name: Case 4, income one dollar above standard of need is not eligible. + period: 2024-01 + input: + people: + person1: + age: 30 + spm_units: + spm_unit: + members: [person1] + nd_tanf_countable_income: 851 + nd_tanf_standard_of_need: 850 + households: + household: + members: [person1] + state_code: ND + output: + # $851 < $850 = false (just over threshold) + nd_tanf_income_eligible: false + +- name: Case 5, income significantly above standard of need. + period: 2024-01 + input: + people: + person1: + age: 30 + spm_units: + spm_unit: + members: [person1] + nd_tanf_countable_income: 1_500 + nd_tanf_standard_of_need: 1_000 + households: + household: + members: [person1] + state_code: ND + output: + # $1,500 < $1,000 = false + nd_tanf_income_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/tanf/nd_tanf_resources_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/tanf/nd_tanf_resources_eligible.yaml new file mode 100644 index 00000000000..f1befcd02f0 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/tanf/nd_tanf_resources_eligible.yaml @@ -0,0 +1,239 @@ +# North Dakota TANF Resource Eligibility Unit Tests +# +# Resource Limits: +# - 1 person: $3,000 +# - 2 persons: $6,000 +# - 3+ persons: $6,000 + $25 per additional person beyond 2 + +- name: Case 1, one person at resource limit. + period: 2024-01 + input: + people: + person1: + age: 25 + is_pregnant: true + spm_units: + spm_unit: + members: [person1] + spm_unit_assets: 3_000 + households: + household: + members: [person1] + state_code: ND + output: + # $3,000 <= $3,000 = true + nd_tanf_resources_eligible: true + +- name: Case 2, one person over resource limit. + period: 2024-01 + input: + people: + person1: + age: 22 + is_pregnant: true + spm_units: + spm_unit: + members: [person1] + spm_unit_assets: 3_001 + households: + household: + members: [person1] + state_code: ND + output: + # $3,001 > $3,000 = false + nd_tanf_resources_eligible: false + +- name: Case 3, two persons at resource limit. + period: 2024-01 + input: + people: + person1: + age: 30 + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_assets: 6_000 + households: + household: + members: [person1, person2] + state_code: ND + output: + # $6,000 <= $6,000 = true + nd_tanf_resources_eligible: true + +- name: Case 4, two persons over resource limit. + period: 2024-01 + input: + people: + person1: + age: 28 + person2: + age: 3 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_assets: 6_001 + households: + household: + members: [person1, person2] + state_code: ND + output: + # $6,001 > $6,000 = false + nd_tanf_resources_eligible: false + +- name: Case 5, four persons at incremental limit. + period: 2024-01 + input: + people: + person1: + age: 35 + person2: + age: 10 + person3: + age: 8 + person4: + age: 5 + spm_units: + spm_unit: + members: [person1, person2, person3, person4] + spm_unit_assets: 6_050 + households: + household: + members: [person1, person2, person3, person4] + state_code: ND + output: + # Limit: $6,000 + $25 * (4-2) = $6,050 + # $6,050 <= $6,050 = true + nd_tanf_resources_eligible: true + +- name: Case 6, four persons over incremental limit. + period: 2024-01 + input: + people: + person1: + age: 32 + person2: + age: 7 + person3: + age: 4 + person4: + age: 2 + spm_units: + spm_unit: + members: [person1, person2, person3, person4] + spm_unit_assets: 6_051 + households: + household: + members: [person1, person2, person3, person4] + state_code: ND + output: + # Limit: $6,000 + $25 * (4-2) = $6,050 + # $6,051 > $6,050 = false + nd_tanf_resources_eligible: false + +# One-dollar-below boundary tests + +- name: Case 7, one person one dollar below resource limit. + period: 2024-01 + input: + people: + person1: + age: 24 + is_pregnant: true + spm_units: + spm_unit: + members: [person1] + spm_unit_assets: 2_999 + households: + household: + members: [person1] + state_code: ND + output: + # $2,999 <= $3,000 = true + nd_tanf_resources_eligible: true + +- name: Case 8, two persons one dollar below resource limit. + period: 2024-01 + input: + people: + person1: + age: 29 + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_assets: 5_999 + households: + household: + members: [person1, person2] + state_code: ND + output: + # $5,999 <= $6,000 = true + nd_tanf_resources_eligible: true + +- name: Case 9, four persons one dollar below incremental limit. + period: 2024-01 + input: + people: + person1: + age: 33 + person2: + age: 9 + person3: + age: 6 + person4: + age: 3 + spm_units: + spm_unit: + members: [person1, person2, person3, person4] + spm_unit_assets: 6_049 + households: + household: + members: [person1, person2, person3, person4] + state_code: ND + output: + # Limit: $6,000 + $25 * (4-2) = $6,050 + # $6,049 <= $6,050 = true + nd_tanf_resources_eligible: true + +# Large household test + +- name: Case 10, ten persons tests large household increment. + period: 2024-01 + input: + people: + person1: + age: 40 + person2: + age: 38 + person3: + age: 17 + person4: + age: 15 + person5: + age: 13 + person6: + age: 11 + person7: + age: 9 + person8: + age: 7 + person9: + age: 5 + person10: + age: 3 + spm_units: + spm_unit: + members: [person1, person2, person3, person4, person5, person6, person7, person8, person9, person10] + spm_unit_assets: 6_200 + households: + household: + members: [person1, person2, person3, person4, person5, person6, person7, person8, person9, person10] + state_code: ND + output: + # Limit: $6,000 + $25 * (10-2) = $6,000 + $200 = $6,200 + # $6,200 <= $6,200 = true + nd_tanf_resources_eligible: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/tanf/nd_tanf_standard_of_need.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/tanf/nd_tanf_standard_of_need.yaml new file mode 100644 index 00000000000..b0ba8b2c78e --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/dhs/tanf/nd_tanf_standard_of_need.yaml @@ -0,0 +1,342 @@ +# North Dakota TANF Standard of Need Unit Tests +# +# The Standard of Need is determined by a 2D table lookup based on: +# - Number of caretakers (adults aged 18+) +# - Number of children (under 18) +# +# Source: https://www.nd.gov/dhs/policymanuals/40019/400_19_110_05.htm + +- name: Case 1, single adult no children. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 25 + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + state_code: ND + output: + # 1 caretaker, 0 children → $523 + nd_tanf_standard_of_need: 523 + +- name: Case 2, one adult with one child. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: ND + output: + # 1 caretaker, 1 child → $739 + nd_tanf_standard_of_need: 739 + +- name: Case 3, one adult with two children. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + person2: + age: 10 + person3: + age: 8 + spm_units: + spm_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: ND + output: + # 1 caretaker, 2 children → $962 + nd_tanf_standard_of_need: 962 + +- name: Case 4, two adults with two children. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + person2: + age: 38 + person3: + age: 12 + person4: + age: 9 + spm_units: + spm_unit: + members: [person1, person2, person3, person4] + households: + household: + members: [person1, person2, person3, person4] + state_code: ND + output: + # 2 caretakers, 2 children → $1,175 + nd_tanf_standard_of_need: 1_175 + +- name: Case 5, two adults with three children. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + person2: + age: 33 + person3: + age: 14 + person4: + age: 11 + person5: + age: 7 + spm_units: + spm_unit: + members: [person1, person2, person3, person4, person5] + households: + household: + members: [person1, person2, person3, person4, person5] + state_code: ND + output: + # 2 caretakers, 3 children → $1,393 + nd_tanf_standard_of_need: 1_393 + +- name: Case 6, two adults with four children. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 42 + person2: + age: 40 + person3: + age: 16 + person4: + age: 13 + person5: + age: 10 + person6: + age: 6 + spm_units: + spm_unit: + members: [person1, person2, person3, person4, person5, person6] + households: + household: + members: [person1, person2, person3, person4, person5, person6] + state_code: ND + output: + # 2 caretakers, 4 children → $1,612 + nd_tanf_standard_of_need: 1_612 + +- name: Case 7, two adults with six children. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + person2: + age: 43 + person3: + age: 17 + person4: + age: 15 + person5: + age: 12 + person6: + age: 9 + person7: + age: 6 + person8: + age: 3 + spm_units: + spm_unit: + members: [person1, person2, person3, person4, person5, person6, person7, person8] + households: + household: + members: [person1, person2, person3, person4, person5, person6, person7, person8] + state_code: ND + output: + # 2 caretakers, 6 children → $2,049 + nd_tanf_standard_of_need: 2_049 + +- name: Case 8, child-only case no caretakers. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 15 + person2: + age: 12 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: ND + output: + # 0 caretakers, 2 children → $536 + nd_tanf_standard_of_need: 536 + +# Age boundary tests at 17/18 + +- name: Case 9, person exactly at age 18 counted as caretaker. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 18 + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: ND + output: + # 18 >= 18 = true, so person1 is caretaker + # 1 caretaker, 1 child → $739 + nd_tanf_standard_of_need: 739 + +- name: Case 10, person at age 17 counted as child not caretaker. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 17 + person2: + age: 5 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: ND + output: + # 17 >= 18 = false, so person1 is child + # 0 caretakers, 2 children → $536 + nd_tanf_standard_of_need: 536 + +# Zero caretaker edge cases + +- name: Case 11, zero caretakers with one child. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 15 + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + state_code: ND + output: + # 0 caretakers, 1 child → $366 + nd_tanf_standard_of_need: 366 + +- name: Case 12, zero caretakers with ten children at max. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 17 + person2: + age: 16 + person3: + age: 15 + person4: + age: 14 + person5: + age: 13 + person6: + age: 12 + person7: + age: 11 + person8: + age: 10 + person9: + age: 9 + person10: + age: 8 + spm_units: + spm_unit: + members: [person1, person2, person3, person4, person5, person6, person7, person8, person9, person10] + households: + household: + members: [person1, person2, person3, person4, person5, person6, person7, person8, person9, person10] + state_code: ND + output: + # 0 caretakers, 10 children → max in caretakers_0 table = $1,859 + nd_tanf_standard_of_need: 1_859 + +- name: Case 13, three caretakers and eleven children both cap. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 50 + person2: + age: 48 + person3: + age: 25 + person4: + age: 17 + person5: + age: 16 + person6: + age: 15 + person7: + age: 14 + person8: + age: 13 + person9: + age: 12 + person10: + age: 11 + person11: + age: 10 + person12: + age: 9 + person13: + age: 8 + person14: + age: 7 + spm_units: + spm_unit: + members: [person1, person2, person3, person4, person5, person6, person7, person8, person9, person10, person11, person12, person13, person14] + households: + household: + members: [person1, person2, person3, person4, person5, person6, person7, person8, person9, person10, person11, person12, person13, person14] + state_code: ND + output: + # 3 caretakers capped at 2, 11 children capped at 10 + # 2 caretakers (capped), 10 children (capped) → $2,922 + nd_tanf_standard_of_need: 2_922 diff --git a/policyengine_us/variables/gov/states/nd/dhs/tanf/eligibility/nd_tanf_eligible.py b/policyengine_us/variables/gov/states/nd/dhs/tanf/eligibility/nd_tanf_eligible.py new file mode 100644 index 00000000000..e55094c4e79 --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/tanf/eligibility/nd_tanf_eligible.py @@ -0,0 +1,24 @@ +from policyengine_us.model_api import * + + +class nd_tanf_eligible(Variable): + value_type = bool + entity = SPMUnit + label = "Eligible for North Dakota TANF" + definition_period = MONTH + reference = "https://www.nd.gov/dhs/policymanuals/40019/400_19_110_15.htm" + defined_for = StateCode.ND + + def formula(spm_unit, period, parameters): + demographic_eligible = spm_unit("is_demographic_tanf_eligible", period) + immigration_eligible = ( + add(spm_unit, period, ["is_citizen_or_legal_immigrant"]) > 0 + ) + income_eligible = spm_unit("nd_tanf_income_eligible", period) + resources_eligible = spm_unit("nd_tanf_resources_eligible", period) + return ( + demographic_eligible + & immigration_eligible + & income_eligible + & resources_eligible + ) diff --git a/policyengine_us/variables/gov/states/nd/dhs/tanf/eligibility/nd_tanf_income_eligible.py b/policyengine_us/variables/gov/states/nd/dhs/tanf/eligibility/nd_tanf_income_eligible.py new file mode 100644 index 00000000000..437c3bb576e --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/tanf/eligibility/nd_tanf_income_eligible.py @@ -0,0 +1,15 @@ +from policyengine_us.model_api import * + + +class nd_tanf_income_eligible(Variable): + value_type = bool + entity = SPMUnit + label = "Eligible for North Dakota TANF due to income" + definition_period = MONTH + reference = "https://www.nd.gov/dhs/policymanuals/40019/400_19_110_15.htm" + defined_for = StateCode.ND + + def formula(spm_unit, period, parameters): + countable_income = spm_unit("nd_tanf_countable_income", period) + standard_of_need = spm_unit("nd_tanf_standard_of_need", period) + return countable_income < standard_of_need diff --git a/policyengine_us/variables/gov/states/nd/dhs/tanf/eligibility/nd_tanf_resources_eligible.py b/policyengine_us/variables/gov/states/nd/dhs/tanf/eligibility/nd_tanf_resources_eligible.py new file mode 100644 index 00000000000..7629572a558 --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/tanf/eligibility/nd_tanf_resources_eligible.py @@ -0,0 +1,21 @@ +from policyengine_us.model_api import * + + +class nd_tanf_resources_eligible(Variable): + value_type = bool + entity = SPMUnit + label = "Eligible for North Dakota TANF due to resources" + definition_period = MONTH + reference = ( + "https://www.nd.gov/dhs/policymanuals/40019/400_19_55_05_10.htm" + ) + defined_for = StateCode.ND + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.nd.dhs.tanf.resources.limit + resources = spm_unit("spm_unit_assets", period.this_year) + unit_size = spm_unit("spm_unit_size", period.this_year) + capped_size = min_(unit_size, 2) + base_limit = p.base[capped_size] + additional = max_(unit_size - 2, 0) * p.increment + return resources <= base_limit + additional diff --git a/policyengine_us/variables/gov/states/nd/dhs/tanf/income/nd_tanf_countable_earned_income.py b/policyengine_us/variables/gov/states/nd/dhs/tanf/income/nd_tanf_countable_earned_income.py new file mode 100644 index 00000000000..1b69d51325c --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/tanf/income/nd_tanf_countable_earned_income.py @@ -0,0 +1,16 @@ +from policyengine_us.model_api import * + + +class nd_tanf_countable_earned_income(Variable): + value_type = float + entity = SPMUnit + label = "North Dakota TANF countable earned income" + unit = USD + definition_period = MONTH + reference = ( + "https://www.nd.gov/dhs/policymanuals/40019/400_19_110_20.htm", + "https://www.law.cornell.edu/regulations/north-dakota/N-D-A-C-75-02-1.2-51", + ) + defined_for = StateCode.ND + + adds = ["nd_tanf_countable_earned_income_person"] diff --git a/policyengine_us/variables/gov/states/nd/dhs/tanf/income/nd_tanf_countable_earned_income_person.py b/policyengine_us/variables/gov/states/nd/dhs/tanf/income/nd_tanf_countable_earned_income_person.py new file mode 100644 index 00000000000..60ad5b84ca2 --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/tanf/income/nd_tanf_countable_earned_income_person.py @@ -0,0 +1,38 @@ +from policyengine_us.model_api import * + + +class nd_tanf_countable_earned_income_person(Variable): + value_type = float + entity = Person + label = "North Dakota TANF countable earned income per person" + unit = USD + definition_period = MONTH + reference = ( + "https://www.nd.gov/dhs/policymanuals/40019/400_19_105_25.htm", + "https://www.law.cornell.edu/regulations/north-dakota/N-D-A-C-75-02-1.2-51", + ) + defined_for = StateCode.ND + + def formula(person, period, parameters): + p = parameters(period).gov.states.nd.dhs.tanf.income.deductions + gross_earned = person("tanf_gross_earned_income", period) + + # Standard Employment Expense: max(27%, $180) per person with earnings + has_earnings = gross_earned > 0 + percentage_amount = gross_earned * p.standard_employment_expense.rate + standard_expense = where( + has_earnings, + max_(percentage_amount, p.standard_employment_expense.minimum), + 0, + ) + + # Time Limited Percentage (TLP): declining rate based on calendar month proxy + # Uses calendar month (1-12) as proxy for participation months. + # In reality, TLP tracks cumulative participation months and ends after 12. + # Months 1-6: 50%, Months 7-9: 35%, Months 10-12: 25% + earned_after_expense = max_(gross_earned - standard_expense, 0) + month = period.start.month + tlp_rate = p.time_limited_percentage.rate.calc(month) + tlp_deduction = earned_after_expense * tlp_rate + + return max_(earned_after_expense - tlp_deduction, 0) diff --git a/policyengine_us/variables/gov/states/nd/dhs/tanf/income/nd_tanf_countable_income.py b/policyengine_us/variables/gov/states/nd/dhs/tanf/income/nd_tanf_countable_income.py new file mode 100644 index 00000000000..1a6520a00af --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/tanf/income/nd_tanf_countable_income.py @@ -0,0 +1,16 @@ +from policyengine_us.model_api import * + + +class nd_tanf_countable_income(Variable): + value_type = float + entity = SPMUnit + label = "North Dakota TANF countable income" + unit = USD + definition_period = MONTH + reference = "https://www.nd.gov/dhs/policymanuals/40019/400_19_110_20.htm" + defined_for = StateCode.ND + + adds = [ + "nd_tanf_countable_earned_income", + "nd_tanf_countable_unearned_income", + ] diff --git a/policyengine_us/variables/gov/states/nd/dhs/tanf/income/nd_tanf_countable_unearned_income.py b/policyengine_us/variables/gov/states/nd/dhs/tanf/income/nd_tanf_countable_unearned_income.py new file mode 100644 index 00000000000..200937c2666 --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/tanf/income/nd_tanf_countable_unearned_income.py @@ -0,0 +1,17 @@ +from policyengine_us.model_api import * + + +class nd_tanf_countable_unearned_income(Variable): + value_type = float + entity = SPMUnit + label = "North Dakota TANF countable unearned income" + unit = USD + definition_period = MONTH + reference = "https://www.nd.gov/dhs/policymanuals/40019/400_19_55_25.htm" + defined_for = StateCode.ND + + # Child support received assigned to Child Support Division is excluded. + # Result cannot be negative since child_support_received is a component + # of tanf_gross_unearned_income. + adds = ["tanf_gross_unearned_income"] + subtracts = ["child_support_received"] diff --git a/policyengine_us/variables/gov/states/nd/dhs/tanf/nd_tanf.py b/policyengine_us/variables/gov/states/nd/dhs/tanf/nd_tanf.py new file mode 100644 index 00000000000..8060f04c49b --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/tanf/nd_tanf.py @@ -0,0 +1,16 @@ +from policyengine_us.model_api import * + + +class nd_tanf(Variable): + value_type = float + entity = SPMUnit + label = "North Dakota Temporary Assistance for Needy Families" + unit = USD + definition_period = MONTH + reference = "https://www.nd.gov/dhs/policymanuals/40019/400_19_110_20.htm" + defined_for = "nd_tanf_eligible" + + def formula(spm_unit, period, parameters): + standard_of_need = spm_unit("nd_tanf_standard_of_need", period) + countable_income = spm_unit("nd_tanf_countable_income", period) + return max_(standard_of_need - countable_income, 0) diff --git a/policyengine_us/variables/gov/states/nd/dhs/tanf/nd_tanf_standard_of_need.py b/policyengine_us/variables/gov/states/nd/dhs/tanf/nd_tanf_standard_of_need.py new file mode 100644 index 00000000000..983c7ea904a --- /dev/null +++ b/policyengine_us/variables/gov/states/nd/dhs/tanf/nd_tanf_standard_of_need.py @@ -0,0 +1,33 @@ +from policyengine_us.model_api import * + + +class nd_tanf_standard_of_need(Variable): + value_type = float + entity = SPMUnit + label = "North Dakota TANF standard of need" + unit = USD + definition_period = MONTH + reference = "https://www.nd.gov/dhs/policymanuals/40019/400_19_110_05.htm" + defined_for = StateCode.ND + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.nd.dhs.tanf.benefit + person = spm_unit.members + + # Children: dependents who meet TANF demographic eligibility + is_dependent = person("is_tax_unit_dependent", period) + is_tanf_eligible = person( + "is_person_demographic_tanf_eligible", period + ) + is_child = is_dependent & is_tanf_eligible + child_count = spm_unit.sum(is_child) + + # Caretakers: head or spouse of tax unit + is_caretaker = person("is_tax_unit_head_or_spouse", period) + caretaker_count = spm_unit.sum(is_caretaker) + + # Cap to match 400-19-110-05 table structure (0-2 caretakers, 0-10 children) + caretaker_count_capped = min_(caretaker_count, 2).astype(int) + child_count_capped = min_(child_count, p.max_children).astype(int) + + return p.standard_of_need[caretaker_count_capped][child_count_capped]