From ad32567af2f98b830df8187882c55be6c537f2ff Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 06:30:45 +0000 Subject: [PATCH 01/14] Exclude SC TANF dependent children's earnings, deduct child support paid, and round to whole dollars TANF Policy Manual 7.5 excludes a dependent child's earned income from eligibility and the benefit, so it now leaves both income tests. 8.11 and 8.16 D/F deduct child support benefit group members pay outside the home after the 8.12 disregards, earned income first, never below zero, from the net test and the benefit only. 114-1140(A)(4) and the 8.15 table notes drop the cents of the need standard, the gross income limit and the payment, before the $10 minimum test. Co-Authored-By: Claude Opus 5.5 --- ...ld-earnings-support-whole-dollars.fixed.md | 1 + .../gov/states/sc/tanf/integration.yaml | 319 ++++++++++++++---- .../baseline/gov/states/sc/tanf/sc_tanf.yaml | 166 +++++++-- .../sc_tanf_countable_income_eligible.yaml | 14 +- .../sc/tanf/sc_tanf_dependent_child.yaml | 87 +++++ .../tanf/sc_tanf_gross_income_eligible.yaml | 209 +++++++++++- .../states/sc/tanf/sc_tanf_need_standard.yaml | 103 +++++- ..._tanf_child_earnings_support_invariants.py | 275 +++++++++++++++ ...test_sc_tanf_unearned_income_invariants.py | 22 +- .../sc_tanf_gross_income_eligible.py | 6 +- .../income/sc_tanf_child_support_deduction.py | 39 +++ .../tanf/income/sc_tanf_countable_income.py | 19 +- .../income/sc_tanf_gross_earned_income.py | 14 +- .../variables/gov/states/sc/tanf/sc_tanf.py | 7 +- .../states/sc/tanf/sc_tanf_dependent_child.py | 34 ++ .../states/sc/tanf/sc_tanf_need_standard.py | 6 +- 16 files changed, 1197 insertions(+), 124 deletions(-) create mode 100644 changelog.d/sc-tanf-child-earnings-support-whole-dollars.fixed.md create mode 100644 policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_dependent_child.yaml create mode 100644 policyengine_us/tests/test_sc_tanf_child_earnings_support_invariants.py create mode 100644 policyengine_us/variables/gov/states/sc/tanf/income/sc_tanf_child_support_deduction.py create mode 100644 policyengine_us/variables/gov/states/sc/tanf/sc_tanf_dependent_child.py diff --git a/changelog.d/sc-tanf-child-earnings-support-whole-dollars.fixed.md b/changelog.d/sc-tanf-child-earnings-support-whole-dollars.fixed.md new file mode 100644 index 00000000000..e6286def81c --- /dev/null +++ b/changelog.d/sc-tanf-child-earnings-support-whole-dollars.fixed.md @@ -0,0 +1 @@ +Exclude dependent children's earned income from South Carolina TANF, deduct child support that benefit group members pay outside the home after the earned income disregards, and round the need standard, gross income limit and payment down to whole dollars (TANF Policy Manual 7.5, 8.11, 8.15, 8.16; S.C. Code Regs. 114-1140(A)(4)). diff --git a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml index 06613c3f582..02547a005a5 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml @@ -21,10 +21,12 @@ members: [person1, person2, person3] state_code: SC output: - sc_tanf: 435.28 + sc_tanf: 434 # tanf_fpg for size 3 (2025-01, uses 2024-10 FPG) = 2151.67/month - # need_standard = 2151.67 * 0.5 = 1075.83 - # benefit = 1075.83 * 0.4046 = 435.28 + # 114-1140(A)(4) and the Section 8.15 table drop the cents of the need + # standard and the payment. + # need_standard = floor(2151.67 * 0.5 = 1075.83) = 1075 + # benefit = floor(1075 * 0.4046 = 434.95) = 434 - name: Case 2, single parent with earned income. absolute_error_margin: 0.1 @@ -49,14 +51,14 @@ members: [person1, person2, person3] state_code: SC output: - sc_tanf: 334.13 + sc_tanf: 333 # monthly earned = 6,000/12 = 500 # January (month 1) uses 50% disregard # countable_earned = 500 * 0.5 = 250 # countable_income = 250 - # need_standard = 1075.83 - # excess = 1075.83 - 250 = 825.83 - # benefit = 825.83 * 0.4046 = 334.13 + # need_standard = 1075 + # excess = 1075 - 250 = 825 + # benefit = floor(825 * 0.4046 = 333.80) = 333 - name: Case 3, income above gross limit. period: 2025-01 @@ -80,9 +82,9 @@ sc_tanf: 0 # monthly earned = 24,000/12 = 2,000 # tanf_fpg for size 2 = 1703.33 - # need_standard = 851.67 - # gross_income_limit = 851.67 * 1.85 = 1575.58 - # 2,000 > 1,575.58 -> not eligible + # need_standard = floor(851.67) = 851 + # gross_income_limit = floor(851 * 1.85 = 1574.35) = 1574 + # 2,000 > 1,574 -> not eligible - name: Case 4, single pregnant person with no income. period: 2025-01 @@ -102,10 +104,10 @@ members: [person1] state_code: SC output: - sc_tanf: 253.89 + sc_tanf: 253 # tanf_fpg for size 1 (2025-01) = 1255/month - # need_standard = 1255 * 0.5 = 627.50 - # benefit = 627.50 * 0.4046 = 253.89 + # need_standard = floor(1255 * 0.5 = 627.50) = 627 + # benefit = floor(627 * 0.4046 = 253.68) = 253 - name: Case 5, unearned income only. period: 2025-01 @@ -128,14 +130,14 @@ members: [person1, person2] state_code: SC output: - sc_tanf: 223.20 + sc_tanf: 222 # SSA benefits count at the gross amount (Section 7.7, page 116) # monthly unearned = 3,600/12 = 300 # tanf_fpg for size 2 = 1703.33 - # need_standard = 1703.33 * 0.5 = 851.67 + # need_standard = floor(1703.33 * 0.5 = 851.67) = 851 # countable_income = 0 (earned) + 300 (unearned) = 300 - # excess = 851.67 - 300 = 551.67 - # benefit = 551.67 * 0.4046 = 223.20 + # excess = 851 - 300 = 551 + # benefit = floor(551 * 0.4046 = 222.93) = 222 - name: Case 6, historical payment rate (pre-Oct 2024). period: 2024-01 @@ -157,10 +159,10 @@ members: [person1, person2] state_code: SC output: - sc_tanf: 277.07 + sc_tanf: 276 # tanf_fpg for size 2 (2024-01, uses 2023-10 FPG) = 1643.33/month - # need_standard = 1643.33 * 0.5 = 821.67 - # benefit = 821.67 * 0.3372 = 277.07 (old rate) + # need_standard = floor(1643.33 * 0.5 = 821.67) = 821 + # benefit = floor(821 * 0.3372 = 276.84) = 276 (old rate) - name: Case 7, two-parent household with 4 children (family of 6). period: 2025-01 @@ -195,9 +197,9 @@ state_code: SC output: sc_tanf: 707 - # tanf_fpg for size 6 (2025-01) = 3496/month (from Section 8.15 table) - # need_standard = 3496 * 0.5 = 1748 - # benefit = 1748 * 0.4046 = 707.24 + # tanf_fpg for size 6 (2025-01) = (15,060 + 5 x 5,380) / 12 = 3,496.67 + # need_standard = floor(3,496.67 * 0.5 = 1,748.33) = 1,748 + # benefit = floor(1,748 * 0.4046 = 707.24) = 707 - name: Case 8, ineligible due to immigration status. period: 2025-01 @@ -252,13 +254,13 @@ sc_tanf_gross_unearned_income: [500, 0] sc_tanf_countable_income: 500 # tanf_fpg for size 2 (2020-01, uses 2019 FPG) = 16,910 / 12 = 1,409.17 - # need_standard = 1,409.17 * 0.5 = 704.58 - # 500 < 704.58 -> countable income eligible + # need_standard = floor(1,409.17 * 0.5 = 704.58) = 704 + # 500 < 704 -> countable income eligible sc_tanf_income_eligible: true - # excess = 704.58 - 500 = 204.58 - # benefit = 204.58 * 0.3372 = 68.99 - # (Counting taxable UC instead gave 704.58 * 0.3372 = 237.59.) - sc_tanf: 68.99 + # excess = 704 - 500 = 204 + # benefit = floor(204 * 0.3372 = 68.79) = 68 + # (Counting taxable UC instead gave floor(704 * 0.3372) = 237.) + sc_tanf: 68 - name: Case 10, 2020 unemployment compensation above the federal exclusion counts in full. period: 2020-01 @@ -293,13 +295,13 @@ sc_tanf_gross_unearned_income: [1_000, 0, 0, 0] sc_tanf_countable_income: 1_000 # tanf_fpg for size 4 (2020-01, uses 2019 FPG) = 25,750 / 12 = 2,145.83 - # need_standard = 2,145.83 * 0.5 = 1,072.92 - # 1,000 < 1,072.92 -> countable income eligible + # need_standard = floor(2,145.83 * 0.5 = 1,072.92) = 1,072 + # 1,000 < 1,072 -> countable income eligible sc_tanf_income_eligible: true - # excess = 1,072.92 - 1,000 = 72.92 - # benefit = 72.92 * 0.3372 = 24.59 - # (Counting taxable UC instead gave (1,072.92 - 150) * 0.3372 = 311.21.) - sc_tanf: 24.59 + # excess = 1,072 - 1,000 = 72 + # benefit = floor(72 * 0.3372 = 24.28) = 24 + # (Counting taxable UC instead gave floor((1,072 - 150) * 0.3372) = 310.) + sc_tanf: 24 - name: Case 11, gross Social Security counts when none of it is federally taxable. period: 2025-01 @@ -329,13 +331,13 @@ sc_tanf_gross_unearned_income: [500, 0] sc_tanf_countable_income: 500 # tanf_fpg for size 2 (2025-01) = 1,703.33 - # need_standard = 1,703.33 * 0.5 = 851.67 - # 500 < 851.67 -> countable income eligible + # need_standard = floor(1,703.33 * 0.5 = 851.67) = 851 + # 500 < 851 -> countable income eligible sc_tanf_income_eligible: true - # excess = 851.67 - 500 = 351.67 - # benefit = 351.67 * 0.4046 = 142.28 - # (Counting taxable Social Security instead gave 851.67 * 0.4046 = 344.58.) - sc_tanf: 142.28 + # excess = 851 - 500 = 351 + # benefit = floor(351 * 0.4046 = 142.01) = 142 + # (Counting taxable Social Security instead gave floor(851 * 0.4046) = 344.) + sc_tanf: 142 - name: Case 12, gross Social Security above the need standard makes the family ineligible. period: 2025-01 @@ -360,9 +362,9 @@ taxable_social_security: [0, 0] # Gross SSA benefit counts: 12,000 / 12 = 1,000 per month. sc_tanf_countable_income: 1_000 - # need_standard = 851.67; 1,000 >= 851.67 -> not income eligible + # need_standard = 851; 1,000 >= 851 -> not income eligible sc_tanf_income_eligible: false - # (Counting taxable Social Security instead paid 344.58.) + # (Counting taxable Social Security instead paid 344.) sc_tanf: 0 - name: Case 13, gross Social Security and unemployment compensation both count once. @@ -397,11 +399,11 @@ sc_tanf_gross_unearned_income: [500, 0, 0] sc_tanf_countable_income: 500 # tanf_fpg for size 3 (2025-01) = 2,151.67 - # need_standard = 2,151.67 * 0.5 = 1,075.83 - # excess = 1,075.83 - 500 = 575.83 - # benefit = 575.83 * 0.4046 = 232.98 - # (Counting taxable SS instead gave (1,075.83 - 200) * 0.4046 = 354.36.) - sc_tanf: 232.98 + # need_standard = floor(2,151.67 * 0.5 = 1,075.83) = 1,075 + # excess = 1,075 - 500 = 575 + # benefit = floor(575 * 0.4046 = 232.65) = 232 + # (Counting taxable SS instead gave floor((1,075 - 200) * 0.4046) = 354.) + sc_tanf: 232 - name: Case 14, child support, a child's Social Security and interest above the exclusion. period: 2025-01 @@ -430,15 +432,15 @@ state_code: SC output: sc_tanf_gross_income: 750 - sc_tanf: 131.83 + sc_tanf: 131 # SC DSS TANF Policy Manual (Volume 67), Section 7.7: # child support paid directly = 3,600/12 = 300 (page 108) # child's SSA benefits, gross = 4,800/12 = 400 (page 116) # interest = (1,000 - 400 exclusion)/12 = 50 (page 113) # gross income = 300 + 400 + 50 = 750 - # tanf_fpg for size 3 = 2151.67; need standard = 1075.83 - # gross income limit = 1075.83 * 1.85 = 1990.29; 750 passes - # benefit = (1075.83 - 750) * 0.4046 = 131.83 + # tanf_fpg for size 3 = 2151.67; need standard = floor(1075.83) = 1075 + # gross income limit = floor(1075 * 1.85 = 1988.75) = 1988; 750 passes + # benefit = floor((1075 - 750) * 0.4046 = 131.50) = 131 - name: Case 15, an SSI child and the child's income are excluded from the benefit group. period: 2025-01 @@ -467,13 +469,13 @@ output: sc_tanf_benefit_group_size: 2 sc_tanf_gross_income: 0 - sc_tanf_need_standard: 851.67 - sc_tanf: 344.58 + sc_tanf_need_standard: 851 + sc_tanf: 344 # SC DSS TANF Policy Manual (Volume 67), Section 3.6 (page 56) and # Section 7.7 (page 117): exclude the SSI recipient, his/her income and # resources. The benefit group is the parent and the other child. - # FPG for 2 = 1703.33; need standard = 851.67 - # benefit = 851.67 * 0.4046 = 344.58 + # FPG for 2 = 1703.33; need standard = floor(851.67) = 851 + # benefit = floor(851 * 0.4046 = 344.31) = 344 - name: Case 16, when the only child receives SSI, the parent is the only benefit group member. period: 2025-01 @@ -497,11 +499,11 @@ state_code: SC output: sc_tanf_benefit_group_size: 1 - sc_tanf: 253.89 + sc_tanf: 253 # Section 3.6 (page 56): "If an SSI child is the only child in the home, # include the parent(s) as the only BG member." - # FPG for 1 = 1255; need standard = 627.50 - # benefit = 627.50 * 0.4046 = 253.89 + # FPG for 1 = 1255; need standard = floor(627.50) = 627 + # benefit = floor(627 * 0.4046 = 253.68) = 253 - name: Case 17, an SSI child's savings do not count against the resource limit. period: 2025-01 @@ -529,7 +531,202 @@ state_code: SC output: sc_tanf_countable_resources: 800 - sc_tanf: 344.58 + sc_tanf: 344 # Sections 3.6 (page 56) and 7.7 (page 117) exclude the SSI child's # resources: 2,600 - 1,800 = 800, within the $2,500 limit (page 91). - # Benefit group of 2: need standard 851.67; benefit = 851.67 * 0.4046 + # Benefit group of 2: need standard 851; benefit = floor(851 * 0.4046) = 344 + +# Rules added for dependent children's earnings (Section 7.5, PDF page 105; +# Section 8.16 B, page 134), child support paid (Section 8.11, page 131; +# Section 8.16 D and F, page 134) and whole dollars (114-1140(A)(4); the +# Section 8.15 table notes, page 133). 2026 months use the 2025 guideline: +# a group of three has a need standard of floor(26,650 / 12 x 0.5) = 1,110. + +- name: Case 18, a parent and two children with no income. + period: 2026-01 + input: + people: + parent: + age: 40 + is_tax_unit_head: true + child1: + age: 16 + is_tax_unit_dependent: true + child2: + age: 10 + is_tax_unit_dependent: true + households: + household: + members: [parent, child1, child2] + state_code: SC + output: + sc_tanf_need_standard: 1_110 + # floor(1,110 x 0.4046 = 449.11) = 449. Without rounding: + # 1,110.42 x 0.4046 = 449.27. + sc_tanf: 449 + +- name: Case 19, a dependent child's earnings are excluded. + period: 2026-01 + input: + people: + parent: + age: 40 + is_tax_unit_head: true + child1: + age: 16 + is_tax_unit_dependent: true + employment_income_before_lsr: 6_000 + child2: + age: 10 + is_tax_unit_dependent: true + households: + household: + members: [parent, child1, child2] + state_code: SC + output: + # Section 7.5: "Earned income of a dependent child(ren) is excluded". + sc_tanf_dependent_child: [false, true, true] + sc_tanf_gross_earned_income: [0, 0, 0] + sc_tanf_gross_income: 0 + sc_tanf_countable_income: 0 + # Counting the child's 500 a month gave a 50% January disregard, 250 + # countable, and (1,110.42 - 250) x 0.4046 = 348.12. + sc_tanf: 449 + +- name: Case 20, a dependent child's unearned income still counts. + period: 2026-01 + input: + people: + parent: + age: 40 + is_tax_unit_head: true + child1: + age: 16 + is_tax_unit_dependent: true + employment_income_before_lsr: 6_000 + social_security_survivors: 2_400 + child2: + age: 10 + is_tax_unit_dependent: true + households: + household: + members: [parent, child1, child2] + state_code: SC + output: + # Section 7.5 excludes only the child's earned income: 2,400 / 12 = 200. + sc_tanf_gross_unearned_income: [0, 200, 0] + sc_tanf_gross_income: 200 + sc_tanf_countable_income: 200 + # floor((1,110 - 200) x 0.4046 = 368.19) = 368. + sc_tanf: 368 + +- name: Case 21, child support paid comes off earned income after the disregard. + period: 2026-05 + input: + people: + parent: + age: 40 + is_tax_unit_head: true + employment_income_before_lsr: 7_200 + child_support_expense: 2_400 + child1: + age: 16 + is_tax_unit_dependent: true + child2: + age: 10 + is_tax_unit_dependent: true + households: + household: + members: [parent, child1, child2] + state_code: SC + output: + # May uses the $100 disregard: E = 600 - 100 = 500; S = 2,400 / 12 = 200. + sc_tanf_child_support_deduction: 200 + sc_tanf_countable_income: 300 + # The Section 8.15 gross income test stays on gross income. + sc_tanf_gross_income: 600 + # floor((1,110 - 300) x 0.4046 = 327.73) = 327. + sc_tanf: 327 + +- name: Case 22, one adult's support payment comes off the other adult's earnings. + period: 2026-05 + input: + people: + adult_a: + age: 40 + is_tax_unit_head: true + child_support_expense: 2_400 + adult_b: + age: 38 + is_tax_unit_spouse: true + employment_income_before_lsr: 7_200 + child: + age: 10 + is_tax_unit_dependent: true + marital_units: + couple: + members: [adult_a, adult_b] + child_unit: + members: [child] + households: + household: + members: [adult_a, adult_b, child] + state_code: SC + output: + # Section 8.11 deducts support paid by a benefit group member from the + # group's income: E = 600 - 100 = 500, S = 200, countable 300. + sc_tanf_child_support_deduction: 200 + sc_tanf_countable_income: 300 + # floor((1,110 - 300) x 0.4046) = 327. Limiting the deduction to the + # payer's own earnings (0) would leave 500 countable and + # floor(610 x 0.4046 = 246.81) = 246. + sc_tanf: 327 + +- name: Case 23, support paid above the group's income stops at zero. + period: 2026-05 + input: + people: + parent: + age: 40 + is_tax_unit_head: true + employment_income_before_lsr: 3_000 + alimony_income: 1_200 + child_support_expense: 3_600 + child1: + age: 16 + is_tax_unit_dependent: true + child2: + age: 10 + is_tax_unit_dependent: true + households: + household: + members: [parent, child1, child2] + state_code: SC + output: + # E = 250 - 100 = 150, U = 100, S = 300: the deduction takes E, then U, + # and stops at min(300, 250) = 250, leaving 0. + sc_tanf_child_support_deduction: 250 + sc_tanf_countable_income: 0 + sc_tanf_gross_income: 350 + sc_tanf: 449 + +- name: Case 24, a pregnant adult's wages count. + period: 2026-01 + input: + people: + parent: + age: 25 + is_pregnant: true + is_tax_unit_head: true + employment_income_before_lsr: 6_000 + households: + household: + members: [parent] + state_code: SC + output: + # Pregnancy does not make an adult a dependent child: 500 x 0.5 = 250 + # countable in January. Need standard floor(15,650 / 12 x 0.5) = 652. + sc_tanf_dependent_child: false + sc_tanf_countable_income: 250 + # floor((652 - 250) x 0.4046 = 162.65) = 162. + sc_tanf: 162 diff --git a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf.yaml b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf.yaml index e525c395486..ab55cfe8b2d 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf.yaml @@ -7,10 +7,10 @@ sc_tanf_eligible: true sc_tanf_benefit_group_size: 2 output: - sc_tanf: 344.58 + sc_tanf: 344 # FPG for a benefit group of 2 (2025-01, uses 2024-10 FPG) = 1703.33/month - # need_standard = 1703.33 * 0.5 = 851.67 - # benefit = 851.67 * 0.4046 = 344.59 + # need_standard = floor(1703.33 * 0.5 = 851.67) = 851 (114-1140(A)(4)) + # benefit = floor(851 * 0.4046 = 344.31) = 344 - name: Case 2, not eligible. period: 2025-01 @@ -31,11 +31,11 @@ sc_tanf_eligible: true sc_tanf_benefit_group_size: 2 output: - sc_tanf: 142.28 + sc_tanf: 142 # FPG for a benefit group of 2 (2025-01) = 1703.33/month - # need_standard = 1703.33 * 0.5 = 851.67 - # excess = 851.67 - 500 = 351.67 - # benefit = 351.67 * 0.4046 = 142.31 + # need_standard = floor(851.67) = 851 + # excess = 851 - 500 = 351 + # benefit = floor(351 * 0.4046 = 142.01) = 142 - name: Case 4, a benefit under $10 is not paid. absolute_error_margin: 0.01 @@ -56,8 +56,8 @@ members: [parent, child] state_code: SC output: - # Need standard for 2 (2025-01) = $1,703.33 * 0.5 = $851.67 - # Benefit = ($851.67 - $840) * 0.4046 = $4.72, under $10 + # Need standard for 2 (2025-01) = floor($1,703.33 * 0.5) = $851 + # Benefit = floor(($851 - $840) * 0.4046 = $4.45) = $4, under $10 # (S.C. Code Regs. 114-1140(A)(5)) sc_tanf: 0 @@ -80,14 +80,12 @@ members: [parent, child] state_code: SC output: - # Need standard for 2 (2025-01) = $851.67 - # Benefit = ($851.67 - $820) * 0.4046 = $12.81 - # The rule rounds the need standard and benefit down - # (114-1140(A)(4)), which is not modeled: - # (851 - 820) * 0.4046 = 12.54, so $12. - sc_tanf: 12.81 + # Need standard for 2 (2025-01) = floor($851.67) = $851 + # 114-1140(A)(4) rounds the payment down: + # floor(($851 - $820) * 0.4046 = $12.54) = $12. + sc_tanf: 12 -- name: Case 6, a benefit of $9.99 is not paid. +- name: Case 6, a benefit of $9.72 is not paid. absolute_error_margin: 0.01 period: 2025-01 input: @@ -106,8 +104,8 @@ members: [parent, child] state_code: SC output: - # Need standard for 2 (2025-01) = $851.67 - # Benefit = ($851.67 - $826.98) * 0.4046 = $9.99, under $10 + # Need standard for 2 (2025-01) = $851 + # Benefit = floor(($851 - $826.98) * 0.4046 = $9.72) = $9, under $10 sc_tanf: 0 - name: Case 7, a benefit just over $10 is paid. @@ -129,8 +127,130 @@ members: [parent, child] state_code: SC output: - # Need standard for 2 (2025-01) = $851.67 - # Benefit = ($851.67 - $826.20) * 0.4046 = $10.30, at least $10 - # With the 114-1140(A)(4) rounding, which is not modeled: - # (851 - 826.20) * 0.4046 = 10.03, so $10, which is also paid. - sc_tanf: 10.30 + # Need standard for 2 (2025-01) = $851 + # Benefit = floor(($851 - $826.20) * 0.4046 = $10.03) = $10, at least $10 + sc_tanf: 10 + +- name: Case 8, a benefit that rounds down to $9 is not paid. + absolute_error_margin: 0.01 + period: 2025-01 + input: + people: + parent: + age: 30 + child: + age: 5 + spm_units: + spm_unit: + members: [parent, child] + sc_tanf_countable_income: 826.8 + sc_tanf_eligible: true + households: + household: + members: [parent, child] + state_code: SC + output: + # Need standard for 2 (2025-01) = $851 + # floor(($851 - $826.80) * 0.4046 = $9.79) = $9, under the $10 minimum + # (114-1140(A)(4)-(5); the South Carolina part of issue 10497). + sc_tanf: 0 + +# The Section 8.15 table (PDF page 133), 2026-01: "multiply the Need Standard +# by .4046 and drop the cents" gives the payment standard, the payment with +# no countable income. + +- name: Section 8.15 table, payment standard for a benefit group of 1. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_countable_income: 0 + sc_tanf_eligible: true + sc_tanf_benefit_group_size: 1 + output: + # Need standard 652; floor(652 x 0.4046 = 263.80) = 263. + sc_tanf: 263 + +- name: Section 8.15 table, payment standard for a benefit group of 2. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_countable_income: 0 + sc_tanf_eligible: true + sc_tanf_benefit_group_size: 2 + output: + # Need standard 881; floor(881 x 0.4046 = 356.45) = 356. + sc_tanf: 356 + +- name: Section 8.15 table, payment standard for a benefit group of 3. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_countable_income: 0 + sc_tanf_eligible: true + sc_tanf_benefit_group_size: 3 + output: + # Need standard 1,110; floor(1,110 x 0.4046 = 449.11) = 449. + sc_tanf: 449 + +- name: Section 8.15 table, payment standard for a benefit group of 4. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_countable_income: 0 + sc_tanf_eligible: true + sc_tanf_benefit_group_size: 4 + output: + # Need standard 1,339; floor(1,339 x 0.4046 = 541.76) = 541. + sc_tanf: 541 + +- name: Section 8.15 table, payment standard for a benefit group of 5. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_countable_income: 0 + sc_tanf_eligible: true + sc_tanf_benefit_group_size: 5 + output: + # Need standard 1,568; floor(1,568 x 0.4046 = 634.41) = 634. + sc_tanf: 634 + +- name: Section 8.15 table, payment standard for a benefit group of 6. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_countable_income: 0 + sc_tanf_eligible: true + sc_tanf_benefit_group_size: 6 + output: + # Need standard 1,797; floor(1,797 x 0.4046 = 727.07) = 727. + sc_tanf: 727 + +- name: Section 8.15 table, payment standard for a benefit group of 7. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_countable_income: 0 + sc_tanf_eligible: true + sc_tanf_benefit_group_size: 7 + output: + # Need standard 2,027; floor(2,027 x 0.4046 = 820.12) = 820. + sc_tanf: 820 + +- name: Section 8.15 table, payment standard for a benefit group of 8. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_countable_income: 0 + sc_tanf_eligible: true + sc_tanf_benefit_group_size: 8 + output: + # Need standard 2,256; floor(2,256 x 0.4046 = 912.78) = 912. + sc_tanf: 912 diff --git a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_countable_income_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_countable_income_eligible.yaml index 624cdcf1949..7f45c6d050a 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_countable_income_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_countable_income_eligible.yaml @@ -15,8 +15,8 @@ output: sc_tanf_countable_income_eligible: true # FPG for a benefit group of 2 (2025-01) = 1703.33/month - # need_standard = 1703.33 * 0.5 = 851.67 - # 800 < 851.67 -> eligible + # need_standard = floor(1703.33 * 0.5 = 851.67) = 851 + # 800 < 851 -> eligible - name: Case 3, countable income above need standard. period: 2025-01 @@ -26,8 +26,8 @@ sc_tanf_countable_income: 900 output: sc_tanf_countable_income_eligible: false - # need_standard = 851.67 - # 900 >= 851.67 -> not eligible + # need_standard = 851 + # 900 >= 851 -> not eligible - name: Case 4, countable income exactly at need standard. period: 2025-01 @@ -35,10 +35,10 @@ input: state_code: SC sc_tanf_benefit_group_size: 2 - sc_tanf_countable_income: 851.67 + sc_tanf_countable_income: 851 output: sc_tanf_countable_income_eligible: false # FPG for a benefit group of 2 = 1703.33 - # need_standard = 1703.33 * 0.5 = 851.67 - # 851.67 < 851.67 is FALSE (strict less than) + # need_standard = floor(1703.33 * 0.5 = 851.67) = 851 + # 851 < 851 is FALSE (strict less than) # -> not eligible diff --git a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_dependent_child.yaml b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_dependent_child.yaml new file mode 100644 index 00000000000..b67d31c00c7 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_dependent_child.yaml @@ -0,0 +1,87 @@ +# SC DSS TANF Policy Manual (Volume 67), Section 7.5 (PDF page 105): a +# dependent child's earned income is excluded; a minor parent's earned income +# counts "if the minor parent is the head-of-household". The age test reuses +# the federal TANF minor-child limits: under 18, or under 19 in secondary +# school. + +- name: Case 1, children on both sides of the age and school boundaries. + period: 2026-01 + input: + people: + parent: + age: 45 + is_tax_unit_head: true + seventeen: + age: 17 + is_tax_unit_dependent: true + eighteen_in_school: + age: 18 + is_in_secondary_school: true + is_tax_unit_dependent: true + eighteen_out_of_school: + age: 18 + is_tax_unit_dependent: true + nineteen_in_school: + age: 19 + is_in_secondary_school: true + is_tax_unit_dependent: true + households: + household: + members: + [parent, seventeen, eighteen_in_school, eighteen_out_of_school, nineteen_in_school] + state_code: SC + output: + sc_tanf_dependent_child: [false, true, true, false, false] + +- name: Case 2, a minor parent who heads the tax unit counts as a parent. + period: 2026-01 + input: + people: + minor_parent: + age: 17 + is_tax_unit_head: true + employment_income_before_lsr: 6_000 + baby: + age: 0 + is_tax_unit_dependent: true + households: + household: + members: [minor_parent, baby] + state_code: SC + output: + sc_tanf_dependent_child: [false, true] + # The minor parent's 6,000 / 12 = 500 a month counts. + sc_tanf_gross_earned_income: [500, 0] + +- name: Case 3, a pregnant adult is not a dependent child. + period: 2026-01 + input: + age: 25 + is_pregnant: true + state_code: SC + output: + sc_tanf_dependent_child: false + +- name: Case 4, a minor parent who is a dependent in the group is a dependent child. + period: 2026-01 + input: + people: + grandparent: + age: 45 + is_tax_unit_head: true + minor_parent: + age: 17 + is_tax_unit_dependent: true + employment_income_before_lsr: 6_000 + baby: + age: 0 + is_tax_unit_dependent: true + households: + household: + members: [grandparent, minor_parent, baby] + state_code: SC + output: + # Section 7.5 excludes a minor parent's earnings "if the minor parent is + # included as a dependent in the TANF BG"; Section 8.16 B likewise. + sc_tanf_dependent_child: [false, true, true] + sc_tanf_gross_earned_income: [0, 0, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_gross_income_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_gross_income_eligible.yaml index 4e3c80b52bd..508c23b8bed 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_gross_income_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_gross_income_eligible.yaml @@ -16,9 +16,9 @@ sc_tanf_gross_income_eligible: true # monthly earned = 18,000 / 12 = 1,500 # FPG for a benefit group of 2 (2025-01) = 1703.33/month - # need_standard = 1703.33 * 0.5 = 851.67 - # gross_income_limit = 851.67 * 1.85 = 1575.58 - # 1,500 <= 1,575.58 -> eligible + # need_standard = floor(1703.33 * 0.5 = 851.67) = 851 + # gross_income_limit = floor(851 * 1.85 = 1574.35) = 1574 + # 1,500 <= 1,574 -> eligible - name: Case 3, income above gross income limit. period: 2025-01 @@ -29,8 +29,8 @@ output: sc_tanf_gross_income_eligible: false # monthly earned = 24,000 / 12 = 2,000 - # gross_income_limit = 1,575.58 - # 2,000 > 1,575.58 -> not eligible + # gross_income_limit = 1,574 + # 2,000 > 1,574 -> not eligible - name: Case 4, gross income exactly at limit. period: 2025-01 @@ -38,10 +38,201 @@ input: state_code: SC sc_tanf_benefit_group_size: 2 - sc_tanf_gross_income: 1_575.58 + sc_tanf_gross_income: 1_574 output: sc_tanf_gross_income_eligible: true # FPG for a benefit group of 2 = 1703.33 - # need_standard = 1703.33 * 0.5 = 851.665 - # gross_income_limit = 851.665 * 1.85 = 1575.58 - # 1575.58 <= 1575.58 -> eligible (uses <=) + # need_standard = floor(851.67) = 851 + # The Section 8.15 table notes: "multiply the Need Standard by 1.85 and + # drop the cents": floor(851 * 1.85 = 1574.35) = 1574. + # 1574 <= 1574 -> eligible (uses <=) + +- name: Case 5, gross income one cent above the whole-dollar limit. + period: 2025-01 + input: + state_code: SC + sc_tanf_benefit_group_size: 2 + sc_tanf_gross_income: 1_574.01 + output: + # 1574.01 > 1574. Without dropping the cents the limit would be + # 1.85 * 851.67 = 1575.58 and this group would pass. + sc_tanf_gross_income_eligible: false + +# The Section 8.15 table (PDF page 133), 2026-01: "multiply the Need Standard +# by 1.85 and drop the cents". Each limit passes and one cent above fails. + +- name: Section 8.15 table, gross income at the limit for a benefit group of 1. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_benefit_group_size: 1 + sc_tanf_gross_income: 1206.00 + output: + # Need standard 652; limit floor(652 x 1.85 = 1,206.20) = 1,206. + sc_tanf_gross_income_eligible: true + +- name: Section 8.15 table, gross income one cent above the limit for a benefit group of 1. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_benefit_group_size: 1 + sc_tanf_gross_income: 1206.01 + output: + # Need standard 652; limit floor(652 x 1.85 = 1,206.20) = 1,206. + sc_tanf_gross_income_eligible: false + +- name: Section 8.15 table, gross income at the limit for a benefit group of 2. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_benefit_group_size: 2 + sc_tanf_gross_income: 1629.00 + output: + # Need standard 881; limit floor(881 x 1.85 = 1,629.85) = 1,629. + sc_tanf_gross_income_eligible: true + +- name: Section 8.15 table, gross income one cent above the limit for a benefit group of 2. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_benefit_group_size: 2 + sc_tanf_gross_income: 1629.01 + output: + # Need standard 881; limit floor(881 x 1.85 = 1,629.85) = 1,629. + sc_tanf_gross_income_eligible: false + +- name: Section 8.15 table, gross income at the limit for a benefit group of 3. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_benefit_group_size: 3 + sc_tanf_gross_income: 2053.00 + output: + # Need standard 1,110; limit floor(1,110 x 1.85 = 2,053.50) = 2,053. + sc_tanf_gross_income_eligible: true + +- name: Section 8.15 table, gross income one cent above the limit for a benefit group of 3. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_benefit_group_size: 3 + sc_tanf_gross_income: 2053.01 + output: + # Need standard 1,110; limit floor(1,110 x 1.85 = 2,053.50) = 2,053. + sc_tanf_gross_income_eligible: false + +- name: Section 8.15 table, gross income at the limit for a benefit group of 4. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_benefit_group_size: 4 + sc_tanf_gross_income: 2477.00 + output: + # Need standard 1,339; limit floor(1,339 x 1.85 = 2,477.15) = 2,477. + sc_tanf_gross_income_eligible: true + +- name: Section 8.15 table, gross income one cent above the limit for a benefit group of 4. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_benefit_group_size: 4 + sc_tanf_gross_income: 2477.01 + output: + # Need standard 1,339; limit floor(1,339 x 1.85 = 2,477.15) = 2,477. + sc_tanf_gross_income_eligible: false + +- name: Section 8.15 table, gross income at the limit for a benefit group of 5. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_benefit_group_size: 5 + sc_tanf_gross_income: 2900.00 + output: + # Need standard 1,568; limit floor(1,568 x 1.85 = 2,900.80) = 2,900. + sc_tanf_gross_income_eligible: true + +- name: Section 8.15 table, gross income one cent above the limit for a benefit group of 5. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_benefit_group_size: 5 + sc_tanf_gross_income: 2900.01 + output: + # Need standard 1,568; limit floor(1,568 x 1.85 = 2,900.80) = 2,900. + sc_tanf_gross_income_eligible: false + +- name: Section 8.15 table, gross income at the limit for a benefit group of 6. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_benefit_group_size: 6 + sc_tanf_gross_income: 3324.00 + output: + # Need standard 1,797; limit floor(1,797 x 1.85 = 3,324.45) = 3,324. + sc_tanf_gross_income_eligible: true + +- name: Section 8.15 table, gross income one cent above the limit for a benefit group of 6. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_benefit_group_size: 6 + sc_tanf_gross_income: 3324.01 + output: + # Need standard 1,797; limit floor(1,797 x 1.85 = 3,324.45) = 3,324. + sc_tanf_gross_income_eligible: false + +- name: Section 8.15 table, gross income at the limit for a benefit group of 7. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_benefit_group_size: 7 + sc_tanf_gross_income: 3749.00 + output: + # Need standard 2,027; limit floor(2,027 x 1.85 = 3,749.95) = 3,749. + sc_tanf_gross_income_eligible: true + +- name: Section 8.15 table, gross income one cent above the limit for a benefit group of 7. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_benefit_group_size: 7 + sc_tanf_gross_income: 3749.01 + output: + # Need standard 2,027; limit floor(2,027 x 1.85 = 3,749.95) = 3,749. + sc_tanf_gross_income_eligible: false + +- name: Section 8.15 table, gross income at the limit for a benefit group of 8. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_benefit_group_size: 8 + sc_tanf_gross_income: 4173.00 + output: + # Need standard 2,256; limit floor(2,256 x 1.85 = 4,173.60) = 4,173. + sc_tanf_gross_income_eligible: true + +- name: Section 8.15 table, gross income one cent above the limit for a benefit group of 8. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_benefit_group_size: 8 + sc_tanf_gross_income: 4173.01 + output: + # Need standard 2,256; limit floor(2,256 x 1.85 = 4,173.60) = 4,173. + sc_tanf_gross_income_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_need_standard.yaml b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_need_standard.yaml index 31ab0230586..cce7fdb1a44 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_need_standard.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_need_standard.yaml @@ -9,8 +9,9 @@ state_code: SC sc_tanf_benefit_group_size: 3 output: - # (15,060 + 2 x 5,380) / 12 = 2,151.67; x 0.5 = 1,075.83 - sc_tanf_need_standard: 1_075.83 + # (15,060 + 2 x 5,380) / 12 = 2,151.67; x 0.5 = 1,075.83, and + # 114-1140(A)(4) rounds the standard of need down: 1,075. + sc_tanf_need_standard: 1_075 - name: Case 2, benefit group of three in October 2025 uses the October 2025 guideline. period: 2025-10 @@ -20,8 +21,9 @@ 2025: SC sc_tanf_benefit_group_size: 3 output: - # (15,650 + 2 x 5,500) / 12 = 2,220.83; x 0.5 = 1,110.42 - sc_tanf_need_standard: 1_110.42 + # (15,650 + 2 x 5,500) / 12 = 2,220.83; x 0.5 = 1,110.42, rounded down + # to 1,110. + sc_tanf_need_standard: 1_110 - name: Case 3, an empty benefit group has no need standard. period: 2025-01 @@ -30,3 +32,96 @@ sc_tanf_benefit_group_size: 0 output: sc_tanf_need_standard: 0 + +# The Section 8.15 table (PDF page 133) for October 2025 to September 2026, +# from the 2025 guideline of $15,650 plus $5,500 for each added person. + +- name: Section 8.15 table, need standard for a benefit group of 1. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_benefit_group_size: 1 + output: + # 15,650 / 12 = 1,304.17; x 0.5 = 652.08, rounded down to 652. + sc_tanf_need_standard: 652 + +- name: Section 8.15 table, need standard for a benefit group of 2. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_benefit_group_size: 2 + output: + # 21,150 / 12 = 1,762.50; x 0.5 = 881.25, rounded down to 881. + sc_tanf_need_standard: 881 + +- name: Section 8.15 table, need standard for a benefit group of 3. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_benefit_group_size: 3 + output: + # 26,650 / 12 = 2,220.83; x 0.5 = 1,110.42, rounded down to 1,110. + sc_tanf_need_standard: 1110 + +- name: Section 8.15 table, need standard for a benefit group of 4. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_benefit_group_size: 4 + output: + # 32,150 / 12 = 2,679.17; x 0.5 = 1,339.58, rounded down to 1,339. + sc_tanf_need_standard: 1339 + +- name: Section 8.15 table, need standard for a benefit group of 5. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_benefit_group_size: 5 + output: + # 37,650 / 12 = 3,137.50; x 0.5 = 1,568.75, rounded down to 1,568. + sc_tanf_need_standard: 1568 + +- name: Section 8.15 table, need standard for a benefit group of 6. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_benefit_group_size: 6 + output: + # 43,150 / 12 = 3,595.83; x 0.5 = 1,797.92, rounded down to 1,797. + sc_tanf_need_standard: 1797 + +- name: Section 8.15 table, need standard for a benefit group of 7. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_benefit_group_size: 7 + output: + # 48,650 / 12 = 4,054.17; x 0.5 = 2,027.08, rounded down to 2,027. + sc_tanf_need_standard: 2027 + +- name: Section 8.15 table, need standard for a benefit group of 8. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_benefit_group_size: 8 + output: + # 54,150 / 12 = 4,512.50; x 0.5 = 2,256.25, rounded down to 2,256. + sc_tanf_need_standard: 2256 + +- name: Section 8.15 table, need standard for a benefit group of 9. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_benefit_group_size: 9 + output: + # 59,650 / 12 = 4,970.83; x 0.5 = 2,485.42, rounded down to 2,485. Size 9 is $229 above size 8, as the table's note says. + sc_tanf_need_standard: 2485 diff --git a/policyengine_us/tests/test_sc_tanf_child_earnings_support_invariants.py b/policyengine_us/tests/test_sc_tanf_child_earnings_support_invariants.py new file mode 100644 index 00000000000..08aa7d5b6c9 --- /dev/null +++ b/policyengine_us/tests/test_sc_tanf_child_earnings_support_invariants.py @@ -0,0 +1,275 @@ +"""Property tests: South Carolina TANF dependent children's earnings, child +support paid and whole-dollar amounts. + +SC DSS TANF Policy Manual, Volume 67: + +- Section 7.5 (PDF page 105): "Earned income of a dependent child(ren) is + excluded" from eligibility and the benefit; Section 8.16 B (page 134). +- Section 8.11 (page 131) and Section 8.16 D and F (page 134): child support + that benefit group members pay outside the home is deducted after the + Section 8.12 earned income disregards, from earned income first and then + from unearned income, never below zero. +- S.C. Code Regs. 114-1140(A)(4) and the Section 8.15 table notes (page 133): + the need standard, gross income limit and payment drop their cents. + +For every generated South Carolina benefit group: + +(a) sc_tanf is a whole number of dollars, and is 0 or at least $10. +(b) Countable income is at least 0. It never rises when child support paid + rises, and $1 more support lowers it by at most $1. +(c) Changing a dependent child's earned income never changes + sc_tanf_gross_income, sc_tanf_countable_income or sc_tanf. +(d) sc_tanf never rises when an adult's wages rise. +(e) Child support paid never changes sc_tanf_gross_income. +(f) With no dependent-child earnings and no child support paid, + sc_tanf_countable_income equals the formula before these rules, frozen + below as `previous_countable_income`. + +Generated groups have no self-employment income and nobody receives SSI, so +earned income is never negative. Each group and its variants share one +vectorized simulation. +""" + +import numpy as np +import pytest +from hypothesis import HealthCheck, example, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +YEAR = 2026 +MONTHS = ("2026-01", "2026-05") +# Section 8.12 disregards: 50% in the first four months, then $100. +INITIAL_MONTHS = 4 +INITIAL_RATE = 0.5 +FLAT_DISREGARD = 100 +MINIMUM_PAYMENT = 10 +WAGES = "employment_income_before_lsr" +UNEARNED = ("alimony_income", "child_support_received", "social_security_survivors") + + +def previous_countable_income(group, month): + """The countable income formula before these rules: every member's wages + after the Section 8.12 disregard, plus gross unearned income.""" + total = 0 + for member in group: + earned = member.get(WAGES, 0) / 12 + if int(month[-2:]) <= INITIAL_MONTHS: + total += earned * (1 - INITIAL_RATE) + else: + total += max(earned - FLAT_DISREGARD, 0) + total += sum(member.get(source, 0) for source in UNEARNED) / 12 + return total + + +def situation(groups): + people, units, marital_units, households = {}, {}, {}, {} + for i, group in enumerate(groups): + members = [] + adults = sum(not m["child"] for m in group) + for j, member in enumerate(group): + name = f"p{i}_{j}" + values = { + "age": member["age"], + "is_pregnant": member.get("pregnant", False), + "is_in_secondary_school": member.get("in_school", False), + "is_tax_unit_head": j == 0, + "is_tax_unit_spouse": j == 1 and adults == 2, + "is_tax_unit_dependent": member["child"], + WAGES: member.get(WAGES, 0), + "child_support_expense": member.get("support_paid", 0), + **{source: member.get(source, 0) for source in UNEARNED}, + } + people[name] = {k: {YEAR: v} for k, v in values.items()} + members.append(name) + adult_names = members[:adults] + marital_units[f"m{i}"] = {"members": adult_names} + for child in members[adults:]: + marital_units[f"m{child}"] = {"members": [child]} + units[f"u{i}"] = {"members": members} + households[f"h{i}"] = {"members": members, "state_code": {YEAR: "SC"}} + return { + "people": people, + "tax_units": units, + "spm_units": {k: dict(v) for k, v in units.items()}, + "families": {k: dict(v) for k, v in units.items()}, + "marital_units": marital_units, + "households": households, + } + + +def calculate(groups, month): + simulation = Simulation(situation=situation(groups)) + return { + name: np.asarray(simulation.calculate(name, month)) + for name in ( + "sc_tanf", + "sc_tanf_gross_income", + "sc_tanf_countable_income", + "sc_tanf_child_support_deduction", + ) + } + + +def variants(group, support_raise, child_wages, wage_raise): + """The group as drawn, with the head's support paid raised, with every + dependent child's wages replaced, and with the head's wages raised.""" + more_support = [dict(m) for m in group] + more_support[0]["support_paid"] = ( + more_support[0].get("support_paid", 0) + support_raise + ) + other_child_wages = [ + {**m, WAGES: child_wages} if m["child"] else dict(m) for m in group + ] + more_wages = [dict(m) for m in group] + more_wages[0][WAGES] = more_wages[0].get(WAGES, 0) + wage_raise + return [group, more_support, other_child_wages, more_wages] + + +def check(cases, month): + groups = [g for group, *changes in cases for g in variants(group, *changes)] + out = {k: v.reshape(-1, 4) for k, v in calculate(groups, month).items()} + benefit = out["sc_tanf"] + countable = out["sc_tanf_countable_income"] + gross = out["sc_tanf_gross_income"] + for k, (group, support_raise, _, _) in enumerate(cases): + context = {"group": group, "month": month} + # (a) Whole dollars, and nothing paid under the minimum. + assert np.all(benefit[k] == np.floor(benefit[k])), context + assert np.all((benefit[k] == 0) | (benefit[k] >= MINIMUM_PAYMENT)), context + # (b) Nonnegative, nonincreasing in support paid, at most $1 per $1. + assert np.all(countable[k] >= 0), context + drop = countable[k, 0] - countable[k, 1] + assert -0.01 <= drop <= support_raise / 12 + 0.01, context + # (c) A dependent child's earnings change nothing. + for values in (gross, countable, benefit): + assert values[k, 2] == pytest.approx(values[k, 0], abs=0.01), context + # (d) More adult wages never raise the benefit. + assert benefit[k, 3] <= benefit[k, 0], context + # (e) Support paid never changes gross income. + assert gross[k, 1] == pytest.approx(gross[k, 0], abs=0.01), context + + +def money(high): + return st.one_of(st.just(0), st.integers(1, high)) + + +@st.composite +def adult(draw): + return { + "child": False, + "age": draw(st.integers(19, 60)), + "pregnant": draw(st.booleans()), + WAGES: draw(money(30_000)), + "support_paid": draw(money(9_000)), + "alimony_income": draw(money(4_000)), + "child_support_received": draw(money(4_000)), + } + + +@st.composite +def child(draw): + age = draw(st.integers(0, 18)) + return { + "child": True, + "age": age, + # An 18-year-old is a dependent child only in secondary school. + "in_school": age >= 14 if age < 18 else True, + WAGES: draw(money(12_000)), + "social_security_survivors": draw(money(3_000)), + } + + +@st.composite +def case(draw): + adults = draw(st.lists(adult(), min_size=1, max_size=2)) + children = draw(st.lists(child(), min_size=1, max_size=3)) + return ( + adults + children, + draw(st.integers(1, 6_000)), + draw(money(12_000)), + draw(st.integers(1, 12_000)), + ) + + +SETTINGS = dict( + max_examples=10, deadline=None, suppress_health_check=[HealthCheck.too_slow] +) + + +@settings(**SETTINGS) +@example( + cases=[ + ( + [ + {"child": False, "age": 40, WAGES: 7_200, "support_paid": 2_400}, + {"child": True, "age": 16, WAGES: 6_000}, + ], + 1, + 0, + 1, + ) + ], + month="2026-05", +) +@given( + cases=st.lists(case(), min_size=1, max_size=6), + month=st.sampled_from(MONTHS), +) +def test_invariants(cases, month): + check(cases, month) + + +def without_child_earnings_or_support(group): + return [{**m, WAGES: 0} if m["child"] else {**m, "support_paid": 0} for m in group] + + +def differential(groups, month): + groups = [without_child_earnings_or_support(g) for g in groups] + countable = calculate(groups, month)["sc_tanf_countable_income"] + expected = [previous_countable_income(g, month) for g in groups] + np.testing.assert_allclose(countable, expected, atol=0.01) + + +@settings(**SETTINGS) +@given( + groups=st.lists(case().map(lambda c: c[0]), min_size=1, max_size=8), + month=st.sampled_from(MONTHS), +) +def test_matches_previous_formula_without_child_earnings_or_support(groups, month): + # (f) The differential on generated groups. + differential(groups, month) + + +@pytest.mark.parametrize("month", MONTHS) +def test_200_seeded_households_match_previous_formula(month): + # (f) The differential on 200 seeded South Carolina households. + rng = np.random.default_rng(10497) + groups = [] + for _ in range(200): + group = [] + for _ in range(rng.integers(1, 3)): + group.append( + { + "child": False, + "age": int(rng.integers(19, 61)), + "pregnant": bool(rng.integers(0, 2)), + WAGES: int(rng.integers(0, 3) * rng.integers(0, 30_001)), + "alimony_income": int(rng.integers(0, 2) * rng.integers(0, 4_001)), + "child_support_received": int( + rng.integers(0, 2) * rng.integers(0, 4_001) + ), + } + ) + for _ in range(rng.integers(0, 4)): + group.append( + { + "child": True, + "age": int(rng.integers(0, 18)), + "social_security_survivors": int( + rng.integers(0, 2) * rng.integers(0, 3_001) + ), + } + ) + groups.append(group) + differential(groups, month) diff --git a/policyengine_us/tests/test_sc_tanf_unearned_income_invariants.py b/policyengine_us/tests/test_sc_tanf_unearned_income_invariants.py index dca9d3e533b..54958b30756 100644 --- a/policyengine_us/tests/test_sc_tanf_unearned_income_invariants.py +++ b/policyengine_us/tests/test_sc_tanf_unearned_income_invariants.py @@ -32,14 +32,16 @@ interest and dividends. 3. Discharged debt and wages never change unearned income, and negative interest never reduces other income (property 1 holds with them present). -4. sc_tanf_gross_earned_income is wages, or zero for an SSI recipient. +4. sc_tanf_gross_earned_income is wages, or zero for an SSI recipient or a + dependent child (Section 7.5, page 105: the children drawn here are all + under 18 and dependents of the head). 5. Raising the head's interest by d raises the group's unearned income by between 0 and d / 12, by exactly the rise in the head's floored interest when the group's interest and dividends already exceed the exclusion, and by 0 when the head receives SSI. 6. The benefit group size is the number of members who do not receive SSI, - and the need standard is 50% of the guideline for that size (0 for an - empty group). + and the need standard is 50% of the guideline for that size, rounded + down to a whole dollar (114-1140(A)(4)), and 0 for an empty group. 7. Net amounts that can be losses (trust and estate, rental and partnership income here) count only when positive, so a loss never offsets another source or another member's income. @@ -112,6 +114,8 @@ def build_situation(groups): person = { "age": {YEAR: member["age"]}, "receives_ssi": {MONTH: member["ssi"]}, + "is_tax_unit_head": {YEAR: j == 0}, + "is_tax_unit_dependent": {YEAR: j > 0}, } for variable in AMOUNT_INPUTS: person[variable] = {YEAR: member.get(variable, 0)} @@ -171,10 +175,13 @@ def reference(group): member_counted(m) - (exclusion * x / group_idr if group_idr > 0 else 0) for m, x in zip(group, idr) ] - earned = [0 if m["ssi"] else m.get(WAGES, 0) / 12 for m in group] + # The head is an adult; every other member is a dependent child under 18. + earned = [ + 0 if m["ssi"] or k > 0 else m.get(WAGES, 0) / 12 for k, m in enumerate(group) + ] size = sum(not m["ssi"] for m in group) fpg = (FPG_FIRST_PERSON + FPG_ADDITIONAL_PERSON * (size - 1)) / 12 - need_standard = NEED_STANDARD_RATE * fpg if size > 0 else 0 + need_standard = np.floor(NEED_STANDARD_RATE * fpg) if size > 0 else 0 resources = sum(sum(m.get(v, 0) for v in ASSETS) for m in group if not m["ssi"]) return unearned, earned, exclusion, size, need_standard, resources @@ -265,9 +272,10 @@ def test_worked_example(): (unearned, _, _, size, need, _), _ = run(groups, [0]) # 300 + (1,000 - 400) / 12 = 350 for the parent; 400 for the child. assert unearned[0] == pytest.approx([350, 400, 0], abs=TOLERANCE) - # Volume 65 Section 8.15 guideline, three persons: (15,060 + 2 x 5,380) / 12 x 0.5. + # Volume 65 Section 8.15 guideline, three persons: (15,060 + 2 x 5,380) / 12 + # x 0.5 = 1,075.83, rounded down to 1,075 (114-1140(A)(4)). assert size[0] == 3 - assert need[0] == pytest.approx(1_075.83, abs=TOLERANCE) + assert need[0] == pytest.approx(1_075, abs=TOLERANCE) try: diff --git a/policyengine_us/variables/gov/states/sc/tanf/eligibility/sc_tanf_gross_income_eligible.py b/policyengine_us/variables/gov/states/sc/tanf/eligibility/sc_tanf_gross_income_eligible.py index 61ac97c146d..d24a36eb114 100644 --- a/policyengine_us/variables/gov/states/sc/tanf/eligibility/sc_tanf_gross_income_eligible.py +++ b/policyengine_us/variables/gov/states/sc/tanf/eligibility/sc_tanf_gross_income_eligible.py @@ -8,12 +8,14 @@ class sc_tanf_gross_income_eligible(Variable): definition_period = MONTH defined_for = StateCode.SC reference = ( - "https://dss.sc.gov/media/ojqddxsk/tanf-policy-manual-volume-65.pdf#page=131" + "https://dss.sc.gov/media/bxwc1jdj/tanf-policy-manual-volume-67-final.pdf#page=133", ) def formula(spm_unit, period, parameters): p = parameters(period).gov.states.sc.tanf.income need_standard = spm_unit("sc_tanf_need_standard", period) - gross_income_limit = need_standard * p.gross_income_limit + # The Section 8.15 table notes: "multiply the Need Standard by 1.85 + # and drop the cents". + gross_income_limit = np.floor(need_standard * p.gross_income_limit) gross_income = spm_unit("sc_tanf_gross_income", period) return gross_income <= gross_income_limit diff --git a/policyengine_us/variables/gov/states/sc/tanf/income/sc_tanf_child_support_deduction.py b/policyengine_us/variables/gov/states/sc/tanf/income/sc_tanf_child_support_deduction.py new file mode 100644 index 00000000000..9c5132b023e --- /dev/null +++ b/policyengine_us/variables/gov/states/sc/tanf/income/sc_tanf_child_support_deduction.py @@ -0,0 +1,39 @@ +from policyengine_us.model_api import * + + +class sc_tanf_child_support_deduction(Variable): + value_type = float + entity = SPMUnit + label = "South Carolina TANF child support paid deduction" + unit = USD + definition_period = MONTH + defined_for = StateCode.SC + reference = ( + # PDF pages 131, 134 + "https://dss.sc.gov/media/bxwc1jdj/tanf-policy-manual-volume-67-final.pdf#page=131", + ) + documentation = ( + "Child support that benefit group members who are not dependent " + "children pay outside the home, deducted after the earned income " + "disregards from earned income first and then from unearned income, " + "never below zero." + ) + + def formula(spm_unit, period, parameters): + person = spm_unit.members + # Section 7.7 leaves SSI recipients out of the benefit group, and + # dependent children's earnings are excluded (Section 7.5), so only + # the other members' support counts. The yearly input is read + # monthly, as sc_tanf_gross_unearned_income reads its sources. + payer = ~person("sc_tanf_ssi_recipient", period) & ~person( + "sc_tanf_dependent_child", period + ) + support_paid = spm_unit.sum( + max_(person("child_support_expense", period), 0) * payer + ) + # Sections 8.11 and 8.16 D and F deduct it from the group's earned + # income after the Section 8.12 disregards, then from unearned + # income. A group with no income gets no deduction (Section 8.11(2)). + earned = add(spm_unit, period, ["sc_tanf_countable_earned_income_person"]) + unearned = add(spm_unit, period, ["sc_tanf_gross_unearned_income"]) + return min_(support_paid, max_(earned + unearned, 0)) diff --git a/policyengine_us/variables/gov/states/sc/tanf/income/sc_tanf_countable_income.py b/policyengine_us/variables/gov/states/sc/tanf/income/sc_tanf_countable_income.py index cb50ab3bddb..8ba90e94916 100644 --- a/policyengine_us/variables/gov/states/sc/tanf/income/sc_tanf_countable_income.py +++ b/policyengine_us/variables/gov/states/sc/tanf/income/sc_tanf_countable_income.py @@ -9,10 +9,19 @@ class sc_tanf_countable_income(Variable): definition_period = MONTH defined_for = StateCode.SC reference = ( - "https://dss.sc.gov/media/ojqddxsk/tanf-policy-manual-volume-65.pdf#page=131" + # PDF pages 131, 134 + "https://dss.sc.gov/media/bxwc1jdj/tanf-policy-manual-volume-67-final.pdf#page=131", ) - adds = [ - "sc_tanf_countable_earned_income_person", - "sc_tanf_gross_unearned_income", - ] + def formula(spm_unit, period, parameters): + income = add( + spm_unit, + period, + [ + "sc_tanf_countable_earned_income_person", + "sc_tanf_gross_unearned_income", + ], + ) + # Section 8.16 H tests and budgets income net of child support paid. + # Countable income keeps its cents; nothing says to round it. + return income - spm_unit("sc_tanf_child_support_deduction", period) diff --git a/policyengine_us/variables/gov/states/sc/tanf/income/sc_tanf_gross_earned_income.py b/policyengine_us/variables/gov/states/sc/tanf/income/sc_tanf_gross_earned_income.py index f63cbfc90fd..76b92b5c18b 100644 --- a/policyengine_us/variables/gov/states/sc/tanf/income/sc_tanf_gross_earned_income.py +++ b/policyengine_us/variables/gov/states/sc/tanf/income/sc_tanf_gross_earned_income.py @@ -8,11 +8,17 @@ class sc_tanf_gross_earned_income(Variable): unit = USD definition_period = MONTH defined_for = StateCode.SC - # PDF pages 102, 112, 117 - reference = "https://dss.sc.gov/media/bxwc1jdj/tanf-policy-manual-volume-67-final.pdf#page=102" # Sections 7.1 and 7.7 + # PDF pages 102, 105, 112, 117, 134 + reference = "https://dss.sc.gov/media/bxwc1jdj/tanf-policy-manual-volume-67-final.pdf#page=102" # Sections 7.1, 7.5, 7.7 and 8.16 B def formula(person, period, parameters): p = parameters(period).gov.states.sc.tanf.income.earned earned = add(person, period, p.earned) - # Section 7.7 excludes the income of SSI recipients. - return earned * ~person("sc_tanf_ssi_recipient", period) + # Section 7.7 excludes the income of SSI recipients. Section 7.5 + # excludes a dependent child's earned income from eligibility and + # the benefit, and Section 8.16 B leaves it out of the budget, so + # neither the gross nor the net income test counts it. + counted = ~person("sc_tanf_ssi_recipient", period) & ~person( + "sc_tanf_dependent_child", period + ) + return earned * counted diff --git a/policyengine_us/variables/gov/states/sc/tanf/sc_tanf.py b/policyengine_us/variables/gov/states/sc/tanf/sc_tanf.py index db4cc9e6c73..12cbbedc369 100644 --- a/policyengine_us/variables/gov/states/sc/tanf/sc_tanf.py +++ b/policyengine_us/variables/gov/states/sc/tanf/sc_tanf.py @@ -12,6 +12,7 @@ class sc_tanf(Variable): # PDF pages 131, 133 "https://dss.sc.gov/media/ojqddxsk/tanf-policy-manual-volume-65.pdf#page=131", "https://www.scstatehouse.gov/coderegs/Chapter%20114.pdf#page=210", + "https://dss.sc.gov/media/bxwc1jdj/tanf-policy-manual-volume-67-final.pdf#page=133", ) def formula(spm_unit, period, parameters): @@ -22,7 +23,11 @@ def formula(spm_unit, period, parameters): benefit = excess_income * p.payment.rate # Cap benefit at need standard to prevent negative income # from inflating benefits above the maximum. - capped_benefit = min_(benefit, need_standard * p.payment.rate) + # 114-1140(A)(4) rounds the payment down to the next lower whole + # dollar, and the Section 8.15 table drops the cents of the payment + # standard. Round before the minimum test: $9.79 becomes $9, which + # pays nothing. + capped_benefit = np.floor(min_(benefit, need_standard * p.payment.rate)) # No payment is made in a month when the benefit is under the # minimum (S.C. Code Regs. 114-1140(A)(5); TANF Policy Manual 8.16 I). return where(capped_benefit >= p.minimum_payment, capped_benefit, 0) diff --git a/policyengine_us/variables/gov/states/sc/tanf/sc_tanf_dependent_child.py b/policyengine_us/variables/gov/states/sc/tanf/sc_tanf_dependent_child.py new file mode 100644 index 00000000000..bb088d48716 --- /dev/null +++ b/policyengine_us/variables/gov/states/sc/tanf/sc_tanf_dependent_child.py @@ -0,0 +1,34 @@ +from policyengine_us.model_api import * + + +class sc_tanf_dependent_child(Variable): + value_type = bool + entity = Person + label = "South Carolina TANF dependent child" + definition_period = MONTH + defined_for = StateCode.SC + reference = ( + # PDF pages 105, 132, 134 + "https://dss.sc.gov/media/bxwc1jdj/tanf-policy-manual-volume-67-final.pdf#page=105", + ) + documentation = ( + "A member who meets the dependent-child age test, under 18 or under " + "19 in secondary school, and is not the head or spouse of the tax " + "unit. A minor parent who heads the household counts as a parent." + ) + + def formula(person, period, parameters): + p = parameters(period).gov.hhs.tanf.cash.eligibility.age_limit + year = period.this_year + age = person("age", year) + # Unlike is_person_demographic_tanf_eligible, pregnancy does not make + # an adult a dependent child, so her wages stay counted. + in_secondary_school = person("is_in_secondary_school", year) + age_limit = where(in_secondary_school, p.student, p.non_student) + meets_age_test = age < age_limit + # Section 7.5 counts a minor parent's earned income "if the minor + # parent is the head-of-household" and excludes it when the minor + # parent is a dependent in the benefit group. The tax unit head or + # spouse stands in for the head of household. + head_or_spouse = person("is_tax_unit_head_or_spouse", year) + return meets_age_test & ~head_or_spouse diff --git a/policyengine_us/variables/gov/states/sc/tanf/sc_tanf_need_standard.py b/policyengine_us/variables/gov/states/sc/tanf/sc_tanf_need_standard.py index 1a869558d4b..6466c6d6f92 100644 --- a/policyengine_us/variables/gov/states/sc/tanf/sc_tanf_need_standard.py +++ b/policyengine_us/variables/gov/states/sc/tanf/sc_tanf_need_standard.py @@ -28,4 +28,8 @@ def formula(spm_unit, period, parameters): p_fpg.first_person[state_group] + p_fpg.additional_person[state_group] * (size - 1) ) / MONTHS_IN_YEAR - return where(size > 0, fpg * p.rate, 0) + # 114-1140(A)(4) rounds the standard of need down to a whole dollar. + # The Section 8.15 table rounds the monthly guideline before halving + # it ($1,762 -> $881 for two); floor(floor(x) / 2) = floor(x / 2) for + # x >= 0, so flooring once gives the same result. + return where(size > 0, np.floor(fpg * p.rate), 0) From 600d93e70acd8c9b6adbeb13d5100a9d98a1d188 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 09:31:20 +0000 Subject: [PATCH 02/14] Pin May cases to calendar-year inputs and clamp rounding in SC countable income A YAML test whose period is a month stores yearly inputs for the year that starts that month, so the May cases now use the 2026 period with May outputs. A January case separates the deduction from the 50% disregard. The property tests found countable income a float32 rounding error below zero when the deduction equals the group's income; floor it there, and leave groups with no deduction unchanged. Co-Authored-By: Claude Opus 5.5 --- .../gov/states/sc/tanf/integration.yaml | 74 +++++++++++++++---- .../tanf/income/sc_tanf_countable_income.py | 6 +- 2 files changed, 65 insertions(+), 15 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml index 02547a005a5..f11c2f4929a 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml @@ -621,7 +621,9 @@ sc_tanf: 368 - name: Case 21, child support paid comes off earned income after the disregard. - period: 2026-05 + # Annual inputs cover calendar 2026; outputs select May, when the $100 + # disregard applies. + period: 2026 input: people: parent: @@ -641,15 +643,21 @@ state_code: SC output: # May uses the $100 disregard: E = 600 - 100 = 500; S = 2,400 / 12 = 200. - sc_tanf_child_support_deduction: 200 - sc_tanf_countable_income: 300 + sc_tanf_child_support_deduction: + 2026-05: 200 + sc_tanf_countable_income: + 2026-05: 300 # The Section 8.15 gross income test stays on gross income. - sc_tanf_gross_income: 600 + sc_tanf_gross_income: + 2026-05: 600 # floor((1,110 - 300) x 0.4046 = 327.73) = 327. - sc_tanf: 327 + sc_tanf: + 2026-05: 327 - name: Case 22, one adult's support payment comes off the other adult's earnings. - period: 2026-05 + # Annual inputs cover calendar 2026; outputs select May, when the $100 + # disregard applies. + period: 2026 input: people: adult_a: @@ -675,15 +683,20 @@ output: # Section 8.11 deducts support paid by a benefit group member from the # group's income: E = 600 - 100 = 500, S = 200, countable 300. - sc_tanf_child_support_deduction: 200 - sc_tanf_countable_income: 300 + sc_tanf_child_support_deduction: + 2026-05: 200 + sc_tanf_countable_income: + 2026-05: 300 # floor((1,110 - 300) x 0.4046) = 327. Limiting the deduction to the # payer's own earnings (0) would leave 500 countable and # floor(610 x 0.4046 = 246.81) = 246. - sc_tanf: 327 + sc_tanf: + 2026-05: 327 - name: Case 23, support paid above the group's income stops at zero. - period: 2026-05 + # Annual inputs cover calendar 2026; outputs select May, when the $100 + # disregard applies. + period: 2026 input: people: parent: @@ -705,10 +718,14 @@ output: # E = 250 - 100 = 150, U = 100, S = 300: the deduction takes E, then U, # and stops at min(300, 250) = 250, leaving 0. - sc_tanf_child_support_deduction: 250 - sc_tanf_countable_income: 0 - sc_tanf_gross_income: 350 - sc_tanf: 449 + sc_tanf_child_support_deduction: + 2026-05: 250 + sc_tanf_countable_income: + 2026-05: 0 + sc_tanf_gross_income: + 2026-05: 350 + sc_tanf: + 2026-05: 449 - name: Case 24, a pregnant adult's wages count. period: 2026-01 @@ -730,3 +747,32 @@ sc_tanf_countable_income: 250 # floor((652 - 250) x 0.4046 = 162.65) = 162. sc_tanf: 162 + +- name: Case 25, in January the deduction follows the 50% disregard. + period: 2026-01 + input: + people: + parent: + age: 40 + is_tax_unit_head: true + employment_income_before_lsr: 7_200 + child_support_expense: 2_400 + child1: + age: 16 + is_tax_unit_dependent: true + child2: + age: 10 + is_tax_unit_dependent: true + households: + household: + members: [parent, child1, child2] + state_code: SC + output: + # Section 8.16 applies the disregard first: E = 600 x 0.5 = 300, less + # S = 200, leaves 100. Deducting support first would leave + # (600 - 200) x 0.5 = 200. + sc_tanf_child_support_deduction: 200 + sc_tanf_countable_income: 100 + sc_tanf_gross_income: 600 + # floor((1,110 - 100) x 0.4046 = 408.65) = 408. + sc_tanf: 408 diff --git a/policyengine_us/variables/gov/states/sc/tanf/income/sc_tanf_countable_income.py b/policyengine_us/variables/gov/states/sc/tanf/income/sc_tanf_countable_income.py index 8ba90e94916..dae2874955f 100644 --- a/policyengine_us/variables/gov/states/sc/tanf/income/sc_tanf_countable_income.py +++ b/policyengine_us/variables/gov/states/sc/tanf/income/sc_tanf_countable_income.py @@ -24,4 +24,8 @@ def formula(spm_unit, period, parameters): ) # Section 8.16 H tests and budgets income net of child support paid. # Countable income keeps its cents; nothing says to round it. - return income - spm_unit("sc_tanf_child_support_deduction", period) + deduction = spm_unit("sc_tanf_child_support_deduction", period) + # The deduction never takes income below zero (Section 8.11(2)); the + # floor only absorbs rounding when it equals the group's income. A + # group with no deduction keeps its income unchanged. + return where(deduction > 0, max_(income - deduction, 0), income) From cdf464ad908976e149908ebfb357b6276c61f36e Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 2 Nov 2026 09:32:21 -0500 Subject: [PATCH 03/14] Record the SC TANF need standard above eight persons in candidates.yaml S.C. Code Regs. 114-1140(A)(2) and (A)(4) set the need standard at fifty percent of the poverty guideline, rounded down; the Policy Manual section 8.15 note adds $229 for each person above eight. The two differ by $1 from 13 persons. The model follows the regulation. Co-Authored-By: Claude Opus 5.5 --- source_conflicts/candidates.yaml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/source_conflicts/candidates.yaml b/source_conflicts/candidates.yaml index 1cea80fc061..168780750ae 100644 --- a/source_conflicts/candidates.yaml +++ b/source_conflicts/candidates.yaml @@ -8236,6 +8236,37 @@ candidates: note: The model computes the couple jointly at $90, which the couple.yaml comment derives from the per-individual allowance in §40-6-27(a)(3); ri_ssp.yaml Case 18 pins [45, 45] for a couple with no income. SSA administered Category E only through December 31, 2010. +- id: us-sc:tanf-need-standard-above-eight + program: tanf + years: + from: 2026 + to: 2026 + type: federal-fiscal + question: For a benefit group of more than eight, is South Carolina's TANF need standard fifty percent of the poverty + guideline rounded down to a whole dollar, or the Policy Manual's eight-person amount plus $229 for each added person? + sources: + - kind: regulation + citation: S.C. Code Regs. 114-1140(A)(2) and (A)(4) + url: https://www.scstatehouse.gov/coderegs/Chapter%20114.pdf#page=210 + - kind: manual + citation: SC DSS TANF Policy Manual Vol. 67 section 8.15 + url: https://dss.sc.gov/media/bxwc1jdj/tanf-policy-manual-volume-67-final.pdf#page=133 + readings: + statute: No statute is supplied; 114-1140(A)(2) sets the need standard "at fifty percent of the current year's" poverty + guidelines, and (A)(4) rounds it "down to the next lower whole dollar". + other: The section 8.15 table stops at eight persons, and its note says "For family sizes over 8, $229 is added to the + need standard for each additional person." + example: Under the 2025 guidelines a group of 13 has a need standard of floor($81,650 / 12 x 0.5) = $3,402 under + 114-1140(A)(2) and (A)(4), and $2,256 + 5 x $229 = $3,401 under the manual note. The two agree through 12 persons. + pe_us: + follows: statute + code: + - policyengine_us/variables/gov/states/sc/tanf/sc_tanf_need_standard.py + prs: + - https://github.com/PolicyEngine/policyengine-us/pull/10581 + state: ruled + note: The regulation governs, and the manual's $229 restates the 2025 added-person guideline ($5,500 / 12 x 0.5 = + $229.17) without its cents. The regulation gives $1 more for 13 to 18 persons and $2 more for 19 to 24. - id: us-sd:liheap-2025-published-income-limit program: liheap years: From 0da9f14e8b1a164e47e3b50387566fceb69f0c40 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 21:53:32 +0000 Subject: [PATCH 04/14] Count the earnings of an SC TANF minor parent who lives with no adult Section 7.5 counts a minor parent's earned income if the minor parent is the head of household. PE-US infers tax-unit roles only for adults, so a 17-year-old living alone with her baby headed no tax unit unless roles were input, and her wages were excluded as a dependent child's: $356 instead of $255. In a group with no adult nobody else can head the household, so the oldest member does, mirroring is_tax_unit_head. With an adult in the group, the tax unit's head or spouse still heads it. Co-Authored-By: Claude Opus 5.5 --- .../gov/states/sc/tanf/integration.yaml | 27 +++++++++++++++++++ .../sc/tanf/sc_tanf_dependent_child.yaml | 21 +++++++++++++++ .../states/sc/tanf/sc_tanf_dependent_child.py | 14 +++++++--- 3 files changed, 59 insertions(+), 3 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml index f11c2f4929a..b69ef10e194 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml @@ -776,3 +776,30 @@ sc_tanf_gross_income: 600 # floor((1,110 - 100) x 0.4046 = 408.65) = 408. sc_tanf: 408 + +- name: Case 26, a minor parent who lives with no adult heads the household. + period: 2026-01 + input: + people: + minor_parent: + age: 17 + employment_income_before_lsr: 6_000 + baby: + age: 0 + households: + household: + members: [minor_parent, baby] + state_code: SC + output: + # No tax-unit role is input. In a group with no adult the oldest member + # heads the household, so Section 7.5 counts her earnings. + sc_tanf_dependent_child: [false, true] + sc_tanf_gross_earned_income: [500, 0] + sc_tanf_gross_income: 500 + # 6,000 / 12 = 500 a month, less the 50% January disregard: 250. + sc_tanf_countable_income: 250 + # Size 2: floor((15,650 + 5,500) / 12 x 0.5 = 881.25) = 881. + sc_tanf_need_standard: 881 + # floor((881 - 250) x 0.4046 = 255.30) = 255. Treating her as a dependent + # child would leave 0 countable and floor(881 x 0.4046 = 356.45) = 356. + sc_tanf: 255 diff --git a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_dependent_child.yaml b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_dependent_child.yaml index b67d31c00c7..fc30bffc437 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_dependent_child.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_dependent_child.yaml @@ -85,3 +85,24 @@ # included as a dependent in the TANF BG"; Section 8.16 B likewise. sc_tanf_dependent_child: [false, true, true] sc_tanf_gross_earned_income: [0, 0, 0] + +- name: Case 5, a minor parent who lives with no adult heads the household. + period: 2026-01 + input: + people: + minor_parent: + age: 17 + employment_income_before_lsr: 6_000 + baby: + age: 0 + households: + household: + members: [minor_parent, baby] + state_code: SC + output: + # No tax-unit role is input, and roles are inferred only for adults, so + # neither member heads the tax unit. In a group with no adult the oldest + # member heads the household, so Section 7.5 counts her earnings. + sc_tanf_dependent_child: [false, true] + # 6,000 / 12 = 500 a month. + sc_tanf_gross_earned_income: [500, 0] diff --git a/policyengine_us/variables/gov/states/sc/tanf/sc_tanf_dependent_child.py b/policyengine_us/variables/gov/states/sc/tanf/sc_tanf_dependent_child.py index bb088d48716..ea266be2e5e 100644 --- a/policyengine_us/variables/gov/states/sc/tanf/sc_tanf_dependent_child.py +++ b/policyengine_us/variables/gov/states/sc/tanf/sc_tanf_dependent_child.py @@ -13,8 +13,10 @@ class sc_tanf_dependent_child(Variable): ) documentation = ( "A member who meets the dependent-child age test, under 18 or under " - "19 in secondary school, and is not the head or spouse of the tax " - "unit. A minor parent who heads the household counts as a parent." + "19 in secondary school, and doesn't head the household. The tax " + "unit's head or spouse heads it, and in a group with no adult the " + "oldest member does, so a minor parent who heads the household " + "counts as a parent." ) def formula(person, period, parameters): @@ -31,4 +33,10 @@ def formula(person, period, parameters): # parent is a dependent in the benefit group. The tax unit head or # spouse stands in for the head of household. head_or_spouse = person("is_tax_unit_head_or_spouse", year) - return meets_age_test & ~head_or_spouse + # Tax-unit roles are inferred only for adults, so a minor parent who + # lives with no adult heads no tax unit unless roles are input. The + # oldest member of a group with no adult heads the household. + no_adult = ~person.spm_unit.any(~person("is_child", year)) + oldest = person.get_rank(person.spm_unit, -age) == 0 + heads_household = head_or_spouse | (no_adult & oldest) + return meets_age_test & ~heads_household From b65a975fe0ea8dad9879d084116823df2c5b893d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 21:53:32 +0000 Subject: [PATCH 05/14] Pin an SC TANF married minor input as the tax unit's spouse A minor married to an adult counts as a parent only when the household input names them the tax unit's head or spouse, because roles are inferred only for adults. This case pins that path: her $500 a month counts, and the group of three receives $347. Co-Authored-By: Claude Opus 5.5 --- .../gov/states/sc/tanf/integration.yaml | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml index b69ef10e194..7ac5ddfa0f0 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml @@ -803,3 +803,34 @@ # floor((881 - 250) x 0.4046 = 255.30) = 255. Treating her as a dependent # child would leave 0 countable and floor(881 x 0.4046 = 356.45) = 356. sc_tanf: 255 + +- name: Case 27, a married minor input as the tax unit's spouse counts as a parent. + period: 2026-01 + input: + people: + adult_spouse: + age: 20 + minor_spouse: + age: 17 + is_tax_unit_spouse: true + employment_income_before_lsr: 6_000 + baby: + age: 0 + marital_units: + couple: + members: [adult_spouse, minor_spouse] + baby_unit: + members: [baby] + households: + household: + members: [adult_spouse, minor_spouse, baby] + state_code: SC + output: + # Tax-unit roles are inferred only for adults, so with an adult in the + # group the 17-year-old counts as a parent through the spouse role input. + sc_tanf_dependent_child: [false, false, true] + sc_tanf_gross_earned_income: [0, 500, 0] + # 6,000 / 12 = 500 a month, less the 50% January disregard: 250. + sc_tanf_countable_income: 250 + # floor((1,110 - 250) x 0.4046 = 347.96) = 347. + sc_tanf: 347 From b58a4031a1c55f55f49bd035e8b287e541bc6bdc Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 21:53:32 +0000 Subject: [PATCH 06/14] Cite TANF Policy Manual Volume 67 for the SC TANF payment The reference kept Volume 65 page 131 beside Volume 67. The payment's rules are the Section 8.15 table notes (page 133), which drop the cents of the payment standard, and Section 8.16 I (page 134), the $10 minimum, with S.C. Code Regs. 114-1140(A)(4). One entry per source document. Co-Authored-By: Claude Opus 5.5 --- policyengine_us/variables/gov/states/sc/tanf/sc_tanf.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/policyengine_us/variables/gov/states/sc/tanf/sc_tanf.py b/policyengine_us/variables/gov/states/sc/tanf/sc_tanf.py index 12cbbedc369..042c1e1a06a 100644 --- a/policyengine_us/variables/gov/states/sc/tanf/sc_tanf.py +++ b/policyengine_us/variables/gov/states/sc/tanf/sc_tanf.py @@ -9,10 +9,9 @@ class sc_tanf(Variable): definition_period = MONTH defined_for = "sc_tanf_eligible" reference = ( - # PDF pages 131, 133 - "https://dss.sc.gov/media/ojqddxsk/tanf-policy-manual-volume-65.pdf#page=131", - "https://www.scstatehouse.gov/coderegs/Chapter%20114.pdf#page=210", + # PDF pages 133-134 "https://dss.sc.gov/media/bxwc1jdj/tanf-policy-manual-volume-67-final.pdf#page=133", + "https://www.scstatehouse.gov/coderegs/Chapter%20114.pdf#page=210", ) def formula(spm_unit, period, parameters): From b7075ee06777d8794f054c79f3f37eccc3b34164 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 12:59:11 +0000 Subject: [PATCH 07/14] Make all members who tie for oldest head a no-adult SC TANF group get_rank picked one of two equally old members as head, so the result depended on input order: twins aged 17, one earning, gave sc_tanf 449 with the non-earner listed first and 347 with the earner first. Every member whose age equals the group's maximum now heads the household, giving 347 in both orders. Adds the tie household in both orders to sc_tanf_dependent_child.yaml and integration.yaml. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6H5RwtLwccqSeGpYbx --- .../gov/states/sc/tanf/integration.yaml | 49 +++++++++++++++++++ .../sc/tanf/sc_tanf_dependent_child.yaml | 42 ++++++++++++++++ .../states/sc/tanf/sc_tanf_dependent_child.py | 10 ++-- 3 files changed, 97 insertions(+), 4 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml index 7ac5ddfa0f0..c17a1890244 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml @@ -834,3 +834,52 @@ sc_tanf_countable_income: 250 # floor((1,110 - 250) x 0.4046 = 347.96) = 347. sc_tanf: 347 + +- name: Case 28, members who tie for oldest in a group with no adult all head it. + period: 2026-01 + input: + people: + sibling: + age: 17 + minor_parent: + age: 17 + employment_income_before_lsr: 6_000 + baby: + age: 0 + households: + household: + members: [sibling, minor_parent, baby] + state_code: SC + output: + # No tax-unit role is input. The two 17-year-olds tie for oldest in a + # group with no adult, so both head the household and the earner's wages + # count whatever order the members are listed in. + sc_tanf_dependent_child: [false, false, true] + sc_tanf_gross_earned_income: [0, 500, 0] + # 6,000 / 12 = 500 a month, less the 50% January disregard: 250. + sc_tanf_countable_income: 250 + # Size 3: floor((15,650 + 2 x 5,500) / 12 x 0.5 = 1,110.42) = 1,110. + # floor((1,110 - 250) x 0.4046 = 347.96) = 347. Dropping her wages would + # give floor(1,110 x 0.4046 = 449.11) = 449. + sc_tanf: 347 + +- name: Case 29, Case 28 with the earner listed first gives the same result. + period: 2026-01 + input: + people: + minor_parent: + age: 17 + employment_income_before_lsr: 6_000 + sibling: + age: 17 + baby: + age: 0 + households: + household: + members: [minor_parent, sibling, baby] + state_code: SC + output: + sc_tanf_dependent_child: [false, false, true] + sc_tanf_gross_earned_income: [500, 0, 0] + sc_tanf_countable_income: 250 + sc_tanf: 347 diff --git a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_dependent_child.yaml b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_dependent_child.yaml index fc30bffc437..4f2035648f4 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_dependent_child.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_dependent_child.yaml @@ -106,3 +106,45 @@ sc_tanf_dependent_child: [false, true] # 6,000 / 12 = 500 a month. sc_tanf_gross_earned_income: [500, 0] + +- name: Case 6, members who tie for oldest in a group with no adult all head it. + period: 2026-01 + input: + people: + sibling: + age: 17 + minor_parent: + age: 17 + employment_income_before_lsr: 6_000 + baby: + age: 0 + households: + household: + members: [sibling, minor_parent, baby] + state_code: SC + output: + # No tax-unit role is input. The two 17-year-olds tie for oldest in a + # group with no adult, so both head the household and neither is a + # dependent child, whatever order they're listed in. + sc_tanf_dependent_child: [false, false, true] + # 6,000 / 12 = 500 a month. + sc_tanf_gross_earned_income: [0, 500, 0] + +- name: Case 7, Case 6 with the earner listed first gives the same result. + period: 2026-01 + input: + people: + minor_parent: + age: 17 + employment_income_before_lsr: 6_000 + sibling: + age: 17 + baby: + age: 0 + households: + household: + members: [minor_parent, sibling, baby] + state_code: SC + output: + sc_tanf_dependent_child: [false, false, true] + sc_tanf_gross_earned_income: [500, 0, 0] diff --git a/policyengine_us/variables/gov/states/sc/tanf/sc_tanf_dependent_child.py b/policyengine_us/variables/gov/states/sc/tanf/sc_tanf_dependent_child.py index ea266be2e5e..f5dbd9e453a 100644 --- a/policyengine_us/variables/gov/states/sc/tanf/sc_tanf_dependent_child.py +++ b/policyengine_us/variables/gov/states/sc/tanf/sc_tanf_dependent_child.py @@ -15,8 +15,8 @@ class sc_tanf_dependent_child(Variable): "A member who meets the dependent-child age test, under 18 or under " "19 in secondary school, and doesn't head the household. The tax " "unit's head or spouse heads it, and in a group with no adult the " - "oldest member does, so a minor parent who heads the household " - "counts as a parent." + "oldest member does, and members who tie for oldest all do, so a " + "minor parent who heads the household counts as a parent." ) def formula(person, period, parameters): @@ -35,8 +35,10 @@ def formula(person, period, parameters): head_or_spouse = person("is_tax_unit_head_or_spouse", year) # Tax-unit roles are inferred only for adults, so a minor parent who # lives with no adult heads no tax unit unless roles are input. The - # oldest member of a group with no adult heads the household. + # oldest member of a group with no adult heads the household; members + # who tie for oldest all head it, so the result doesn't depend on the + # order in which members are listed. no_adult = ~person.spm_unit.any(~person("is_child", year)) - oldest = person.get_rank(person.spm_unit, -age) == 0 + oldest = age == person.spm_unit.max(age) heads_household = head_or_spouse | (no_adult & oldest) return meets_age_test & ~heads_household From 39ddd6888732e686b2e69d5be68f6be79bc889ab Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 01:04:30 +0000 Subject: [PATCH 08/14] Test that a younger minor in a no-adult SC TANF group is a dependent child The round 3 review's mutant (h), which makes every member within a year of the oldest a head, survived both YAML files because each no-adult case paired 17-year-olds with a baby. Case 8 (sc_tanf_dependent_child.yaml) and Case 30 (integration.yaml) add a 17-year-old sibling, an earning 16-year-old and a baby, which kills it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6FqQp8Bf92tRiedzTm --- .../gov/states/sc/tanf/integration.yaml | 26 +++++++++++++++++++ .../sc/tanf/sc_tanf_dependent_child.yaml | 26 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml index c17a1890244..20795dc419d 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml @@ -883,3 +883,29 @@ sc_tanf_gross_earned_income: [500, 0, 0] sc_tanf_countable_income: 250 sc_tanf: 347 + +- name: Case 30, a younger minor in a group with no adult counts as a dependent child. + period: 2026-01 + input: + people: + sibling: + age: 17 + minor_parent: + age: 16 + employment_income_before_lsr: 6_000 + baby: + age: 0 + households: + household: + members: [sibling, minor_parent, baby] + state_code: SC + output: + # The inputs set no tax-unit role. With no adult in the group, the + # 17-year-old, its oldest member, alone heads the household. The + # 16-year-old counts as a dependent child, so DSS leaves out the + # 16-year-old's earnings (Section 7.5). + sc_tanf_dependent_child: [false, true, true] + sc_tanf_gross_earned_income: [0, 0, 0] + sc_tanf_countable_income: 0 + # floor(1,110 x 0.4046 = 449.11) = 449. + sc_tanf: 449 diff --git a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_dependent_child.yaml b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_dependent_child.yaml index 4f2035648f4..ae4181e23c2 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_dependent_child.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_dependent_child.yaml @@ -148,3 +148,29 @@ output: sc_tanf_dependent_child: [false, false, true] sc_tanf_gross_earned_income: [500, 0, 0] + +- name: Case 8, a younger minor in a group with no adult counts as a dependent child. + period: 2026-01 + input: + people: + sibling: + age: 17 + minor_parent: + age: 16 + employment_income_before_lsr: 6_000 + baby: + age: 0 + households: + household: + members: [sibling, minor_parent, baby] + state_code: SC + output: + # The inputs set no tax-unit role. With no adult in the group, the + # 17-year-old, its oldest member, alone heads the household. The + # 16-year-old counts as a dependent child, so DSS leaves out the + # 16-year-old's earnings (Section 7.5). + sc_tanf_dependent_child: [false, true, true] + sc_tanf_gross_earned_income: [0, 0, 0] + sc_tanf_countable_income: 0 + # floor(1,110 x 0.4046 = 449.11) = 449. + sc_tanf: 449 From 95e9baf6ead65d8fa4e59cac05562b1fe7e0b1ca Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 4 Nov 2026 23:15:37 -0500 Subject: [PATCH 09/14] Pin the oldest member heading a South Carolina group with no adult In a group with no adult, the oldest member heads it even when a younger member holds the tax unit head role, so both count as heads and the oldest's earnings count. Add the case to sc_tanf_dependent_child.yaml and integration.yaml: a 16-year-old input as head, a 17-year-old sibling who earns $6,000 and a baby give a $347 payment, against $449 if the sibling counted as a dependent child. Co-Authored-By: Claude Opus 5.5 --- .../gov/states/sc/tanf/integration.yaml | 35 +++++++++++++++++++ .../sc/tanf/sc_tanf_dependent_child.yaml | 35 +++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml index 20795dc419d..8098a35b83f 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml @@ -909,3 +909,38 @@ sc_tanf_countable_income: 0 # floor(1,110 x 0.4046 = 449.11) = 449. sc_tanf: 449 + +- name: Case 31, in a group with no adult the oldest member heads it even when a younger member is input as the tax unit head. + period: 2026-01 + input: + people: + minor_parent: + age: 16 + is_tax_unit_head: true + sibling: + age: 17 + is_tax_unit_dependent: true + employment_income_before_lsr: 6_000 + baby: + age: 0 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [minor_parent, sibling, baby] + spm_units: + spm_unit: + members: [minor_parent, sibling, baby] + households: + household: + members: [minor_parent, sibling, baby] + state_code: SC + output: + # The 16-year-old heads as the tax unit head, and the 17-year-old heads + # as the oldest member of a group with no adult, so the 17-year-old's + # earnings count. + sc_tanf_dependent_child: [false, false, true] + sc_tanf_gross_earned_income: [0, 500, 0] + # 6,000 / 12 = 500 a month, less the 50% January disregard: 250. + sc_tanf_countable_income: 250 + # floor((1,110 - 250) x 0.4046 = 347.96) = 347. + sc_tanf: 347 diff --git a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_dependent_child.yaml b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_dependent_child.yaml index ae4181e23c2..8a624cb0dd1 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_dependent_child.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_dependent_child.yaml @@ -174,3 +174,38 @@ sc_tanf_countable_income: 0 # floor(1,110 x 0.4046 = 449.11) = 449. sc_tanf: 449 + +- name: Case 9, in a group with no adult the oldest member heads it even when a younger member is input as the tax unit head. + period: 2026-01 + input: + people: + minor_parent: + age: 16 + is_tax_unit_head: true + sibling: + age: 17 + is_tax_unit_dependent: true + employment_income_before_lsr: 6_000 + baby: + age: 0 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [minor_parent, sibling, baby] + spm_units: + spm_unit: + members: [minor_parent, sibling, baby] + households: + household: + members: [minor_parent, sibling, baby] + state_code: SC + output: + # The 16-year-old heads as the tax unit head, and the 17-year-old heads + # as the oldest member of a group with no adult, so the 17-year-old's + # earnings count. + sc_tanf_dependent_child: [false, false, true] + sc_tanf_gross_earned_income: [0, 500, 0] + # 6,000 / 12 = 500 a month, less the 50% January disregard: 250. + sc_tanf_countable_income: 250 + # floor((1,110 - 250) x 0.4046 = 347.96) = 347. + sc_tanf: 347 From a1cc4925aeba6e5cf7504b62069530cc9fab9b7d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Nov 2026 14:48:23 +0000 Subject: [PATCH 10/14] Deduct SC TANF child support only from the payer's own income S.C. Code Regs. 114-1140(L) says support a budget group member pays outside the group "may be deducted from the income of the payor". The deduction pooled the group's income instead, so one adult's support came off another adult's earnings. Compute each payer's deduction from that payer's countable earned and unearned income, and sum it. Drop the SSI and dependent-child payer conditions: an SSI recipient's counted income is already zero, and the regulation covers any budget group member, a dependent child included. Floor countable income at the sum of the members' negative incomes instead of zero. With a per-payer deduction, a zero floor would raise countable income when another member has a self-employment loss. Cite Section 8.1 for keeping countable income's cents. Case 22 now pays 246, not 327. New Cases 32 to 36 pin an SSI payer, a dependent-child payer, a negative support input, and a self-employment loss with and without a payer. Record the conflict as us-sc:tanf-child-support-deduction-payer. Co-Authored-By: Claude Opus 5.5 --- .../gov/states/sc/tanf/integration.yaml | 210 ++++++++++++++++-- .../income/sc_tanf_child_support_deduction.py | 41 ++-- .../tanf/income/sc_tanf_countable_income.py | 32 +-- source_conflicts/candidates.yaml | 32 +++ 4 files changed, 266 insertions(+), 49 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml index 8098a35b83f..6c33e1d5c5b 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml @@ -536,11 +536,12 @@ # resources: 2,600 - 1,800 = 800, within the $2,500 limit (page 91). # Benefit group of 2: need standard 851; benefit = floor(851 * 0.4046) = 344 -# Rules added for dependent children's earnings (Section 7.5, PDF page 105; -# Section 8.16 B, page 134), child support paid (Section 8.11, page 131; -# Section 8.16 D and F, page 134) and whole dollars (114-1140(A)(4); the -# Section 8.15 table notes, page 133). 2026 months use the 2025 guideline: -# a group of three has a need standard of floor(26,650 / 12 x 0.5) = 1,110. +# Rules added for dependent children's earnings (114-1140(N)(1); Section 7.5, +# PDF page 105; Section 8.16 B, page 134), child support paid (114-1140(L); +# Section 8.11, page 131; Section 8.16 D and F, page 134) and whole dollars +# (114-1140(A)(4); the Section 8.15 table notes, page 133). 2026 months use +# the 2025 guideline: a group of three has a need standard of +# floor(26,650 / 12 x 0.5) = 1,110. - name: Case 18, a parent and two children with no income. period: 2026-01 @@ -654,7 +655,7 @@ sc_tanf: 2026-05: 327 -- name: Case 22, one adult's support payment comes off the other adult's earnings. +- name: Case 22, support paid by an adult with no income doesn't come off another adult's earnings. # Annual inputs cover calendar 2026; outputs select May, when the $100 # disregard applies. period: 2026 @@ -681,19 +682,20 @@ members: [adult_a, adult_b, child] state_code: SC output: - # Section 8.11 deducts support paid by a benefit group member from the - # group's income: E = 600 - 100 = 500, S = 200, countable 300. + # 114-1140(L) deducts support "from the income of the payor". Adult A + # has no income, so nothing is deducted, and B's earnings count: + # E = 600 - 100 = 500. sc_tanf_child_support_deduction: - 2026-05: 200 + 2026-05: 0 sc_tanf_countable_income: - 2026-05: 300 - # floor((1,110 - 300) x 0.4046) = 327. Limiting the deduction to the - # payer's own earnings (0) would leave 500 countable and - # floor(610 x 0.4046 = 246.81) = 246. + 2026-05: 500 + # floor((1,110 - 500) x 0.4046 = 246.81) = 246. Taking A's 200 from the + # group's income would leave 300 countable and + # floor(810 x 0.4046 = 327.73) = 327. sc_tanf: - 2026-05: 327 + 2026-05: 246 -- name: Case 23, support paid above the group's income stops at zero. +- name: Case 23, support paid above the payer's income stops at zero. # Annual inputs cover calendar 2026; outputs select May, when the $100 # disregard applies. period: 2026 @@ -944,3 +946,181 @@ sc_tanf_countable_income: 250 # floor((1,110 - 250) x 0.4046 = 347.96) = 347. sc_tanf: 347 + +- name: Case 32, support paid by an SSI recipient isn't deducted from another member's earnings. + # Annual inputs cover calendar 2026; outputs select May, when the $100 + # disregard applies. + period: 2026 + input: + people: + ssi_parent: + age: 40 + is_tax_unit_head: true + receives_ssi: true + child_support_expense: 2_400 + earner: + age: 38 + is_tax_unit_spouse: true + employment_income_before_lsr: 7_200 + child: + age: 10 + is_tax_unit_dependent: true + marital_units: + couple: + members: [ssi_parent, earner] + child_unit: + members: [child] + households: + household: + members: [ssi_parent, earner, child] + state_code: SC + output: + # Section 7.7 (page 117) excludes the SSI recipient and that person's + # income in determining benefits for the other members, so the payer has + # no counted income to deduct from (114-1140(L)). + sc_tanf_ssi_recipient: + 2026-05: [true, false, false] + sc_tanf_child_support_deduction: + 2026-05: 0 + # E = 600 - 100 = 500. + sc_tanf_countable_income: + 2026-05: 500 + # Size 2: floor((15,650 + 5,500) / 12 x 0.5 = 881.25) = 881. + sc_tanf_need_standard: + 2026-05: 881 + # floor((881 - 500) x 0.4046 = 154.15) = 154. Taking the 200 from the + # group's income would leave 300 and floor(581 x 0.4046 = 235.07) = 235. + sc_tanf: + 2026-05: 154 + +- name: Case 33, a dependent child who pays support deducts it from the child's own unearned income. + # Annual inputs cover calendar 2026; outputs select May. + period: 2026 + input: + people: + parent: + age: 40 + is_tax_unit_head: true + teen: + age: 17 + is_tax_unit_dependent: true + social_security_survivors: 2_400 + child_support_expense: 1_200 + child: + age: 10 + is_tax_unit_dependent: true + households: + household: + members: [parent, teen, child] + state_code: SC + output: + # 114-1140(L) deducts support paid by "a budget group member", which a + # dependent child is, from that member's income: 1,200 / 12 = 100 off + # the 17-year-old's 2,400 / 12 = 200 of survivor benefits. + sc_tanf_dependent_child: + 2026-05: [false, true, true] + sc_tanf_gross_unearned_income: + 2026-05: [0, 200, 0] + sc_tanf_child_support_deduction: + 2026-05: 100 + sc_tanf_countable_income: + 2026-05: 100 + # floor((1,110 - 100) x 0.4046 = 408.65) = 408. Limiting the deduction + # to adults would leave 200 and floor(910 x 0.4046 = 368.19) = 368. + sc_tanf: + 2026-05: 408 + +- name: Case 34, a negative child support input gives no deduction. + # Annual inputs cover calendar 2026; outputs select May, when the $100 + # disregard applies. + period: 2026 + input: + people: + parent: + age: 40 + is_tax_unit_head: true + employment_income_before_lsr: 7_200 + child_support_expense: -2_400 + child1: + age: 16 + is_tax_unit_dependent: true + child2: + age: 10 + is_tax_unit_dependent: true + households: + household: + members: [parent, child1, child2] + state_code: SC + output: + # A negative payment counts as none, so it can't add to income: + # E = 600 - 100 = 500. + sc_tanf_child_support_deduction: + 2026-05: 0 + sc_tanf_countable_income: + 2026-05: 500 + # floor((1,110 - 500) x 0.4046 = 246.81) = 246. + sc_tanf: + 2026-05: 246 + +- name: Case 35, a self-employment loss with no support paid keeps countable income below zero. + period: 2026-01 + input: + people: + parent: + age: 40 + is_tax_unit_head: true + self_employment_income_before_lsr: -14_400 + child1: + age: 16 + is_tax_unit_dependent: true + child2: + age: 10 + is_tax_unit_dependent: true + households: + household: + members: [parent, child1, child2] + state_code: SC + output: + # -14,400 / 12 = -1,200 a month, and the 50% January disregard leaves + # -600. With no support paid, countable income stays at -600. + sc_tanf_gross_earned_income: [-1_200, 0, 0] + sc_tanf_child_support_deduction: 0 + sc_tanf_countable_income: -600 + # The payment stops at the payment standard: + # floor(1,110 x 0.4046 = 449.11) = 449. + sc_tanf: 449 + +- name: Case 36, a payer's support comes off the payer's income when another member has a self-employment loss. + period: 2026-01 + input: + people: + adult_a: + age: 40 + is_tax_unit_head: true + employment_income_before_lsr: 7_200 + child_support_expense: 2_400 + adult_b: + age: 38 + is_tax_unit_spouse: true + self_employment_income_before_lsr: -14_400 + child: + age: 10 + is_tax_unit_dependent: true + marital_units: + couple: + members: [adult_a, adult_b] + child_unit: + members: [child] + households: + household: + members: [adult_a, adult_b, child] + state_code: SC + output: + # A: 600 x 0.5 = 300, less A's 2,400 / 12 = 200 of support, leaves 100. + # B: -1,200 x 0.5 = -600. Countable income is 100 - 600 = -500. + # Deducting from the group's income (300 - 600 = -300) would leave + # nothing to deduct from, and flooring the total at zero would give 0. + sc_tanf_child_support_deduction: 200 + sc_tanf_countable_income: -500 + # floor(1,110 x 0.4046 = 449.11) = 449. + sc_tanf: 449 diff --git a/policyengine_us/variables/gov/states/sc/tanf/income/sc_tanf_child_support_deduction.py b/policyengine_us/variables/gov/states/sc/tanf/income/sc_tanf_child_support_deduction.py index 9c5132b023e..fbeb47bb18c 100644 --- a/policyengine_us/variables/gov/states/sc/tanf/income/sc_tanf_child_support_deduction.py +++ b/policyengine_us/variables/gov/states/sc/tanf/income/sc_tanf_child_support_deduction.py @@ -9,31 +9,34 @@ class sc_tanf_child_support_deduction(Variable): definition_period = MONTH defined_for = StateCode.SC reference = ( + "https://www.scstatehouse.gov/coderegs/Chapter%20114.pdf#page=212", # PDF pages 131, 134 "https://dss.sc.gov/media/bxwc1jdj/tanf-policy-manual-volume-67-final.pdf#page=131", ) documentation = ( - "Child support that benefit group members who are not dependent " - "children pay outside the home, deducted after the earned income " - "disregards from earned income first and then from unearned income, " - "never below zero." + "Child support that benefit group members pay to legal dependents " + "outside the group, taken from each payer's own income after the " + "earned income disregards. It never takes a payer's income below zero." ) def formula(spm_unit, period, parameters): person = spm_unit.members - # Section 7.7 leaves SSI recipients out of the benefit group, and - # dependent children's earnings are excluded (Section 7.5), so only - # the other members' support counts. The yearly input is read - # monthly, as sc_tanf_gross_unearned_income reads its sources. - payer = ~person("sc_tanf_ssi_recipient", period) & ~person( - "sc_tanf_dependent_child", period + # 114-1140(L): support that a budget group member pays to legal + # dependents outside the group "may be deducted from the income of + # the payor". The yearly input is read monthly, as + # sc_tanf_gross_unearned_income reads its sources. + support_paid = max_(person("child_support_expense", period), 0) + # The payer's income is earned income after the Section 8.12 + # disregards (Section 8.16 C and D), then unearned income (8.16 F). + # Neither variable counts an SSI recipient's income, and the earned + # one leaves out a dependent child's earnings, so that income can't + # absorb a deduction. + own_income = add( + person, + period, + [ + "sc_tanf_countable_earned_income_person", + "sc_tanf_gross_unearned_income", + ], ) - support_paid = spm_unit.sum( - max_(person("child_support_expense", period), 0) * payer - ) - # Sections 8.11 and 8.16 D and F deduct it from the group's earned - # income after the Section 8.12 disregards, then from unearned - # income. A group with no income gets no deduction (Section 8.11(2)). - earned = add(spm_unit, period, ["sc_tanf_countable_earned_income_person"]) - unearned = add(spm_unit, period, ["sc_tanf_gross_unearned_income"]) - return min_(support_paid, max_(earned + unearned, 0)) + return spm_unit.sum(min_(support_paid, max_(own_income, 0))) diff --git a/policyengine_us/variables/gov/states/sc/tanf/income/sc_tanf_countable_income.py b/policyengine_us/variables/gov/states/sc/tanf/income/sc_tanf_countable_income.py index dae2874955f..19c650a7592 100644 --- a/policyengine_us/variables/gov/states/sc/tanf/income/sc_tanf_countable_income.py +++ b/policyengine_us/variables/gov/states/sc/tanf/income/sc_tanf_countable_income.py @@ -9,23 +9,25 @@ class sc_tanf_countable_income(Variable): definition_period = MONTH defined_for = StateCode.SC reference = ( - # PDF pages 131, 134 - "https://dss.sc.gov/media/bxwc1jdj/tanf-policy-manual-volume-67-final.pdf#page=131", + # PDF pages 121, 131, 134 + "https://dss.sc.gov/media/bxwc1jdj/tanf-policy-manual-volume-67-final.pdf#page=121", + "https://www.scstatehouse.gov/coderegs/Chapter%20114.pdf#page=212", ) def formula(spm_unit, period, parameters): - income = add( - spm_unit, - period, - [ - "sc_tanf_countable_earned_income_person", - "sc_tanf_gross_unearned_income", - ], - ) + sources = [ + "sc_tanf_countable_earned_income_person", + "sc_tanf_gross_unearned_income", + ] + income = add(spm_unit, period, sources) # Section 8.16 H tests and budgets income net of child support paid. - # Countable income keeps its cents; nothing says to round it. + # Section 8.1 says "Do not round at any step of the TANF calculation", + # so countable income keeps its cents. deduction = spm_unit("sc_tanf_child_support_deduction", period) - # The deduction never takes income below zero (Section 8.11(2)); the - # floor only absorbs rounding when it equals the group's income. A - # group with no deduction keeps its income unchanged. - return where(deduction > 0, max_(income - deduction, 0), income) + # Each payer's deduction stops at that payer's own income + # (114-1140(L)), so the total never falls below the sum of the + # members' negative incomes, which only a self-employment or farm + # loss produces. That floor absorbs float32 rounding in the deduction. + member_income = add(spm_unit.members, period, sources) + floor = spm_unit.sum(min_(member_income, 0)) + return max_(income - deduction, floor) diff --git a/source_conflicts/candidates.yaml b/source_conflicts/candidates.yaml index 4008a06f01f..f3d99b58f5c 100644 --- a/source_conflicts/candidates.yaml +++ b/source_conflicts/candidates.yaml @@ -9728,6 +9728,38 @@ candidates: state: ruled note: 'Ruled 2026-11-02: income other than minors'' income follows the plan.' +- id: us-sc:tanf-child-support-deduction-payer + program: tanf + years: + from: 2026 + type: federal-fiscal + question: Does child support that a South Carolina TANF benefit group member pays to dependents outside the group + come off only that member's own income, or off the group's income as a whole? + sources: + - kind: regulation + citation: S.C. Code Regs. 114-1140(L) + url: https://www.scstatehouse.gov/coderegs/Chapter%20114.pdf#page=212 + - kind: manual + citation: SC DSS TANF Policy Manual Vol. 67 sections 8.11 and 8.16 D and F + url: https://dss.sc.gov/media/bxwc1jdj/tanf-policy-manual-volume-67-final.pdf#page=131 + readings: + statute: No statute is supplied; 114-1140(L) says support "made by a budget group member" to legal dependents outside + the group "may be deducted from the income of the payor". + other: Section 8.16 D says to subtract support "from the remaining earned income of any adult", and 8.16 F takes + support "paid by any adult" from the unearned income; section 8.11 deducts it "as income to the BG". + example: In May 2026 adult A pays $2,400 a year and has no income, and adult B earns $600 a month, with one child. + From the payer's income the deduction is $0, countable income $500 and the payment floor(610 x 0.4046) = $246. + Pooled, the deduction is $200, countable income $300 and the payment $327. + pe_us: + follows: statute + code: + - policyengine_us/variables/gov/states/sc/tanf/income/sc_tanf_child_support_deduction.py + prs: + - https://github.com/PolicyEngine/policyengine-us/pull/10581 + state: ruled + note: The regulation governs. The model deducts each payer's support from that payer's earned income after the + section 8.12 disregards, then unearned income, never below zero. Because 114-1140(L) covers any budget group + member, a dependent child's support comes off the child's unearned income. - id: us-sc:tanf-need-standard-above-eight program: tanf years: From 4f58041cb916779f2b42d6230d8c510a1216a76f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Nov 2026 14:48:34 +0000 Subject: [PATCH 11/14] Apply the SC TANF oldest-member rule only when no member heads the group In a group with no adult, the oldest member headed the household even when the inputs made a younger member the tax unit head and the oldest member a tax dependent. That counted a dependent child's wages, which S.C. Code Regs. 114-1140(N)(1) excludes. Apply the rule only when no member holds a head or spouse role. Cases 9 and 31 now expect the 17-year-old to be a dependent child, countable income of 0 and a payment of 449. Groups with no role inputs don't move. Cite 114-1130(E) for the age test and 114-1140(N)(1) for the exclusion. Give Cases 8 and 30's members neutral names, and say that those cases pin the inference rule, since no input says which minor is the parent. Co-Authored-By: Claude Opus 5.5 --- .../gov/states/sc/tanf/integration.yaml | 37 +++++++------- .../sc/tanf/sc_tanf_dependent_child.yaml | 48 +++++++++++-------- .../income/sc_tanf_gross_earned_income.py | 16 ++++--- .../states/sc/tanf/sc_tanf_dependent_child.py | 28 +++++++---- 4 files changed, 76 insertions(+), 53 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml index 6c33e1d5c5b..172a275b09e 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml @@ -890,29 +890,32 @@ period: 2026-01 input: people: - sibling: + older_minor: age: 17 - minor_parent: + younger_minor: age: 16 employment_income_before_lsr: 6_000 baby: age: 0 households: household: - members: [sibling, minor_parent, baby] + members: [older_minor, younger_minor, baby] state_code: SC output: - # The inputs set no tax-unit role. With no adult in the group, the - # 17-year-old, its oldest member, alone heads the household. The - # 16-year-old counts as a dependent child, so DSS leaves out the - # 16-year-old's earnings (Section 7.5). + # No tax-unit role is input, so no input says which minor is the baby's + # parent. The case pins PolicyEngine's inference rule: with no adult and + # no head or spouse role in the group, the 17-year-old, its oldest + # member, alone heads it. The 16-year-old is a dependent child, and the + # 16-year-old's earnings drop out. If the 16-year-old is the minor + # parent, DSS counts those earnings (glossary, page 328; Section 7.5); + # inputting is_tax_unit_head for the 16-year-old gives that result. sc_tanf_dependent_child: [false, true, true] sc_tanf_gross_earned_income: [0, 0, 0] sc_tanf_countable_income: 0 # floor(1,110 x 0.4046 = 449.11) = 449. sc_tanf: 449 -- name: Case 31, in a group with no adult the oldest member heads it even when a younger member is input as the tax unit head. +- name: Case 31, in a group with no adult a member input as the tax unit head heads it in place of the oldest member. period: 2026-01 input: people: @@ -937,15 +940,17 @@ members: [minor_parent, sibling, baby] state_code: SC output: - # The 16-year-old heads as the tax unit head, and the 17-year-old heads - # as the oldest member of a group with no adult, so the 17-year-old's - # earnings count. - sc_tanf_dependent_child: [false, false, true] - sc_tanf_gross_earned_income: [0, 500, 0] - # 6,000 / 12 = 500 a month, less the 50% January disregard: 250. - sc_tanf_countable_income: 250 + # The 16-year-old is input as the tax unit head, so the oldest-member + # rule doesn't apply. The 17-year-old, input as a tax dependent, is a + # dependent child, and 114-1140(N)(1) excludes the 17-year-old's + # earnings. + sc_tanf_dependent_child: [false, true, true] + sc_tanf_gross_earned_income: [0, 0, 0] + sc_tanf_countable_income: 0 + # floor(1,110 x 0.4046 = 449.11) = 449. If the oldest member headed the + # group too, 500 less the 50% January disregard, 250, would count, and # floor((1,110 - 250) x 0.4046 = 347.96) = 347. - sc_tanf: 347 + sc_tanf: 449 - name: Case 32, support paid by an SSI recipient isn't deducted from another member's earnings. # Annual inputs cover calendar 2026; outputs select May, when the $100 diff --git a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_dependent_child.yaml b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_dependent_child.yaml index 8a624cb0dd1..c8b99662c6a 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_dependent_child.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_dependent_child.yaml @@ -1,8 +1,9 @@ -# SC DSS TANF Policy Manual (Volume 67), Section 7.5 (PDF page 105): a -# dependent child's earned income is excluded; a minor parent's earned income -# counts "if the minor parent is the head-of-household". The age test reuses -# the federal TANF minor-child limits: under 18, or under 19 in secondary -# school. +# S.C. Code Regs. 114-1140(N)(1) (PDF page 213) and SC DSS TANF Policy +# Manual (Volume 67), Section 7.5 (PDF page 105): a dependent child's earned +# income is excluded; a minor parent's earned income counts "if the minor +# parent is the head-of-household". 114-1130(E) (PDF page 204) sets the age +# test, which reuses the federal TANF minor-child limits: under 18, or under +# 19 in secondary school. - name: Case 1, children on both sides of the age and school boundaries. period: 2026-01 @@ -153,29 +154,32 @@ period: 2026-01 input: people: - sibling: + older_minor: age: 17 - minor_parent: + younger_minor: age: 16 employment_income_before_lsr: 6_000 baby: age: 0 households: household: - members: [sibling, minor_parent, baby] + members: [older_minor, younger_minor, baby] state_code: SC output: - # The inputs set no tax-unit role. With no adult in the group, the - # 17-year-old, its oldest member, alone heads the household. The - # 16-year-old counts as a dependent child, so DSS leaves out the - # 16-year-old's earnings (Section 7.5). + # No tax-unit role is input, so no input says which minor is the baby's + # parent. The case pins PolicyEngine's inference rule: with no adult and + # no head or spouse role in the group, the 17-year-old, its oldest + # member, alone heads it. The 16-year-old is a dependent child, and the + # 16-year-old's earnings drop out. If the 16-year-old is the minor + # parent, DSS counts those earnings (glossary, page 328; Section 7.5); + # inputting is_tax_unit_head for the 16-year-old gives that result. sc_tanf_dependent_child: [false, true, true] sc_tanf_gross_earned_income: [0, 0, 0] sc_tanf_countable_income: 0 # floor(1,110 x 0.4046 = 449.11) = 449. sc_tanf: 449 -- name: Case 9, in a group with no adult the oldest member heads it even when a younger member is input as the tax unit head. +- name: Case 9, in a group with no adult a member input as the tax unit head heads it in place of the oldest member. period: 2026-01 input: people: @@ -200,12 +204,14 @@ members: [minor_parent, sibling, baby] state_code: SC output: - # The 16-year-old heads as the tax unit head, and the 17-year-old heads - # as the oldest member of a group with no adult, so the 17-year-old's - # earnings count. - sc_tanf_dependent_child: [false, false, true] - sc_tanf_gross_earned_income: [0, 500, 0] - # 6,000 / 12 = 500 a month, less the 50% January disregard: 250. - sc_tanf_countable_income: 250 + # The 16-year-old is input as the tax unit head, so the oldest-member + # rule doesn't apply. The 17-year-old, input as a tax dependent, is a + # dependent child, and 114-1140(N)(1) excludes the 17-year-old's + # earnings. + sc_tanf_dependent_child: [false, true, true] + sc_tanf_gross_earned_income: [0, 0, 0] + sc_tanf_countable_income: 0 + # floor(1,110 x 0.4046 = 449.11) = 449. If the oldest member headed the + # group too, 500 less the 50% January disregard, 250, would count, and # floor((1,110 - 250) x 0.4046 = 347.96) = 347. - sc_tanf: 347 + sc_tanf: 449 diff --git a/policyengine_us/variables/gov/states/sc/tanf/income/sc_tanf_gross_earned_income.py b/policyengine_us/variables/gov/states/sc/tanf/income/sc_tanf_gross_earned_income.py index 76b92b5c18b..26e7df0e0bc 100644 --- a/policyengine_us/variables/gov/states/sc/tanf/income/sc_tanf_gross_earned_income.py +++ b/policyengine_us/variables/gov/states/sc/tanf/income/sc_tanf_gross_earned_income.py @@ -8,16 +8,20 @@ class sc_tanf_gross_earned_income(Variable): unit = USD definition_period = MONTH defined_for = StateCode.SC - # PDF pages 102, 105, 112, 117, 134 - reference = "https://dss.sc.gov/media/bxwc1jdj/tanf-policy-manual-volume-67-final.pdf#page=102" # Sections 7.1, 7.5, 7.7 and 8.16 B + reference = ( + # Sections 7.1, 7.5, 7.7 and 8.16 B + # PDF pages 102, 105, 112, 117, 134 + "https://dss.sc.gov/media/bxwc1jdj/tanf-policy-manual-volume-67-final.pdf#page=102", + "https://www.scstatehouse.gov/coderegs/Chapter%20114.pdf#page=213", + ) def formula(person, period, parameters): p = parameters(period).gov.states.sc.tanf.income.earned earned = add(person, period, p.earned) - # Section 7.7 excludes the income of SSI recipients. Section 7.5 - # excludes a dependent child's earned income from eligibility and - # the benefit, and Section 8.16 B leaves it out of the budget, so - # neither the gross nor the net income test counts it. + # Section 7.7 excludes the income of SSI recipients. 114-1140(N)(1) + # excludes "the earned income of dependent children" in "the FI gross + # income limit test and the need and benefit determination", as do + # Sections 7.5 and 8.16 B, so neither income test counts it. counted = ~person("sc_tanf_ssi_recipient", period) & ~person( "sc_tanf_dependent_child", period ) diff --git a/policyengine_us/variables/gov/states/sc/tanf/sc_tanf_dependent_child.py b/policyengine_us/variables/gov/states/sc/tanf/sc_tanf_dependent_child.py index f5dbd9e453a..d3b1cb93dd4 100644 --- a/policyengine_us/variables/gov/states/sc/tanf/sc_tanf_dependent_child.py +++ b/policyengine_us/variables/gov/states/sc/tanf/sc_tanf_dependent_child.py @@ -8,37 +8,45 @@ class sc_tanf_dependent_child(Variable): definition_period = MONTH defined_for = StateCode.SC reference = ( + # PDF pages 204, 213 + "https://www.scstatehouse.gov/coderegs/Chapter%20114.pdf#page=204", # PDF pages 105, 132, 134 "https://dss.sc.gov/media/bxwc1jdj/tanf-policy-manual-volume-67-final.pdf#page=105", ) documentation = ( "A member who meets the dependent-child age test, under 18 or under " "19 in secondary school, and doesn't head the household. The tax " - "unit's head or spouse heads it, and in a group with no adult the " - "oldest member does, and members who tie for oldest all do, so a " - "minor parent who heads the household counts as a parent." + "unit's head or spouse heads it. In a group with no adult where no " + "member holds either role, the oldest member heads it, and members " + "who tie for oldest all do, so a minor parent who heads the household " + "counts as a parent." ) def formula(person, period, parameters): p = parameters(period).gov.hhs.tanf.cash.eligibility.age_limit year = period.this_year age = person("age", year) - # Unlike is_person_demographic_tanf_eligible, pregnancy does not make - # an adult a dependent child, so her wages stay counted. + # 114-1130(E): a dependent child is "under eighteen years of age or + # under nineteen years of age and a full-time student in a secondary + # school". Unlike is_person_demographic_tanf_eligible, pregnancy does + # not make an adult a dependent child, so her wages stay counted. in_secondary_school = person("is_in_secondary_school", year) age_limit = where(in_secondary_school, p.student, p.non_student) meets_age_test = age < age_limit + # 114-1140(N)(1) excludes "the earned income of dependent children". # Section 7.5 counts a minor parent's earned income "if the minor # parent is the head-of-household" and excludes it when the minor # parent is a dependent in the benefit group. The tax unit head or # spouse stands in for the head of household. head_or_spouse = person("is_tax_unit_head_or_spouse", year) # Tax-unit roles are inferred only for adults, so a minor parent who - # lives with no adult heads no tax unit unless roles are input. The - # oldest member of a group with no adult heads the household; members - # who tie for oldest all head it, so the result doesn't depend on the - # order in which members are listed. + # lives with no adult heads no tax unit unless roles are input. When + # a group has no adult and no member holds a head or spouse role, the + # oldest member heads the household; members who tie for oldest all + # head it, so the result doesn't depend on the order in which members + # are listed. no_adult = ~person.spm_unit.any(~person("is_child", year)) + no_head = ~person.spm_unit.any(head_or_spouse) oldest = age == person.spm_unit.max(age) - heads_household = head_or_spouse | (no_adult & oldest) + heads_household = head_or_spouse | (no_adult & no_head & oldest) return meets_age_test & ~heads_household From fafee0044565601583da312cb617eee272de17a1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Nov 2026 14:48:42 +0000 Subject: [PATCH 12/14] Keep the cents of the SC TANF gross income limit S.C. Code Regs. 114-1150(A) sets the limit at 185 percent of the need standard, and 114-1140(A)(4) rounds down only the standard of need and the payment. Flooring the limit, as the Section 8.15 note does, failed groups up to a dollar below the regulation's limit. Drop the floor, and move the boundary cases to one cent on each side of the exact limit. Record the conflict as us-sc:tanf-gross-income-limit-cents. Bring the property test's sources, the stale need and limit comments and the changelog line up to this round's rules. Co-Authored-By: Claude Opus 5.5 --- ...ld-earnings-support-whole-dollars.fixed.md | 2 +- .../gov/states/sc/tanf/integration.yaml | 6 +- .../tanf/sc_tanf_gross_income_eligible.yaml | 113 +++++++++--------- .../sc/tanf/sc_tanf_income_eligible.yaml | 18 +-- ..._tanf_child_earnings_support_invariants.py | 22 ++-- .../sc_tanf_gross_income_eligible.py | 8 +- source_conflicts/candidates.yaml | 33 +++++ 7 files changed, 121 insertions(+), 81 deletions(-) diff --git a/changelog.d/sc-tanf-child-earnings-support-whole-dollars.fixed.md b/changelog.d/sc-tanf-child-earnings-support-whole-dollars.fixed.md index e6286def81c..2174e899770 100644 --- a/changelog.d/sc-tanf-child-earnings-support-whole-dollars.fixed.md +++ b/changelog.d/sc-tanf-child-earnings-support-whole-dollars.fixed.md @@ -1 +1 @@ -Exclude dependent children's earned income from South Carolina TANF, deduct child support that benefit group members pay outside the home after the earned income disregards, and round the need standard, gross income limit and payment down to whole dollars (TANF Policy Manual 7.5, 8.11, 8.15, 8.16; S.C. Code Regs. 114-1140(A)(4)). +Exclude dependent children's earned income from South Carolina TANF, and deduct child support a benefit group member pays outside the home from that member's own income after the earned income disregards. Round the need standard and payment down to whole dollars (S.C. Code Regs. 114-1140(A)(4), (L) and (N)(1); TANF Policy Manual 7.5, 8.11, 8.15, 8.16). diff --git a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml index 172a275b09e..fef86e29f66 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml @@ -83,8 +83,8 @@ # monthly earned = 24,000/12 = 2,000 # tanf_fpg for size 2 = 1703.33 # need_standard = floor(851.67) = 851 - # gross_income_limit = floor(851 * 1.85 = 1574.35) = 1574 - # 2,000 > 1,574 -> not eligible + # gross_income_limit = 851 * 1.85 = 1,574.35 (114-1150(A)) + # 2,000 > 1,574.35 -> not eligible - name: Case 4, single pregnant person with no income. period: 2025-01 @@ -439,7 +439,7 @@ # interest = (1,000 - 400 exclusion)/12 = 50 (page 113) # gross income = 300 + 400 + 50 = 750 # tanf_fpg for size 3 = 2151.67; need standard = floor(1075.83) = 1075 - # gross income limit = floor(1075 * 1.85 = 1988.75) = 1988; 750 passes + # gross income limit = 1075 * 1.85 = 1,988.75 (114-1150(A)); 750 passes # benefit = floor((1075 - 750) * 0.4046 = 131.50) = 131 - name: Case 15, an SSI child and the child's income are excluded from the benefit group. diff --git a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_gross_income_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_gross_income_eligible.yaml index 508c23b8bed..0c1a1169601 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_gross_income_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_gross_income_eligible.yaml @@ -17,8 +17,8 @@ # monthly earned = 18,000 / 12 = 1,500 # FPG for a benefit group of 2 (2025-01) = 1703.33/month # need_standard = floor(1703.33 * 0.5 = 851.67) = 851 - # gross_income_limit = floor(851 * 1.85 = 1574.35) = 1574 - # 1,500 <= 1,574 -> eligible + # gross_income_limit = 851 * 1.85 = 1,574.35 (114-1150(A)) + # 1,500 <= 1,574.35 -> eligible - name: Case 3, income above gross income limit. period: 2025-01 @@ -29,47 +29,50 @@ output: sc_tanf_gross_income_eligible: false # monthly earned = 24,000 / 12 = 2,000 - # gross_income_limit = 1,574 - # 2,000 > 1,574 -> not eligible + # gross_income_limit = 1,574.35 + # 2,000 > 1,574.35 -> not eligible -- name: Case 4, gross income exactly at limit. +- name: Case 4, gross income one cent below the limit. period: 2025-01 absolute_error_margin: 0.1 input: state_code: SC sc_tanf_benefit_group_size: 2 - sc_tanf_gross_income: 1_574 + sc_tanf_gross_income: 1_574.34 output: sc_tanf_gross_income_eligible: true # FPG for a benefit group of 2 = 1703.33 # need_standard = floor(851.67) = 851 - # The Section 8.15 table notes: "multiply the Need Standard by 1.85 and - # drop the cents": floor(851 * 1.85 = 1574.35) = 1574. - # 1574 <= 1574 -> eligible (uses <=) + # 114-1150(A): gross income "must not exceed 185 percent of the need + # standard", so the limit keeps its cents: 851 * 1.85 = 1,574.35. + # 1,574.34 <= 1,574.35 -> eligible. A whole-dollar limit of 1,574 would + # fail this group. -- name: Case 5, gross income one cent above the whole-dollar limit. +- name: Case 5, gross income one cent above the limit. period: 2025-01 input: state_code: SC sc_tanf_benefit_group_size: 2 - sc_tanf_gross_income: 1_574.01 + sc_tanf_gross_income: 1_574.36 output: - # 1574.01 > 1574. Without dropping the cents the limit would be - # 1.85 * 851.67 = 1575.58 and this group would pass. + # 1,574.36 > 1,574.35 -> not eligible. sc_tanf_gross_income_eligible: false -# The Section 8.15 table (PDF page 133), 2026-01: "multiply the Need Standard -# by 1.85 and drop the cents". Each limit passes and one cent above fails. +# The Section 8.15 table (PDF page 133) gives the need standards, 2026-01. +# 114-1150(A) sets the gross income limit at 185 percent of the need +# standard, cents included. One cent below each limit passes and one cent +# above fails. The table's whole-dollar limits would fail each case one cent +# below. -- name: Section 8.15 table, gross income at the limit for a benefit group of 1. +- name: Section 8.15 table, gross income one cent below the limit for a benefit group of 1. period: 2026-01 input: state_code: 2026: SC sc_tanf_benefit_group_size: 1 - sc_tanf_gross_income: 1206.00 + sc_tanf_gross_income: 1206.19 output: - # Need standard 652; limit floor(652 x 1.85 = 1,206.20) = 1,206. + # Need standard 652; limit 652 x 1.85 = 1,206.20. sc_tanf_gross_income_eligible: true - name: Section 8.15 table, gross income one cent above the limit for a benefit group of 1. @@ -78,20 +81,20 @@ state_code: 2026: SC sc_tanf_benefit_group_size: 1 - sc_tanf_gross_income: 1206.01 + sc_tanf_gross_income: 1206.21 output: - # Need standard 652; limit floor(652 x 1.85 = 1,206.20) = 1,206. + # Need standard 652; limit 652 x 1.85 = 1,206.20. sc_tanf_gross_income_eligible: false -- name: Section 8.15 table, gross income at the limit for a benefit group of 2. +- name: Section 8.15 table, gross income one cent below the limit for a benefit group of 2. period: 2026-01 input: state_code: 2026: SC sc_tanf_benefit_group_size: 2 - sc_tanf_gross_income: 1629.00 + sc_tanf_gross_income: 1629.84 output: - # Need standard 881; limit floor(881 x 1.85 = 1,629.85) = 1,629. + # Need standard 881; limit 881 x 1.85 = 1,629.85. sc_tanf_gross_income_eligible: true - name: Section 8.15 table, gross income one cent above the limit for a benefit group of 2. @@ -100,20 +103,20 @@ state_code: 2026: SC sc_tanf_benefit_group_size: 2 - sc_tanf_gross_income: 1629.01 + sc_tanf_gross_income: 1629.86 output: - # Need standard 881; limit floor(881 x 1.85 = 1,629.85) = 1,629. + # Need standard 881; limit 881 x 1.85 = 1,629.85. sc_tanf_gross_income_eligible: false -- name: Section 8.15 table, gross income at the limit for a benefit group of 3. +- name: Section 8.15 table, gross income one cent below the limit for a benefit group of 3. period: 2026-01 input: state_code: 2026: SC sc_tanf_benefit_group_size: 3 - sc_tanf_gross_income: 2053.00 + sc_tanf_gross_income: 2053.49 output: - # Need standard 1,110; limit floor(1,110 x 1.85 = 2,053.50) = 2,053. + # Need standard 1,110; limit 1,110 x 1.85 = 2,053.50. sc_tanf_gross_income_eligible: true - name: Section 8.15 table, gross income one cent above the limit for a benefit group of 3. @@ -122,20 +125,20 @@ state_code: 2026: SC sc_tanf_benefit_group_size: 3 - sc_tanf_gross_income: 2053.01 + sc_tanf_gross_income: 2053.51 output: - # Need standard 1,110; limit floor(1,110 x 1.85 = 2,053.50) = 2,053. + # Need standard 1,110; limit 1,110 x 1.85 = 2,053.50. sc_tanf_gross_income_eligible: false -- name: Section 8.15 table, gross income at the limit for a benefit group of 4. +- name: Section 8.15 table, gross income one cent below the limit for a benefit group of 4. period: 2026-01 input: state_code: 2026: SC sc_tanf_benefit_group_size: 4 - sc_tanf_gross_income: 2477.00 + sc_tanf_gross_income: 2477.14 output: - # Need standard 1,339; limit floor(1,339 x 1.85 = 2,477.15) = 2,477. + # Need standard 1,339; limit 1,339 x 1.85 = 2,477.15. sc_tanf_gross_income_eligible: true - name: Section 8.15 table, gross income one cent above the limit for a benefit group of 4. @@ -144,20 +147,20 @@ state_code: 2026: SC sc_tanf_benefit_group_size: 4 - sc_tanf_gross_income: 2477.01 + sc_tanf_gross_income: 2477.16 output: - # Need standard 1,339; limit floor(1,339 x 1.85 = 2,477.15) = 2,477. + # Need standard 1,339; limit 1,339 x 1.85 = 2,477.15. sc_tanf_gross_income_eligible: false -- name: Section 8.15 table, gross income at the limit for a benefit group of 5. +- name: Section 8.15 table, gross income one cent below the limit for a benefit group of 5. period: 2026-01 input: state_code: 2026: SC sc_tanf_benefit_group_size: 5 - sc_tanf_gross_income: 2900.00 + sc_tanf_gross_income: 2900.79 output: - # Need standard 1,568; limit floor(1,568 x 1.85 = 2,900.80) = 2,900. + # Need standard 1,568; limit 1,568 x 1.85 = 2,900.80. sc_tanf_gross_income_eligible: true - name: Section 8.15 table, gross income one cent above the limit for a benefit group of 5. @@ -166,20 +169,20 @@ state_code: 2026: SC sc_tanf_benefit_group_size: 5 - sc_tanf_gross_income: 2900.01 + sc_tanf_gross_income: 2900.81 output: - # Need standard 1,568; limit floor(1,568 x 1.85 = 2,900.80) = 2,900. + # Need standard 1,568; limit 1,568 x 1.85 = 2,900.80. sc_tanf_gross_income_eligible: false -- name: Section 8.15 table, gross income at the limit for a benefit group of 6. +- name: Section 8.15 table, gross income one cent below the limit for a benefit group of 6. period: 2026-01 input: state_code: 2026: SC sc_tanf_benefit_group_size: 6 - sc_tanf_gross_income: 3324.00 + sc_tanf_gross_income: 3324.44 output: - # Need standard 1,797; limit floor(1,797 x 1.85 = 3,324.45) = 3,324. + # Need standard 1,797; limit 1,797 x 1.85 = 3,324.45. sc_tanf_gross_income_eligible: true - name: Section 8.15 table, gross income one cent above the limit for a benefit group of 6. @@ -188,20 +191,20 @@ state_code: 2026: SC sc_tanf_benefit_group_size: 6 - sc_tanf_gross_income: 3324.01 + sc_tanf_gross_income: 3324.46 output: - # Need standard 1,797; limit floor(1,797 x 1.85 = 3,324.45) = 3,324. + # Need standard 1,797; limit 1,797 x 1.85 = 3,324.45. sc_tanf_gross_income_eligible: false -- name: Section 8.15 table, gross income at the limit for a benefit group of 7. +- name: Section 8.15 table, gross income one cent below the limit for a benefit group of 7. period: 2026-01 input: state_code: 2026: SC sc_tanf_benefit_group_size: 7 - sc_tanf_gross_income: 3749.00 + sc_tanf_gross_income: 3749.94 output: - # Need standard 2,027; limit floor(2,027 x 1.85 = 3,749.95) = 3,749. + # Need standard 2,027; limit 2,027 x 1.85 = 3,749.95. sc_tanf_gross_income_eligible: true - name: Section 8.15 table, gross income one cent above the limit for a benefit group of 7. @@ -210,20 +213,20 @@ state_code: 2026: SC sc_tanf_benefit_group_size: 7 - sc_tanf_gross_income: 3749.01 + sc_tanf_gross_income: 3749.96 output: - # Need standard 2,027; limit floor(2,027 x 1.85 = 3,749.95) = 3,749. + # Need standard 2,027; limit 2,027 x 1.85 = 3,749.95. sc_tanf_gross_income_eligible: false -- name: Section 8.15 table, gross income at the limit for a benefit group of 8. +- name: Section 8.15 table, gross income one cent below the limit for a benefit group of 8. period: 2026-01 input: state_code: 2026: SC sc_tanf_benefit_group_size: 8 - sc_tanf_gross_income: 4173.00 + sc_tanf_gross_income: 4173.59 output: - # Need standard 2,256; limit floor(2,256 x 1.85 = 4,173.60) = 4,173. + # Need standard 2,256; limit 2,256 x 1.85 = 4,173.60. sc_tanf_gross_income_eligible: true - name: Section 8.15 table, gross income one cent above the limit for a benefit group of 8. @@ -232,7 +235,7 @@ state_code: 2026: SC sc_tanf_benefit_group_size: 8 - sc_tanf_gross_income: 4173.01 + sc_tanf_gross_income: 4173.61 output: - # Need standard 2,256; limit floor(2,256 x 1.85 = 4,173.60) = 4,173. + # Need standard 2,256; limit 2,256 x 1.85 = 4,173.60. sc_tanf_gross_income_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_income_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_income_eligible.yaml index cbeec5cd65a..92c02011af8 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_income_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_income_eligible.yaml @@ -18,9 +18,9 @@ output: sc_tanf_income_eligible: false # FPG for a benefit group of 2 (2025-01) = 1703.33/month - # need_standard = 1703.33 * 0.5 = 851.67 - # gross_income_limit = 851.67 * 1.85 = 1575.58 - # gross_income 2,000 > 1,575.58 -> not eligible + # need_standard = floor(1703.33 * 0.5 = 851.67) = 851 + # gross_income_limit = 851 * 1.85 = 1,574.35 + # gross_income 2,000 > 1,574.35 -> not eligible - name: Case 3, pass gross income test but higher than need standard. period: 2025-01 @@ -31,10 +31,10 @@ sc_tanf_benefit_group_size: 2 output: sc_tanf_income_eligible: false - # need_standard = 851.67 - # gross_income_limit = 1,575.58 - # gross 1,200 < 1,575.58 -> passes gross test - # countable 900 >= 851.67 -> fails countable test + # need_standard = 851 + # gross_income_limit = 1,574.35 + # gross 1,200 < 1,574.35 -> passes gross test + # countable 900 >= 851 -> fails countable test - name: Case 4, pass both income tests. period: 2025-01 @@ -45,5 +45,5 @@ sc_tanf_benefit_group_size: 2 output: sc_tanf_income_eligible: true - # gross 1,000 < 1,575.58 -> passes gross test - # countable 800 < 851.67 -> passes countable test + # gross 1,000 < 1,574.35 -> passes gross test + # countable 800 < 851 -> passes countable test diff --git a/policyengine_us/tests/test_sc_tanf_child_earnings_support_invariants.py b/policyengine_us/tests/test_sc_tanf_child_earnings_support_invariants.py index 08aa7d5b6c9..cd814f0a5ac 100644 --- a/policyengine_us/tests/test_sc_tanf_child_earnings_support_invariants.py +++ b/policyengine_us/tests/test_sc_tanf_child_earnings_support_invariants.py @@ -1,16 +1,18 @@ """Property tests: South Carolina TANF dependent children's earnings, child support paid and whole-dollar amounts. -SC DSS TANF Policy Manual, Volume 67: - -- Section 7.5 (PDF page 105): "Earned income of a dependent child(ren) is - excluded" from eligibility and the benefit; Section 8.16 B (page 134). -- Section 8.11 (page 131) and Section 8.16 D and F (page 134): child support - that benefit group members pay outside the home is deducted after the - Section 8.12 earned income disregards, from earned income first and then - from unearned income, never below zero. -- S.C. Code Regs. 114-1140(A)(4) and the Section 8.15 table notes (page 133): - the need standard, gross income limit and payment drop their cents. +S.C. Code Regs. Chapter 114 and SC DSS TANF Policy Manual, Volume 67: + +- 114-1140(N)(1) and Section 7.5 (PDF page 105): "Earned income of a + dependent child(ren) is excluded" from eligibility and the benefit; + Section 8.16 B (page 134). +- 114-1140(L), Section 8.11 (page 131) and Section 8.16 D and F (page 134): + child support that a benefit group member pays outside the home comes off + that member's own income, after the Section 8.12 earned income + disregards, and never takes it below zero. +- 114-1140(A)(4) and the Section 8.15 table notes (page 133): the need + standard and the payment drop their cents. 114-1150(A) sets the gross + income limit at 185 percent of the need standard, cents included. For every generated South Carolina benefit group: diff --git a/policyengine_us/variables/gov/states/sc/tanf/eligibility/sc_tanf_gross_income_eligible.py b/policyengine_us/variables/gov/states/sc/tanf/eligibility/sc_tanf_gross_income_eligible.py index d24a36eb114..1e2868f8116 100644 --- a/policyengine_us/variables/gov/states/sc/tanf/eligibility/sc_tanf_gross_income_eligible.py +++ b/policyengine_us/variables/gov/states/sc/tanf/eligibility/sc_tanf_gross_income_eligible.py @@ -8,14 +8,16 @@ class sc_tanf_gross_income_eligible(Variable): definition_period = MONTH defined_for = StateCode.SC reference = ( + "https://www.scstatehouse.gov/coderegs/Chapter%20114.pdf#page=217", "https://dss.sc.gov/media/bxwc1jdj/tanf-policy-manual-volume-67-final.pdf#page=133", ) def formula(spm_unit, period, parameters): p = parameters(period).gov.states.sc.tanf.income need_standard = spm_unit("sc_tanf_need_standard", period) - # The Section 8.15 table notes: "multiply the Need Standard by 1.85 - # and drop the cents". - gross_income_limit = np.floor(need_standard * p.gross_income_limit) + # 114-1150(A): gross income "must not exceed 185 percent of the need + # standard by family size". 114-1140(A)(4) rounds only the standard + # of need and the payment amount, so the limit keeps its cents. + gross_income_limit = need_standard * p.gross_income_limit gross_income = spm_unit("sc_tanf_gross_income", period) return gross_income <= gross_income_limit diff --git a/source_conflicts/candidates.yaml b/source_conflicts/candidates.yaml index f3d99b58f5c..4851f80aee1 100644 --- a/source_conflicts/candidates.yaml +++ b/source_conflicts/candidates.yaml @@ -9760,6 +9760,39 @@ candidates: note: The regulation governs. The model deducts each payer's support from that payer's earned income after the section 8.12 disregards, then unearned income, never below zero. Because 114-1140(L) covers any budget group member, a dependent child's support comes off the child's unearned income. +- id: us-sc:tanf-gross-income-limit-cents + program: tanf + years: + from: 2026 + type: federal-fiscal + question: Is South Carolina's TANF gross income limit 185 percent of the need standard with its cents, or that amount + with the cents dropped? + sources: + - kind: regulation + citation: S.C. Code Regs. 114-1150(A) + url: https://www.scstatehouse.gov/coderegs/Chapter%20114.pdf#page=217 + - kind: regulation + citation: S.C. Code Regs. 114-1140(A)(4) + url: https://www.scstatehouse.gov/coderegs/Chapter%20114.pdf#page=210 + - kind: manual + citation: SC DSS TANF Policy Manual Vol. 67 section 8.15 + url: https://dss.sc.gov/media/bxwc1jdj/tanf-policy-manual-volume-67-final.pdf#page=133 + readings: + statute: No statute is supplied; 114-1150(A) says gross income "must not exceed 185 percent of the need standard by + family size", and 114-1140(A)(4) rounds down only "the standard of need or the payment amount". + other: The section 8.15 note says "To determine the Gross Income Limit, multiply the Need Standard by 1.85 and drop + the cents." + example: For two persons in 2026 the need standard is $881. Gross income of $1,629.50 a month passes the limit of + 881 x 1.85 = $1,629.85 and fails the manual's $1,629. + pe_us: + follows: statute + code: + - policyengine_us/variables/gov/states/sc/tanf/eligibility/sc_tanf_gross_income_eligible.py + prs: + - https://github.com/PolicyEngine/policyengine-us/pull/10581 + state: ruled + note: The regulation governs. Section 8.1 also says "Do not round at any step of the TANF calculation." The model + keeps rounding the need standard and the payment down to whole dollars under 114-1140(A)(4). - id: us-sc:tanf-need-standard-above-eight program: tanf years: From 675ca2d1153ff9d4c9f7414e3f09636069f1a3d1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Nov 2026 14:50:40 +0000 Subject: [PATCH 13/14] Test the SC TANF gross limit's cents through the payment A parent and child with 1,629.50 a month of wages in January 2026 pass 114-1150(A)'s limit of 1,629.85 and receive 26. The whole-dollar limit would fail them and pay nothing. Case 37 pins that end to end. Co-Authored-By: Claude Opus 5.5 --- .../gov/states/sc/tanf/integration.yaml | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml index fef86e29f66..53414844eaa 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/integration.yaml @@ -1129,3 +1129,29 @@ sc_tanf_countable_income: -500 # floor(1,110 x 0.4046 = 449.11) = 449. sc_tanf: 449 + +- name: Case 37, gross income above the whole-dollar limit but within its cents passes the gross test. + period: 2026-01 + input: + people: + parent: + age: 30 + is_tax_unit_head: true + employment_income_before_lsr: 19_554 + child: + age: 5 + is_tax_unit_dependent: true + households: + household: + members: [parent, child] + state_code: SC + output: + # 19,554 / 12 = 1,629.50 a month. 114-1150(A) sets the limit at + # 881 x 1.85 = 1,629.85, so the group passes. A limit with the cents + # dropped, 1,629, would fail it and pay nothing. + sc_tanf_gross_income: 1_629.5 + sc_tanf_gross_income_eligible: true + # The 50% January disregard leaves 814.75, below the need standard of 881. + sc_tanf_countable_income: 814.75 + # floor((881 - 814.75) x 0.4046 = 26.80) = 26. + sc_tanf: 26 From e4e6861e6a938c08e6587f99c6def41360a63176 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Nov 2026 15:05:07 +0000 Subject: [PATCH 14/14] Pin the SC TANF countable income floor with a fixed example When a payer's support exceeds the payer's own income, the stored float32 deduction can exceed the float64 income sum by about 3e-5. The floor in sc_tanf_countable_income absorbs that, but only random draws reached it. A fixed January example with $9,005 of wages, $2,298 of alimony and $9,000 of support now fails property (b) without the floor. Co-Authored-By: Claude Opus 5.5 --- ..._tanf_child_earnings_support_invariants.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/policyengine_us/tests/test_sc_tanf_child_earnings_support_invariants.py b/policyengine_us/tests/test_sc_tanf_child_earnings_support_invariants.py index cd814f0a5ac..3961555d739 100644 --- a/policyengine_us/tests/test_sc_tanf_child_earnings_support_invariants.py +++ b/policyengine_us/tests/test_sc_tanf_child_earnings_support_invariants.py @@ -214,6 +214,29 @@ def case(draw): ], month="2026-05", ) +# A payer whose support exceeds the payer's own income. The stored float32 +# deduction exceeds the float64 sum of that income by about 3e-5, and the +# floor in sc_tanf_countable_income keeps countable income at 0. +@example( + cases=[ + ( + [ + { + "child": False, + "age": 40, + WAGES: 9_005, + "alimony_income": 2_298, + "support_paid": 9_000, + }, + {"child": True, "age": 5}, + ], + 1, + 0, + 1, + ) + ], + month="2026-01", +) @given( cases=st.lists(case(), min_size=1, max_size=6), month=st.sampled_from(MONTHS),