From 8df8c5043a85ef0a463e4256cbfc70d56e3a61d0 Mon Sep 17 00:00:00 2001 From: Ziming Date: Wed, 19 Aug 2026 18:32:44 -0400 Subject: [PATCH 1/6] Fix SNAP expected contribution rounding per 7 CFR 273.10(e) Round net income to the nearest dollar (1-49 cents down, 50-99 cents up, 7 CFR 273.10(e)(1)(ii)(A)) and round 30 percent of net income up to the next higher dollar (7 CFR 273.10(e)(2)(ii)(A)(1)), replacing the prior floor-then-multiply formula. Add boundary tests against the FNS Basis of Issuance table and categorical-eligibility ceiling tests. Fixes #9312 Co-Authored-By: Claude Fable 5 --- ...ap-expected-contribution-rounding.fixed.md | 1 + ...ent_disqualification_income_treatment.yaml | 2 +- .../ineligible_members/integration.yaml | 24 +- .../policy/baseline/gov/usda/snap/snap.yaml | 8 +- .../snap_categorical_eligibility_ceiling.yaml | 113 +++++ .../usda/snap/snap_expected_contribution.yaml | 7 +- ...ap_normal_allotment_basis_of_issuance.yaml | 398 ++++++++++++++++++ .../usda/snap/snap_expected_contribution.py | 20 +- 8 files changed, 550 insertions(+), 23 deletions(-) create mode 100644 changelog.d/fix-snap-expected-contribution-rounding.fixed.md create mode 100644 policyengine_us/tests/policy/baseline/gov/usda/snap/snap_categorical_eligibility_ceiling.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/usda/snap/snap_normal_allotment_basis_of_issuance.yaml diff --git a/changelog.d/fix-snap-expected-contribution-rounding.fixed.md b/changelog.d/fix-snap-expected-contribution-rounding.fixed.md new file mode 100644 index 00000000000..eb6f63e6d64 --- /dev/null +++ b/changelog.d/fix-snap-expected-contribution-rounding.fixed.md @@ -0,0 +1 @@ +- Round SNAP net income to the nearest dollar and round 30 percent of net income up to the next dollar when computing the expected food contribution, per 7 CFR 273.10(e)(1)(ii)(A) and 273.10(e)(2)(ii)(A)(1). diff --git a/policyengine_us/tests/policy/baseline/gov/usda/snap/eligibility/work_requirements/snap_work_requirement_disqualification_income_treatment.yaml b/policyengine_us/tests/policy/baseline/gov/usda/snap/eligibility/work_requirements/snap_work_requirement_disqualification_income_treatment.yaml index d6cd717aceb..695fa9217a0 100644 --- a/policyengine_us/tests/policy/baseline/gov/usda/snap/eligibility/work_requirements/snap_work_requirement_disqualification_income_treatment.yaml +++ b/policyengine_us/tests/policy/baseline/gov/usda/snap/eligibility/work_requirements/snap_work_requirement_disqualification_income_treatment.yaml @@ -40,7 +40,7 @@ is_snap_eligible: true # 1-person allotment on the compliant worker's income; strictly below # the ~4,466 the unit receives when both members comply. - snap: 1_487.17 + snap: 1_477.57 - name: Case 2, over-income household newly qualifies when the earner fails the time limit and income is prorated. period: 2026 diff --git a/policyengine_us/tests/policy/baseline/gov/usda/snap/income/ineligible_members/integration.yaml b/policyengine_us/tests/policy/baseline/gov/usda/snap/income/ineligible_members/integration.yaml index 09d4357d077..1bf45126855 100644 --- a/policyengine_us/tests/policy/baseline/gov/usda/snap/income/ineligible_members/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/usda/snap/income/ineligible_members/integration.yaml @@ -27,7 +27,7 @@ output: snap_unit_size: 3 snap_earned_income: 2_000 - snap: 367.7 + snap: 367 - name: Case 2, refugee earner is prorated after OBBBA. period: 2026-01 @@ -55,7 +55,7 @@ # Counted share: 2 eligible / 3 household members. # $2,000 x 2/3 = $1,333.33 snap_earned_income: 1_333.33 - snap: 288.9 + snap: 288 - name: Case 3, undocumented earner in a prorating state. period: 2026-01 @@ -81,7 +81,7 @@ output: snap_unit_size: 2 snap_earned_income: 1_333.33 - snap: 288.9 + snap: 288 - name: Case 4, undocumented earner in a count-all state. period: 2026-01 @@ -108,7 +108,7 @@ snap_unit_size: 2 # Massachusetts counts all of the undocumented member's income. snap_earned_income: 2_000 - snap: 194.4 + snap: 194 - name: Case 5, undocumented earner in a hybrid gross-test state. period: 2026-01 @@ -136,7 +136,7 @@ # Net side prorated, gross test counts the full income. snap_earned_income: 1_333.33 snap_gross_test_income: 2_000 - snap: 288.9 + snap: 288 - name: Case 6, prorated member's shares of shelter and dependent care expenses are not deductible. period: 2026-01 @@ -201,7 +201,7 @@ snap_earned_income: 1_000 # The ineligible student's dividend income is excluded entirely. snap_unearned_income: 0 - snap: 120.7 + snap: 120 - name: Case 8, California delays OBBBA so refugees remain eligible in January 2026. period: 2026-01 @@ -228,7 +228,7 @@ snap_unit_size: 3 snap_income_counted_share: [1, 1, 1] snap_earned_income: 2_000 - snap: 225.2 + snap: 225 - name: Case 9, household with no eligible members receives nothing. period: 2026-01 @@ -277,8 +277,8 @@ # excluded under 7 CFR 273.9(c)(7). snap_earned_income: 2_000 # Net income: $2,000 - $400 earned income deduction - $209 standard - # deduction = $1,391; benefit: $546 - 0.3 x $1,391 = $128.70. - snap: 128.7 + # deduction = $1,391; benefit: $546 - $418 (30% of $1,391 rounded up) = $128. + snap: 128 - name: Case 11, prorated member's self-employment income is prorated like employment income. period: 2026-01 @@ -305,7 +305,7 @@ snap_unit_size: 2 # $2,000 monthly self-employment income x 2/3, mirroring Case 3. snap_earned_income: 1_333.33 - snap: 288.9 + snap: 288 - name: Case 12, prorated member's self-employment loss does not offset other members' income. period: 2026-01 @@ -338,8 +338,8 @@ # member's $2,000 monthly income counts in full. snap_earned_income: 2_000 # Net income: $2,000 - $400 earned income deduction - $209 standard - # deduction = $1,391; benefit: $546 - 0.3 x $1,391 = $128.70. - snap: 128.7 + # deduction = $1,391; benefit: $546 - $418 (30% of $1,391 rounded up) = $128. + snap: 128 - name: Case 13, excluded elderly member does not confer the gross income test exemption. period: 2026-01 diff --git a/policyengine_us/tests/policy/baseline/gov/usda/snap/snap.yaml b/policyengine_us/tests/policy/baseline/gov/usda/snap/snap.yaml index e4d2d863d93..211b7fd8bbb 100644 --- a/policyengine_us/tests/policy/baseline/gov/usda/snap/snap.yaml +++ b/policyengine_us/tests/policy/baseline/gov/usda/snap/snap.yaml @@ -88,7 +88,7 @@ snap_earned_income: 5_000 snap_gross_income: 5_000 snap_net_income: 5_000 - snap: 2_024.40 + snap: 2_010 - name: Counterfactual test, self-employment income deduction allowed period: 2025 @@ -196,7 +196,7 @@ snap_income_counted_share: [1, 0.5] snap_earned_income: 500 snap_gross_income: 500 - snap: 230.40 + snap: 230 - name: Case 7, income of an adult sanctioned under the general work requirements is counted in full. period: 2024-01 @@ -237,8 +237,8 @@ snap_earned_income: 1_000 snap_gross_income: 1_000 # Net income: $1,000 - $200 earned income deduction - $198 standard - # deduction = $602; benefit: $291 - 0.3 x $602 = $110.40. - snap: 110.40 + # deduction = $602; benefit: $291 - $181 (30% of $602 rounded up) = $110. + snap: 110 # Acceptance-criteria integration tests for issue #8862 (person-level SNAP work # requirements). Period 2024-01 is pre-HR1 (ABAWD band 18-52, dependent-child diff --git a/policyengine_us/tests/policy/baseline/gov/usda/snap/snap_categorical_eligibility_ceiling.yaml b/policyengine_us/tests/policy/baseline/gov/usda/snap/snap_categorical_eligibility_ceiling.yaml new file mode 100644 index 00000000000..35076cde558 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/usda/snap/snap_categorical_eligibility_ceiling.yaml @@ -0,0 +1,113 @@ +# Categorical eligibility above the issuance-table maximum. +# FY2026 (2025-10-01) contiguous-US values: max allotment 2 persons $546, +# 3 persons $785; standard deduction 1-3 persons $209; earned income +# deduction 20%; minimum allotment round(8% x $298) = $24 for 1-2 persons only. +# The reported TANF grant ($100/month) confers categorical eligibility and is +# counted as unearned income. +- name: Categorically eligible 2-person household above the income limit receives the minimum allotment + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 35 + employment_income: 60_000 + weekly_hours_worked_before_lsr: 40 + child: + age: 8 + spm_units: + spm_unit: + members: [parent, child] + tanf: 1_200 + housing_cost: 0 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: MO + output: + meets_snap_categorical_eligibility: true + meets_snap_gross_income_test: false + is_snap_eligible: true + # Gross 5,000 + 100 = 5,100; net 5,100 - 1,000 (20%) - 209 = 3,891; + # 546 - 30% x 3,891 < 0, floored to the minimum allotment. + snap_net_income: 3_891 + snap_min_allotment: 24 + snap_normal_allotment: 24 + snap: 24 + +- name: Categorically eligible 3-person household above the issuance-table maximum receives zero + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 35 + employment_income: 60_000 + weekly_hours_worked_before_lsr: 40 + child1: + age: 8 + child2: + age: 4 + spm_units: + spm_unit: + members: [parent, child1, child2] + tanf: 1_200 + housing_cost: 0 + tax_units: + tax_unit: + members: [parent, child1, child2] + households: + household: + members: [parent, child1, child2] + state_code: MO + output: + meets_snap_categorical_eligibility: true + meets_snap_gross_income_test: false + # Still flagged eligible: PE does not convert a zero-benefit 3+ unit + # into a denial (7 CFR 273.10(e)(2)(iii)). + is_snap_eligible: true + # Net income 3,891; 785 - 30% x 3,891 < 0 and no minimum allotment + # applies to households of 3 or more. + snap_net_income: 3_891 + snap_min_allotment: 0 + snap_normal_allotment: 0 + snap: 0 + +- name: Categorically eligible 3-person household just under the issuance-table maximum receives a small allotment + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 35 + employment_income: 40_500 + weekly_hours_worked_before_lsr: 40 + child1: + age: 8 + child2: + age: 4 + spm_units: + spm_unit: + members: [parent, child1, child2] + tanf: 1_200 + housing_cost: 0 + tax_units: + tax_unit: + members: [parent, child1, child2] + households: + household: + members: [parent, child1, child2] + state_code: MO + output: + meets_snap_categorical_eligibility: true + meets_snap_gross_income_test: false + is_snap_eligible: true + # Gross 3,375 + 100 = 3,475; net 3,475 - 675 - 209 = 2,591; + # 30% = 777.30, rounded up to 778 (7 CFR 273.10(e)(2)(ii)(A)); 785 - 778 = 7. + snap_net_income: 2_591 + snap_expected_contribution: 778 + snap_normal_allotment: 7 + snap: 7 diff --git a/policyengine_us/tests/policy/baseline/gov/usda/snap/snap_expected_contribution.yaml b/policyengine_us/tests/policy/baseline/gov/usda/snap/snap_expected_contribution.yaml index 458208461d5..16fc2b44471 100644 --- a/policyengine_us/tests/policy/baseline/gov/usda/snap/snap_expected_contribution.yaml +++ b/policyengine_us/tests/policy/baseline/gov/usda/snap/snap_expected_contribution.yaml @@ -26,7 +26,8 @@ household: state_group: CONTIGUOUS_US output: - snap_expected_contribution: (833 * 0.3) * 12 + # 30% of the $833 rounded net income is $249.90, rounded up to $250. + snap_expected_contribution: 250 * 12 - name: Family of 4, member(s) with disabilities, income of 24k/year (Standard Test) period: 2022 @@ -57,4 +58,6 @@ household: state_group: CONTIGUOUS_US output: - snap_expected_contribution: (428 * 0.3) * 12 # Net income is rounded down each month + # Net income of $428.50 rounds half-up to $429 each month; 30% is + # $128.70, rounded up to $129. + snap_expected_contribution: 129 * 12 diff --git a/policyengine_us/tests/policy/baseline/gov/usda/snap/snap_normal_allotment_basis_of_issuance.yaml b/policyengine_us/tests/policy/baseline/gov/usda/snap/snap_normal_allotment_basis_of_issuance.yaml new file mode 100644 index 00000000000..7cc9d9413e8 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/usda/snap/snap_normal_allotment_basis_of_issuance.yaml @@ -0,0 +1,398 @@ +# Net income boundaries from the FNS Basis of Coupon/EBT Issuance table, +# 48 States & DC, October 1, 2025 (3-person column, maximum allotment $785): +# https://dssmanuals.mo.gov/wp-content/uploads/2022/09/snap-basis-issuance-table.pdf#page=1 +# The household's only income is a reported TANF grant, so net income equals +# the grant minus the $209 standard deduction (FY2026, 1-3 persons). +# 7 CFR 273.10(e)(2)(ii)(A): 30% of net income is rounded up to the next dollar. + +- name: 3-person household, net income $0 (table row 0 - 0) receives $785 + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 35 + child1: + age: 8 + child2: + age: 4 + spm_units: + spm_unit: + members: [parent, child1, child2] + tanf: 2508 + housing_cost: 0 + tax_units: + tax_unit: + members: [parent, child1, child2] + households: + household: + members: [parent, child1, child2] + state_code: MO + output: + is_snap_eligible: true + # 0 - 0 + snap_net_income: 0 + snap_normal_allotment: 785 + snap: 785 + +- name: 3-person household, net income $3 (table row 1 - 3) receives $784 + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 35 + child1: + age: 8 + child2: + age: 4 + spm_units: + spm_unit: + members: [parent, child1, child2] + tanf: 2544 + housing_cost: 0 + tax_units: + tax_unit: + members: [parent, child1, child2] + households: + household: + members: [parent, child1, child2] + state_code: MO + output: + is_snap_eligible: true + # 1 - 3: 30% = 0.90 rounds up to 1 + snap_net_income: 3 + snap_normal_allotment: 784 + snap: 784 + +- name: 3-person household, net income $4 (table row 4 - 6) receives $783 + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 35 + child1: + age: 8 + child2: + age: 4 + spm_units: + spm_unit: + members: [parent, child1, child2] + tanf: 2556 + housing_cost: 0 + tax_units: + tax_unit: + members: [parent, child1, child2] + households: + household: + members: [parent, child1, child2] + state_code: MO + output: + is_snap_eligible: true + # 4 - 6: 30% = 1.20 rounds up to 2 + snap_net_income: 4 + snap_normal_allotment: 783 + snap: 783 + +- name: 3-person household, net income $6 (table row 4 - 6) receives $783 + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 35 + child1: + age: 8 + child2: + age: 4 + spm_units: + spm_unit: + members: [parent, child1, child2] + tanf: 2580 + housing_cost: 0 + tax_units: + tax_unit: + members: [parent, child1, child2] + households: + household: + members: [parent, child1, child2] + state_code: MO + output: + is_snap_eligible: true + # 4 - 6: 30% = 1.80 rounds up to 2 + snap_net_income: 6 + snap_normal_allotment: 783 + snap: 783 + +- name: 3-person household, net income $7 (table row 7 - 10) receives $782 + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 35 + child1: + age: 8 + child2: + age: 4 + spm_units: + spm_unit: + members: [parent, child1, child2] + tanf: 2592 + housing_cost: 0 + tax_units: + tax_unit: + members: [parent, child1, child2] + households: + household: + members: [parent, child1, child2] + state_code: MO + output: + is_snap_eligible: true + # 7 - 10: 30% = 2.10 rounds up to 3 + snap_net_income: 7 + snap_normal_allotment: 782 + snap: 782 + +- name: 3-person household, net income $10 (table row 7 - 10) receives $782 + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 35 + child1: + age: 8 + child2: + age: 4 + spm_units: + spm_unit: + members: [parent, child1, child2] + tanf: 2628 + housing_cost: 0 + tax_units: + tax_unit: + members: [parent, child1, child2] + households: + household: + members: [parent, child1, child2] + state_code: MO + output: + is_snap_eligible: true + # 7 - 10: 30% = 3.00 exactly + snap_net_income: 10 + snap_normal_allotment: 782 + snap: 782 + +- name: 3-person household, net income $11 (table row 11 - 13) receives $781 + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 35 + child1: + age: 8 + child2: + age: 4 + spm_units: + spm_unit: + members: [parent, child1, child2] + tanf: 2640 + housing_cost: 0 + tax_units: + tax_unit: + members: [parent, child1, child2] + households: + household: + members: [parent, child1, child2] + state_code: MO + output: + is_snap_eligible: true + # 11 - 13: 30% = 3.30 rounds up to 4 + snap_net_income: 11 + snap_normal_allotment: 781 + snap: 781 + +- name: 3-person household, net income $13 (table row 11 - 13) receives $781 + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 35 + child1: + age: 8 + child2: + age: 4 + spm_units: + spm_unit: + members: [parent, child1, child2] + tanf: 2664 + housing_cost: 0 + tax_units: + tax_unit: + members: [parent, child1, child2] + households: + household: + members: [parent, child1, child2] + state_code: MO + output: + is_snap_eligible: true + # 11 - 13: 30% = 3.90 rounds up to 4 + snap_net_income: 13 + snap_normal_allotment: 781 + snap: 781 + +- name: 3-person household, net income $14 (table row 14 - 16) receives $780 + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 35 + child1: + age: 8 + child2: + age: 4 + spm_units: + spm_unit: + members: [parent, child1, child2] + tanf: 2676 + housing_cost: 0 + tax_units: + tax_unit: + members: [parent, child1, child2] + households: + household: + members: [parent, child1, child2] + state_code: MO + output: + is_snap_eligible: true + # 14 - 16: 30% = 4.20 rounds up to 5 + snap_net_income: 14 + snap_normal_allotment: 780 + snap: 780 + +- name: 3-person household, net income $50 (table row 47 - 50) receives $770 + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 35 + child1: + age: 8 + child2: + age: 4 + spm_units: + spm_unit: + members: [parent, child1, child2] + tanf: 3108 + housing_cost: 0 + tax_units: + tax_unit: + members: [parent, child1, child2] + households: + household: + members: [parent, child1, child2] + state_code: MO + output: + is_snap_eligible: true + # 47 - 50: 30% = 15.00 exactly + snap_net_income: 50 + snap_normal_allotment: 770 + snap: 770 + +- name: 3-person household, net income $51 (table row 51 - 53) receives $769 + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 35 + child1: + age: 8 + child2: + age: 4 + spm_units: + spm_unit: + members: [parent, child1, child2] + tanf: 3120 + housing_cost: 0 + tax_units: + tax_unit: + members: [parent, child1, child2] + households: + household: + members: [parent, child1, child2] + state_code: MO + output: + is_snap_eligible: true + # 51 - 53: 30% = 15.30 rounds up to 16 + snap_net_income: 51 + snap_normal_allotment: 769 + snap: 769 + +- name: 3-person household, net income $3.49 rounds down to $3 and receives $784 + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 35 + child1: + age: 8 + child2: + age: 4 + spm_units: + spm_unit: + members: [parent, child1, child2] + tanf: 2_549.88 + housing_cost: 0 + tax_units: + tax_unit: + members: [parent, child1, child2] + households: + household: + members: [parent, child1, child2] + state_code: MO + output: + is_snap_eligible: true + # TANF 212.49/month; net 3.49 -> 3 (1-49 cents round down); 30% = 0.90 -> 1 + snap_net_income: 3.49 + snap_expected_contribution: 1 + snap_normal_allotment: 784 + snap: 784 + +- name: 3-person household, net income $3.50 rounds up to $4 and receives $783 + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 35 + child1: + age: 8 + child2: + age: 4 + spm_units: + spm_unit: + members: [parent, child1, child2] + tanf: 2_550 + housing_cost: 0 + tax_units: + tax_unit: + members: [parent, child1, child2] + households: + household: + members: [parent, child1, child2] + state_code: MO + output: + is_snap_eligible: true + # TANF 212.50/month; net 3.50 -> 4 (50-99 cents round up); 30% = 1.20 -> 2 + snap_net_income: 3.50 + snap_expected_contribution: 2 + snap_normal_allotment: 783 + snap: 783 diff --git a/policyengine_us/variables/gov/usda/snap/snap_expected_contribution.py b/policyengine_us/variables/gov/usda/snap/snap_expected_contribution.py index 21eb3bb5b39..ef0ac62ab86 100644 --- a/policyengine_us/variables/gov/usda/snap/snap_expected_contribution.py +++ b/policyengine_us/variables/gov/usda/snap/snap_expected_contribution.py @@ -8,12 +8,24 @@ class snap_expected_contribution(Variable): documentation = "Expected food contribution from SNAP net income" label = "SNAP expected food contribution" unit = USD - reference = "https://www.law.cornell.edu/uscode/text/7/2017#a" + reference = ( + "https://www.law.cornell.edu/uscode/text/7/2017#a", + "https://www.law.cornell.edu/cfr/text/7/273.10#e_8_ii_A", + "https://www.law.cornell.edu/cfr/text/7/273.10#e_2_ii_A_1", + ) def formula(spm_unit, period, parameters): expected_food_contribution = parameters( period ).gov.usda.snap.expected_contribution - return ( - np.floor(spm_unit("snap_net_income", period)) * expected_food_contribution - ) + # 7 CFR 273.10(e)(1)(ii)(A): net income is rounded to the nearest + # dollar (1-49 cents down, 50-99 cents up), so use half-up rounding + # rather than np.round's round-half-to-even. + net_income = np.floor(spm_unit("snap_net_income", period) + 0.5) + # 7 CFR 273.10(e)(2)(ii)(A)(1): 30 percent of net income is rounded + # up to the next higher dollar, which is equivalent to rounding the + # allotment down to the nearest lower whole dollar (7 USC 2017(a)). + # Round to cents first so float noise (e.g. 50 * 0.3 = 15.0000001) + # does not push an exact dollar amount up to the next dollar. + contribution = np.round(net_income * expected_food_contribution, 2) + return np.ceil(contribution) From 09972e07e38167457353e9da276cee198b1e452c Mon Sep 17 00:00:00 2001 From: Ziming Date: Thu, 20 Aug 2026 09:37:55 -0400 Subject: [PATCH 2/6] Update partner SNAP pins for corrected expected-contribution rounding The 31 SNAP pins in the analytics-coverage partner tests shift by $1-$11 per year under the legally correct rounding (net income to the nearest dollar, 30 percent rounded up to the next dollar). Approved by Ziming. Co-Authored-By: Claude Fable 5 --- .../edge_cases/federal/nutrition/snap/ca.yaml | 8 ++--- .../edge_cases/federal/nutrition/snap/co.yaml | 2 +- .../federal/nutrition/snap/federal.yaml | 34 +++++++++---------- .../edge_cases/federal/nutrition/snap/il.yaml | 2 +- .../edge_cases/federal/nutrition/snap/ks.yaml | 6 ++-- .../edge_cases/federal/nutrition/snap/ma.yaml | 4 +-- .../edge_cases/federal/nutrition/snap/nc.yaml | 4 +-- .../edge_cases/federal/nutrition/snap/wa.yaml | 2 +- 8 files changed, 31 insertions(+), 31 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/ca.yaml b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/ca.yaml index a671baebac9..b5f85c34f7c 100644 --- a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/ca.yaml +++ b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/ca.yaml @@ -77,7 +77,7 @@ # 0.3 = 657.30 -> 785 - 657.30 = 127.70/month Jan-Sep; Oct-Dec: net # 2,183.62 -> EC 654.90 -> 812.72 - 654.90 = 157.82. # Annual = 9 x 127.70 + 3 x 157.82 = 1,622.76. - snap: 1622.76 + snap: 1613.17 is_snap_eligible: true - name: snap_ca_net_income_at_fy2026_net_limit_36447 (ca) period: 2026 @@ -144,7 +144,7 @@ # EC = floor(2,220) x 0.3 = 666.00 -> 785 - 666.00 = 119.00/month # Jan-Sep; Oct-Dec: net 2,213.42 -> EC 663.90 -> 812.72 - 663.90 = # 148.82. Annual = 9 x 119.00 + 3 x 148.82 = 1,517.46. - snap: 1517.46 + snap: 1508.17 is_snap_eligible: true - name: snap_ca_net_income_above_net_limit_all_year_38000 (ca) period: 2026 @@ -280,7 +280,7 @@ # fully counted; from April the head is excluded (size 2) and their # earnings count at the 2/3 prorated share under 7 CFR # 273.11(c)(2)-(3), raising the benefit relative to full counting. - snap: 2186.94 + snap: 2181.84 is_snap_eligible: true - name: snap_ca_head_immigration_undocumented_calworks_interaction (ca) period: 2026 @@ -346,5 +346,5 @@ # 2, the CalWORKs grant excludes the head from the AU but counts in # full as unit-level unearned income, and the head's earnings count # at the 2/3 prorated share under 7 CFR 273.11(c)(2)-(3). - snap: 3026.94 + snap: 3018.84 is_snap_eligible: true diff --git a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/co.yaml b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/co.yaml index d9319a3f5fa..65a50247cf4 100644 --- a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/co.yaml +++ b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/co.yaml @@ -70,7 +70,7 @@ # expense = 1,500 housing + 594 CO SUA = 2,094 - 1,428.83 = 665.17 -> # net = 2,192.50 -> EC 657.60 -> 785 - 657.60 = 127.40/month; Oct-Dec: # 812.72 - 654.30 = 158.42. Annual = 9 x 127.40 + 3 x 158.42 = 1,621.86. - snap: 1621.86 + snap: 1616.17 is_snap_eligible: true - name: snap_income_above_200_pct_bbce_limit_54740 (co) period: 2026 diff --git a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/federal.yaml b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/federal.yaml index e74fc672556..3f7b0565475 100644 --- a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/federal.yaml +++ b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/federal.yaml @@ -84,7 +84,7 @@ # contribution = floor(2,128) x 0.3 = 638.40 -> 785 - 638.40 = 146.60/month. # Oct-Dec: std ded 216.38 -> net 2,117.63 -> EC 635.10 -> 812.72 - 635.10 # = 177.62/month. Annual = 9 x 146.60 + 3 x 177.62 = 1,852.26. - snap: 1852.26 + snap: 1844.17 is_snap_eligible: true - name: snap_income_at_fy2026_bbce_gross_limit_43972 (federal) period: 2026 @@ -154,7 +154,7 @@ # 1,945 - 1,361.23 = 583.77 -> net = 2,138.70 -> EC 641.40 -> # 785 - 641.40 = 143.60/month. Oct-Dec: 812.72 - 638.10 = 174.62/month. # Annual = 9 x 143.60 + 3 x 174.62 = 1,816.26. - snap: 1816.26 + snap: 1808.17 is_snap_eligible: true - name: snap_income_above_bbce_gross_limit_all_year_45178 (federal) period: 2026 @@ -373,7 +373,7 @@ # 62.50/month. Oct-Dec: net = 994 - 216.38 = 777.62 -> EC floor(777) x # 0.3 = 233.10 -> 308.52 - 233.10 = 75.42/month. # Annual = 9 x 62.50 + 3 x 75.42 = 788.76. - snap: 788.77 + snap: 781.57 is_snap_eligible: true - name: snap_childless_couple_20k (federal) period: 2026 @@ -429,7 +429,7 @@ # -> 546 - 337.20 = 208.80/month Jan-Sep; Oct-Dec: net 1,116.95 -> EC # 334.80 -> 565.28 - 334.80 = 230.48. Annual = 9 x 208.80 + 3 x 230.48 # = 2,570.64. (Also the benchmark for the excluded-immigrant cases below.) - snap: 2570.64 + snap: 2559.84 is_snap_eligible: true - name: snap_single_parent_1_child_15k_mid_phase_out (federal) period: 2026 @@ -485,7 +485,7 @@ # EC 237.30 -> 546 - 237.30 = 308.70/month Jan-Sep; Oct-Dec: net 783.62 # -> EC 234.90 -> 565.28 - 234.90 = 330.38. Annual = 9 x 308.70 + # 3 x 330.38 = 3,769.44. (TX TANF is 0 at this income.) - snap: 3769.44 + snap: 3759.84 is_snap_eligible: true - name: snap_size_4_couple_2_children_20k (federal) period: 2026 @@ -563,7 +563,7 @@ # - 223 = 1,110.33 -> EC 333.00 -> 994 - 333.00 = 661.00/month Jan-Sep; # Oct-Dec: net 1,102.46 -> EC 330.60 -> 1,029.10 - 330.60 = 698.50. # Annual = 9 x 661.00 + 3 x 698.50 = 8,044.50. - snap: 8044.51 + snap: 8043.31 is_snap_eligible: true - name: snap_shelter_deduction_capped_non_elderly (federal) period: 2026 @@ -623,7 +623,7 @@ # 744 -> net = 647.00 -> EC 194.10 -> 546 - 194.10 = 351.90/month # Jan-Sep; Oct-Dec (cap 770.27): 565.28 - 183.90 = 381.38. # Annual = 9 x 351.90 + 3 x 381.38 = 4,311.24. - snap: 4311.24 + snap: 4302.84 is_snap_eligible: true - name: snap_shelter_deduction_uncapped_elderly (federal) period: 2026 @@ -682,7 +682,7 @@ # 949.50 = 441.50 -> EC 132.30 -> 546 - 132.30 = 413.70/month Jan-Sep; # Oct-Dec: net 430.43 -> EC 129.00 -> 565.28 - 129.00 = 436.28. # Annual = 9 x 413.70 + 3 x 436.28 = 5,032.14 (vs 4,311.24 capped). - snap: 5032.15 + snap: 5025.85 is_snap_eligible: true - name: snap_head_immigration_lpr (federal) period: 2026 @@ -748,7 +748,7 @@ # 1,666.67 x 0.8 - 209 = 1,124.33 -> EC 337.20 -> 785 - 337.20 = # 447.80/month Jan-Sep; Oct-Dec: 812.72 - 334.80 = 477.92. # Annual = 9 x 447.80 + 3 x 477.92 = 5,463.96. - snap: 5463.97 + snap: 5453.17 is_snap_eligible: true - name: snap_head_immigration_refugee_excluded_post_obbb (federal) period: 2026 @@ -817,7 +817,7 @@ # 1,111.11/mo, net income 679.89 -> EC = floor(679.89) x 0.3 = 203.70 # -> 546 - 203.70 = 342.30/month Jan-Sep; Oct-Dec (FY2027): 565.28 - # 201.60 = 363.68. Annual = 9 x 342.30 + 3 x 363.68 = 4,171.74. - snap: 4171.75 + snap: 4167.84 is_snap_eligible: true - name: snap_head_immigration_undocumented_excluded (federal) period: 2026 @@ -882,7 +882,7 @@ # Undocumented head is excluded from the unit (size 2) with earnings # counted at the 2/3 prorated share; identical arithmetic to the # refugee case: annual = 9 x 342.30 + 3 x 363.68 = 4,171.74. - snap: 4171.75 + snap: 4167.84 is_snap_eligible: true - name: snap_abawd_zero_hours_fails_work_requirements (federal) period: 2026 @@ -979,7 +979,7 @@ # 20% - 209 = 657.67 -> EC 197.10 -> 298 - 197.10 = 100.90/month # Jan-Sep; Oct-Dec: 308.52 - 195.00 = 113.52. # Annual = 9 x 100.90 + 3 x 113.52 = 1,248.66. - snap: 1248.67 + snap: 1240.57 is_snap_eligible: true - name: snap_ineligible_student_sole_member (federal) period: 2026 @@ -1159,7 +1159,7 @@ # unearned income ($200/month): net = 1,124.33 + 200 = 1,324.33 -> EC # 397.20 -> 785 - 397.20 = 387.80/month Jan-Sep; Oct-Dec: 812.72 - # 394.80 = 417.92. Annual = 9 x 387.80 + 3 x 417.92 = 4,743.96. - snap: 4743.97 + snap: 4733.17 is_snap_eligible: true - name: snap_partner_override_snap_earned_and_unearned_income (federal) period: 2026 @@ -1232,7 +1232,7 @@ # unearned 200/month. Net = 1,250 - 250 (20%) + 200 - 209 = 991.00 -> # EC 297.30 -> 785 - 297.30 = 487.70/month Jan-Sep; Oct-Dec: 812.72 - # 294.90 = 517.82. Annual = 9 x 487.70 + 3 x 517.82 = 5,942.76. - snap: 5942.77 + snap: 5933.17 is_snap_eligible: true - name: snap_negative_self_employment_income_robustness (federal) period: 2026 @@ -1291,7 +1291,7 @@ # 546 - 297.30 = 248.70/month Jan-Sep; Oct-Dec: 565.28 - 294.90 = # 270.38. Annual = 9 x 248.70 + 3 x 270.38 = 3,049.44 (no benefit # inflation from negative income). - snap: 3049.44 + snap: 3039.84 is_snap_eligible: true - name: snap_2025_single_parent_2_children_20k (federal) period: 2025 @@ -1360,7 +1360,7 @@ # 204): net = 1,666.67 x 0.8 - 204 = 1,129.33 -> EC 338.70 -> 768 - # 338.70 = 429.30/month; Oct-Dec 2025 use FY2026 values: 785 - 337.20 # = 447.80. Annual = 9 x 429.30 + 3 x 447.80 = 5,207.10. - snap: 5_207.10 + snap: 5_202 is_snap_eligible: true - name: snap_limited_utility_allowance_two_utilities (federal) period: 2026 @@ -1433,7 +1433,7 @@ # 0.5 x 1,124.33 = 437.83 (uncapped) -> net = 686.50 -> EC 205.80 -> # 785 - 205.80 = 579.20/month Jan-Sep; Oct-Dec (LUA uprated to 414.13): # 812.72 - 198.30 = 614.42. Annual = 9 x 579.20 + 3 x 614.42 = 7,056.06. - snap: 7056.07 + snap: 7052.17 is_snap_eligible: true - name: snap_ineligible_student_income_excluded_mixed_household (federal) diff --git a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/il.yaml b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/il.yaml index 31cba51e429..e1ab2664d09 100644 --- a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/il.yaml +++ b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/il.yaml @@ -71,5 +71,5 @@ # visible) -> net = 540.50 -> EC 162.00 -> 785 - 162.00 = 623.00/month; # Oct-Dec: 812.72 - 158.70 = 654.02. # Annual = 9 x 623.00 + 3 x 654.02 = 7,569.06. - snap: 7569.06 + snap: 7568.17 is_snap_eligible: true diff --git a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/ks.yaml b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/ks.yaml index 604981d702e..0c1131081e8 100644 --- a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/ks.yaml +++ b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/ks.yaml @@ -64,7 +64,7 @@ # 546 - 485.10 = 60.90/month Jan-Sep; Oct-Dec: net 1,609.95 -> EC # 482.70 -> 565.28 - 482.70 = 82.58. # Annual = 9 x 60.90 + 3 x 82.58 = 795.84. - snap: 795.84 + snap: 786.84 is_snap_eligible: true - name: snap_couple_income_at_fy2026_gross_limit_27495 (ks) period: 2026 @@ -121,7 +121,7 @@ # -> EC 487.20 -> 546 - 487.20 = 58.80/month Jan-Sep; Oct-Dec: net # 1,616.62 -> EC 484.80 -> 565.28 - 484.80 = 80.48. # Annual = 9 x 58.80 + 3 x 80.48 = 770.64. - snap: 770.64 + snap: 759.84 is_snap_eligible: true - name: snap_couple_income_above_gross_limit_all_year_28232 (ks) period: 2026 @@ -292,5 +292,5 @@ # positive). Net = 20,000/12 x 0.8 - 209 = 1,124.33 -> EC 337.20 -> # 546 - 337.20 = 208.80/month Jan-Sep; Oct-Dec: 565.28 - 334.80 = # 230.48. Annual = 9 x 208.80 + 3 x 230.48 = 2,570.64. - snap: 2570.64 + snap: 2559.84 is_snap_eligible: true diff --git a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/ma.yaml b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/ma.yaml index 036576f631d..c28d57eda79 100644 --- a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/ma.yaml +++ b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/ma.yaml @@ -71,7 +71,7 @@ # 2,113.67 -> EC 633.90 -> 785 - 633.90 = 151.10/month; Oct-Dec (cap # 770.27): 812.72 - 624.00 = 188.72. # Annual = 9 x 151.10 + 3 x 188.72 = 1,926.06. - snap: 1926.06 + snap: 1916.17 is_snap_eligible: true - name: snap_income_above_200_pct_bbce_limit_54740 (ma) period: 2026 @@ -205,5 +205,5 @@ output: spm_units: spm_unit: - snap: 3840.54 + snap: 3834.84 is_snap_eligible: true diff --git a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/nc.yaml b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/nc.yaml index 826992a0362..ea70e932fad 100644 --- a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/nc.yaml +++ b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/nc.yaml @@ -72,7 +72,7 @@ # -> capped at 744 -> net = 2,113.67 -> EC 633.90 -> 785 - 633.90 = # 151.10/month; Oct-Dec (cap 770.27): 812.72 - 624.00 = 188.72. # Annual = 9 x 151.10 + 3 x 188.72 = 1,926.06. - snap: 1926.06 + snap: 1916.17 is_snap_eligible: true - name: snap_income_above_200_pct_bbce_limit_54740 (nc) period: 2026 @@ -275,5 +275,5 @@ # The full $1,666.67/month passes the gross screens and the benefit # is computed from the 2/3-prorated income, matching the TX # prorate-state baseline. - snap: 4171.75 + snap: 4167.84 is_snap_eligible: true diff --git a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/wa.yaml b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/wa.yaml index 5eb9f9afba4..c4b51f7f3bf 100644 --- a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/wa.yaml +++ b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/wa.yaml @@ -73,7 +73,7 @@ # test, so the unit is still eligible. EC 681.30 -> 785 - 681.30 = # 103.70/month; Oct-Dec: 812.72 - 678.00 = 134.72. # Annual = 9 x 103.70 + 3 x 134.72 = 1,337.46. - snap: 1337.46 + snap: 1331.17 is_snap_eligible: true - name: snap_income_above_200_pct_bbce_limit_54740 (wa) period: 2026 From dacce5571b4e8a65d91a9a9235117f5ed75f771a Mon Sep 17 00:00:00 2001 From: Ziming Date: Thu, 20 Aug 2026 10:55:00 -0400 Subject: [PATCH 3/6] Update partner signature pins and arithmetic comments for the rounding fix The 33 signature-replay SNAP pins (masked in the first CI run by the edge-case failures) shift under the corrected rounding, and the edge-case comments now show the ceil-of-30-percent arithmetic that produces the updated pins. Approved by Ziming. Co-Authored-By: Claude Fable 5 --- .../edge_cases/federal/nutrition/snap/ca.yaml | 14 +-- .../edge_cases/federal/nutrition/snap/co.yaml | 5 +- .../federal/nutrition/snap/federal.yaml | 107 +++++++++--------- .../edge_cases/federal/nutrition/snap/il.yaml | 7 +- .../edge_cases/federal/nutrition/snap/ks.yaml | 20 ++-- .../edge_cases/federal/nutrition/snap/ma.yaml | 7 +- .../edge_cases/federal/nutrition/snap/nc.yaml | 7 +- .../edge_cases/federal/nutrition/snap/wa.yaml | 7 +- .../analytics_coverage/signatures/ca.yaml | 46 ++++---- .../signatures/federal.yaml | 20 ++-- 10 files changed, 125 insertions(+), 115 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/ca.yaml b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/ca.yaml index b5f85c34f7c..19f178d009d 100644 --- a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/ca.yaml +++ b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/ca.yaml @@ -73,10 +73,10 @@ # Gross ratio 36,000/26,650 = 1.35 fails the federal 130% test, but CA # BBCE (200% gross + net test) rescues: net = 36,000/12 x 0.8 - 209 = # 2,191.00 (SUA 663 < half of pre-shelter net, so no shelter deduction) - # <= 2,220.83 Jan-Sep limit -> eligible all year. EC = floor(2,191) x - # 0.3 = 657.30 -> 785 - 657.30 = 127.70/month Jan-Sep; Oct-Dec: net - # 2,183.62 -> EC 654.90 -> 812.72 - 654.90 = 157.82. - # Annual = 9 x 127.70 + 3 x 157.82 = 1,622.76. + # <= 2,220.83 Jan-Sep limit -> eligible all year. EC = ceil(2,191 x + # 0.3) = 658 -> 785 - 658 = 127.00/month Jan-Sep; Oct-Dec: net + # 2,183.62 rounds to 2,184 -> EC 656 -> 812.72 - 656 = 156.72. + # Annual = 9 x 127.00 + 3 x 156.72 = 1,613.17. snap: 1613.17 is_snap_eligible: true - name: snap_ca_net_income_at_fy2026_net_limit_36447 (ca) @@ -141,9 +141,9 @@ spm_unit: # At the binding CA net limit: net = 36,447/12 x 0.8 - 209 = 2,220.80, # just under the Jan-Sep limit of 26,650/12 = 2,220.83 -> eligible. - # EC = floor(2,220) x 0.3 = 666.00 -> 785 - 666.00 = 119.00/month - # Jan-Sep; Oct-Dec: net 2,213.42 -> EC 663.90 -> 812.72 - 663.90 = - # 148.82. Annual = 9 x 119.00 + 3 x 148.82 = 1,517.46. + # EC = ceil(2,221 x 0.3) = 667 -> 785 - 667 = 118.00/month + # Jan-Sep; Oct-Dec: net 2,213.42 rounds to 2,213 -> EC 664 -> + # 812.72 - 664 = 148.72. Annual = 9 x 118.00 + 3 x 148.72 = 1,508.17. snap: 1508.17 is_snap_eligible: true - name: snap_ca_net_income_above_net_limit_all_year_38000 (ca) diff --git a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/co.yaml b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/co.yaml index 65a50247cf4..953db610c0b 100644 --- a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/co.yaml +++ b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/co.yaml @@ -68,8 +68,9 @@ # stays under CO's 200% limit all year (Jan-Sep: 2 x 26,650 = 53,300). # Jan-Sep: pre-shelter net = 46,000/12 x 0.8 - 209 = 2,857.67; shelter # expense = 1,500 housing + 594 CO SUA = 2,094 - 1,428.83 = 665.17 -> - # net = 2,192.50 -> EC 657.60 -> 785 - 657.60 = 127.40/month; Oct-Dec: - # 812.72 - 654.30 = 158.42. Annual = 9 x 127.40 + 3 x 158.42 = 1,621.86. + # net = 2,192.50 -> EC 658 (30% rounded up) -> 785 - 658 = 127.00/month; + # Oct-Dec: 812.72 - 655 = 157.72. Annual = 9 x 127.00 + 3 x 157.72 = + # 1,616.17. snap: 1616.17 is_snap_eligible: true - name: snap_income_above_200_pct_bbce_limit_54740 (co) diff --git a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/federal.yaml b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/federal.yaml index 3f7b0565475..f4ac3b36c18 100644 --- a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/federal.yaml +++ b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/federal.yaml @@ -80,10 +80,10 @@ # gross test (ratio 1.65 > 1.3), so BBCE is the binding route. # Jan-Sep benefit: net income pre-shelter = 3,656.00 x 0.8 - 209 = 2,715.80; # shelter expense = 1,500 housing + 445 SUA (heating/cooling expense present) - # = 1,945 - 0.5 x 2,715.80 = 587.10 (< 744 cap); net = 2,128.70 -> - # contribution = floor(2,128) x 0.3 = 638.40 -> 785 - 638.40 = 146.60/month. - # Oct-Dec: std ded 216.38 -> net 2,117.63 -> EC 635.10 -> 812.72 - 635.10 - # = 177.62/month. Annual = 9 x 146.60 + 3 x 177.62 = 1,852.26. + # = 1,945 - 0.5 x 2,715.80 = 587.10 (< 744 cap); net = 2,128.70 rounds to + # 2,129 -> contribution = ceil(2,129 x 0.3) = 639 -> 785 - 639 = 146.00/month. + # Oct-Dec: std ded 216.38 -> net 2,117.63 rounds to 2,118 -> EC 636 -> + # 812.72 - 636 = 176.72/month. Annual = 9 x 146.00 + 3 x 176.72 = 1,844.17. snap: 1844.17 is_snap_eligible: true - name: snap_income_at_fy2026_bbce_gross_limit_43972 (federal) @@ -151,9 +151,9 @@ # At the binding FY2026 TX BBCE gross limit: monthly 43,972/12 = 3,664.33, # limit 1.65 x 26,650 / 12 = 3,664.38 -> still eligible. # Jan-Sep: pre-shelter net = 3,664.33 x 0.8 - 209 = 2,722.47; shelter = - # 1,945 - 1,361.23 = 583.77 -> net = 2,138.70 -> EC 641.40 -> - # 785 - 641.40 = 143.60/month. Oct-Dec: 812.72 - 638.10 = 174.62/month. - # Annual = 9 x 143.60 + 3 x 174.62 = 1,816.26. + # 1,945 - 1,361.23 = 583.77 -> net = 2,138.70 rounds to 2,139 -> EC + # ceil(641.70) = 642 -> 785 - 642 = 143.00/month. Oct-Dec: 812.72 - 639 = + # 173.72/month. Annual = 9 x 143.00 + 3 x 173.72 = 1,808.17. snap: 1808.17 is_snap_eligible: true - name: snap_income_above_bbce_gross_limit_all_year_45178 (federal) @@ -369,10 +369,10 @@ spm_unit: # Zero-income aged-65 single receives SSI (2026 federal benefit rate # $994/month), which counts as SNAP unearned income: - # Jan-Sep: net = 994 - 209 = 785 -> EC 235.50 -> 298 - 235.50 = - # 62.50/month. Oct-Dec: net = 994 - 216.38 = 777.62 -> EC floor(777) x - # 0.3 = 233.10 -> 308.52 - 233.10 = 75.42/month. - # Annual = 9 x 62.50 + 3 x 75.42 = 788.76. + # Jan-Sep: net = 994 - 209 = 785 -> EC ceil(235.50) = 236 -> 298 - 236 = + # 62.00/month. Oct-Dec: net = 994 - 216.38 = 777.62 rounds to 778 -> EC + # ceil(233.40) = 234 -> 308.52 - 234 = 74.52/month. + # Annual = 9 x 62.00 + 3 x 74.52 = 781.57. snap: 781.57 is_snap_eligible: true - name: snap_childless_couple_20k (federal) @@ -425,10 +425,11 @@ output: spm_units: spm_unit: - # 2-person baseline: net = 20,000/12 x 0.8 - 209 = 1,124.33 -> EC 337.20 - # -> 546 - 337.20 = 208.80/month Jan-Sep; Oct-Dec: net 1,116.95 -> EC - # 334.80 -> 565.28 - 334.80 = 230.48. Annual = 9 x 208.80 + 3 x 230.48 - # = 2,570.64. (Also the benchmark for the excluded-immigrant cases below.) + # 2-person baseline: net = 20,000/12 x 0.8 - 209 = 1,124.33 -> EC + # ceil(1,124 x 0.3) = 338 -> 546 - 338 = 208.00/month Jan-Sep; Oct-Dec: + # net 1,116.95 rounds to 1,117 -> EC 336 -> 565.28 - 336 = 229.28. + # Annual = 9 x 208.00 + 3 x 229.28 = 2,559.84. (Also the benchmark for + # the excluded-immigrant cases below.) snap: 2559.84 is_snap_eligible: true - name: snap_single_parent_1_child_15k_mid_phase_out (federal) @@ -482,9 +483,9 @@ spm_units: spm_unit: # Mid-phase-out partial benefit: net = 15,000/12 x 0.8 - 209 = 791.00 -> - # EC 237.30 -> 546 - 237.30 = 308.70/month Jan-Sep; Oct-Dec: net 783.62 - # -> EC 234.90 -> 565.28 - 234.90 = 330.38. Annual = 9 x 308.70 + - # 3 x 330.38 = 3,769.44. (TX TANF is 0 at this income.) + # EC ceil(237.30) = 238 -> 546 - 238 = 308.00/month Jan-Sep; Oct-Dec: net + # 783.62 rounds to 784 -> EC 236 -> 565.28 - 236 = 329.28. Annual = + # 9 x 308.00 + 3 x 329.28 = 3,759.84. (TX TANF is 0 at this income.) snap: 3759.84 is_snap_eligible: true - name: snap_size_4_couple_2_children_20k (federal) @@ -560,9 +561,10 @@ spm_units: spm_unit: # Size 4 uses the higher standard deduction ($223): net = 1,666.67 x 0.8 - # - 223 = 1,110.33 -> EC 333.00 -> 994 - 333.00 = 661.00/month Jan-Sep; - # Oct-Dec: net 1,102.46 -> EC 330.60 -> 1,029.10 - 330.60 = 698.50. - # Annual = 9 x 661.00 + 3 x 698.50 = 8,044.50. + # - 223 = 1,110.33 -> EC = 1,110 x 0.3 = 333 exactly -> 994 - 333 = + # 661.00/month Jan-Sep; Oct-Dec: net 1,102.46 rounds to 1,102 -> EC + # ceil(330.60) = 331 -> 1,029.10 - 331 = 698.10. + # Annual = 9 x 661.00 + 3 x 698.10 = 8,043.31. snap: 8043.31 is_snap_eligible: true - name: snap_shelter_deduction_capped_non_elderly (federal) @@ -620,9 +622,9 @@ # Excess shelter deduction hits the cap for a non-elderly unit: # pre-shelter net = 24,000/12 x 0.8 - 209 = 1,391.00; shelter expense = # 1,200 housing + 445 SUA = 1,645 - 0.5 x 1,391 = 949.50 -> capped at - # 744 -> net = 647.00 -> EC 194.10 -> 546 - 194.10 = 351.90/month - # Jan-Sep; Oct-Dec (cap 770.27): 565.28 - 183.90 = 381.38. - # Annual = 9 x 351.90 + 3 x 381.38 = 4,311.24. + # 744 -> net = 647.00 -> EC ceil(194.10) = 195 -> 546 - 195 = + # 351.00/month Jan-Sep; Oct-Dec (cap 770.27): 565.28 - 184 = 381.28. + # Annual = 9 x 351.00 + 3 x 381.28 = 4,302.84. snap: 4302.84 is_snap_eligible: true - name: snap_shelter_deduction_uncapped_elderly (federal) @@ -679,9 +681,10 @@ spm_unit: # Same unit as the previous case but the head is 60+, so the shelter # deduction is NOT capped: full 949.50 deducted -> net = 1,391.00 - - # 949.50 = 441.50 -> EC 132.30 -> 546 - 132.30 = 413.70/month Jan-Sep; - # Oct-Dec: net 430.43 -> EC 129.00 -> 565.28 - 129.00 = 436.28. - # Annual = 9 x 413.70 + 3 x 436.28 = 5,032.14 (vs 4,311.24 capped). + # 949.50 = 441.50 rounds to 442 -> EC ceil(132.60) = 133 -> 546 - 133 = + # 413.00/month Jan-Sep; Oct-Dec: net 430.43 rounds to 430 -> EC = 129 + # exactly -> 565.28 - 129 = 436.28. + # Annual = 9 x 413.00 + 3 x 436.28 = 5,025.85 (vs 4,302.84 capped). snap: 5025.85 is_snap_eligible: true - name: snap_head_immigration_lpr (federal) @@ -745,9 +748,9 @@ spm_units: spm_unit: # LPR head remains a full unit member post-OBBB (size 3): net = - # 1,666.67 x 0.8 - 209 = 1,124.33 -> EC 337.20 -> 785 - 337.20 = - # 447.80/month Jan-Sep; Oct-Dec: 812.72 - 334.80 = 477.92. - # Annual = 9 x 447.80 + 3 x 477.92 = 5,463.96. + # 1,666.67 x 0.8 - 209 = 1,124.33 -> EC ceil(1,124 x 0.3) = 338 -> + # 785 - 338 = 447.00/month Jan-Sep; Oct-Dec: 812.72 - 336 = 476.72. + # Annual = 9 x 447.00 + 3 x 476.72 = 5,453.17. snap: 5453.17 is_snap_eligible: true - name: snap_head_immigration_refugee_excluded_post_obbb (federal) @@ -814,9 +817,9 @@ # immigration statuses: the refugee head is excluded (unit size 2, max # allotment 546) and their earnings count at the 2/3 prorated share # under 7 CFR 273.11(c)(2)-(3) (TX prorates): counted earnings - # 1,111.11/mo, net income 679.89 -> EC = floor(679.89) x 0.3 = 203.70 - # -> 546 - 203.70 = 342.30/month Jan-Sep; Oct-Dec (FY2027): 565.28 - - # 201.60 = 363.68. Annual = 9 x 342.30 + 3 x 363.68 = 4,171.74. + # 1,111.11/mo, net income 679.89 rounds to 680 -> EC = 680 x 0.3 = 204 + # exactly -> 546 - 204 = 342.00/month Jan-Sep; Oct-Dec (FY2027): + # 565.28 - 202 = 363.28. Annual = 9 x 342.00 + 3 x 363.28 = 4,167.84. snap: 4167.84 is_snap_eligible: true - name: snap_head_immigration_undocumented_excluded (federal) @@ -881,7 +884,7 @@ spm_unit: # Undocumented head is excluded from the unit (size 2) with earnings # counted at the 2/3 prorated share; identical arithmetic to the - # refugee case: annual = 9 x 342.30 + 3 x 363.68 = 4,171.74. + # refugee case: annual = 9 x 342.00 + 3 x 363.28 = 4,167.84. snap: 4167.84 is_snap_eligible: true - name: snap_abawd_zero_hours_fails_work_requirements (federal) @@ -976,9 +979,10 @@ # (is_snap_work_program_participant, a partner-sent input) supplies # general-requirement compliance, flipping the unit to eligible # (without it this person is ineligible). Benefit: net = 13,000/12 - - # 20% - 209 = 657.67 -> EC 197.10 -> 298 - 197.10 = 100.90/month - # Jan-Sep; Oct-Dec: 308.52 - 195.00 = 113.52. - # Annual = 9 x 100.90 + 3 x 113.52 = 1,248.66. + # 20% - 209 = 657.67 rounds to 658 -> EC ceil(197.40) = 198 -> + # 298 - 198 = 100.00/month Jan-Sep; Oct-Dec: net rounds to 650 -> EC = + # 195 exactly -> 308.52 - 195 = 113.52. + # Annual = 9 x 100.00 + 3 x 113.52 = 1,240.57. snap: 1240.57 is_snap_eligible: true - name: snap_ineligible_student_sole_member (federal) @@ -1157,8 +1161,8 @@ # TANF cash ($2,400/year override): TANF receipt confers categorical # eligibility, overriding the failed asset test. TANF also counts as # unearned income ($200/month): net = 1,124.33 + 200 = 1,324.33 -> EC - # 397.20 -> 785 - 397.20 = 387.80/month Jan-Sep; Oct-Dec: 812.72 - - # 394.80 = 417.92. Annual = 9 x 387.80 + 3 x 417.92 = 4,743.96. + # ceil(1,324 x 0.3) = 398 -> 785 - 398 = 387.00/month Jan-Sep; Oct-Dec: + # 812.72 - 396 = 416.72. Annual = 9 x 387.00 + 3 x 416.72 = 4,733.17. snap: 4733.17 is_snap_eligible: true - name: snap_partner_override_snap_earned_and_unearned_income (federal) @@ -1230,8 +1234,8 @@ # (2,400/year) inputs replace the model's own aggregation, so SNAP sees # earned 1,250/month (not the 1,500 implied by employment income) and # unearned 200/month. Net = 1,250 - 250 (20%) + 200 - 209 = 991.00 -> - # EC 297.30 -> 785 - 297.30 = 487.70/month Jan-Sep; Oct-Dec: 812.72 - - # 294.90 = 517.82. Annual = 9 x 487.70 + 3 x 517.82 = 5,942.76. + # EC ceil(297.30) = 298 -> 785 - 298 = 487.00/month Jan-Sep; Oct-Dec: + # 812.72 - 296 = 516.72. Annual = 9 x 487.00 + 3 x 516.72 = 5,933.17. snap: 5933.17 is_snap_eligible: true - name: snap_negative_self_employment_income_robustness (federal) @@ -1287,10 +1291,10 @@ spm_unit: # Robustness: a $6,000 self-employment LOSS does not offset wages in # SNAP earned income (floored at zero), so the benefit equals the - # wages-only case: net = 18,000/12 x 0.8 - 209 = 991.00 -> EC 297.30 -> - # 546 - 297.30 = 248.70/month Jan-Sep; Oct-Dec: 565.28 - 294.90 = - # 270.38. Annual = 9 x 248.70 + 3 x 270.38 = 3,049.44 (no benefit - # inflation from negative income). + # wages-only case: net = 18,000/12 x 0.8 - 209 = 991.00 -> EC + # ceil(297.30) = 298 -> 546 - 298 = 248.00/month Jan-Sep; Oct-Dec: + # 565.28 - 296 = 269.28. Annual = 9 x 248.00 + 3 x 269.28 = 3,039.84 + # (no benefit inflation from negative income). snap: 3039.84 is_snap_eligible: true - name: snap_2025_single_parent_2_children_20k (federal) @@ -1357,9 +1361,9 @@ spm_unit: # 2025 regime pin (partners query 2025 and 2026; FY2025/FY2026 COLA # split): Jan-Sep 2025 use FY2025 values (max 3-person 768, std ded - # 204): net = 1,666.67 x 0.8 - 204 = 1,129.33 -> EC 338.70 -> 768 - - # 338.70 = 429.30/month; Oct-Dec 2025 use FY2026 values: 785 - 337.20 - # = 447.80. Annual = 9 x 429.30 + 3 x 447.80 = 5,207.10. + # 204): net = 1,666.67 x 0.8 - 204 = 1,129.33 -> EC ceil(1,129 x 0.3) + # = 339 -> 768 - 339 = 429.00/month; Oct-Dec 2025 use FY2026 values: + # 785 - 338 = 447.00. Annual = 9 x 429.00 + 3 x 447.00 = 5,202. snap: 5_202 is_snap_eligible: true - name: snap_limited_utility_allowance_two_utilities (federal) @@ -1430,9 +1434,10 @@ # Two non-heating utility bills (electricity + water, partner-sent # expense inputs) select TX's Limited Utility Allowance ($400 FY2026) # instead of the SUA: shelter expense = 600 housing + 400 LUA = 1,000 - - # 0.5 x 1,124.33 = 437.83 (uncapped) -> net = 686.50 -> EC 205.80 -> - # 785 - 205.80 = 579.20/month Jan-Sep; Oct-Dec (LUA uprated to 414.13): - # 812.72 - 198.30 = 614.42. Annual = 9 x 579.20 + 3 x 614.42 = 7,056.06. + # 0.5 x 1,124.33 = 437.83 (uncapped) -> net = 686.50 (686.4998 unrounded, + # so it rounds to 686) -> EC ceil(205.80) = 206 -> 785 - 206 = + # 579.00/month Jan-Sep; Oct-Dec (LUA uprated to 414.13): 812.72 - 199 = + # 613.72. Annual = 9 x 579.00 + 3 x 613.72 = 7,052.17. snap: 7052.17 is_snap_eligible: true diff --git a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/il.yaml b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/il.yaml index e1ab2664d09..3a8708ff9e3 100644 --- a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/il.yaml +++ b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/il.yaml @@ -68,8 +68,9 @@ # selects the $546 IL SUA. Jan-Sep: pre-shelter net = 20,000/12 x 0.8 # - 209 = 1,124.33; shelter expense = 600 housing + 546 = 1,146 - # 562.17 = 583.83 (below the 744 cap, so the IL SUA value is fully - # visible) -> net = 540.50 -> EC 162.00 -> 785 - 162.00 = 623.00/month; - # Oct-Dec: 812.72 - 158.70 = 654.02. - # Annual = 9 x 623.00 + 3 x 654.02 = 7,569.06. + # visible) -> net = 540.50 (540.4998 unrounded, so it rounds to 540) -> + # EC = 540 x 0.3 = 162 exactly -> 785 - 162 = 623.00/month; + # Oct-Dec: 812.72 - 159 = 653.72. + # Annual = 9 x 623.00 + 3 x 653.72 = 7,568.17. snap: 7568.17 is_snap_eligible: true diff --git a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/ks.yaml b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/ks.yaml index 0c1131081e8..93a56805e13 100644 --- a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/ks.yaml +++ b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/ks.yaml @@ -60,10 +60,10 @@ spm_unit: # Monthly gross 27,395/12 = 2,282.92 vs Jan-Sep limit 1.3 x 21,150/12 # = 2,291.25 -> passes the federal gross test all year (Oct-Dec limit - # 2,344.33). Net = 2,282.92 x 0.8 - 209 = 1,617.33 -> EC 485.10 -> - # 546 - 485.10 = 60.90/month Jan-Sep; Oct-Dec: net 1,609.95 -> EC - # 482.70 -> 565.28 - 482.70 = 82.58. - # Annual = 9 x 60.90 + 3 x 82.58 = 795.84. + # 2,344.33). Net = 2,282.92 x 0.8 - 209 = 1,617.33 -> EC ceil(1,617 x + # 0.3) = 486 -> 546 - 486 = 60.00/month Jan-Sep; Oct-Dec: net 1,609.95 + # rounds to 1,610 -> EC = 483 exactly -> 565.28 - 483 = 82.28. + # Annual = 9 x 60.00 + 3 x 82.28 = 786.84. snap: 786.84 is_snap_eligible: true - name: snap_couple_income_at_fy2026_gross_limit_27495 (ks) @@ -118,9 +118,9 @@ spm_unit: # Exactly at the binding FY2026 limit: 27,495 = 1.3 x 21,150 -> gross # ratio 1.30 -> still eligible. Net = 2,291.25 x 0.8 - 209 = 1,624.00 - # -> EC 487.20 -> 546 - 487.20 = 58.80/month Jan-Sep; Oct-Dec: net - # 1,616.62 -> EC 484.80 -> 565.28 - 484.80 = 80.48. - # Annual = 9 x 58.80 + 3 x 80.48 = 770.64. + # -> EC ceil(487.20) = 488 -> 546 - 488 = 58.00/month Jan-Sep; Oct-Dec: + # net 1,616.62 rounds to 1,617 -> EC 486 -> 565.28 - 486 = 79.28. + # Annual = 9 x 58.00 + 3 x 79.28 = 759.84. snap: 759.84 is_snap_eligible: true - name: snap_couple_income_above_gross_limit_all_year_28232 (ks) @@ -289,8 +289,8 @@ # Same unit as the previous case but the head is 60 (elderly): the # higher elderly/disabled asset limit ($4,500) applies, so $4,000 # passes and the age-59/60 asset distinction is observable (0 vs - # positive). Net = 20,000/12 x 0.8 - 209 = 1,124.33 -> EC 337.20 -> - # 546 - 337.20 = 208.80/month Jan-Sep; Oct-Dec: 565.28 - 334.80 = - # 230.48. Annual = 9 x 208.80 + 3 x 230.48 = 2,570.64. + # positive). Net = 20,000/12 x 0.8 - 209 = 1,124.33 -> EC ceil(1,124 x + # 0.3) = 338 -> 546 - 338 = 208.00/month Jan-Sep; Oct-Dec: 565.28 - + # 336 = 229.28. Annual = 9 x 208.00 + 3 x 229.28 = 2,559.84. snap: 2559.84 is_snap_eligible: true diff --git a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/ma.yaml b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/ma.yaml index c28d57eda79..436cd0022ec 100644 --- a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/ma.yaml +++ b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/ma.yaml @@ -68,9 +68,10 @@ # stays under MA's 200% limit all year (Jan-Sep: 2 x 26,650 = 53,300). # Jan-Sep: pre-shelter net = 2,857.67; shelter expense = 1,500 housing # + 914 MA SUA = 2,414 - 1,428.83 = 985.17 -> capped at 744 -> net = - # 2,113.67 -> EC 633.90 -> 785 - 633.90 = 151.10/month; Oct-Dec (cap - # 770.27): 812.72 - 624.00 = 188.72. - # Annual = 9 x 151.10 + 3 x 188.72 = 1,926.06. + # 2,113.67 rounds to 2,114 -> EC ceil(634.20) = 635 -> 785 - 635 = + # 150.00/month; Oct-Dec (cap 770.27): net rounds to 2,080 -> EC = 624 + # exactly -> 812.72 - 624 = 188.72. + # Annual = 9 x 150.00 + 3 x 188.72 = 1,916.17. snap: 1916.17 is_snap_eligible: true - name: snap_income_above_200_pct_bbce_limit_54740 (ma) diff --git a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/nc.yaml b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/nc.yaml index ea70e932fad..24ffbd2348c 100644 --- a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/nc.yaml +++ b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/nc.yaml @@ -69,9 +69,10 @@ # NC is not an always-SUA state, so the heating/cooling expense input # selects the SUA (NC 3-person: $768). Jan-Sep: pre-shelter net = # 2,857.67; shelter expense = 1,500 + 768 = 2,268 - 1,428.83 = 839.17 - # -> capped at 744 -> net = 2,113.67 -> EC 633.90 -> 785 - 633.90 = - # 151.10/month; Oct-Dec (cap 770.27): 812.72 - 624.00 = 188.72. - # Annual = 9 x 151.10 + 3 x 188.72 = 1,926.06. + # -> capped at 744 -> net = 2,113.67 rounds to 2,114 -> EC ceil(634.20) + # = 635 -> 785 - 635 = 150.00/month; Oct-Dec (cap 770.27): net rounds + # to 2,080 -> EC = 624 exactly -> 812.72 - 624 = 188.72. + # Annual = 9 x 150.00 + 3 x 188.72 = 1,916.17. snap: 1916.17 is_snap_eligible: true - name: snap_income_above_200_pct_bbce_limit_54740 (nc) diff --git a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/wa.yaml b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/wa.yaml index c4b51f7f3bf..ae687211432 100644 --- a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/wa.yaml +++ b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/federal/nutrition/snap/wa.yaml @@ -70,9 +70,10 @@ # Jan-Sep: pre-shelter net = 2,857.67; shelter expense = 1,500 + 515 # WA SUA = 2,015 - 1,428.83 = 586.17 -> net = 2,271.50, which is ABOVE # the regular 100%-FPG net limit (2,220.83) - WA's BBCE has no net - # test, so the unit is still eligible. EC 681.30 -> 785 - 681.30 = - # 103.70/month; Oct-Dec: 812.72 - 678.00 = 134.72. - # Annual = 9 x 103.70 + 3 x 134.72 = 1,337.46. + # test, so the unit is still eligible. EC ceil(2,271 x 0.3) = 682 -> + # 785 - 682 = 103.00/month; Oct-Dec: net rounds to 2,260 -> EC = 678 + # exactly -> 812.72 - 678 = 134.72. + # Annual = 9 x 103.00 + 3 x 134.72 = 1,331.17. snap: 1331.17 is_snap_eligible: true - name: snap_income_above_200_pct_bbce_limit_54740 (wa) diff --git a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/signatures/ca.yaml b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/signatures/ca.yaml index a7a6ac0c75e..634f294ddfd 100644 --- a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/signatures/ca.yaml +++ b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/signatures/ca.yaml @@ -170,7 +170,7 @@ la_general_relief: 0.0 la_general_relief_eligible: false lifeline: 228.0 - snap: 3756.91 + snap: 3750.31 people: head: ca_ala_general_assistance_eligible_person: false @@ -372,7 +372,7 @@ la_general_relief: 0.0 la_general_relief_eligible: false lifeline: 228.0 - snap: 3756.91 + snap: 3750.31 people: head: ca_ala_general_assistance_eligible_person: false @@ -752,7 +752,7 @@ la_general_relief: 0.0 la_general_relief_eligible: false lifeline: 228.0 - snap: 10448.41 + snap: 10443.31 people: head: ca_ala_general_assistance_eligible_person: false @@ -1124,7 +1124,7 @@ la_general_relief: 0.0 la_general_relief_eligible: false lifeline: 228.0 - snap: 7990.51 + snap: 7977.31 people: head: ca_ala_general_assistance_eligible_person: false @@ -1323,7 +1323,7 @@ la_general_relief: 0.0 la_general_relief_eligible: false lifeline: 228.0 - snap: 3756.91 + snap: 3750.31 people: head: ca_ala_general_assistance_eligible_person: false @@ -1517,7 +1517,7 @@ la_general_relief: 0.0 la_general_relief_eligible: false lifeline: 228.0 - snap: 3756.91 + snap: 3750.31 people: head: ca_ala_general_assistance_eligible_person: false @@ -1706,7 +1706,7 @@ la_general_relief: 0.0 la_general_relief_eligible: false lifeline: 228.0 - snap: 7990.51 + snap: 7977.31 people: head: ca_ala_general_assistance_eligible_person: false @@ -1907,7 +1907,7 @@ la_general_relief: 0.0 la_general_relief_eligible: false lifeline: 228.0 - snap: 3756.91 + snap: 3750.31 people: head: ca_ala_general_assistance_eligible_person: false @@ -2092,7 +2092,7 @@ la_general_relief: 0.0 la_general_relief_eligible: false lifeline: 228.0 - snap: 7990.51 + snap: 7977.31 people: head: ca_ala_general_assistance_eligible_person: false @@ -2486,7 +2486,7 @@ la_general_relief: 0.0 la_general_relief_eligible: false lifeline: 228.0 - snap: 7990.51 + snap: 7977.31 people: head: ca_ala_general_assistance_eligible_person: false @@ -2678,7 +2678,7 @@ la_general_relief: 0.0 la_general_relief_eligible: false lifeline: 228.0 - snap: 3756.91 + snap: 3750.31 people: head: ca_ala_general_assistance_eligible_person: false @@ -2878,7 +2878,7 @@ la_general_relief: 0.0 la_general_relief_eligible: false lifeline: 228.0 - snap: 3756.91 + snap: 3750.31 people: head: ca_ala_general_assistance_eligible_person: false @@ -3066,7 +3066,7 @@ la_general_relief: 0.0 la_general_relief_eligible: false lifeline: 228.0 - snap: 7990.51 + snap: 7977.31 people: head: ca_ala_general_assistance_eligible_person: false @@ -3450,7 +3450,7 @@ la_general_relief: 0.0 la_general_relief_eligible: false lifeline: 228.0 - snap: 7990.51 + snap: 7977.31 people: head: ca_ala_general_assistance_eligible_person: false @@ -3640,7 +3640,7 @@ la_general_relief: 0.0 la_general_relief_eligible: false lifeline: 228.0 - snap: 10448.41 + snap: 10443.31 people: head: ca_ala_general_assistance_eligible_person: false @@ -3838,7 +3838,7 @@ la_general_relief: 0.0 la_general_relief_eligible: false lifeline: 228.0 - snap: 3756.91 + snap: 3750.31 people: head: ca_ala_general_assistance_eligible_person: false @@ -4032,7 +4032,7 @@ la_general_relief: 0.0 la_general_relief_eligible: false lifeline: 228.0 - snap: 3756.91 + snap: 3750.31 people: head: ca_ala_general_assistance_eligible_person: false @@ -4224,7 +4224,7 @@ la_general_relief: 0.0 la_general_relief_eligible: false lifeline: 228.0 - snap: 10448.41 + snap: 10443.31 people: head: ca_ala_general_assistance_eligible_person: false @@ -4415,7 +4415,7 @@ la_general_relief: 0.0 la_general_relief_eligible: false lifeline: 228.0 - snap: 10448.41 + snap: 10443.31 people: head: ca_ala_general_assistance_eligible_person: false @@ -4799,7 +4799,7 @@ la_general_relief: 0.0 la_general_relief_eligible: false lifeline: 228.0 - snap: 3756.91 + snap: 3750.31 people: head: ca_ala_general_assistance_eligible_person: false @@ -5187,7 +5187,7 @@ la_general_relief: 0.0 la_general_relief_eligible: false lifeline: 228.0 - snap: 10448.41 + snap: 10443.31 people: head: ca_ala_general_assistance_eligible_person: false @@ -5377,7 +5377,7 @@ la_general_relief: 0.0 la_general_relief_eligible: false lifeline: 228.0 - snap: 10448.41 + snap: 10443.31 people: head: ca_ala_general_assistance_eligible_person: false @@ -5562,7 +5562,7 @@ la_general_relief: 0.0 la_general_relief_eligible: false lifeline: 228.0 - snap: 7990.51 + snap: 7977.31 people: head: ca_ala_general_assistance_eligible_person: false diff --git a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/signatures/federal.yaml b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/signatures/federal.yaml index 6897165876a..638e0d4732f 100644 --- a/policyengine_us/tests/policy/baseline/partners/analytics_coverage/signatures/federal.yaml +++ b/policyengine_us/tests/policy/baseline/partners/analytics_coverage/signatures/federal.yaml @@ -90,7 +90,7 @@ is_medicaid_eligible: true spm_units: spm_unit: - snap: 1323.31 + snap: 1314.31 - name: signature_8 (25 reqs, +2 dupes) period: 2026 absolute_error_margin: 0.1 @@ -353,7 +353,7 @@ spm_units: spm_unit: is_snap_eligible: true - snap: 3756.91 + snap: 3750.31 people: head: is_wic_eligible: false @@ -515,7 +515,7 @@ spm_units: spm_unit: is_snap_eligible: true - snap: 3756.91 + snap: 3750.31 people: head: is_wic_eligible: false @@ -618,7 +618,7 @@ spm_units: spm_unit: is_snap_eligible: true - snap: 3756.91 + snap: 3750.31 people: head: is_wic_eligible: false @@ -845,7 +845,7 @@ spm_units: spm_unit: is_snap_eligible: true - snap: 5826.01 + snap: 5823.31 people: head: is_wic_eligible: false @@ -953,7 +953,7 @@ spm_units: spm_unit: is_snap_eligible: true - snap: 5826.01 + snap: 5823.31 people: head: is_wic_eligible: false @@ -1149,7 +1149,7 @@ spm_units: spm_unit: is_snap_eligible: true - snap: 3756.91 + snap: 3750.31 people: head: is_wic_eligible: false @@ -1349,7 +1349,7 @@ spm_units: spm_unit: is_snap_eligible: true - snap: 3756.91 + snap: 3750.31 people: head: is_wic_eligible: false @@ -1650,7 +1650,7 @@ spm_units: spm_unit: is_snap_eligible: true - snap: 3756.91 + snap: 3750.31 people: head: is_wic_eligible: false @@ -1761,7 +1761,7 @@ spm_units: spm_unit: is_snap_eligible: true - snap: 3756.91 + snap: 3750.31 people: head: is_wic_eligible: false From 9669fcfc4f86cba10a8ca3c7a277bcaf02457171 Mon Sep 17 00:00:00 2001 From: Ziming Date: Mon, 24 Aug 2026 10:26:46 -0400 Subject: [PATCH 4/6] Update amplifi and impactica partner pins for the rounding fix The last partner layer (masked while earlier directories failed): five SNAP pins shift under the corrected rounding; 2025 totals become whole dollars. Verified locally with the full partner folder (630 passed). Approved by Ziming. Co-Authored-By: Claude Fable 5 --- .../tests/policy/baseline/partners/amplifi/2025.yaml | 2 +- .../tests/policy/baseline/partners/amplifi/2026.yaml | 6 +++--- .../tests/policy/baseline/partners/impactica/2025.yaml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/partners/amplifi/2025.yaml b/policyengine_us/tests/policy/baseline/partners/amplifi/2025.yaml index 65c943122a4..2a22450525e 100644 --- a/policyengine_us/tests/policy/baseline/partners/amplifi/2025.yaml +++ b/policyengine_us/tests/policy/baseline/partners/amplifi/2025.yaml @@ -110,7 +110,7 @@ spm_unit: ca_tanf: 0.0 ca_tanf_eligible: false - snap: 2114.10 + snap: 2106.00 is_snap_eligible: true lifeline: 228.0 is_lifeline_eligible: true diff --git a/policyengine_us/tests/policy/baseline/partners/amplifi/2026.yaml b/policyengine_us/tests/policy/baseline/partners/amplifi/2026.yaml index df52fbe89fe..b6e1b103b92 100644 --- a/policyengine_us/tests/policy/baseline/partners/amplifi/2026.yaml +++ b/policyengine_us/tests/policy/baseline/partners/amplifi/2026.yaml @@ -110,7 +110,7 @@ spm_unit: ca_tanf: 0.0 ca_tanf_eligible: false - snap: 2392.44 + snap: 2388.84 is_snap_eligible: true lifeline: 228.0 is_lifeline_eligible: true @@ -260,7 +260,7 @@ spm_unit: ca_tanf: 0.0 ca_tanf_eligible: false - snap: 1942.26 + snap: 1940.17 is_snap_eligible: true lifeline: 228.0 is_lifeline_eligible: true @@ -411,7 +411,7 @@ spm_unit: ca_tanf: 13280.0 ca_tanf_eligible: true - snap: 4157.17 + snap: 4148.17 is_snap_eligible: true lifeline: 228.0 is_lifeline_eligible: true diff --git a/policyengine_us/tests/policy/baseline/partners/impactica/2025.yaml b/policyengine_us/tests/policy/baseline/partners/impactica/2025.yaml index 2c6b12ee390..9432e54e934 100644 --- a/policyengine_us/tests/policy/baseline/partners/impactica/2025.yaml +++ b/policyengine_us/tests/policy/baseline/partners/impactica/2025.yaml @@ -33,4 +33,4 @@ is_medicaid_eligible: true spm_units: spm_unit: - snap: 5422.5 + snap: 5412.0 From 809c4d8e3e8157f830bfc4e6f5c91019de5dedbf Mon Sep 17 00:00:00 2001 From: Ziming Date: Mon, 24 Aug 2026 11:31:46 -0400 Subject: [PATCH 5/6] Add half-up tie-break test cases and tighten CFR citations Review response: add net $6.50 and $10.50 cases that pin half-up rounding against round-half-to-even (the $3.50 boundary cannot distinguish them), cite 7 CFR 273.10(e)(2)(ii)(A)(2) for the allotment down-round equivalence, and deduplicate the Cornell CFR reference link. Co-Authored-By: Claude Fable 5 --- ...ap_normal_allotment_basis_of_issuance.yaml | 68 +++++++++++++++++++ .../usda/snap/snap_expected_contribution.py | 5 +- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/usda/snap/snap_normal_allotment_basis_of_issuance.yaml b/policyengine_us/tests/policy/baseline/gov/usda/snap/snap_normal_allotment_basis_of_issuance.yaml index 7cc9d9413e8..fbce1562c00 100644 --- a/policyengine_us/tests/policy/baseline/gov/usda/snap/snap_normal_allotment_basis_of_issuance.yaml +++ b/policyengine_us/tests/policy/baseline/gov/usda/snap/snap_normal_allotment_basis_of_issuance.yaml @@ -396,3 +396,71 @@ snap_expected_contribution: 2 snap_normal_allotment: 783 snap: 783 + +# The two cases below pin half-up rounding against round-half-to-even +# (banker's rounding, np.round's default). At $3.50 both rules give $4, so +# only an even-dollar .50 boundary that also straddles an expected- +# contribution band can distinguish them. +- name: 3-person household, net income $6.50 rounds up to $7 and receives $782 + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 35 + child1: + age: 8 + child2: + age: 4 + spm_units: + spm_unit: + members: [parent, child1, child2] + tanf: 2_586 + housing_cost: 0 + tax_units: + tax_unit: + members: [parent, child1, child2] + households: + household: + members: [parent, child1, child2] + state_code: MO + output: + is_snap_eligible: true + # TANF 215.50/month; net 6.50 -> 7 (50 cents round up); 30% = 2.10 -> 3. + # Round-half-to-even would give 6 -> 30% = 1.80 -> 2 -> $783 instead. + snap_net_income: 6.50 + snap_expected_contribution: 3 + snap_normal_allotment: 782 + snap: 782 + +- name: 3-person household, net income $10.50 rounds up to $11 and receives $781 + period: 2026-01 + absolute_error_margin: 0.1 + input: + people: + parent: + age: 35 + child1: + age: 8 + child2: + age: 4 + spm_units: + spm_unit: + members: [parent, child1, child2] + tanf: 2_634 + housing_cost: 0 + tax_units: + tax_unit: + members: [parent, child1, child2] + households: + household: + members: [parent, child1, child2] + state_code: MO + output: + is_snap_eligible: true + # TANF 219.50/month; net 10.50 -> 11 (50 cents round up); 30% = 3.30 -> 4. + # Round-half-to-even would give 10 -> 30% = 3.00 exactly -> 3 -> $782. + snap_net_income: 10.50 + snap_expected_contribution: 4 + snap_normal_allotment: 781 + snap: 781 diff --git a/policyengine_us/variables/gov/usda/snap/snap_expected_contribution.py b/policyengine_us/variables/gov/usda/snap/snap_expected_contribution.py index ef0ac62ab86..f0236de5e0d 100644 --- a/policyengine_us/variables/gov/usda/snap/snap_expected_contribution.py +++ b/policyengine_us/variables/gov/usda/snap/snap_expected_contribution.py @@ -10,7 +10,7 @@ class snap_expected_contribution(Variable): unit = USD reference = ( "https://www.law.cornell.edu/uscode/text/7/2017#a", - "https://www.law.cornell.edu/cfr/text/7/273.10#e_8_ii_A", + # 7 CFR 273.10(e)(1)(ii)(A) and (e)(2)(ii)(A)(1). "https://www.law.cornell.edu/cfr/text/7/273.10#e_2_ii_A_1", ) @@ -24,7 +24,8 @@ def formula(spm_unit, period, parameters): net_income = np.floor(spm_unit("snap_net_income", period) + 0.5) # 7 CFR 273.10(e)(2)(ii)(A)(1): 30 percent of net income is rounded # up to the next higher dollar, which is equivalent to rounding the - # allotment down to the nearest lower whole dollar (7 USC 2017(a)). + # allotment down to the nearest lower whole dollar + # (7 CFR 273.10(e)(2)(ii)(A)(2); 7 USC 2017(a)). # Round to cents first so float noise (e.g. 50 * 0.3 = 15.0000001) # does not push an exact dollar amount up to the next dollar. contribution = np.round(net_income * expected_food_contribution, 2) From 949546af53c0d179ad5007910e859f59e2929103 Mon Sep 17 00:00:00 2001 From: Ziming Date: Tue, 25 Aug 2026 14:05:19 -0400 Subject: [PATCH 6/6] Round net income at the source and address review findings - Move the 7 CFR 273.10(e)(1)(ii)(A) half-up rounding into snap_net_income so the eligibility test, FPG ratio, and expected contribution all read the same rounded figure; the expected contribution keeps only its 273.10(e)(2)(ii)(A)(1) round-up. - Compare BBCE net income against the same rounded-up whole-dollar standard as the federal net test in meets_tanf_non_cash_net_income_test; the previous raw ratio comparison denied households sitting exactly at the published standard once net income is rounded. - Update the stale CBPP integration pins (47 / 611) and the North Carolina yearly pin (72) hidden behind wide error margins, and tighten those margins. - Cite each rounding provision in its own variable, mark the net-$50 case as the float32 cents-guard regression test, pin the issuance table to a Wayback capture, note the state-option elections, and drop the changelog fragment's leading bullet. Co-Authored-By: Claude Fable 5 --- ...ap-expected-contribution-rounding.fixed.md | 2 +- .../meets_tanf_non_cash_net_income_test.yaml | 25 +++++++++++++++--- .../gov/usda/snap/income/snap_net_income.yaml | 8 +++--- .../baseline/gov/usda/snap/integration.yaml | 9 +++++-- .../policy/baseline/gov/usda/snap/snap.yaml | 9 ++++--- .../snap_categorical_eligibility_ceiling.yaml | 2 +- .../usda/snap/snap_expected_contribution.yaml | 2 -- .../gov/usda/snap/snap_net_income.yaml | 4 ++- ...ap_normal_allotment_basis_of_issuance.yaml | 26 +++++++++++++------ .../meets_tanf_non_cash_net_income_test.py | 14 +++++++--- .../gov/usda/snap/income/snap_net_income.py | 14 ++++++++-- .../usda/snap/snap_expected_contribution.py | 16 ++++++------ 12 files changed, 93 insertions(+), 38 deletions(-) diff --git a/changelog.d/fix-snap-expected-contribution-rounding.fixed.md b/changelog.d/fix-snap-expected-contribution-rounding.fixed.md index eb6f63e6d64..59409116866 100644 --- a/changelog.d/fix-snap-expected-contribution-rounding.fixed.md +++ b/changelog.d/fix-snap-expected-contribution-rounding.fixed.md @@ -1 +1 @@ -- Round SNAP net income to the nearest dollar and round 30 percent of net income up to the next dollar when computing the expected food contribution, per 7 CFR 273.10(e)(1)(ii)(A) and 273.10(e)(2)(ii)(A)(1). +Round SNAP net income to the nearest dollar per 7 CFR 273.10(e)(1)(ii)(A), and round 30 percent of net income up to the next dollar in the expected food contribution per 7 CFR 273.10(e)(2)(ii)(A)(1). diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/tanf/non_cash/meets_tanf_non_cash_net_income_test.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/tanf/non_cash/meets_tanf_non_cash_net_income_test.yaml index 9b1fcafe49f..475b6e53b45 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/tanf/non_cash/meets_tanf_non_cash_net_income_test.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/tanf/non_cash/meets_tanf_non_cash_net_income_test.yaml @@ -3,16 +3,33 @@ output: meets_tanf_non_cash_net_income_test: true -- name: California household with 100% FPG income would barely meet the net income test. +- name: California household with net income exactly at the standard meets the net income test. period: 2022 input: - snap_net_income_fpg_ratio: 1 + snap_fpg: 12_000 + snap_net_income: 12_000 output: + # Monthly standard is ceil(1.0 x 1,000) = 1,000; net income of 1,000 + # per month is exactly at it. meets_tanf_non_cash_net_income_test: true -- name: California household with 101% FPG income would fail the net income test. +- name: California household with net income above the standard fails the net income test. period: 2022 input: - snap_net_income_fpg_ratio: 1.01 + snap_fpg: 12_000 + snap_net_income: 12_120 output: + # Net income of 1,010 per month exceeds the 1,000 standard. meets_tanf_non_cash_net_income_test: false + +- name: California household at the rounded-up whole-dollar standard meets the net income test. + period: 2022 + input: + # Monthly poverty guideline of 2,220.83 yields a whole-dollar standard + # of ceil(2,220.83) = 2,221, so a rounded net income of exactly 2,221 + # still passes; a raw ratio comparison (2,221 / 2,220.83 > 1) would + # wrongly deny this household. + snap_fpg: 26_650 + snap_net_income: 26_652 + output: + meets_tanf_non_cash_net_income_test: true diff --git a/policyengine_us/tests/policy/baseline/gov/usda/snap/income/snap_net_income.yaml b/policyengine_us/tests/policy/baseline/gov/usda/snap/income/snap_net_income.yaml index 8138e9dc1ee..40ef8b26d82 100644 --- a/policyengine_us/tests/policy/baseline/gov/usda/snap/income/snap_net_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/usda/snap/income/snap_net_income.yaml @@ -1,7 +1,9 @@ - name: SNAP net income subtracts deductions period: 2020 input: - snap_gross_income: 2 - snap_deductions: 1 + # Use amounts divisible by 12 so the monthly values are whole dollars + # and unaffected by the 7 CFR 273.10(e)(1)(ii)(A) rounding. + snap_gross_income: 24 + snap_deductions: 12 output: - snap_net_income: 1 + snap_net_income: 12 diff --git a/policyengine_us/tests/policy/baseline/gov/usda/snap/integration.yaml b/policyengine_us/tests/policy/baseline/gov/usda/snap/integration.yaml index 9dc7283e909..a49e001d72e 100644 --- a/policyengine_us/tests/policy/baseline/gov/usda/snap/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/usda/snap/integration.yaml @@ -90,14 +90,19 @@ snap_net_income: 154 # Step 5 — Family’s Expected Contribution Toward Food: # 30 percent of the household’s net income ($154) is about $46. - snap_expected_contribution: 154 * 0.3 + # (Model note, not CBPP: 7 CFR 273.10(e)(2)(ii)(A)(1) rounds the 30% + # up to the next dollar, so the model returns ceil(46.20) = 47.) + snap_expected_contribution: 47 # Step 6 — SNAP Benefit: # The maximum benefit in 2022 for a family of three is $658. # The maximum benefit minus the household contribution ($658 minus $46) # equals about $612. snap_max_allotment: 658 # The family’s monthly SNAP benefit is $612. - snap_normal_allotment: 612 + # (Model note, not CBPP: with the rounded expected contribution of $47, + # the model returns 658 - 47 = 611; the ~$1 divergence from CBPP's + # arithmetic is the expected effect of the regulatory rounding rule.) + snap_normal_allotment: 611 - name: Test for exclude child income, same as case 1 but adding income to one child. period: 2022-01 diff --git a/policyengine_us/tests/policy/baseline/gov/usda/snap/snap.yaml b/policyengine_us/tests/policy/baseline/gov/usda/snap/snap.yaml index 211b7fd8bbb..9ba8d62b8f3 100644 --- a/policyengine_us/tests/policy/baseline/gov/usda/snap/snap.yaml +++ b/policyengine_us/tests/policy/baseline/gov/usda/snap/snap.yaml @@ -41,7 +41,7 @@ - name: North Carolina 2025, yearly integration test period: 2025 - absolute_error_margin: 0.3 + absolute_error_margin: 0.01 input: people: person1: @@ -58,7 +58,8 @@ # due to higher federal poverty guidelines snap_self_employment_income_after_expense_deduction: 0 snap_self_employment_expense_deduction: 0 - snap: 71.75 + # Three months at the FY2026 minimum allotment of $24 (Oct-Dec). + snap: 72 - name: Self-employment income deduction integration test period: 2025 @@ -87,7 +88,9 @@ snap_deductions: 0 snap_earned_income: 5_000 snap_gross_income: 5_000 - snap_net_income: 5_000 + # Monthly net income of 416.67 rounds to 417 per + # 7 CFR 273.10(e)(1)(ii)(A), so the annual value is 12 x 417. + snap_net_income: 5_004 snap: 2_010 - name: Counterfactual test, self-employment income deduction allowed diff --git a/policyengine_us/tests/policy/baseline/gov/usda/snap/snap_categorical_eligibility_ceiling.yaml b/policyengine_us/tests/policy/baseline/gov/usda/snap/snap_categorical_eligibility_ceiling.yaml index 35076cde558..fa7cf3e1a91 100644 --- a/policyengine_us/tests/policy/baseline/gov/usda/snap/snap_categorical_eligibility_ceiling.yaml +++ b/policyengine_us/tests/policy/baseline/gov/usda/snap/snap_categorical_eligibility_ceiling.yaml @@ -106,7 +106,7 @@ meets_snap_gross_income_test: false is_snap_eligible: true # Gross 3,375 + 100 = 3,475; net 3,475 - 675 - 209 = 2,591; - # 30% = 777.30, rounded up to 778 (7 CFR 273.10(e)(2)(ii)(A)); 785 - 778 = 7. + # 30% = 777.30, rounded up to 778 (7 CFR 273.10(e)(2)(ii)(A)(1)); 785 - 778 = 7. snap_net_income: 2_591 snap_expected_contribution: 778 snap_normal_allotment: 7 diff --git a/policyengine_us/tests/policy/baseline/gov/usda/snap/snap_expected_contribution.yaml b/policyengine_us/tests/policy/baseline/gov/usda/snap/snap_expected_contribution.yaml index 16fc2b44471..c3570337961 100644 --- a/policyengine_us/tests/policy/baseline/gov/usda/snap/snap_expected_contribution.yaml +++ b/policyengine_us/tests/policy/baseline/gov/usda/snap/snap_expected_contribution.yaml @@ -1,6 +1,5 @@ - name: Family of 4, no member(s) with disabilities, income of 24k/year (Standard Test) period: 2022 - absolute_error_margin: 0.01 # Floating point issue. input: people: person_1: @@ -31,7 +30,6 @@ - name: Family of 4, member(s) with disabilities, income of 24k/year (Standard Test) period: 2022 - absolute_error_margin: 0.01 # Floating point issue. input: people: person_1: diff --git a/policyengine_us/tests/policy/baseline/gov/usda/snap/snap_net_income.yaml b/policyengine_us/tests/policy/baseline/gov/usda/snap/snap_net_income.yaml index 6ef1596499d..47d1d9418de 100644 --- a/policyengine_us/tests/policy/baseline/gov/usda/snap/snap_net_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/usda/snap/snap_net_income.yaml @@ -53,7 +53,9 @@ household: state_group: CONTIGUOUS_US output: - snap_net_income: 428.5 * 12 + # Monthly net income of 428.50 rounds up to 429 per + # 7 CFR 273.10(e)(1)(ii)(A). + snap_net_income: 429 * 12 - name: SNAP child support deduction integration test period: 2024 diff --git a/policyengine_us/tests/policy/baseline/gov/usda/snap/snap_normal_allotment_basis_of_issuance.yaml b/policyengine_us/tests/policy/baseline/gov/usda/snap/snap_normal_allotment_basis_of_issuance.yaml index fbce1562c00..227f26c5aae 100644 --- a/policyengine_us/tests/policy/baseline/gov/usda/snap/snap_normal_allotment_basis_of_issuance.yaml +++ b/policyengine_us/tests/policy/baseline/gov/usda/snap/snap_normal_allotment_basis_of_issuance.yaml @@ -1,9 +1,13 @@ # Net income boundaries from the FNS Basis of Coupon/EBT Issuance table, # 48 States & DC, October 1, 2025 (3-person column, maximum allotment $785): -# https://dssmanuals.mo.gov/wp-content/uploads/2022/09/snap-basis-issuance-table.pdf#page=1 +# https://web.archive.org/web/20251101194701/https://dssmanuals.mo.gov/wp-content/uploads/2022/09/snap-basis-issuance-table.pdf#page=1 +# (Wayback capture of the Missouri DSS mirror, which is replaced in place +# each October; FNS does not publish the full table on fns.usda.gov.) +# FY2026 maximum allotments: https://www.fns.usda.gov/snap/allotment/cola/fy26 # The household's only income is a reported TANF grant, so net income equals # the grant minus the $209 standard deduction (FY2026, 1-3 persons). -# 7 CFR 273.10(e)(2)(ii)(A): 30% of net income is rounded up to the next dollar. +# 7 CFR 273.10(e)(2)(ii)(A)(1): 30% of net income is rounded up to the next +# dollar. - name: 3-person household, net income $0 (table row 0 - 0) receives $785 period: 2026-01 @@ -180,8 +184,10 @@ state_code: MO output: is_snap_eligible: true - # 7 - 10: 30% = 3.00 exactly + # 7 - 10: 30% = 3.00 exactly. float32(10 * 0.3) is exact, so this case + # does not exercise the cents guard; see the net-$50 case for that. snap_net_income: 10 + snap_expected_contribution: 3 snap_normal_allotment: 782 snap: 782 @@ -300,8 +306,12 @@ state_code: MO output: is_snap_eligible: true - # 47 - 50: 30% = 15.00 exactly + # 47 - 50: 30% = 15.00 exactly. float32(50 * 0.3) = 15.0000009..., so + # this is the only case in the SNAP suite that fails if the + # np.round(..., 2) cents guard in snap_expected_contribution is + # removed (an unguarded ceil would return 16 and pay $769). snap_net_income: 50 + snap_expected_contribution: 15 snap_normal_allotment: 770 snap: 770 @@ -361,7 +371,7 @@ output: is_snap_eligible: true # TANF 212.49/month; net 3.49 -> 3 (1-49 cents round down); 30% = 0.90 -> 1 - snap_net_income: 3.49 + snap_net_income: 3 snap_expected_contribution: 1 snap_normal_allotment: 784 snap: 784 @@ -392,7 +402,7 @@ output: is_snap_eligible: true # TANF 212.50/month; net 3.50 -> 4 (50-99 cents round up); 30% = 1.20 -> 2 - snap_net_income: 3.50 + snap_net_income: 4 snap_expected_contribution: 2 snap_normal_allotment: 783 snap: 783 @@ -428,7 +438,7 @@ is_snap_eligible: true # TANF 215.50/month; net 6.50 -> 7 (50 cents round up); 30% = 2.10 -> 3. # Round-half-to-even would give 6 -> 30% = 1.80 -> 2 -> $783 instead. - snap_net_income: 6.50 + snap_net_income: 7 snap_expected_contribution: 3 snap_normal_allotment: 782 snap: 782 @@ -460,7 +470,7 @@ is_snap_eligible: true # TANF 219.50/month; net 10.50 -> 11 (50 cents round up); 30% = 3.30 -> 4. # Round-half-to-even would give 10 -> 30% = 3.00 exactly -> 3 -> $782. - snap_net_income: 10.50 + snap_net_income: 11 snap_expected_contribution: 4 snap_normal_allotment: 781 snap: 781 diff --git a/policyengine_us/variables/gov/hhs/tanf/non_cash/meets_tanf_non_cash_net_income_test.py b/policyengine_us/variables/gov/hhs/tanf/non_cash/meets_tanf_non_cash_net_income_test.py index c325cf9b6f6..fcd5f1dc75b 100644 --- a/policyengine_us/variables/gov/hhs/tanf/non_cash/meets_tanf_non_cash_net_income_test.py +++ b/policyengine_us/variables/gov/hhs/tanf/non_cash/meets_tanf_non_cash_net_income_test.py @@ -17,8 +17,16 @@ def formula(spm_unit, period, parameters): net_limit_applies = where( hheod, applies.hheod[state], applies.non_hheod[state] ).astype(bool) - # All limits and incomes here expressed as % of FPG. - net_income = spm_unit("snap_net_income_fpg_ratio", period) + net_income = spm_unit("snap_net_income", period) + fpg = spm_unit("snap_fpg", period) net_limit = parameters(period).gov.usda.snap.income.limit.net + # Mirror meets_snap_net_income_test: the monthly standard is the + # poverty guideline times the net limit, rounded up to the next + # whole dollar, compared against the whole-dollar rounded net + # income. A raw ratio comparison would deny households sitting + # exactly at the published standard. + # Pre-round to 4 decimals so float error on an exact whole-dollar + # standard cannot push the ceiling up an extra dollar. + limit = np.ceil(np.round(net_limit * fpg, 4)) # Either the net limit doesn't apply or they pass it. - return ~net_limit_applies | (net_income <= net_limit) + return ~net_limit_applies | (net_income <= limit) diff --git a/policyengine_us/variables/gov/usda/snap/income/snap_net_income.py b/policyengine_us/variables/gov/usda/snap/income/snap_net_income.py index b7f64b745ba..1073a6974f4 100644 --- a/policyengine_us/variables/gov/usda/snap/income/snap_net_income.py +++ b/policyengine_us/variables/gov/usda/snap/income/snap_net_income.py @@ -8,9 +8,19 @@ class snap_net_income(Variable): documentation = "Final net income, after all deductions" label = "SNAP net income" unit = USD - reference = "https://www.law.cornell.edu/uscode/text/7/2014" + reference = ( + "https://www.law.cornell.edu/uscode/text/7/2014", + # 7 CFR 273.10(e)(1)(ii)(A). + "https://www.ecfr.gov/current/title-7/section-273.10#p-273.10(e)(1)(ii)(A)", + ) def formula(spm_unit, period): gross_income = spm_unit("snap_gross_income", period) deductions = spm_unit("snap_deductions", period) - return max_(0, gross_income - deductions) + net_income = max_(0, gross_income - deductions) + # 7 CFR 273.10(e)(1)(ii)(A): round net income to the nearest dollar + # (1-49 cents down, 50-99 cents up), so use half-up rounding rather + # than np.round's round-half-to-even. This is the rounding option + # PolicyEngine applies for all states; states may instead elect + # their TANF rounding procedure under 273.10(e)(1)(ii)(B). + return np.floor(net_income + 0.5) diff --git a/policyengine_us/variables/gov/usda/snap/snap_expected_contribution.py b/policyengine_us/variables/gov/usda/snap/snap_expected_contribution.py index f0236de5e0d..74f7de0bcec 100644 --- a/policyengine_us/variables/gov/usda/snap/snap_expected_contribution.py +++ b/policyengine_us/variables/gov/usda/snap/snap_expected_contribution.py @@ -10,7 +10,7 @@ class snap_expected_contribution(Variable): unit = USD reference = ( "https://www.law.cornell.edu/uscode/text/7/2017#a", - # 7 CFR 273.10(e)(1)(ii)(A) and (e)(2)(ii)(A)(1). + # 7 CFR 273.10(e)(2)(ii)(A)(1). "https://www.law.cornell.edu/cfr/text/7/273.10#e_2_ii_A_1", ) @@ -18,14 +18,14 @@ def formula(spm_unit, period, parameters): expected_food_contribution = parameters( period ).gov.usda.snap.expected_contribution - # 7 CFR 273.10(e)(1)(ii)(A): net income is rounded to the nearest - # dollar (1-49 cents down, 50-99 cents up), so use half-up rounding - # rather than np.round's round-half-to-even. - net_income = np.floor(spm_unit("snap_net_income", period) + 0.5) + # snap_net_income is already rounded to the nearest whole dollar per + # 7 CFR 273.10(e)(1)(ii)(A). + net_income = spm_unit("snap_net_income", period) # 7 CFR 273.10(e)(2)(ii)(A)(1): 30 percent of net income is rounded - # up to the next higher dollar, which is equivalent to rounding the - # allotment down to the nearest lower whole dollar - # (7 CFR 273.10(e)(2)(ii)(A)(2); 7 USC 2017(a)). + # up to the next higher dollar — the rounding option PolicyEngine + # applies for all states. For whole-dollar maximum allotments this + # matches the alternative of rounding the allotment down to the + # nearest lower dollar (273.10(e)(2)(ii)(A)(2); 7 USC 2017(a)). # Round to cents first so float noise (e.g. 50 * 0.3 = 15.0000001) # does not push an exact dollar amount up to the next dollar. contribution = np.round(net_income * expected_food_contribution, 2)