Plan: SNAP Categorical Eligibility and BBCE
On this page
Status
| Step | Description | Status |
|---|---|---|
1 |
snap_program_participations and snap_student_status tables in canopy-snap |
Done (2026-03-28) |
2 |
Categorical eligibility evaluation (Rust, not JDM — see errata) |
Done (2026-03-28) |
3 |
Student exclusion logic |
Done (2026-03-28) |
4 |
Pre-screen integration in SNAP eligibility evaluation flow |
Done (2026-03-28) |
5 |
API endpoints and integration tests |
Done (2026-03-28) |
MR: !15
Epic: &33, &39
Branch: feature/snap-categorical-eligibility
Context
Standard categorical eligibility and Broad-Based Categorical Eligibility (BBCE) are pre-screen pathways that bypass the income and/or asset tests for certain households. Both are required for federal SNAP certification.
Standard categorical eligibility (7 CFR 273.2(j)(2)): Mandatory. All household members receive SSI, TANF cash, or General Assistance → auto-approved, no income or asset test. Benefit is still calculated normally using the income test.
BBCE (7 CFR 273.2(j)(3)): State option.
Georgia exercises BBCE — any household that receives (or is provided) a TANF-funded non-cash benefit or service qualifies.
Georgia’s BBCE: income limit 130% FPL (same as gross income limit, so narrow), asset test eliminated.
These values are jurisdiction-specific and MUST come from jurisdiction.toml:
[snap.bbce] income_limit_pct_fpl = 130, asset_test_eliminated = true.
Other states' BBCE may extend income limits up to 200% FPL — the ruleset must read these values from config, not hardcode them.
The practical effect in Georgia is that the asset test is waived for any household that receives even a SNAP-funded pamphlet — a common Georgia practice.
Student exclusion (7 CFR 273.5): Mandatory. Students enrolled half-time or more at institutions of higher education are individually ineligible. However, mandatory exceptions exist — employment, work-study, dependent child under 6, TANF recipient, SSI, disability, job training referral. A household is not automatically denied because one member is an ineligible student; only that member is excluded.
This plan depends on snap-eligibility plan for the canopy-snap database and evaluation flow.
Scope
In scope:
-
snap_program_participationstable — records SSI, TANF cash, GA receipt per person -
snap_student_statustable — records enrollment status and exception for each person -
rulesets/georgia/snap-categorical-eligibility.json— new JDM ruleset evaluating all CE pathways -
Updates to
rulesets/georgia/snap-eligibility.json— call categorical eligibility pre-screen as first step -
POST /v1/categorical-eligibility/participations— record program participation -
POST /v1/student-status— record student enrollment status -
Integration into the determination evaluation flow
Out of scope:
-
Express Lane Eligibility (ELE) — state option, post-UAT
-
Medicaid categorical eligibility — handled in medicaid-eligibility plan
-
TANF-funded service enrollment tracking — BBCE trigger is assumed from SNAP application submission date
Design
snap_program_participations table
In canopy-snap isolated database (postgres-snap:5433) per ADR-004. IEVS data does not flow through this table — this is self-attested or externally verified categorical receipt data.
CREATE TABLE snap_program_participations (
id UUID PRIMARY KEY,
person_id UUID NOT NULL,
household_id UUID NOT NULL,
program TEXT NOT NULL,
-- 'ssi', 'tanf_cash', 'tanf_bbce', 'general_assistance'
case_number TEXT,
effective_date DATE NOT NULL,
expiration_date DATE,
verification_status TEXT NOT NULL DEFAULT 'self_attested',
-- 'self_attested', 'verified_ievs', 'verified_document', 'verified_agency'
verified_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
active BOOLEAN NOT NULL DEFAULT true
);
CREATE TABLE snap_student_status (
id UUID PRIMARY KEY,
person_id UUID NOT NULL,
household_id UUID NOT NULL,
enrollment_half_time_plus BOOLEAN NOT NULL DEFAULT false,
institution_name TEXT,
enrollment_verified BOOLEAN NOT NULL DEFAULT false,
exception_type TEXT,
-- 'employed_20hr', 'work_study', 'dependent_child_under_6',
-- 'tanf_recipient', 'ssi_recipient', 'disability', 'job_training'
exception_verified BOOLEAN NOT NULL DEFAULT false,
assessed_at TIMESTAMPTZ,
active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
Categorical eligibility ruleset
New file: rulesets/georgia/snap-categorical-eligibility.json
The ruleset takes as input the household’s program participations and returns a categorical eligibility classification:
Input node:
{
"household_members": [
{
"person_id": "uuid",
"participations": ["ssi", "tanf_cash"],
"student_status": {
"enrolled_half_time_plus": false,
"exception_type": null
}
}
]
}
Output:
{
"categorical_eligibility_type": "standard | bbce | none",
"all_members_ineligible_students": false,
"ineligible_student_person_ids": []
}
Decision table logic:
- If all household members have SSI, TANF cash, or GA participation → standard
- Else if any member has tanf_bbce participation OR SNAP application received (Georgia BBCE trigger) → bbce
- Else → none
For student exclusion, the ruleset also outputs which person_ids are ineligible students (enrolled half-time+ with no exception):
- These members are excluded from household composition for eligibility but their income still counts
- all_members_ineligible_students = true → household denied
Integration with snap-eligibility.json
The snap-eligibility ruleset must be updated to call categorical eligibility as the first decision node:
{
"nodes": [
{
"id": "categorical_screen",
"type": "decisionNode",
"name": "Categorical Eligibility Pre-Screen",
"ruleset": "snap-categorical-eligibility"
},
{
"id": "income_test",
"type": "decisionNode",
"name": "Gross Income Test",
"condition": "categorical_screen.categorical_eligibility_type == 'none'"
},
{
"id": "asset_test",
"type": "decisionNode",
"name": "Asset Test",
"condition": "categorical_screen.categorical_eligibility_type == 'none'"
// BBCE bypasses asset test: add condition categorical_screen.categorical_eligibility_type != 'bbce'
}
]
}
BLOCKER: The exact JDM node structure depends on zen-engine’s multi-ruleset composition API.
The rules-engine plan must complete first and validate that zen-engine can compose multiple rulesets (categorical-eligibility + income-test + deductions) in a single evaluation pass.
If zen-engine cannot do this natively, the Rust code in canopy-snap must orchestrate ruleset evaluation sequentially.
Consult zen-engine documentation (https://docs.gorules.io/) and verify the API against the actual crate version in Cargo.toml before implementing.
BBCE gross income limit
For BBCE households, the gross income limit is 130% FPL (same as the standard limit for Georgia). The asset test is eliminated. Net income test still applies for benefit calculation. This is already handled by the standard income test — no special BBCE income test needed for Georgia’s narrow BBCE.
Steps
Step 1: Database migrations
Files:
- services/canopy-snap/migrations/YYYYMMDD_create_snap_program_participations.sql
- services/canopy-snap/migrations/YYYYMMDD_create_snap_student_status.sql
Step 2: Categorical eligibility ruleset
Files: rulesets/georgia/snap-categorical-eligibility.json
Implement the full decision logic as described. Replace the existing pass-through stub with real logic. Test with known inputs (all SSI household, BBCE trigger household, no CE household).
Step 3: Student exclusion
Files: rulesets/georgia/snap-eligibility.json
Add student exclusion check to the pre-screen section. Excluded students: removed from household count for eligibility, income still counted. If all adult members are ineligible students: deny.
Step 4: Store and API
Files: services/canopy-snap/src/categorical.rs (new), services/canopy-snap/src/api/mod.rs
Store layer for participations and student status.
Endpoints:
- POST /v1/categorical-eligibility/participations → 201
- GET /v1/categorical-eligibility/participations?household_id={id}
- POST /v1/student-status → 201
- GET /v1/student-status?household_id={id}
Step 5: Evaluation integration
Files: services/canopy-snap/src/evaluation.rs
Update the evaluation flow to:
1. Load program participations from snap_program_participations for all household members
2. Load student status from snap_student_status for all members
3. Pass both to the ruleset as input alongside household income/assets
4. Record categorical_eligibility_basis on the determination if CE applies
Step 6: Integration tests
Scenarios to cover: - Household where all members receive SSI → approved via standard CE, no income/asset test - Household receiving TANF cash → approved via standard CE - Household with BBCE trigger → asset test skipped; income test at 130% FPL still applies - Household with one ineligible student, other members eligible → student excluded, household eligible - Household where all members are ineligible students → denied
Files Touched
| File | Change |
|---|---|
|
New migration |
|
New migration |
|
New ruleset replacing pass-through stub |
|
Add categorical pre-screen and student exclusion nodes |
|
New: store + participation/student status domain types |
|
Add participations and student status endpoints |
|
Integrate categorical pre-screen into evaluation flow |
Verification
-
cargo nextest run -p canopy-snap— all integration tests pass -
UAT scenario: SSI household → approved, benefit calculated,
categorical_eligibility_basis: "ssi_recipient"on determination -
UAT scenario: BBCE household, assets at $5,000 → approved (asset test skipped), income test applied
-
UAT scenario: full-time student, no exception, no other household members → denied
Documentation Updates
-
.claude/docs/services.md— add snap_program_participations, snap_student_status tables; add new endpoints -
CHANGELOG.adoc— entry under== Unreleased
Errata
Rust evaluation instead of JDM ruleset (2026-03-27)
The plan specified a JDM ruleset (snap-categorical-eligibility.json) evaluated by
zen-engine. The implementation uses pure Rust in categorical.rs instead.
Why: The plan itself flagged this as a blocker: "The exact JDM node structure depends on zen-engine’s multi-ruleset composition API." The Rust implementation is the reference that a future JDM ruleset must match. This approach is consistent with how CRAIG handles categorical eligibility (Rust logic, not rules engine).
How to apply: When zen-engine multi-ruleset composition is validated, a JDM ruleset can be added for jurisdiction customization. The Rust implementation remains the authoritative reference for correctness.