# PROJ-11: Correspondents CRUD

## Status: QA Passed
**Created:** 2026-03-06
**Last Updated:** 2026-03-07

## Dependencies
- Requires: PROJ-1 (HTTP Client Core) — for authenticated HTTP requests and error mapping

## User Stories
- As a developer, I want to list all correspondents so that I can inspect who is tracked in my paperless-ngx instance.
- As a developer, I want to filter correspondents by name substring so that I can find one without loading the full list.
- As a developer, I want to fetch a single correspondent by ID so that I can read its properties.
- As a developer, I want to create a new correspondent with name, auto-match settings, and permissions so that I can manage correspondents programmatically.
- As a developer, I want to update an existing correspondent so that I can rename it or adjust its auto-match rules.
- As a developer, I want to delete a correspondent so that I can remove unused ones without using the web UI.

## Acceptance Criteria

### list_correspondents
- [ ] `list_correspondents(*, ids, name_contains, page, page_size, ordering, descending) -> list[Correspondent]` fetches `GET /correspondents/` and returns validated `Correspondent` instances.
- [ ] `ids` filters to only correspondents whose ID is in the list (`id__in` query param).
- [ ] `name_contains` does a case-insensitive substring match on correspondent name (`name__icontains`).
- [ ] When `page` is omitted, all pages are fetched automatically (auto-pagination).
- [ ] When `page` is set, only that page is returned (disables auto-pagination).
- [ ] `ordering` + `descending` control sort order; `descending=True` prepends `-` to the field name.

### get_correspondent
- [ ] `get_correspondent(id: int) -> Correspondent` fetches `GET /correspondents/{id}/` and returns a validated `Correspondent`.
- [ ] Raises `NotFoundError` on HTTP 404.

### create_correspondent
- [ ] `create_correspondent(*, name, match, matching_algorithm, is_insensitive, owner, set_permissions) -> Correspondent` sends `POST /correspondents/` and returns the created `Correspondent`.
- [ ] `name` is required; all other fields are optional.
- [ ] `matching_algorithm` integer values: `0`=none, `1`=any word, `2`=all words, `3`=exact, `4`=regex, `5`=fuzzy, `6`=auto (ML).
- [ ] `owner` sets the owning user ID; `None` leaves the correspondent without an owner.
- [ ] `set_permissions` sets explicit view/change permissions via `SetPermissions` model.

### update_correspondent
- [ ] `update_correspondent(id: int, *, name, match, matching_algorithm, is_insensitive) -> Correspondent` sends `PATCH /correspondents/{id}/` and returns the updated `Correspondent`.
- [ ] Only fields explicitly passed (not `None`) are included in the payload.
- [ ] Raises `NotFoundError` on HTTP 404.
- [ ] `owner` and `set_permissions` are **not yet supported** in `update_correspondent` (planned — consistent with the same gap in `update_document` and `update_tag`).

### delete_correspondent
- [ ] `delete_correspondent(id: int) -> None` sends `DELETE /correspondents/{id}/` and returns `None` on success.
- [ ] Raises `NotFoundError` on HTTP 404.

### General
- [ ] The `Correspondent` model exposes: `id`, `name`, `slug`, `match`, `matching_algorithm`, `is_insensitive`, `document_count`, `last_correspondence`, `owner`, `user_can_change`.
- [ ] All methods are available on `SyncPaperlessClient` with the same signatures (blocking wrapper).

## Edge Cases
- Creating a correspondent with a name that already exists → server returns an error (likely HTTP 400); propagated as-is.
- Deleting a correspondent that is assigned to documents — paperless-ngx clears the correspondent field on those documents; no error is raised.
- `update_correspondent` called with no keyword arguments → empty PATCH payload; server returns the correspondent unchanged.

---
<!-- Sections below are added by subsequent skills -->

## Tech Design (Solution Architect)
_To be added by /architecture_

## QA Test Results

**Date:** 2026-03-07
**Tester:** QA Engineer (automated)
**Branch:** master (commit 9f689e7)

### Test Environment
- Python 3.13.12, pytest 9.0.2, mypy (strict), ruff
- All 353 tests pass (0 failures), 95% overall coverage
- Correspondent mixin coverage: **100%** (async), **94%** (sync — 1 uncovered line)
- mypy: **0 issues** across 38 source files
- ruff: **0 issues** on all correspondent source files

### Acceptance Criteria Results

#### list_correspondents
- [x] Signature matches spec: `list_correspondents(*, ids, name_contains, page, page_size, ordering, descending) -> list[Correspondent]` — **PASS**
- [x] `ids` filter sends `id__in` query param — **PASS** (test_list_correspondents_ids)
- [x] `name_contains` sends `name__icontains` query param — **PASS** (test_list_correspondents_name_contains)
- [x] Auto-pagination when `page` is omitted — **PASS** (test_list_correspondents, uses `get_all_pages`)
- [x] Single page when `page` is set — **PASS** (test_list_correspondents_page_size_ordering)
- [x] `ordering` + `descending` control sort; `descending=True` prepends `-` — **PASS** (test_list_correspondents_page_size_ordering)

#### get_correspondent
- [x] `get_correspondent(id: int) -> Correspondent` fetches `GET /correspondents/{id}/` — **PASS** (test_get_correspondent)
- [x] Raises `NotFoundError` on HTTP 404 — **PASS** (test_get_correspondent_not_found)

#### create_correspondent
- [x] Signature matches spec with `name` required, all others optional — **PASS** (test_create_correspondent, test_create_correspondent_all_params)
- [x] `matching_algorithm` integer enum values 0-6 — **PASS** (MatchingAlgorithm IntEnum verified)
- [x] `owner` sets owning user ID; `None` sends null — **PASS** (test_create_correspondent_all_params verifies body)
- [x] `set_permissions` sets explicit permissions via `SetPermissions` model — **PASS** (test_create_correspondent_all_params)

#### update_correspondent
- [x] `update_correspondent(id, *, name, match, matching_algorithm, is_insensitive) -> Correspondent` sends PATCH — **PASS** (test_update_correspondent)
- [x] Only fields explicitly passed (not `None`) are included in payload — **PASS** (test_update_correspondent_only_sends_provided_fields)
- [x] Raises `NotFoundError` on HTTP 404 — **PASS** (test_update_correspondent_not_found)
- [x] `owner` and `set_permissions` are not supported in update (consistent with tags) — **PASS** (verified in mixin signature)

#### delete_correspondent
- [x] `delete_correspondent(id: int) -> None` sends DELETE — **PASS** (test_delete_correspondent)
- [x] Raises `NotFoundError` on HTTP 404 — **PASS** (test_delete_correspondent_not_found)

#### General
- [x] `Correspondent` model exposes all required fields: `id`, `name`, `slug`, `match`, `matching_algorithm`, `is_insensitive`, `document_count`, `last_correspondence`, `owner`, `user_can_change` — **PASS** (test_correspondent_model_all_fields)
- [x] All methods available on `SyncPaperlessClient` with same signatures — **PASS** (test_sync_list/get/create/delete_correspondent)

### Edge Cases Tested
- [x] `update_correspondent` with no keyword arguments sends empty PATCH payload — **PASS** (test_update_correspondent_empty_patch)
- [x] Cache invalidation on create/update/delete — **PASS** (3 dedicated tests)
- [x] `last_correspondence` parsed as `datetime.date` — **PASS** (test_correspondent_last_correspondence_is_date)
- [x] `extra="ignore"` on model (unknown API fields silently dropped) — **PASS** (ConfigDict verified)

### Observations (Low Severity)
1. **Missing sync test for `update_correspondent`:** The `test_sync.py` file tests `list`, `get`, `create`, and `delete` for correspondents but omits `update_correspondent`. The async version is thoroughly tested and the sync wrapper is a trivial delegation, so risk is minimal. Severity: **Low**. **FIXED** — `test_sync_update_correspondent` added in commit `55044d0`.

### Summary
- **Acceptance criteria:** 20/20 passed
- **Edge cases:** 4/4 passed
- **Bugs found:** 0
- **Observations:** 1 (low severity, fixed)
- **Production-ready:** YES

## Deployment
_To be added by /deploy_
