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_participations table — records SSI, TANF cash, GA receipt per person

  • snap_student_status table — 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

services/canopy-snap/migrations/YYYYMMDD_create_snap_program_participations.sql

New migration

services/canopy-snap/migrations/YYYYMMDD_create_snap_student_status.sql

New migration

rulesets/georgia/snap-categorical-eligibility.json

New ruleset replacing pass-through stub

rulesets/georgia/snap-eligibility.json

Add categorical pre-screen and student exclusion nodes

services/canopy-snap/src/categorical.rs

New: store + participation/student status domain types

services/canopy-snap/src/api/mod.rs

Add participations and student status endpoints

services/canopy-snap/src/evaluation.rs

Integrate categorical pre-screen into evaluation flow

Verification

  1. cargo nextest run -p canopy-snap — all integration tests pass

  2. UAT scenario: SSI household → approved, benefit calculated, categorical_eligibility_basis: "ssi_recipient" on determination

  3. UAT scenario: BBCE household, assets at $5,000 → approved (asset test skipped), income test applied

  4. 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.

Edit this page · default