Plan: Overpayment Recovery Pipeline (Issue #382)
On this page
Status
| Step | Description | Status |
|---|---|---|
1 |
Shared crate scaffolding. New |
Done (2026-05-10) |
2 |
canopy-snap migration + API + store. New migration |
Done (2026-05-10) |
3 |
canopy-tanf migration + API + store. Same shape as Step 2 but in canopy-tanf’s DB. Migration filename |
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 |
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 ( |
Done (2026-05-10) |
7 |
Docs. CHANGELOG |
Done (2026-05-10) |
8 |
OpenAPI sync. |
Done (2026-05-10) |
9 |
Citations. PAMMS 9000 series + 7 CFR 273.18 + applicable Medicaid + TANF overpayment regs added to |
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-overpaymentsshared types crate. -
3 program migrations + store + API surfaces (SNAP, TANF, Medicaid).
-
canopy-reportingroll-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.
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?}. ReturnsOverpaymentClaim. -
GET /v1/{program}/overpayments/{id}— read claim. -
POST /v1/{program}/overpayments/{id}/repayment-plans— create plan. Body:{monthly_amount_cents, starts_on}. ReturnsRepaymentPlan. -
POST /v1/{program}/overpayments/{id}/recoupments— record recoupment. Body:{amount_cents, method, repayment_plan_id?, notes?}. ReturnsRecoupmentLedgerEntry. -
GET /v1/{program}/overpayments/{id}/ledger— read full ledger. ReturnsVec<RecoupmentLedgerEntry>+ computedtotal_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 |
|---|---|
|
New crate |
|
Types + canonical schema constants |
|
Canonical schema reference |
|
Pattern documentation |
|
New migration (canonical) |
|
New store module |
|
New API module |
|
New migration (canonical) |
|
New store module |
|
New API module |
|
New migration (canonical) |
|
New store module |
|
New API module |
|
Register routes |
|
New roll-up CSV module |
|
Register |
|
3 integration tests |
|
Regenerated snapshots |
|
New shared-crate doc page |
|
Route counts + table lists |
|
|
Verification
-
cargo nextest run -p canopy-overpayments -p canopy-snap -p canopy-tanf -p canopy-medicaid -p canopy-reporting --lib— unit tests pass. -
cargo xtask api-docs— 4 OpenAPI snapshots regenerate clean. -
cargo xtask dev start && cargo nextest run --workspace --test overpayments_test --run-ignored only— 3 integration tests pass. -
Manual smoke: file an overpayment in SNAP, attach a repayment plan, record 3 recoupments totalling the claim, GET the ledger, confirm
outstanding == 0and status ==closed. -
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