Plan: ACF-196 Expenditures Pipeline (Issue #378)
On this page
Status
| Step | Description | Status |
|---|---|---|
1 |
canopy-tanf schema. New migration |
Not started |
2 |
canopy-tanf store + API. New |
Not started |
3 |
canopy-reporting |
Not started |
4 |
canopy-reporting reader + generator. Extend |
Not started |
5 |
canopy-reporting endpoint. New |
Not started |
6 |
Tests + docs. 5 unit tests in |
Not started |
Issue: #378
Branch: feat/acf-196-expenditures-pipeline
Labels: type::feature, priority::medium, service::reporting, service::tanf, program::tanf, federal-partner::acf, workflow::ready
Context
services/canopy-reporting/src/reporting/tanf.rs ships ACF-199 enriched (work hours, sanctions, time limits) and a stub for ACF-196 (the financial expenditures report). ACF requires quarterly ACF-196 submission covering 19 expenditure categories — basic assistance, work activities, refundable EITC, etc. Today the ACF-196 path is empty.
The canonical TANF expenditure data does not yet exist anywhere in canopy: there is no tanf_expenditures table (verified during meta-plan authoring). This plan adds the table, ingest path, and the ACF-196 generator on top.
The ingest source is out of band: financial systems push expenditures to canopy-tanf via the POST /v1/tanf/expenditures endpoint as they’re recorded. The endpoint is the integration point; building the upstream financial-system bridge is out of scope.
Scope
In scope:
-
tanf_expenditurestable + CRUD endpoints in canopy-tanf. -
acf196_snapshottable in canopy-reporting. -
CSV generator + endpoint.
-
Unit + integration tests.
Out of scope:
-
Upstream financial-system bridge (where the expenditure rows come from).
-
ACF-196 amendment / corrections workflow.
-
TANF MOE (Maintenance of Effort) reporting — separate ACF data set, separate plan.
-
Real-time expenditure tracking — ingest is push-based and quarterly.
-
CMS-64 cross-program shared infrastructure — see
cms-64-expenditure-aggregation.adoc(#379).
Dependencies
-
Predecessor
tanf-federal-reportingplan (already archived). -
canopy-mq-persistent-outbox.adoc(#388) does not block; expenditure ingest does not currently publish events.
Design
tanf_expenditures schema:
CREATE TABLE tanf_expenditures (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
category TEXT NOT NULL,
amount_cents BIGINT NOT NULL,
fiscal_quarter TEXT NOT NULL,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT now(),
recorded_by UUID,
supporting_doc_url TEXT
);
CREATE INDEX tanf_expenditures_by_quarter
ON tanf_expenditures (fiscal_quarter, category);
The 19 ACF-196 categories live as an enum in crates/canopy-reference/src/program/tanf.rs (or a new acf196.rs). Validation at POST time rejects any category outside the enum.
CSV generator:
pub async fn generate_acf196_csv(
client: &TanfClient,
fy_q: &str,
) -> Result<String> {
let expenditures = client.list_expenditures(fy_q).await?;
let mut totals = HashMap::<Acf196Category, i64>::new();
for e in expenditures {
*totals.entry(e.category).or_insert(0) += e.amount_cents;
}
let mut wtr = csv::Writer::from_writer(vec![]);
wtr.write_record(&Acf196Category::headers())?;
wtr.write_record(&Acf196Category::values(&totals))?;
Ok(String::from_utf8(wtr.into_inner()?)?)
}
Files Touched
| File | Change |
|---|---|
|
New migration |
|
New store module |
|
New API module |
|
Register routes |
|
New migration |
|
Extend at line 413 with |
|
Register |
|
Add |
|
New devstack integration test |
canopy-reporting + canopy-tanf entry updates |
|
|
|
|
Regenerated snapshot |
|
Regenerated snapshot |
Verification
-
cargo nextest run -p canopy-tanf -p canopy-reporting --lib— unit tests pass. -
cargo xtask api-docs— OpenAPI snapshots regenerate clean. -
cargo xtask dev start && cargo nextest run -p canopy-reporting --test acf196_test --run-ignored only— integration test passes. -
Manual smoke: POST 5 expenditures across categories, GET
/v1/reporting/acf-196?fy=2026q3, confirm CSV with totals matches the recorded sums. -
cargo xtask validate— full battery green.
Documentation Updates
-
Service Catalog — canopy-tanf + canopy-reporting entries (status / tables)
-
CHANGELOG.adoc— entry under== Unreleased/=== Added -
docs/modules/ROOT/pages/services/canopy-reporting.adoc— list ACF-196 path -
docs/modules/ROOT/pages/federal-requirements.adoc— ACF-196 row update -
Plan archive: move to
plans/archive/post-merge