POST /api/v1/documents/{job_id}/pii/approve
POST /api/v1/documents/{job_id}/pii/deny
The token-based equivalent at /api/v1/approval/{token}/decision works
for the operator-driven flow that already exists. These by-job-id
variants exist so machine clients (the reflow-canvas-lti Canvas
connector) can forward a faculty decision after a status poll — they
already authenticated with an API key and they already know the
job_id, so a token round-trip isn't needed.
Body shape mirrors ApprovalDecisionInput minus `decision` (implicit
from the URL): `{justification?, reviewed_by}`. Response is the same
shape as the token endpoint returns. Status codes:
200 approval recorded; quick_approve/quick_deny ran; background
task continues with S3 cleanup / queue dispatch.
404 job_id not found.
409 job exists but isn't in `awaiting_approval` — race with another
reviewer in a parallel tab, or a timeout sweeper already
decided. The Canvas LTI connector surfaces this verbatim to
faculty.
422 body validation (justification < 10 chars when present,
reviewed_by missing) — FastAPI handles automatically.
The 409 pre-check is intentional: quick_approve/quick_deny set the
job status unconditionally, which is safe for the token-based
endpoint (the token proves the job was in awaiting_approval at issue
time) but unsafe by job_id where two concurrent calls could otherwise
both succeed and overwrite each other.
Tests cover happy approve, happy deny, 404, and 409, all against the
live testcontainers stack via the integration tier. Verified locally
with `pytest -m integration` inside the api-gateway image.