Plan: Applications — Authorized Representatives CRUD (Issue #401)
On this page
Status
| Step | Description | Status |
|---|---|---|
1 |
Store layer. New |
Done (2026-05-10) |
2 |
API layer. New |
Done (2026-05-10) |
3 |
Router wiring. Register the 5 routes in |
Done (2026-05-10) |
4 |
OpenAPI snapshot regeneration. |
Done (2026-05-10) |
5 |
Tests + docs. 6 unit tests in |
Done (2026-05-10) |
Issue: #401
Branch: feat/applications-authorized-representatives
Labels: type::feature, priority::medium, service::applications, program::cross-program, workflow::ready
Context
The authorized_representatives table exists in canopy-applications (services/canopy-applications/migrations/20260401000000_create_applications_tables.sql:48-62) with columns id, application_id, person_id, relationship, power_of_attorney, valid_through, contact_email, contact_phone, created_at, updated_at. The applications table holds an authorized_representative_id UUID FK referencing it. Application intake records the FK on submit, but no API or store path lets a worker create, read, update, or delete a rep — they exist as ghost rows with no handle.
ACA §1413 single-streamlined application allows an applicant to designate an authorized representative for any benefit application; states must accept and act on rep designations. Without CRUD, the canopy-applications service satisfies the data model on paper but cannot operationally support the workflow.
Code references
-
services/canopy-applications/migrations/20260401000000_create_applications_tables.sql:48-62— table definition. -
services/canopy-applications/migrations/20260401000000_create_applications_tables.sql:4-25— applications table with the FK. -
services/canopy-applications/src/api/mod.rs:68-84— Router registration to extend. -
services/canopy-applications/src/api/mod.rs:278-289—update_applicationhandler that already wiresauthorized_representative_id(handles assignment but not rep CRUD). -
services/canopy-applications/src/store.rs:15-39—create_applicationtemplate for store layer.
Scope
In scope:
-
5 endpoints under
/v1/applications/{id}/authorized-representativesand/v1/authorized-representatives/{id}. -
Store + API + OpenAPI sync.
-
Unit + integration tests.
Out of scope:
-
Rep-aware notice rendering (separate plan if/when needed — notices currently address the applicant only).
-
Worker-portal UI for rep management (separate plan; would extend canopy-web case detail).
-
Rep-signed application submission (would require additional auth / signature semantics; out of scope here).
-
Cross-program rep designation propagation — each program service tracks its own representative if needed; canopy-applications is the system of record for the application-level rep.
Design
AuthorizedRep Rust type (lives in services/canopy-applications/src/store/authorized_reps.rs):
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)]
pub struct AuthorizedRep {
pub id: Uuid,
pub application_id: Uuid,
pub person_id: Option<Uuid>,
pub relationship: String,
pub power_of_attorney: bool,
pub valid_through: Option<NaiveDate>,
pub contact_email: Option<String>,
pub contact_phone: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
Store API:
pub async fn create(pool: &PgPool, req: CreateAuthorizedRepRequest) -> sqlx::Result<AuthorizedRep>;
pub async fn get(pool: &PgPool, id: Uuid) -> sqlx::Result<Option<AuthorizedRep>>;
pub async fn list_by_application(pool: &PgPool, application_id: Uuid) -> sqlx::Result<Vec<AuthorizedRep>>;
pub async fn update(pool: &PgPool, id: Uuid, req: UpdateAuthorizedRepRequest) -> sqlx::Result<Option<AuthorizedRep>>;
pub async fn delete(pool: &PgPool, id: Uuid) -> sqlx::Result<bool>;
API handlers receive Json<CreateAuthorizedRepRequest>, return Json<AuthorizedRep>, propagate sqlx::Error to existing AppError shape (4xx on FK / unique violations, 500 on other DB errors).
Files Touched
| File | Change |
|---|---|
|
New file: store CRUD |
|
Re-export |
|
New file: 5 handlers |
|
Register routes + extend ApiDoc components |
|
New integration test |
|
Regenerated OpenAPI snapshot |
|
canopy-applications route table + table list |
|
|
Verification
-
cargo nextest run -p canopy-applications --lib— store unit tests pass. -
cargo nextest run -p canopy-applications --test authorized_reps_test— integration test passes. -
cargo xtask api-docs— OpenAPI snapshot regenerates clean (no diff on second run). -
cargo xtask validate— full battery green; no drift gate failures. -
Manual smoke against devstack:
curl -X POST http://localhost:…/v1/applications/{id}/authorized-representatives -d '{…}'returns 201 + the persisted row; subsequent GET returns it.
Documentation Updates
-
.claude/docs/services.md— bump canopy-applications domain route count from 8 to 13; addauthorized_representativesto the table list -
CHANGELOG.adoc— entry under== Unreleased/=== Added -
docs/modules/ROOT/pages/services/canopy-applications.adoc— extend the API reference page -
Plan archive: move this file to
plans/archive/post-merge