diff --git a/changelog.d/sc-tanf-unearned-gross.fixed.md b/changelog.d/sc-tanf-unearned-gross.fixed.md new file mode 100644 index 00000000000..f0683951987 --- /dev/null +++ b/changelog.d/sc-tanf-unearned-gross.fixed.md @@ -0,0 +1 @@ +South Carolina TANF now counts Social Security, unemployment compensation, pensions, retirement account withdrawals and interest at their gross amounts; adds child support, alimony, veterans benefits, workers' compensation, disability insurance, survivor and railroad retirement benefits, cash contributions, guaranteed income and trust income; counts capital gains, rent, partnership, trust and other net income only when positive, so a loss no longer offsets other income; drops discharged debt; treats farm income as earned; excludes the first $400 a year of a benefit group's interest and dividends; and leaves SSI recipients, their income and their resources out of the benefit group, as the TANF Policy Manual requires. diff --git a/policyengine_us/parameters/gov/states/sc/tanf/income/earned/earned.yaml b/policyengine_us/parameters/gov/states/sc/tanf/income/earned/earned.yaml index 02b09ea4d32..308693e947f 100644 --- a/policyengine_us/parameters/gov/states/sc/tanf/income/earned/earned.yaml +++ b/policyengine_us/parameters/gov/states/sc/tanf/income/earned/earned.yaml @@ -4,10 +4,11 @@ values: - employment_income_before_lsr - self_employment_income_before_lsr - sstb_self_employment_income_before_lsr + - farm_operations_income # Farm income is self-employment income metadata: unit: list - period: month - label: South Carolina TANF earned income + period: month + label: South Carolina TANF earned income reference: - - title: SC DSS TANF Policy Manual Section 7.1 - href: https://dss.sc.gov/media/ojqddxsk/tanf-policy-manual-volume-65.pdf#page=101 + - title: "SC DSS TANF Policy Manual (Volume 67), Sections 7.1 and 7.7 #page 102,112" + href: https://dss.sc.gov/media/bxwc1jdj/tanf-policy-manual-volume-67-final.pdf#page=102 diff --git a/policyengine_us/parameters/gov/states/sc/tanf/income/unearned.yaml b/policyengine_us/parameters/gov/states/sc/tanf/income/unearned.yaml deleted file mode 100644 index 247415c9c25..00000000000 --- a/policyengine_us/parameters/gov/states/sc/tanf/income/unearned.yaml +++ /dev/null @@ -1,28 +0,0 @@ -description: South Carolina counts these income sources as unearned income under the Temporary Assistance for Needy Families program. -values: - 1996-07-01: - - partnership_s_corp_income - - farm_operations_income - - farm_rent_income - - capital_gains - - taxable_interest_income - - rental_income - # Royalties included in rental income - - dividend_income - - taxable_pension_income - # Pension income includes annuities and endowment contracts - - debt_relief - # Decedent income - # Estate interest income - - taxable_unemployment_compensation - - taxable_social_security - - illicit_income - - taxable_retirement_distributions - - miscellaneous_income -metadata: - unit: list - period: month - label: South Carolina TANF unearned income - reference: - - title: SC DSS TANF Policy Manual Section 7.1 - href: https://dss.sc.gov/media/ojqddxsk/tanf-policy-manual-volume-65.pdf#page=101 diff --git a/policyengine_us/parameters/gov/states/sc/tanf/income/unearned/interest_and_dividends/exclusion.yaml b/policyengine_us/parameters/gov/states/sc/tanf/income/unearned/interest_and_dividends/exclusion.yaml new file mode 100644 index 00000000000..75e3c54d269 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sc/tanf/income/unearned/interest_and_dividends/exclusion.yaml @@ -0,0 +1,11 @@ +description: South Carolina excludes this amount of interest, dividends and royalties per benefit group each year under the Temporary Assistance for Needy Families program. +values: + 1996-07-01: 400 + +metadata: + unit: currency-USD + period: year + label: South Carolina TANF interest and dividend exclusion + reference: + - title: SC DSS TANF Policy Manual (Volume 67), Section 7.7 + href: https://dss.sc.gov/media/bxwc1jdj/tanf-policy-manual-volume-67-final.pdf#page=113 diff --git a/policyengine_us/parameters/gov/states/sc/tanf/income/unearned/interest_and_dividends/sources.yaml b/policyengine_us/parameters/gov/states/sc/tanf/income/unearned/interest_and_dividends/sources.yaml new file mode 100644 index 00000000000..3a5de9446f7 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sc/tanf/income/unearned/interest_and_dividends/sources.yaml @@ -0,0 +1,14 @@ +description: South Carolina counts these income sources as unearned income after the interest, dividend and royalty exclusion under the Temporary Assistance for Needy Families program. +values: + 1996-07-01: + - interest_income + - dividend_income + # Royalties are included in rental income, which has no separate + # royalty input, so they are counted without this exclusion. +metadata: + unit: list + period: year + label: South Carolina TANF interest and dividend sources + reference: + - title: SC DSS TANF Policy Manual (Volume 67), Section 7.7 + href: https://dss.sc.gov/media/bxwc1jdj/tanf-policy-manual-volume-67-final.pdf#page=113 diff --git a/policyengine_us/parameters/gov/states/sc/tanf/income/unearned/positive_sources.yaml b/policyengine_us/parameters/gov/states/sc/tanf/income/unearned/positive_sources.yaml new file mode 100644 index 00000000000..b5de332b214 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sc/tanf/income/unearned/positive_sources.yaml @@ -0,0 +1,19 @@ +description: South Carolina counts the positive amount of each of these income sources as unearned income under the Temporary Assistance for Needy Families program. +values: + 1996-07-01: + # These are net amounts that can be losses. Income is money received + # (Section 7.1), so a net loss counts as zero rather than offsetting other + # income; Section 8.16 allows no deduction for one. + - capital_gains # Capital gains (other) + - rental_income # Rent payments, after the cost of doing business; includes royalties + - farm_rent_income + - partnership_s_corp_income + - estate_income # Trust and estate income; estate settlements are resources (Section 6.6) + - miscellaneous_income +metadata: + unit: list + period: month + label: South Carolina TANF unearned income sources counted when positive + reference: + - title: "SC DSS TANF Policy Manual (Volume 67), Sections 6.6, 7.1, 7.7 and 8.16 #page 92,102,108,116,134" + href: https://dss.sc.gov/media/bxwc1jdj/tanf-policy-manual-volume-67-final.pdf#page=92 diff --git a/policyengine_us/parameters/gov/states/sc/tanf/income/unearned/sources.yaml b/policyengine_us/parameters/gov/states/sc/tanf/income/unearned/sources.yaml new file mode 100644 index 00000000000..17c0fd64194 --- /dev/null +++ b/policyengine_us/parameters/gov/states/sc/tanf/income/unearned/sources.yaml @@ -0,0 +1,31 @@ +description: South Carolina counts these income sources, along with interest and dividends, as unearned income under the Temporary Assistance for Needy Families program. +values: + 1996-07-01: + # Section 7.1 defines income as money received from any source, and the + # Section 7.7 chart counts each source below at its gross amount. + # Interest and dividends are counted separately, after the $400 + # exclusion (see interest_and_dividends), and net amounts that can be + # losses only when positive (see positive_sources). + - social_security # SSA benefits: count the gross amount + - total_unemployment_compensation # Reported, or modeled state UI + - pension_income # Pension; annuities and endowment contracts + - retirement_distributions # IRA and 401(k) withdrawals: money received + - railroad_benefits # Retirement Pay (Railroad Retirement) + - survivor_benefits # Pension and retirement pay to survivors + - veterans_benefits + - workers_compensation + - disability_benefits # Income maintenance insurance, including disability insurance + - child_support_received # Child support/direct + - alimony_income + - financial_assistance # Cash contributions + - gi_cash_assistance + - illicit_income + # Not listed: farm operations income is earned self-employment income + # (Section 7.7), and discharged debt is not money received (Section 7.1). +metadata: + unit: list + period: month + label: South Carolina TANF unearned income sources + reference: + - title: "SC DSS TANF Policy Manual (Volume 67), Sections 7.1 and 7.7 #page 102,107-120" + href: https://dss.sc.gov/media/bxwc1jdj/tanf-policy-manual-volume-67-final.pdf#page=102 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 41aecd45e29..132ca63c7e7 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 @@ -24,7 +24,7 @@ sc_tanf: 435.28 # 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.49 + # benefit = 1075.83 * 0.4046 = 435.28 - name: Case 2, single parent with earned income. absolute_error_margin: 0.1 @@ -115,7 +115,7 @@ person1: age: 35 employment_income_before_lsr: 0 - taxable_social_security: 3_600 + social_security_disability: 3_600 person2: age: 8 is_child_dependent: true @@ -129,6 +129,7 @@ state_code: SC output: sc_tanf: 223.20 + # 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 @@ -222,3 +223,134 @@ sc_tanf: 0 # Both members are undocumented, so no one qualifies as # citizen or legal immigrant -> ineligible + +- name: Case 9, child support, a child's Social Security and interest above the exclusion. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income_before_lsr: 0 + child_support_received: 3_600 + taxable_interest_income: 1_000 + person2: + age: 9 + is_child_dependent: true + social_security_survivors: 4_800 + person3: + age: 6 + is_child_dependent: true + spm_units: + spm_unit: + members: [person1, person2, person3] + spm_unit_cash_assets: 500 + households: + household: + members: [person1, person2, person3] + state_code: SC + output: + sc_tanf_gross_income: 750 + sc_tanf: 131.83 + # 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 + +- name: Case 10, an SSI child and the child's income are excluded from the benefit group. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income_before_lsr: 0 + person2: + age: 9 + is_child_dependent: true + receives_ssi: true + taxable_private_pension_income: 3_600 + person3: + age: 6 + is_child_dependent: true + spm_units: + spm_unit: + members: [person1, person2, person3] + spm_unit_cash_assets: 500 + households: + household: + members: [person1, person2, person3] + state_code: SC + output: + sc_tanf_benefit_group_size: 2 + sc_tanf_gross_income: 0 + sc_tanf_need_standard: 851.67 + sc_tanf: 344.58 + # 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 + +- name: Case 11, when the only child receives SSI, the parent is the only benefit group member. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income_before_lsr: 0 + person2: + age: 9 + is_child_dependent: true + receives_ssi: true + spm_units: + spm_unit: + members: [person1, person2] + spm_unit_cash_assets: 500 + households: + household: + members: [person1, person2] + state_code: SC + output: + sc_tanf_benefit_group_size: 1 + sc_tanf: 253.89 + # 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 + +- name: Case 12, an SSI child's savings do not count against the resource limit. + period: 2025-01 + absolute_error_margin: 0.1 + input: + people: + person1: + age: 30 + employment_income_before_lsr: 0 + bank_account_assets: 800 + person2: + age: 9 + is_child_dependent: true + receives_ssi: true + bank_account_assets: 1_800 + person3: + age: 6 + is_child_dependent: true + spm_units: + spm_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: SC + output: + sc_tanf_countable_resources: 800 + sc_tanf: 344.58 + # 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 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 01f7c7cfc9a..b8b094df985 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 @@ -5,10 +5,10 @@ state_code: SC sc_tanf_countable_income: 0 sc_tanf_eligible: true - spm_unit_size: 2 + sc_tanf_benefit_group_size: 2 output: sc_tanf: 344.58 - # tanf_fpg for size 2 (2025-01, uses 2024-10 FPG) = 1703.33/month + # 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 @@ -18,7 +18,7 @@ state_code: SC sc_tanf_countable_income: 1_000 sc_tanf_eligible: false - spm_unit_size: 2 + sc_tanf_benefit_group_size: 2 output: sc_tanf: 0 @@ -29,10 +29,10 @@ state_code: SC sc_tanf_countable_income: 500 sc_tanf_eligible: true - spm_unit_size: 2 + sc_tanf_benefit_group_size: 2 output: sc_tanf: 142.28 - # tanf_fpg for size 2 (2025-01) = 1703.33/month + # 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 diff --git a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_benefit_group_size.yaml b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_benefit_group_size.yaml new file mode 100644 index 00000000000..3153dc06e45 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_benefit_group_size.yaml @@ -0,0 +1,63 @@ +# SC DSS TANF Policy Manual (Volume 67), Section 3.6 (page 56) and Section 7.7 +# (page 117): exclude an SSI recipient living with the benefit group. + +- name: Case 1, everyone in the unit is in the benefit group. + period: 2025-01 + input: + people: + person1: + age: 30 + person2: + age: 9 + person3: + age: 6 + spm_units: + spm_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: SC + output: + sc_tanf_benefit_group_size: 3 + +- name: Case 2, an SSI recipient is left out. + period: 2025-01 + input: + people: + person1: + age: 30 + person2: + age: 9 + receives_ssi: true + person3: + age: 6 + spm_units: + spm_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: SC + output: + sc_tanf_benefit_group_size: 2 + +- name: Case 3, a unit of SSI recipients has no benefit group members. + period: 2025-01 + input: + people: + person1: + age: 30 + receives_ssi: true + person2: + age: 9 + receives_ssi: true + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: SC + output: + sc_tanf_benefit_group_size: 0 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 ed35d4779a1..624cdcf1949 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 @@ -2,7 +2,7 @@ period: 2025-01 input: state_code: SC - spm_unit_size: 2 + sc_tanf_benefit_group_size: 2 output: sc_tanf_countable_income_eligible: true @@ -10,11 +10,11 @@ period: 2025-01 input: state_code: SC - spm_unit_size: 2 + sc_tanf_benefit_group_size: 2 sc_tanf_countable_income: 800 output: sc_tanf_countable_income_eligible: true - # tanf_fpg for size 2 (2025-01) = 1703.33/month + # FPG for a benefit group of 2 (2025-01) = 1703.33/month # need_standard = 1703.33 * 0.5 = 851.67 # 800 < 851.67 -> eligible @@ -22,7 +22,7 @@ period: 2025-01 input: state_code: SC - spm_unit_size: 2 + sc_tanf_benefit_group_size: 2 sc_tanf_countable_income: 900 output: sc_tanf_countable_income_eligible: false @@ -34,11 +34,11 @@ absolute_error_margin: 0.1 input: state_code: SC - spm_unit_size: 2 + sc_tanf_benefit_group_size: 2 sc_tanf_countable_income: 851.67 output: sc_tanf_countable_income_eligible: false - # tanf_fpg for size 2 = 1703.33 + # 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) # -> not eligible diff --git a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_countable_resources.yaml b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_countable_resources.yaml new file mode 100644 index 00000000000..d94d83f5f8a --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_countable_resources.yaml @@ -0,0 +1,60 @@ +# SC DSS TANF Policy Manual (Volume 67), Sections 3.6 (page 56) and 7.7 +# (page 117): exclude the SSI recipient's resources; Section 6.4 (page 91): +# $2,500 limit per benefit group. + +- name: Case 1, an SSI recipient's own assets are left out. + period: 2025-01 + input: + people: + person1: + age: 30 + bank_account_assets: 800 + person2: + age: 9 + receives_ssi: true + bank_account_assets: 1_800 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: SC + output: + # 2,600 in the unit, less the SSI child's 1,800. + sc_tanf_countable_resources: 800 + sc_tanf_resources_eligible: true + +- name: Case 2, stocks and bonds of an SSI recipient are left out too. + period: 2025-01 + input: + people: + person1: + age: 30 + bank_account_assets: 500 + stock_assets: 1_000 + person2: + age: 40 + receives_ssi: true + stock_assets: 2_000 + bond_assets: 1_000 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: SC + output: + sc_tanf_countable_resources: 1_500 + +- name: Case 3, an aggregate asset input is counted as given. + period: 2025-01 + input: + state_code: SC + receives_ssi: true + spm_unit_cash_assets: 3_000 + output: + # Without person-level assets nothing can be attributed to the SSI + # recipient, so the aggregate stands. + sc_tanf_countable_resources: 3_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_gross_earned_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_gross_earned_income.yaml new file mode 100644 index 00000000000..e2fb138f4e1 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_gross_earned_income.yaml @@ -0,0 +1,23 @@ +# SC DSS TANF Policy Manual (Volume 67), Sections 7.1 and 7.7. + +- name: Case 1, wages and farm income are earned income. + period: 2025-01 + absolute_error_margin: 0.01 + input: + state_code: SC + employment_income_before_lsr: 12_000 + farm_operations_income: 2_400 + output: + # Wages (page 102) and farm income (self-employment, page 112) are earned. + # (12,000 + 2,400) / 12 = 1,200. + sc_tanf_gross_earned_income: 1_200 + +- name: Case 2, an SSI recipient's earnings are excluded. + period: 2025-01 + input: + state_code: SC + receives_ssi: true + employment_income_before_lsr: 6_000 + output: + # SSI benefit: exclude the SSI recipient and his or her income (page 117). + sc_tanf_gross_earned_income: 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 dd0656ebf42..4e3c80b52bd 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 @@ -2,7 +2,7 @@ period: 2025-01 input: state_code: SC - spm_unit_size: 2 + sc_tanf_benefit_group_size: 2 output: sc_tanf_gross_income_eligible: true @@ -10,12 +10,12 @@ period: 2025-01 input: state_code: SC - spm_unit_size: 2 + sc_tanf_benefit_group_size: 2 employment_income_before_lsr: 18_000 output: sc_tanf_gross_income_eligible: true # monthly earned = 18,000 / 12 = 1,500 - # tanf_fpg for size 2 (2025-01) = 1703.33/month + # 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 @@ -24,7 +24,7 @@ period: 2025-01 input: state_code: SC - spm_unit_size: 2 + sc_tanf_benefit_group_size: 2 employment_income_before_lsr: 24_000 output: sc_tanf_gross_income_eligible: false @@ -37,11 +37,11 @@ absolute_error_margin: 0.1 input: state_code: SC - spm_unit_size: 2 + sc_tanf_benefit_group_size: 2 sc_tanf_gross_income: 1_575.58 output: sc_tanf_gross_income_eligible: true - # tanf_fpg for size 2 = 1703.33 + # 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 <=) diff --git a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_gross_unearned_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_gross_unearned_income.yaml new file mode 100644 index 00000000000..c9fece1b5b0 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_gross_unearned_income.yaml @@ -0,0 +1,283 @@ +# SC DSS TANF Policy Manual (Volume 67), Section 7.1 (page 102) defines income +# as money received from any source; the Section 7.7 chart (pages 107-120) +# lists how each source is counted. + +- name: Case 1, Social Security counts at the gross amount, not the taxable part. + period: 2025-01 + absolute_error_margin: 0.01 + input: + state_code: SC + social_security_retirement: 6_000 + output: + # SSA benefits: "Count gross amount" (page 116). + # 6,000 / 12 = 500. The federally taxable part is 0 at this income. + taxable_social_security: 0 + sc_tanf_gross_unearned_income: 500 + +- name: Case 2, 2020 unemployment compensation counts in full despite the IRC 85(c) exclusion. + period: 2020-01 + absolute_error_margin: 0.01 + input: + state_code: SC + unemployment_compensation: 12_000 + output: + # Unemployment compensation benefits are counted (page 118). + # 12,000 / 12 = 1,000. Federal law excluded $10,200 for 2020, so the + # taxable part was 1,800 a year (150 a month); TANF counts all 1,000. + sc_tanf_gross_unearned_income: 1_000 + +- name: Case 3, taxable and tax-exempt pension income both count. + period: 2025-01 + absolute_error_margin: 0.01 + input: + state_code: SC + taxable_private_pension_income: 2_400 + tax_exempt_private_pension_income: 1_200 + output: + # Pension and retirement pay are counted (pages 115-116). + # (2,400 + 1,200) / 12 = 300. + sc_tanf_gross_unearned_income: 300 + +- name: Case 4, non-taxable cash income the chart counts. + period: 2025-01 + absolute_error_margin: 0.01 + input: + state_code: SC + child_support_received: 1_200 + alimony_income: 1_200 + veterans_benefits: 1_200 + workers_compensation: 1_200 + disability_benefits: 1_200 + survivor_benefits: 1_200 + railroad_benefits: 1_200 + financial_assistance: 1_200 + gi_cash_assistance: 1_200 + estate_income: 1_200 + output: + # Child support/direct, alimony, cash contributions (page 108), income + # maintenance insurance including disability insurance (page 113), + # retirement pay including railroad retirement (page 116), VA benefits + # (page 119) and workers' compensation (page 120) are counted, as are + # guaranteed income payments and trust income, which are money received + # (Section 7.1, page 102). + # 10 sources x 1,200 / 12 = 1,000. + sc_tanf_gross_unearned_income: 1_000 + +- name: Case 5, retirement account withdrawals count at the gross amount and discharged debt does not. + period: 2025-01 + absolute_error_margin: 0.01 + input: + state_code: SC + taxable_ira_distributions: 9_600 + taxable_401k_distributions: 2_400 + debt_relief: 6_000 + output: + # Withdrawals are money received (Section 7.1, page 102). A discharged + # debt is not money received, and in-kind gains are excluded. + # (9,600 + 2,400) / 12 = 1,000. + sc_tanf_gross_unearned_income: 1_000 + +- name: Case 6, farm operations income is earned, not unearned. + period: 2025-01 + input: + state_code: SC + farm_operations_income: 6_000 + output: + # Farm income is self-employment income (page 112). + sc_tanf_gross_unearned_income: 0 + sc_tanf_gross_earned_income: 500 + +- name: Case 7, interest within the 400 dollar annual exclusion is not counted. + period: 2025-01 + input: + state_code: SC + taxable_interest_income: 300 + output: + # Up to $400 per benefit group annually is excluded (page 113). + # 300 / 12 = 25 a month, below the 400 / 12 = 33.33 monthly exclusion. + sc_tanf_gross_unearned_income: 0 + +- name: Case 8, interest above the exclusion is averaged over 12 months, including tax-exempt interest. + period: 2025-01 + absolute_error_margin: 0.01 + input: + state_code: SC + taxable_interest_income: 600 + tax_exempt_interest_income: 1_000 + output: + # Interest is money received whether or not it is taxable. + # (600 + 1,000 - 400) / 12 = 100. + sc_tanf_gross_unearned_income: 100 + +- name: Case 9, the exclusion is shared across the benefit group in proportion to each member's interest and dividends. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + taxable_interest_income: 600 + person2: + age: 10 + qualified_dividend_income: 600 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: SC + output: + # Each member has 600 / 12 = 50 a month; the group has 100. + # Group exclusion = min(100, 400 / 12 = 33.33) = 33.33, half to each. + # Each member: 50 - 16.67 = 33.33. Group total = (1,200 - 400) / 12 = 66.67. + sc_tanf_gross_unearned_income: [33.33, 33.33] + +- name: Case 10, an SSI recipient's income is excluded. + period: 2025-01 + input: + state_code: SC + receives_ssi: true + social_security_disability: 4_800 + taxable_interest_income: 1_200 + output: + # SSI benefit: exclude the SSI recipient and his or her income (page 117). + sc_tanf_ssi_recipient: true + sc_tanf_gross_unearned_income: 0 + +- name: Case 11, an SSI recipient's interest does not use up the group's exclusion. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + taxable_interest_income: 600 + person2: + age: 60 + receives_ssi: true + taxable_interest_income: 1_200 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: SC + output: + # Only person1's income counts: 600 / 12 = 50, less the full 33.33 + # exclusion = 16.67. Person2's income is excluded. + sc_tanf_gross_unearned_income: [16.67, 0] + +- name: Case 12, negative interest does not offset other income. + period: 2025-01 + absolute_error_margin: 0.01 + input: + state_code: SC + taxable_interest_income: -1_200 + taxable_private_pension_income: 2_400 + output: + # Interest and dividends count only as money received, floored at zero, + # before the exclusion. Pension 2,400 / 12 = 200. + sc_tanf_gross_unearned_income: 200 + +- name: Case 13, one member's negative interest does not reduce another member's. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + taxable_interest_income: 600 + person2: + age: 10 + taxable_interest_income: -600 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: SC + output: + # Person1: 600 / 12 = 50, less the 33.33 exclusion = 16.67. Person2: 0. + sc_tanf_gross_unearned_income: [16.67, 0] + +- name: Case 14, dividends entered as the legacy dividend_income input count. + period: 2025-01 + absolute_error_margin: 0.01 + input: + state_code: SC + dividend_income: 1_200 + output: + # (1,200 - 400) / 12 = 66.67. + sc_tanf_gross_unearned_income: 66.67 + +- name: Case 15, a trust or estate loss does not offset other income. + period: 2025-01 + absolute_error_margin: 0.01 + input: + state_code: SC + estate_income: -1_200 + taxable_private_pension_income: 2_400 + output: + # A pass-through loss is not money received (Section 7.1, page 102). + # Pension 2,400 / 12 = 200. + sc_tanf_gross_unearned_income: 200 + +- name: Case 16, one member's estate loss does not offset another member's income. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + taxable_private_pension_income: 2_400 + person2: + age: 10 + estate_income: -1_200 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: SC + output: + sc_tanf_gross_unearned_income: [200, 0] + +- name: Case 17, one member's partnership loss does not offset another member's pension. + period: 2025-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + taxable_private_pension_income: 2_400 + person2: + age: 10 + partnership_income: -1_200 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: SC + output: + # A net loss is not money received (Section 7.1, page 102). + sc_tanf_gross_unearned_income: [200, 0] + +- name: Case 18, capital and rental losses do not offset the same member's income. + period: 2025-01 + absolute_error_margin: 0.01 + input: + state_code: SC + short_term_capital_gains: -3_000 + rental_income: -2_400 + farm_rent_income: 1_200 + taxable_private_pension_income: 2_400 + output: + # Each net source is floored at zero on its own: farm rent 1,200 / 12 = + # 100 plus pension 2,400 / 12 = 200. + sc_tanf_gross_unearned_income: 300 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 da84d0c22da..cbeec5cd65a 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 @@ -4,7 +4,7 @@ state_code: SC sc_tanf_countable_income: 0 sc_tanf_gross_income: 0 - spm_unit_size: 2 + sc_tanf_benefit_group_size: 2 output: sc_tanf_income_eligible: true @@ -14,10 +14,10 @@ state_code: SC sc_tanf_countable_income: 500 sc_tanf_gross_income: 2_000 - spm_unit_size: 2 + sc_tanf_benefit_group_size: 2 output: sc_tanf_income_eligible: false - # tanf_fpg for size 2 (2025-01) = 1703.33/month + # 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 @@ -28,7 +28,7 @@ state_code: SC sc_tanf_countable_income: 900 sc_tanf_gross_income: 1_200 - spm_unit_size: 2 + sc_tanf_benefit_group_size: 2 output: sc_tanf_income_eligible: false # need_standard = 851.67 @@ -42,7 +42,7 @@ state_code: SC sc_tanf_countable_income: 800 sc_tanf_gross_income: 1_000 - spm_unit_size: 2 + sc_tanf_benefit_group_size: 2 output: sc_tanf_income_eligible: true # gross 1,000 < 1,575.58 -> passes gross 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 new file mode 100644 index 00000000000..31ab0230586 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_need_standard.yaml @@ -0,0 +1,32 @@ +# SC DSS TANF Policy Manual (Volume 67), Section 8.15 (page 133): the need +# standard is 50% of the federal poverty guideline for the number of persons +# in the benefit group, revised each October 1. + +- name: Case 1, benefit group of three in January 2025 uses the October 2024 guideline. + period: 2025-01 + absolute_error_margin: 0.01 + input: + 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 + +- name: Case 2, benefit group of three in October 2025 uses the October 2025 guideline. + period: 2025-10 + absolute_error_margin: 0.01 + input: + state_code: + 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 + +- name: Case 3, an empty benefit group has no need standard. + period: 2025-01 + input: + state_code: SC + sc_tanf_benefit_group_size: 0 + output: + sc_tanf_need_standard: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_ssi_recipient.yaml b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_ssi_recipient.yaml new file mode 100644 index 00000000000..61ea5e4862f --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/sc/tanf/sc_tanf_ssi_recipient.yaml @@ -0,0 +1,33 @@ +# SC DSS TANF Policy Manual (Volume 67), Section 7.7, SSI benefit (page 117). + +- name: Case 1, reported SSI receipt. + period: 2025-01 + input: + state_code: SC + receives_ssi: true + output: + sc_tanf_ssi_recipient: true + +- name: Case 2, an SSI payment. + period: 2025-01 + input: + state_code: SC + ssi: 500 + output: + sc_tanf_ssi_recipient: true + +- name: Case 3, no SSI. + period: 2025-01 + input: + state_code: SC + age: 35 + output: + sc_tanf_ssi_recipient: false + +- name: Case 4, not defined outside South Carolina. + period: 2025-01 + input: + state_code: NC + receives_ssi: true + output: + sc_tanf_ssi_recipient: false diff --git a/policyengine_us/tests/test_sc_tanf_unearned_income_invariants.py b/policyengine_us/tests/test_sc_tanf_unearned_income_invariants.py new file mode 100644 index 00000000000..b1008275d5f --- /dev/null +++ b/policyengine_us/tests/test_sc_tanf_unearned_income_invariants.py @@ -0,0 +1,344 @@ +"""Property tests: South Carolina TANF gross income and need standard. + +SC DSS TANF Policy Manual, Volume 67 (effective August 1, 2026): + +- Section 7.1 (PDF page 102): "Income is money received by a BG member (s) + from any source." A discharged debt is not money received. +- Section 7.7 (pages 107-120) counts Social Security at the gross amount + (page 116), unemployment compensation (page 118), pensions (page 115), + child support (page 108) and VA benefits (page 119), and counts interest, + dividends and royalties with "Up to $400 per BG annually ... excluded. + Income above $400 must be averaged over the period of the remaining 12 + months" (page 113). +- Sections 3.6 (page 56) and 7.7 (page 117): "Exclude the SSI recipient, + his/her income, resources and SSI payments in determining benefits for + other BG members." +- Section 8.15 (page 133): the need standard is 50% of the federal poverty + guideline for the number of persons in the benefit group, revised each + October 1. For January 2025 the guideline is the one in Volume 65's + table (PDF page 132): $15,060 for one person plus $5,380 for each + additional person. + +For every benefit group, in January 2025: + +1. Each member's sc_tanf_gross_unearned_income equals a reference written + from the manual, not from the model's parameter list: the member's + counted sources plus their interest and dividends (floored at zero), less + a share of min(group interest and dividends, 400 / 12) proportional to + the member's interest and dividends. SSI recipients count nothing and + take no share. +2. The group's total is its counted sources less min(group interest and + dividends, 400 / 12), and no member's exclusion exceeds the member's own + 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. +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). +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. +8. Countable resources are the unit's cash assets less SSI recipients' own + bank, stock and bond assets (Sections 3.6 and 7.7). + +Each generated group is simulated twice in one vectorized simulation: as +drawn, and with the head's interest raised (property 5). +""" + +import numpy as np +import pytest + +from policyengine_us import Simulation + +YEAR = 2025 +MONTH = f"{YEAR}-01" +EXCLUSION = 400 # Section 7.7, page 113, per benefit group per year. +# Volume 65, Section 8.15 (PDF page 132): in effect from October 2024. +FPG_FIRST_PERSON = 15_060 +FPG_ADDITIONAL_PERSON = 5_380 +NEED_STANDARD_RATE = 0.5 +TOLERANCE = 0.01 + +# Sources counted at the gross amount, in model input names. +COUNTED = [ + "social_security_retirement", + "unemployment_compensation", + "taxable_private_pension_income", + "tax_exempt_private_pension_income", + "taxable_ira_distributions", + "child_support_received", + "veterans_benefits", +] +INTEREST_AND_DIVIDENDS = [ + "taxable_interest_income", + "tax_exempt_interest_income", + "qualified_dividend_income", + "non_qualified_dividend_income", +] +# Net amounts counted only when positive. partnership_income is the only +# component drawn of partnership_s_corp_income, so flooring it matches the +# model's floor on that total. +POSITIVE_ONLY = ["estate_income", "rental_income", "partnership_income"] +# Inputs that must not count as unearned income. +IGNORED = ["debt_relief"] +ASSETS = ["bank_account_assets", "stock_assets", "bond_assets"] +WAGES = "employment_income_before_lsr" +AMOUNT_INPUTS = [ + *COUNTED, + *POSITIVE_ONLY, + *INTEREST_AND_DIVIDENDS, + *IGNORED, + WAGES, + *ASSETS, +] + + +def build_situation(groups): + """One household, SPM unit, tax unit and family per group; each person + is unmarried. Adults are 30 to 50 and children 1 to 17, and nobody is + disabled, so the model computes no SSI; SSI receipt comes only from + receives_ssi.""" + people, tax_units, spm_units, families, households = {}, {}, {}, {}, {} + marital_units = {} + for i, group in enumerate(groups): + members = [] + for j, member in enumerate(group): + name = f"person_{i}_{j}" + person = { + "age": {YEAR: member["age"]}, + "receives_ssi": {MONTH: member["ssi"]}, + } + for variable in AMOUNT_INPUTS: + person[variable] = {YEAR: member.get(variable, 0)} + people[name] = person + members.append(name) + marital_units[f"marital_unit_{i}_{j}"] = {"members": [name]} + tax_units[f"tax_unit_{i}"] = {"members": members} + spm_units[f"spm_unit_{i}"] = {"members": members} + families[f"family_{i}"] = {"members": members} + households[f"household_{i}"] = { + "members": members, + "state_code": {YEAR: "SC"}, + } + return { + "people": people, + "tax_units": tax_units, + "spm_units": spm_units, + "families": families, + "marital_units": marital_units, + "households": households, + } + + +def raise_head_interest(group, d): + raised = [dict(member) for member in group] + raised[0]["taxable_interest_income"] = ( + raised[0].get("taxable_interest_income", 0) + d + ) + return raised + + +def member_interest_and_dividends(member): + """Monthly interest and dividends counted for a member: floored at zero, + and zero for an SSI recipient.""" + if member["ssi"]: + return 0 + return max(sum(member.get(v, 0) for v in INTEREST_AND_DIVIDENDS), 0) / 12 + + +def member_counted(member): + """Monthly counted income before the exclusion.""" + if member["ssi"]: + return 0 + other = sum(member.get(v, 0) for v in COUNTED) / 12 + other += sum(max(member.get(v, 0), 0) for v in POSITIVE_ONLY) / 12 + return other + member_interest_and_dividends(member) + + +def reference(group): + """Monthly gross unearned and earned income per member, the group's + exclusion, and the benefit group's size and need standard, from the + manual.""" + idr = [member_interest_and_dividends(m) for m in group] + group_idr = sum(idr) + exclusion = min(group_idr, EXCLUSION / 12) + unearned = [ + 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] + 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 + 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 + + +def run(groups, raises): + """Simulate each group as drawn and with the head's interest raised. + Return per-member and per-group values for both copies.""" + doubled = groups + [raise_head_interest(g, d) for g, d in zip(groups, raises)] + sim = Simulation(situation=build_situation(doubled)) + + def calc(variable): + return np.asarray(sim.calculate(variable, MONTH)) + + unearned = calc("sc_tanf_gross_unearned_income") + earned = calc("sc_tanf_gross_earned_income") + ssi = calc("sc_tanf_ssi_recipient") + size = calc("sc_tanf_benefit_group_size") + need = calc("sc_tanf_need_standard") + resources = calc("sc_tanf_countable_resources") + bounds = np.cumsum([0, *[len(g) for g in doubled]]) + n = len(groups) + + def split(a): + return [a[bounds[k] : bounds[k + 1]] for k in range(len(doubled))] + + u, e, s = split(unearned), split(earned), split(ssi) + drawn = (u[:n], e[:n], s[:n], size[:n], need[:n], resources[:n]) + raised = (u[n:], e[n:], s[n:], size[n:], need[n:], resources[n:]) + return drawn, raised + + +def check(groups, raises): + (unearned, earned, ssi, size, need, resources), raised = run(groups, raises) + raised_unearned = raised[0] + for k, group in enumerate(groups): + expected = reference(group) + expected_unearned, expected_earned, exclusion = expected[:3] + expected_size, expected_need, expected_resources = expected[3:] + context = {"group": group} + # Properties 1 and 3: the reference from the manual. + assert unearned[k] == pytest.approx(expected_unearned, abs=TOLERANCE), context + # Property 4. + assert earned[k] == pytest.approx(expected_earned, abs=TOLERANCE), context + assert list(ssi[k]) == [bool(m["ssi"]) for m in group], context + # Property 2: group conservation and per-member bounds. + counted = [member_counted(m) for m in group] + assert unearned[k].sum() == pytest.approx( + sum(counted) - exclusion, abs=TOLERANCE + ), context + for member, value, before in zip(group, unearned[k], counted): + own = member_interest_and_dividends(member) + assert -TOLERANCE <= before - value <= own + TOLERANCE, context + # Property 5: monotone in the head's interest. + change = raised_unearned[k].sum() - unearned[k].sum() + d = raises[k] / 12 + assert -TOLERANCE <= change <= d + TOLERANCE, context + head = group[0] + head_rise = member_interest_and_dividends( + raise_head_interest([head], raises[k])[0] + ) - member_interest_and_dividends(head) + group_idr = sum(member_interest_and_dividends(m) for m in group) + if head["ssi"]: + assert change == pytest.approx(0, abs=TOLERANCE), context + elif group_idr >= EXCLUSION / 12: + assert change == pytest.approx(head_rise, abs=TOLERANCE), context + # Property 6: benefit group size and need standard. + assert size[k] == expected_size, context + assert need[k] == pytest.approx(expected_need, abs=TOLERANCE), context + # Property 8: SSI recipients' own assets are left out. + assert resources[k] == pytest.approx(expected_resources, abs=TOLERANCE), context + + +def test_worked_example(): + """Integration Case 9's benefit group, from the manual: child support + 3,600, interest 1,000, and a child's Social Security 4,800 a year.""" + groups = [ + [ + { + "age": 30, + "ssi": False, + "child_support_received": 3_600, + "taxable_interest_income": 1_000, + }, + {"age": 9, "ssi": False, "social_security_retirement": 4_800}, + {"age": 6, "ssi": False}, + ] + ] + (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. + assert size[0] == 3 + assert need[0] == pytest.approx(1_075.83, abs=TOLERANCE) + + +try: + import hypothesis + import hypothesis.strategies as st +except ImportError: # pragma: no cover + hypothesis = None + +if hypothesis is not None: + SLOW = [hypothesis.HealthCheck.too_slow] + + def amount(high): + return st.one_of(st.just(0), st.integers(min_value=1, max_value=high)) + + def signed(high): + return st.one_of(amount(high), st.integers(min_value=-high, max_value=-1)) + + @st.composite + def member(draw, age): + return { + "age": age, + "ssi": draw(st.booleans()), + "social_security_retirement": draw(amount(24_000)), + "unemployment_compensation": draw(amount(15_000)), + "taxable_private_pension_income": draw(amount(10_000)), + "tax_exempt_private_pension_income": draw(amount(5_000)), + "taxable_ira_distributions": draw(amount(20_000)), + "child_support_received": draw(amount(8_000)), + "veterans_benefits": draw(amount(12_000)), + # Negative interest is allowed as an input and must not offset + # other income. + "taxable_interest_income": draw( + st.one_of(amount(3_000), st.integers(min_value=-3_000, max_value=-1)) + ), + "tax_exempt_interest_income": draw(amount(1_000)), + "qualified_dividend_income": draw(amount(2_000)), + "non_qualified_dividend_income": draw(amount(1_000)), + # Signed net amounts, like Schedule E income or loss. + "estate_income": draw(signed(6_000)), + "rental_income": draw(signed(8_000)), + "partnership_income": draw(signed(8_000)), + "debt_relief": draw(amount(5_000)), + "bank_account_assets": draw(amount(3_000)), + "stock_assets": draw(amount(2_000)), + "bond_assets": draw(amount(1_000)), + "employment_income_before_lsr": draw(amount(30_000)), + } + + @st.composite + def group(draw): + head = draw(member(draw(st.integers(min_value=30, max_value=50)))) + children = draw( + st.lists( + member(draw(st.integers(min_value=1, max_value=17))), + min_size=0, + max_size=3, + ) + ) + return [head, *children] + + @hypothesis.settings( + max_examples=15, derandomize=True, deadline=None, suppress_health_check=SLOW + ) + @hypothesis.given( + st.lists( + st.tuples(group(), st.integers(min_value=1, max_value=5_000)), + min_size=1, + max_size=8, + ) + ) + def test_properties(cases): + groups = [g for g, _ in cases] + raises = [d for _, d in cases] + check(groups, raises) diff --git a/policyengine_us/variables/gov/states/sc/tanf/eligibility/sc_tanf_countable_income_eligible.py b/policyengine_us/variables/gov/states/sc/tanf/eligibility/sc_tanf_countable_income_eligible.py index d91f33f82ef..f6583d47d06 100644 --- a/policyengine_us/variables/gov/states/sc/tanf/eligibility/sc_tanf_countable_income_eligible.py +++ b/policyengine_us/variables/gov/states/sc/tanf/eligibility/sc_tanf_countable_income_eligible.py @@ -12,8 +12,6 @@ class sc_tanf_countable_income_eligible(Variable): ) def formula(spm_unit, period, parameters): - p = parameters(period).gov.states.sc.tanf.income - fpg = spm_unit("tanf_fpg", period) - need_standard = fpg * p.need_standard.rate + need_standard = spm_unit("sc_tanf_need_standard", period) countable_income = spm_unit("sc_tanf_countable_income", period) return countable_income < need_standard diff --git a/policyengine_us/variables/gov/states/sc/tanf/eligibility/sc_tanf_countable_resources.py b/policyengine_us/variables/gov/states/sc/tanf/eligibility/sc_tanf_countable_resources.py new file mode 100644 index 00000000000..dabae216069 --- /dev/null +++ b/policyengine_us/variables/gov/states/sc/tanf/eligibility/sc_tanf_countable_resources.py @@ -0,0 +1,32 @@ +from policyengine_us.model_api import * + + +class sc_tanf_countable_resources(Variable): + value_type = float + entity = SPMUnit + label = "South Carolina TANF countable resources" + unit = USD + definition_period = MONTH + quantity_type = STOCK + defined_for = StateCode.SC + # PDF pages 56, 91, 117 + reference = "https://dss.sc.gov/media/bxwc1jdj/tanf-policy-manual-volume-67-final.pdf#page=56" # Sections 3.6, 6.4 and 7.7 + + def formula(spm_unit, period, parameters): + # Sections 3.6 and 7.7 exclude the SSI recipient's resources along + # with his or her income and needs. The excluded people's assets are + # subtracted from the unit aggregate rather than the total being + # rebuilt from person-level values, so households that supply only + # the spm_unit_cash_assets aggregate keep the aggregate behavior. + total = spm_unit("spm_unit_cash_assets", period.this_year) + person = spm_unit.members + ssi_recipient = person("sc_tanf_ssi_recipient", period) + # This component list must mirror spm_unit_cash_assets.adds, so the + # subtraction removes exactly the excluded members' share. + person_assets = add( + person, + period.this_year, + ["bank_account_assets", "stock_assets", "bond_assets"], + ) + excluded_assets = spm_unit.sum(person_assets * ssi_recipient) + return max_(total - excluded_assets, 0) 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 63da1a846f1..61ac97c146d 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 @@ -13,8 +13,7 @@ class sc_tanf_gross_income_eligible(Variable): def formula(spm_unit, period, parameters): p = parameters(period).gov.states.sc.tanf.income - fpg = spm_unit("tanf_fpg", period) - need_standard = fpg * p.need_standard.rate + need_standard = spm_unit("sc_tanf_need_standard", period) 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/eligibility/sc_tanf_resources_eligible.py b/policyengine_us/variables/gov/states/sc/tanf/eligibility/sc_tanf_resources_eligible.py index 181d4a32c21..1e6c3f92bac 100644 --- a/policyengine_us/variables/gov/states/sc/tanf/eligibility/sc_tanf_resources_eligible.py +++ b/policyengine_us/variables/gov/states/sc/tanf/eligibility/sc_tanf_resources_eligible.py @@ -11,5 +11,5 @@ class sc_tanf_resources_eligible(Variable): def formula(spm_unit, period, parameters): p = parameters(period).gov.states.sc.tanf.resources - assets = spm_unit("spm_unit_cash_assets", period.this_year) + assets = spm_unit("sc_tanf_countable_resources", period) return assets <= p.limit 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 7c599d576d3..f63cbfc90fd 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,5 +8,11 @@ class sc_tanf_gross_earned_income(Variable): unit = USD definition_period = MONTH defined_for = StateCode.SC - reference = "https://dss.sc.gov/media/ojqddxsk/tanf-policy-manual-volume-65.pdf#page=100" # Section 7.1 - adds = "gov.states.sc.tanf.income.earned.earned" + # 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 + + 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) diff --git a/policyengine_us/variables/gov/states/sc/tanf/income/sc_tanf_gross_unearned_income.py b/policyengine_us/variables/gov/states/sc/tanf/income/sc_tanf_gross_unearned_income.py index 36273d0a264..2368dd07f73 100644 --- a/policyengine_us/variables/gov/states/sc/tanf/income/sc_tanf_gross_unearned_income.py +++ b/policyengine_us/variables/gov/states/sc/tanf/income/sc_tanf_gross_unearned_income.py @@ -8,5 +8,32 @@ class sc_tanf_gross_unearned_income(Variable): unit = USD definition_period = MONTH defined_for = StateCode.SC - reference = "https://dss.sc.gov/media/ojqddxsk/tanf-policy-manual-volume-65.pdf#page=100" # Section 7.1 - adds = "gov.states.sc.tanf.income.unearned" + # PDF pages 102, 107-120 + reference = "https://dss.sc.gov/media/bxwc1jdj/tanf-policy-manual-volume-67-final.pdf#page=102" # Sections 7.1 and 7.7 + + def formula(person, period, parameters): + p = parameters(period).gov.states.sc.tanf.income.unearned + # Section 7.7 excludes the income of SSI recipients. + counted = ~person("sc_tanf_ssi_recipient", period) + other_sources = add(person, period, p.sources) + # Net amounts that can be losses count only when positive: a loss is + # not money received (Section 7.1). + for source in p.positive_sources: + other_sources = other_sources + max_(add(person, period, [source]), 0) + other_sources = other_sources * counted + # Section 7.7 excludes up to $400 of interest, dividends and royalties + # per benefit group each year and averages the rest over 12 months. + # Each member's exclusion is proportional to their share of that income. + p_idr = p.interest_and_dividends + interest_and_dividends = max_(add(person, period, p_idr.sources), 0) * counted + unit_interest_and_dividends = person.spm_unit.sum(interest_and_dividends) + unit_exclusion = min_( + unit_interest_and_dividends, p_idr.exclusion / MONTHS_IN_YEAR + ) + share = np.divide( + interest_and_dividends, + unit_interest_and_dividends, + out=np.zeros_like(interest_and_dividends), + where=unit_interest_and_dividends > 0, + ) + return other_sources + interest_and_dividends - unit_exclusion * share 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 061c427ad58..c5e491e4d7a 100644 --- a/policyengine_us/variables/gov/states/sc/tanf/sc_tanf.py +++ b/policyengine_us/variables/gov/states/sc/tanf/sc_tanf.py @@ -14,9 +14,7 @@ class sc_tanf(Variable): def formula(spm_unit, period, parameters): p = parameters(period).gov.states.sc.tanf - # Compute need standard based on federal poverty guidelines - fpg = spm_unit("tanf_fpg", period) - need_standard = fpg * p.income.need_standard.rate + need_standard = spm_unit("sc_tanf_need_standard", period) countable_income = spm_unit("sc_tanf_countable_income", period) excess_income = max_(need_standard - countable_income, 0) benefit = excess_income * p.payment.rate diff --git a/policyengine_us/variables/gov/states/sc/tanf/sc_tanf_benefit_group_size.py b/policyengine_us/variables/gov/states/sc/tanf/sc_tanf_benefit_group_size.py new file mode 100644 index 00000000000..0a73415430c --- /dev/null +++ b/policyengine_us/variables/gov/states/sc/tanf/sc_tanf_benefit_group_size.py @@ -0,0 +1,17 @@ +from policyengine_us.model_api import * + + +class sc_tanf_benefit_group_size(Variable): + value_type = int + entity = SPMUnit + label = "South Carolina TANF benefit group size" + definition_period = MONTH + defined_for = StateCode.SC + # PDF pages 56, 117 + reference = "https://dss.sc.gov/media/bxwc1jdj/tanf-policy-manual-volume-67-final.pdf#page=56" # Sections 3.6 and 7.7 + + def formula(spm_unit, period, parameters): + # Section 3.6 excludes an SSI recipient living with the benefit group; + # if an SSI child is the only child, the parents are the only members. + ssi_recipient = spm_unit.members("sc_tanf_ssi_recipient", period) + return spm_unit.sum(~ssi_recipient) 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 new file mode 100644 index 00000000000..1a869558d4b --- /dev/null +++ b/policyengine_us/variables/gov/states/sc/tanf/sc_tanf_need_standard.py @@ -0,0 +1,31 @@ +from policyengine_us.model_api import * + + +class sc_tanf_need_standard(Variable): + value_type = float + entity = SPMUnit + label = "South Carolina TANF need standard" + unit = USD + definition_period = MONTH + defined_for = StateCode.SC + # PDF pages 133-134 + reference = "https://dss.sc.gov/media/bxwc1jdj/tanf-policy-manual-volume-67-final.pdf#page=133" # Sections 8.15 and 8.16 + + def formula(spm_unit, period, parameters): + p = parameters(period).gov.states.sc.tanf.income.need_standard + size = spm_unit("sc_tanf_benefit_group_size", period) + # Section 8.15 sets the need standard as a share of the federal + # poverty guideline for the number of persons in the benefit group, + # revised each October 1, as in tanf_fpg. + year = period.start.year + if period.start.month >= 10: + instant_str = f"{year}-10-01" + else: + instant_str = f"{year - 1}-10-01" + p_fpg = parameters(instant_str).gov.hhs.fpg + state_group = spm_unit.household("state_group_str", period.this_year) + fpg = ( + 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) diff --git a/policyengine_us/variables/gov/states/sc/tanf/sc_tanf_ssi_recipient.py b/policyengine_us/variables/gov/states/sc/tanf/sc_tanf_ssi_recipient.py new file mode 100644 index 00000000000..276a3442394 --- /dev/null +++ b/policyengine_us/variables/gov/states/sc/tanf/sc_tanf_ssi_recipient.py @@ -0,0 +1,16 @@ +from policyengine_us.model_api import * + + +class sc_tanf_ssi_recipient(Variable): + value_type = bool + entity = Person + label = "South Carolina TANF SSI recipient" + definition_period = MONTH + defined_for = StateCode.SC + reference = "https://dss.sc.gov/media/bxwc1jdj/tanf-policy-manual-volume-67-final.pdf#page=117" # Section 7.7, SSI benefit + + def formula(person, period, parameters): + # Section 7.7 excludes the SSI recipient and his or her income when + # determining benefits for the other benefit group members. Reported + # receipt also counts, covering people the model computes as $0. + return (person("ssi", period) > 0) | person("receives_ssi", period)