> ## Documentation Index
> Fetch the complete documentation index at: https://docs-terra.withunify.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Eligibility Engine

> How Pathfinder matches users to benefit programs based on household data

# Eligibility Engine

> The eligibility engine evaluates household data against program rules to determine potential eligibility.

***

## Overview

The engine takes two inputs and produces match results:

```mermaid theme={null}
flowchart LR
    subgraph Input
        Household[Household Data]
        Programs[Program Rules]
    end

    subgraph Engine
        Geo[Geographic Filter]
        Rules[Rule Evaluator]
        Score[Score Calculator]
    end

    subgraph Output
        Likely[Likely Eligible]
        Maybe[Maybe Eligible]
        Not[Not Eligible]
    end

    Household --> Geo
    Programs --> Geo
    Geo --> Rules
    Rules --> Score
    Score --> Likely
    Score --> Maybe
    Score --> Not
```

***

## Household Data Model

The screener collects data incrementally. Each field has a defined type and affects matching.

```typescript theme={null}
interface HouseholdData {
  // Location (Step 1)
  zipCode?: string;
  state?: string;
  countyFips?: string;
  city?: string;

  // Household composition (Step 2)
  householdSize?: number;
  adults?: number;
  children?: number;
  childrenAges?: number[];

  // Income (Step 3)
  grossMonthlyIncome?: number;
  netMonthlyIncome?: number;
  incomeType?: IncomeType[];       // wages, self_employment, benefits, etc.
  hasIncome?: boolean;

  // Demographics (Step 4)
  citizenshipStatus?: CitizenshipStatus;
  hasElderly?: boolean;            // Anyone 60+
  hasDisabled?: boolean;
  isPregnant?: boolean;
  isVeteran?: boolean;
  isStudent?: boolean;

  // Assets (Step 5, optional)
  countableResources?: number;     // Bank accounts, etc.
  hasVehicle?: boolean;
  ownsHome?: boolean;

  // Current benefits (Step 6, optional)
  currentBenefits?: string[];      // Programs already receiving
}

type CitizenshipStatus =
  | "citizen"
  | "permanent_resident"
  | "qualified_alien"
  | "refugee"
  | "asylee"
  | "undocumented"
  | "unknown";

type IncomeType =
  | "wages"
  | "self_employment"
  | "social_security"
  | "ssi"
  | "ssdi"
  | "unemployment"
  | "child_support"
  | "alimony"
  | "pension"
  | "investment"
  | "rental"
  | "other";
```

***

## Federal Poverty Level (FPL)

Many programs use FPL-based income limits. The engine calculates FPL dynamically.

```typescript theme={null}
// 2024 Federal Poverty Guidelines (48 contiguous states)
const FPL_2024 = {
  baseAmount: 15060,      // For 1 person
  perPersonIncrement: 5380,
};

function calculateFPL(householdSize: number): number {
  return FPL_2024.baseAmount + (householdSize - 1) * FPL_2024.perPersonIncrement;
}

function calculateFPLPercent(
  monthlyIncome: number,
  householdSize: number
): number {
  const annualIncome = monthlyIncome * 12;
  const fpl = calculateFPL(householdSize);
  return Math.round((annualIncome / fpl) * 100);
}

// Example: Family of 4 earning $3,000/month
// Annual: $36,000
// FPL for 4: $15,060 + (3 * $5,380) = $31,200
// Percent: 36000 / 31200 * 100 = 115% FPL
```

### FPL Thresholds by Program

| Program              | Income Limit          | Notes                                |
| -------------------- | --------------------- | ------------------------------------ |
| SNAP                 | 130% gross / 100% net | Categorical eligibility may override |
| Medicaid (adults)    | 138%                  | ACA expansion states                 |
| Medicaid (children)  | 200-300%+             | Varies by state                      |
| CHIP                 | 200-400%              | Above Medicaid, varies by state      |
| LIHEAP               | 150%                  | Federal minimum, states may differ   |
| Head Start           | 100%                  | Some slots for 100-130%              |
| WIC                  | 185%                  | Or on Medicaid/SNAP                  |
| Free School Meals    | 130%                  |                                      |
| Reduced School Meals | 185%                  |                                      |
| Lifeline             | 135%                  | Or on qualifying program             |

***

## Rule Evaluation

The engine evaluates each rule and produces a tri-state result.

```typescript theme={null}
type EligibilityResult = "eligible" | "maybe" | "ineligible";

interface RuleEvaluationResult {
  ruleId: string;
  result: EligibilityResult;
  reason?: string;
}

function evaluateRule(
  rule: EligibilityRule,
  household: HouseholdData
): RuleEvaluationResult {
  const fieldValue = getFieldValue(household, rule.field);

  // If data is missing, result is "maybe"
  if (fieldValue === undefined || fieldValue === null) {
    return {
      ruleId: rule.id,
      result: "maybe",
      reason: `Missing data for ${rule.field}`
    };
  }

  const passes = evaluateCondition(fieldValue, rule.operator, rule.value, household);

  return {
    ruleId: rule.id,
    result: passes ? "eligible" : "ineligible",
    reason: passes ? undefined : `${rule.field} does not meet requirement`
  };
}
```

### Operator Implementations

```typescript theme={null}
function evaluateCondition(
  fieldValue: any,
  operator: EligibilityOperator,
  ruleValue: RuleValue,
  household: HouseholdData
): boolean {
  switch (operator) {
    case "equals":
      return fieldValue === ruleValue;

    case "not_equals":
      return fieldValue !== ruleValue;

    case "greater_than":
      return fieldValue > ruleValue;

    case "less_than":
      return fieldValue < ruleValue;

    case "greater_than_or_equal":
      return fieldValue >= ruleValue;

    case "less_than_or_equal":
      return fieldValue <= ruleValue;

    case "in":
      return (ruleValue as string[]).includes(fieldValue);

    case "not_in":
      return !(ruleValue as string[]).includes(fieldValue);

    case "contains":
      return Array.isArray(fieldValue) && fieldValue.includes(ruleValue);

    case "less_than_fpl_percent": {
      const fplPercent = calculateFPLPercent(
        household.grossMonthlyIncome || 0,
        household.householdSize || 1
      );
      return fplPercent <= (ruleValue as number);
    }

    case "age_at_least":
      return Math.max(...(household.childrenAges || [0])) >= (ruleValue as number);

    case "age_under":
      return (household.childrenAges || []).some(age => age < (ruleValue as number));

    case "has_dependent_under":
      return (household.childrenAges || []).some(age => age < (ruleValue as number));

    case "is_citizen_or_qualified":
      return ["citizen", "permanent_resident", "qualified_alien", "refugee", "asylee"]
        .includes(household.citizenshipStatus || "");

    default:
      return false;
  }
}
```

***

## Matching Algorithm

The full matching process combines geography, rules, and scoring.

```typescript theme={null}
interface MatchResult {
  programId: string;
  program: Program;
  eligibility: "likely" | "possible" | "unlikely";
  score: number;              // 0-100
  matchedRules: string[];     // Rules that passed
  failedRules: string[];      // Rules that failed
  unknownRules: string[];     // Rules with missing data
  reasons: string[];          // Human-readable explanations
}

async function matchPrograms(
  household: HouseholdData
): Promise<MatchResult[]> {
  // Step 1: Get geographically available programs
  const programs = await getProgramsByLocation(
    household.state,
    household.countyFips
  );

  // Step 2: Evaluate each program
  const results: MatchResult[] = [];

  for (const program of programs) {
    const result = evaluateProgram(program, household);
    results.push(result);
  }

  // Step 3: Sort by eligibility and score
  return results.sort((a, b) => {
    // Likely > Possible > Unlikely
    const eligibilityOrder = { likely: 0, possible: 1, unlikely: 2 };
    if (eligibilityOrder[a.eligibility] !== eligibilityOrder[b.eligibility]) {
      return eligibilityOrder[a.eligibility] - eligibilityOrder[b.eligibility];
    }
    // Then by score descending
    return b.score - a.score;
  });
}
```

### Program Evaluation

```typescript theme={null}
function evaluateProgram(
  program: Program,
  household: HouseholdData
): MatchResult {
  const rules = program.eligibilityRules || [];
  const ruleResults = rules.map(rule => evaluateRule(rule, household));

  const passed = ruleResults.filter(r => r.result === "eligible");
  const failed = ruleResults.filter(r => r.result === "ineligible");
  const unknown = ruleResults.filter(r => r.result === "maybe");

  // Calculate eligibility
  let eligibility: "likely" | "possible" | "unlikely";

  if (failed.length > 0) {
    eligibility = "unlikely";
  } else if (unknown.length > 0) {
    eligibility = "possible";
  } else {
    eligibility = "likely";
  }

  // Calculate score
  const totalRules = rules.length || 1;
  const score = Math.round((passed.length / totalRules) * 100);

  return {
    programId: program.id,
    program,
    eligibility,
    score,
    matchedRules: passed.map(r => r.ruleId),
    failedRules: failed.map(r => r.ruleId),
    unknownRules: unknown.map(r => r.ruleId),
    reasons: failed.map(r => r.reason).filter(Boolean) as string[]
  };
}
```

***

## Categorical Eligibility

Some programs grant automatic eligibility based on participation in other programs.

```typescript theme={null}
const CATEGORICAL_ELIGIBILITY: Record<string, string[]> = {
  // If you receive SNAP, you qualify for:
  "snap": ["free_school_meals", "lifeline", "wic"],

  // If you receive TANF, you qualify for:
  "tanf": ["snap_categorical", "free_school_meals", "medicaid"],

  // If you receive SSI, you qualify for:
  "ssi": ["snap_categorical", "medicaid", "lifeline"],

  // If you receive Medicaid, you qualify for:
  "medicaid": ["wic", "lifeline"],
};

function applyCategoricalEligibility(
  household: HouseholdData,
  results: MatchResult[]
): MatchResult[] {
  const currentBenefits = household.currentBenefits || [];

  for (const benefit of currentBenefits) {
    const qualifiesFor = CATEGORICAL_ELIGIBILITY[benefit.toLowerCase()] || [];

    for (const result of results) {
      if (qualifiesFor.includes(result.program.slug)) {
        result.eligibility = "likely";
        result.score = 100;
        result.reasons.push(`Categorical eligibility from ${benefit}`);
      }
    }
  }

  return results;
}
```

***

## Progressive Refinement

As users provide more data, matches become more accurate.

```mermaid theme={null}
flowchart TD
    Start[Start Screener] --> |"2,500 programs"| All[All Programs]

    All --> |Enter ZIP| Geo["847 programs<br/>(geographic filter)"]

    Geo --> |Enter household size| Size["600 programs<br/>(family-based)"]

    Size --> |Enter income| Income["45 programs<br/>(income-based)"]

    Income --> |Demographics| Final["23 likely<br/>22 possible"]
```

### Confidence Scoring

```typescript theme={null}
interface MatchConfidence {
  overall: number;          // 0-100
  geographic: number;       // Location data completeness
  household: number;        // Household data completeness
  income: number;           // Income data completeness
  demographic: number;      // Demographics completeness
}

function calculateConfidence(household: HouseholdData): MatchConfidence {
  const geographic =
    (household.state ? 25 : 0) +
    (household.countyFips ? 50 : 0) +
    (household.city ? 25 : 0);

  const householdData =
    (household.householdSize ? 50 : 0) +
    (household.children !== undefined ? 25 : 0) +
    (household.childrenAges?.length ? 25 : 0);

  const income =
    (household.hasIncome !== undefined ? 25 : 0) +
    (household.grossMonthlyIncome ? 50 : 0) +
    (household.incomeType?.length ? 25 : 0);

  const demographic =
    (household.citizenshipStatus ? 40 : 0) +
    (household.hasElderly !== undefined ? 20 : 0) +
    (household.hasDisabled !== undefined ? 20 : 0) +
    (household.isVeteran !== undefined ? 20 : 0);

  return {
    overall: Math.round((geographic + householdData + income + demographic) / 4),
    geographic,
    household: householdData,
    income,
    demographic
  };
}
```

***

## Performance Considerations

### Caching Strategy

```typescript theme={null}
// Cache FPL calculations (rarely change)
const fplCache = new Map<number, number>();

// Cache geographic lookups (expensive)
const geoCache = new Map<string, GeocodedLocation>();

// Don't cache program matches (user-specific)
```

### Query Optimization

```sql theme={null}
-- Use covering index for common query pattern
CREATE INDEX idx_programs_matching ON programs(
  is_active,
  coverage_type,
  category
) INCLUDE (
  name,
  short_description,
  benefit_value_cents
);

-- Partial index for active programs only
CREATE INDEX idx_programs_active_search ON programs
USING GIN(search_vector)
WHERE is_active = true;
```

***

## Edge Cases

### Handling Missing Data

```typescript theme={null}
// If household size is missing, use 1 (conservative)
const effectiveHouseholdSize = household.householdSize || 1;

// If income is missing, don't filter by income (permissive)
if (!household.grossMonthlyIncome) {
  result.eligibility = "possible";
  result.reasons.push("Provide income for more accurate matching");
}
```

### State-Specific Rules

Some rules vary by state (e.g., Medicaid expansion, SNAP policies).

```typescript theme={null}
interface StateOverride {
  state: string;
  programSlug: string;
  overrideRules: EligibilityRule[];
}

// Example: California has higher SNAP income limits
const CA_SNAP_OVERRIDE: StateOverride = {
  state: "CA",
  programSlug: "snap",
  overrideRules: [
    {
      id: "income",
      field: "household_income_monthly",
      operator: "less_than_fpl_percent",
      value: 200  // California uses 200% FPL
    }
  ]
};
```

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Screener Implementation" icon="clipboard-check" href="/pathfinder/features/screener">
    How the multi-step screener collects data
  </Card>

  <Card title="Programs Database" icon="database" href="/pathfinder/core-systems/programs-database">
    Schema and query patterns
  </Card>
</CardGroup>
