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..2174e899770 --- /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, 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 06613c3f582..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 @@ -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 = 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 @@ -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 = 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. 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,627 @@ 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 (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 + 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. + # 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: + # May uses the $100 disregard: E = 600 - 100 = 500; S = 2,400 / 12 = 200. + 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: + 2026-05: 600 + # floor((1,110 - 300) x 0.4046 = 327.73) = 327. + sc_tanf: + 2026-05: 327 + +- 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 + 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: + # 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: 0 + sc_tanf_countable_income: + 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: 246 + +- 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 + 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: + 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 + 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 + +- 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 + +- 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 + +- 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 + +- 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 + +- name: Case 30, a younger minor in a group with no adult counts as a dependent child. + period: 2026-01 + input: + people: + older_minor: + age: 17 + younger_minor: + age: 16 + employment_income_before_lsr: 6_000 + baby: + age: 0 + households: + household: + members: [older_minor, younger_minor, baby] + state_code: SC + output: + # 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 a member input as the tax unit head heads it in place of the oldest member. + 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 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: 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 + # 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 + +- 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 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..c8b99662c6a --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_dependent_child.yaml @@ -0,0 +1,217 @@ +# 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 + 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] + +- 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] + +- 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] + +- name: Case 8, a younger minor in a group with no adult counts as a dependent child. + period: 2026-01 + input: + people: + older_minor: + age: 17 + younger_minor: + age: 16 + employment_income_before_lsr: 6_000 + baby: + age: 0 + households: + household: + members: [older_minor, younger_minor, baby] + state_code: SC + output: + # 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 a member input as the tax unit head heads it in place of the oldest member. + 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 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: 449 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..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 @@ -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 = 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,19 +29,213 @@ 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.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_575.58 + 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 = 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 + # 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 limit. + period: 2025-01 + input: + state_code: SC + sc_tanf_benefit_group_size: 2 + sc_tanf_gross_income: 1_574.36 + output: + # 1,574.36 > 1,574.35 -> not eligible. + sc_tanf_gross_income_eligible: false + +# 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 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.19 + output: + # 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. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_benefit_group_size: 1 + sc_tanf_gross_income: 1206.21 + output: + # Need standard 652; limit 652 x 1.85 = 1,206.20. + sc_tanf_gross_income_eligible: false + +- 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.84 + output: + # 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. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_benefit_group_size: 2 + sc_tanf_gross_income: 1629.86 + output: + # Need standard 881; limit 881 x 1.85 = 1,629.85. + sc_tanf_gross_income_eligible: false + +- 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.49 + output: + # 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. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_benefit_group_size: 3 + sc_tanf_gross_income: 2053.51 + output: + # 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 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.14 + output: + # 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. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_benefit_group_size: 4 + sc_tanf_gross_income: 2477.16 + output: + # 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 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.79 + output: + # 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. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_benefit_group_size: 5 + sc_tanf_gross_income: 2900.81 + output: + # 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 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.44 + output: + # 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. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_benefit_group_size: 6 + sc_tanf_gross_income: 3324.46 + output: + # 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 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.94 + output: + # 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. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_benefit_group_size: 7 + sc_tanf_gross_income: 3749.96 + output: + # 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 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.59 + output: + # 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. + period: 2026-01 + input: + state_code: + 2026: SC + sc_tanf_benefit_group_size: 8 + sc_tanf_gross_income: 4173.61 + output: + # 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/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..3961555d739 --- /dev/null +++ b/policyengine_us/tests/test_sc_tanf_child_earnings_support_invariants.py @@ -0,0 +1,300 @@ +"""Property tests: South Carolina TANF dependent children's earnings, child +support paid and whole-dollar amounts. + +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: + +(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", +) +# 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), +) +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..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,12 +8,16 @@ 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://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) + # 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/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..fbeb47bb18c --- /dev/null +++ b/policyengine_us/variables/gov/states/sc/tanf/income/sc_tanf_child_support_deduction.py @@ -0,0 +1,42 @@ +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 = ( + "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 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 + # 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", + ], + ) + 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 cb50ab3bddb..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,10 +9,25 @@ 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 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", ) - adds = [ - "sc_tanf_countable_earned_income_person", - "sc_tanf_gross_unearned_income", - ] + def formula(spm_unit, period, parameters): + 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. + # 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) + # 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/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..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,11 +8,21 @@ 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 + 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. - return earned * ~person("sc_tanf_ssi_recipient", period) + # 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 + ) + 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..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,8 +9,8 @@ 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", + # 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", ) @@ -22,7 +22,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..d3b1cb93dd4 --- /dev/null +++ b/policyengine_us/variables/gov/states/sc/tanf/sc_tanf_dependent_child.py @@ -0,0 +1,52 @@ +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 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. 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) + # 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. 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 & no_head & oldest) + return meets_age_test & ~heads_household 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) diff --git a/source_conflicts/candidates.yaml b/source_conflicts/candidates.yaml index 6427e4fd8e9..4851f80aee1 100644 --- a/source_conflicts/candidates.yaml +++ b/source_conflicts/candidates.yaml @@ -9728,6 +9728,102 @@ 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-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: 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: