Plan: Applications — Authorized Representatives CRUD (Issue #401)

On this page

Status

Step Description Status

1

Store layer. New services/canopy-applications/src/store/authorized_reps.rs implementing create, get, list_by_application, update, delete against the existing authorized_representatives table (services/canopy-applications/migrations/20260401000000_create_applications_tables.sql:48-62). Mirror the pattern from services/canopy-applications/src/store.rs:15-39 (create_application). All operations bind created_at = now() explicitly + use RETURNING * so handlers always return the DB-canonical row.

Done (2026-05-10)

2

API layer. New services/canopy-applications/src/api/authorized_reps.rs exposing POST /v1/applications/{id}/authorized-representatives, GET /v1/authorized-representatives/{id}, GET /v1/applications/{id}/authorized-representatives, PUT /v1/authorized-representatives/{id}, DELETE /v1/authorized-representatives/{id}. All handlers carry #[utoipa::path] decorators with full request/response schemas. JWT-auth required (existing canopy-auth middleware applies).

Done (2026-05-10)

3

Router wiring. Register the 5 routes in services/canopy-applications/src/api/mod.rs:68-84 next to the existing application routes. Add the new types to the ApiDoc #[openapi(components(schemas(…)))] list.

Done (2026-05-10)

4

OpenAPI snapshot regeneration. cargo xtask api-docs regenerates docs/modules/ROOT/openapi/canopy-applications.json. The OpenAPI drift gate (xtask/src/cmd/validate.rs) fails pre-push if the snapshot is stale; commit the regenerated file.

Done (2026-05-10)

5

Tests + docs. 6 unit tests in services/canopy-applications/src/store/authorized_reps.rs (create + read + list + update + delete + foreign-key violation rejected) using the in-memory sqlx-postgres test harness. 1 integration test at services/canopy-applications/tests/authorized_reps_test.rs that creates an application, attaches a rep, fetches the application, asserts the FK round-trips. Update .claude/docs/services.md canopy-applications route count + table list. CHANGELOG entry under === Added. Plan moves to plans/archive/ post-merge.

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-289update_application handler that already wires authorized_representative_id (handles assignment but not rep CRUD).

  • services/canopy-applications/src/store.rs:15-39create_application template for store layer.

Scope

In scope:

  • 5 endpoints under /v1/applications/{id}/authorized-representatives and /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.

Dependencies

  • None on other open plans. The schema is already in place.

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

services/canopy-applications/src/store/authorized_reps.rs

New file: store CRUD

services/canopy-applications/src/store.rs (or src/store/mod.rs)

Re-export authorized_reps module

services/canopy-applications/src/api/authorized_reps.rs

New file: 5 handlers

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

Register routes + extend ApiDoc components

services/canopy-applications/tests/authorized_reps_test.rs

New integration test

docs/modules/ROOT/openapi/canopy-applications.json

Regenerated OpenAPI snapshot

.claude/docs/services.md

canopy-applications route table + table list

CHANGELOG.adoc

=== Added entry

Verification

  1. cargo nextest run -p canopy-applications --lib — store unit tests pass.

  2. cargo nextest run -p canopy-applications --test authorized_reps_test — integration test passes.

  3. cargo xtask api-docs — OpenAPI snapshot regenerates clean (no diff on second run).

  4. cargo xtask validate — full battery green; no drift gate failures.

  5. 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; add authorized_representatives to 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

Edit this page · default