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:

  1. `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`).
  2. `DELETE /v1/persons/{id}/income/{income_id}` — soft-delete: set `active = false`, `end_date = today`. Row stays for audit.
  3. New `UpdateIncome` request struct with all fields optional (use `#[serde(default)]` + `Option`); validator constraints applied to whatever fields are present.
  4. `store::income::update()` and `store::income::soft_delete()` added.
  5. Path nesting: `/persons/{id}/income/{income_id}` (matches existing `POST /persons/{id}/income` nesting).
  6. OpenAPI snapshot regenerated.
  7. 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.

🤖 Generated with Claude Code