feat(canopy-persons): PUT/DELETE /v1/persons/{id}/income/{income_id} mutation endpoints
Source: prereq for #409 (closed) (canopy-web income editing UI). Surfaced by the Tier B plan-refresh pass — the #409 (closed) plan claimed these endpoints already exist; they do not.
`services/canopy-persons/src/api/mod.rs:44` currently registers only `POST` + `GET` on `/persons/{id}/income`:
```rust .route("/persons/{id}/income", post(add_income).get(list_income)) ```
`services/canopy-persons/src/store/income.rs` (31 LOC) has only `add()` and `list_by_person()`. No `update()` or `soft_delete()`.
Per the architectural decision (locked 2026-05-05), income mutates in place — no versioning. Determinations carry their own income snapshot in the signed JWS (`SignableDetermination.program_extension`), so historical reproducibility is preserved without an `income_versions` layer.
Acceptance:
- `PUT /v1/persons/{id}/income/{income_id}` — update one or more fields on an existing income row. Soft-delete-aware (returns 404 if `active = false`).
- `DELETE /v1/persons/{id}/income/{income_id}` — soft-delete: set `active = false`, `end_date = today`. Row stays for audit.
- New `UpdateIncome` request struct with all fields optional (use `#[serde(default)]` + `Option`); validator constraints applied to whatever fields are present.
- `store::income::update()` and `store::income::soft_delete()` added.
- Path nesting: `/persons/{id}/income/{income_id}` (matches existing `POST /persons/{id}/income` nesting).
- OpenAPI snapshot regenerated.
- Integration tests for both endpoints.
Why `priority::low` not blocker:
#409 (closed) is also `priority::low` and its BFF wiring can ship the moment these endpoints land. Sequencing only.
Surfaced by: Tier B plan-refresh pass 2026-05-11.