Plan: Overpayment Recovery Pipeline (Issue #382)

On this page

Status

Step Description Status

1

Shared crate scaffolding. New crates/canopy-overpayments/ with SPDX header, types OverpaymentClaim, RepaymentPlan, RecoupmentLedgerEntry (Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema). Three error variants (AlreadyClosed, InvalidAmount, LedgerInconsistent). Mirrors the canopy-signing / canopy-policy precedent (types shared, data isolated per program).

Done (2026-05-10)

2

canopy-snap migration + API + store. New migration services/canopy-snap/migrations/20260506000040_create_overpayments.sql adding overpayment_claims, repayment_plans, recoupment_ledger (canonical schema in the shared crate’s README). New services/canopy-snap/src/store/overpayments.rs and services/canopy-snap/src/api/overpayments.rs exposing 5 endpoints: POST /v1/snap/overpayments, GET /v1/snap/overpayments/{id}, POST /v1/snap/overpayments/{id}/repayment-plans, POST /v1/snap/overpayments/{id}/recoupments, GET /v1/snap/overpayments/{id}/ledger. Register routes.

Done (2026-05-10)

3

canopy-tanf migration + API + store. Same shape as Step 2 but in canopy-tanf’s DB. Migration filename 20260506000040_create_overpayments.sql mirrors canonical schema byte-for-byte.

Done (2026-05-10)

4

canopy-medicaid migration + API + store. Same shape as Steps 2-3 but in canopy-medicaid’s DB.

Done (2026-05-10)

5

canopy-reporting roll-up CSV. New services/canopy-reporting/src/reporting/overpayments.rs exposing GET /v1/reporting/overpayments?program=snap|tanf|medicaid&fy=… returning a per-program CSV with columns: claim_id, original_amount, recouped_amount, outstanding_amount, status, opened_at, closed_at.

Done (2026-05-10)

6

Tests. 15 unit tests (3 programs × 5 endpoints) plus 5 store tests in the shared crate covering type roundtrip + invalid-amount rejection. 1 integration test per program (services/canopy-{snap,tanf,medicaid}/tests/overpayments_test.rs) exercising the full claim → plan → recoupment → ledger lifecycle through devstack.

Done (2026-05-10)

7

Docs. CHANGELOG === Added. Update .claude/docs/services.md per-service route counts + table lists. New docs/modules/ROOT/pages/services/canopy-overpayments.adoc describing the shared crate. Plan archives.

Done (2026-05-10)

8

OpenAPI sync. cargo xtask api-docs regenerates 4 service snapshots.

Done (2026-05-10)

9

Citations. PAMMS 9000 series + 7 CFR 273.18 + applicable Medicaid + TANF overpayment regs added to citations.toml if any new policy values land. The thresholds for "small overpayment" (e.g., < $35 SNAP) become jurisdiction.toml entries with full citations per ADR-011.

Done (2026-05-10)

Issue: #382
Branch: feat/overpayment-recovery-pipeline
Labels: type::feature, priority::medium, service::shared-crates, service::snap, service::tanf, service::medicaid, program::cross-program, workflow::ready

Context

PAMMS 9000 series (Georgia DFCS) and 7 CFR 273.18 require benefit-program states to track overpayment claims, repayment plans, and recoupments. ACF-196 has a column for it; CMS-64 has a line for it. Today no canopy service tracks any of this. A worker who identifies an overpayment has nowhere to record it; the federal reports paper over the gap.

The architecturally-locked direction (2026-05-05) is a shared crate (crates/canopy-overpayments) exposing types + the canonical schema, with each program service running its own copy of three tables in its own DB. Pattern matches canopy-signing and canopy-policy: types are shared, data is isolated per ADR-001.

Code references

  • crates/canopy-signing/ — precedent for shared-types-no-shared-DB.

  • crates/canopy-policy/ — same precedent.

  • services/canopy-reporting/src/reporting/tanf.rs — CSV-export pattern to mirror.

  • services/canopy-snap/migrations/ / canopy-tanf/migrations/ / canopy-medicaid/migrations/ — directories to extend.

  • PAMMS 9000-9999 (Georgia DFCS overpayment manual).

  • 7 CFR 273.18 — federal SNAP overpayment regulation.

Scope

In scope:

  • crates/canopy-overpayments shared types crate.

  • 3 program migrations + store + API surfaces (SNAP, TANF, Medicaid).

  • canopy-reporting roll-up CSV per program.

  • Unit + integration tests for the lifecycle.

Out of scope:

  • Treasury Offset Program (TOP) integration — automated tax-intercept; separate plan.

  • Wage-garnishment paths.

  • Offset-against-future-benefits automation — only manual recoupment lands here.

  • CAPS / WIC overpayments — those programs have different recovery semantics under different regs; out of scope here, separate plans if/when scoped.

Dependencies

  • No prerequisite plans.

Design

Canonical schema (in crates/canopy-overpayments/migrations/canonical.sql, byte-identical when stamped per-service):

CREATE TABLE overpayment_claims (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    person_id UUID NOT NULL,
    household_id UUID NOT NULL,
    determination_id UUID,
    claim_amount_cents BIGINT NOT NULL,
    claim_basis TEXT NOT NULL,
    error_type TEXT NOT NULL,
    discovered_at DATE NOT NULL,
    discovered_by UUID,
    status TEXT NOT NULL DEFAULT 'open',
    closed_at TIMESTAMPTZ,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE repayment_plans (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    overpayment_claim_id UUID NOT NULL REFERENCES overpayment_claims(id),
    monthly_amount_cents BIGINT NOT NULL,
    starts_on DATE NOT NULL,
    ends_on DATE,
    status TEXT NOT NULL DEFAULT 'active',
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE recoupment_ledger (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    overpayment_claim_id UUID NOT NULL REFERENCES overpayment_claims(id),
    repayment_plan_id UUID REFERENCES repayment_plans(id),
    amount_cents BIGINT NOT NULL,
    method TEXT NOT NULL,
    occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    notes TEXT
);

CREATE INDEX overpayment_claims_status ON overpayment_claims (status);
CREATE INDEX repayment_plans_by_claim ON repayment_plans (overpayment_claim_id);
CREATE INDEX recoupment_ledger_by_claim ON recoupment_ledger (overpayment_claim_id);

Endpoints (per program):

  • POST /v1/{program}/overpayments — file claim. Body: {person_id, household_id, claim_amount_cents, claim_basis, error_type, determination_id?}. Returns OverpaymentClaim.

  • GET /v1/{program}/overpayments/{id} — read claim.

  • POST /v1/{program}/overpayments/{id}/repayment-plans — create plan. Body: {monthly_amount_cents, starts_on}. Returns RepaymentPlan.

  • POST /v1/{program}/overpayments/{id}/recoupments — record recoupment. Body: {amount_cents, method, repayment_plan_id?, notes?}. Returns RecoupmentLedgerEntry.

  • GET /v1/{program}/overpayments/{id}/ledger — read full ledger. Returns Vec<RecoupmentLedgerEntry> + computed total_recouped + outstanding.

Outstanding-balance calc: claim_amount_cents - sum(recoupment_ledger.amount_cents WHERE overpayment_claim_id = …). Computed at read time, not stored, so the ledger is the system of record.

When outstanding reaches 0 the claim’s status auto-flips to closed and closed_at = now() (in the same TX as the recoupment row insert).

Files Touched

File Change

crates/canopy-overpayments/Cargo.toml

New crate

crates/canopy-overpayments/src/lib.rs

Types + canonical schema constants

crates/canopy-overpayments/migrations/canonical.sql

Canonical schema reference

crates/canopy-overpayments/README.md

Pattern documentation

services/canopy-snap/migrations/20260506000040_create_overpayments.sql

New migration (canonical)

services/canopy-snap/src/store/overpayments.rs

New store module

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

New API module

services/canopy-tanf/migrations/20260506000040_create_overpayments.sql

New migration (canonical)

services/canopy-tanf/src/store/overpayments.rs

New store module

services/canopy-tanf/src/api/overpayments.rs

New API module

services/canopy-medicaid/migrations/20260506000040_create_overpayments.sql

New migration (canonical)

services/canopy-medicaid/src/store/overpayments.rs

New store module

services/canopy-medicaid/src/api/overpayments.rs

New API module

services/canopy-{snap,tanf,medicaid}/src/api/mod.rs

Register routes

services/canopy-reporting/src/reporting/overpayments.rs

New roll-up CSV module

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

Register /v1/reporting/overpayments endpoint

services/canopy-{snap,tanf,medicaid}/tests/overpayments_test.rs

3 integration tests

docs/modules/ROOT/openapi/canopy-{snap,tanf,medicaid,reporting}.json

Regenerated snapshots

docs/modules/ROOT/pages/services/canopy-overpayments.adoc

New shared-crate doc page

.claude/docs/services.md

Route counts + table lists

CHANGELOG.adoc

=== Added

Verification

  1. cargo nextest run -p canopy-overpayments -p canopy-snap -p canopy-tanf -p canopy-medicaid -p canopy-reporting --lib — unit tests pass.

  2. cargo xtask api-docs — 4 OpenAPI snapshots regenerate clean.

  3. cargo xtask dev start && cargo nextest run --workspace --test overpayments_test --run-ignored only — 3 integration tests pass.

  4. Manual smoke: file an overpayment in SNAP, attach a repayment plan, record 3 recoupments totalling the claim, GET the ledger, confirm outstanding == 0 and status == closed.

  5. cargo xtask validate — full battery green.

Documentation Updates

  • CHANGELOG.adoc=== Added

  • .claude/docs/services.md — per-service route + table updates

  • docs/modules/ROOT/pages/services/canopy-overpayments.adoc — new

  • docs/modules/ROOT/pages/services/canopy-{snap,tanf,medicaid,reporting}.adoc — extend per-program coverage

  • Plan archive: move to plans/archive/ post-merge

Edit this page · default