fix(financial): void all placement payments on placement.ended
Summary
placement.ended voiding only scans first 100 pending payments; status filter excludes 'approved'
Evidence
handle_placement_ended lists payments with limit:100/offset:0/status:Some('pending') then voids matching placement_id in Rust. list_payments_paged has no placement_id predicate, so the page is the 100 most-recent PENDING payments GLOBALLY — a target placement's payments outside the global top-100 are silently skipped. can_payment_transition permits Pending|Approved -> Voided, but approved payments never enter the list. approve_payment is a standalone action with no placement-active recheck. No test covers this path.
Locations
services/craig-financial/src/main.rs:356-399
Why it matters
When a placement ends, an APPROVED-but-not-yet-issued payment for it is NOT voided, so the agency can still issue a payment for an ended placement — an overpayment path for public IV-E funds. Both failures are silent (only voided_count logged).
Recommended fix
Add placement_id to ListPaymentsParams and filter in SQL; drop the 100-row cap or paginate to exhaustion; select all statuses for which can_payment_transition(status, Voided) is true (pending AND approved), letting the transition predicate gate.
Acceptance criteria
- Add placement_id to ListPaymentsParams and filter in SQL
- drop the 100-row cap or paginate to exhaustion
- select all statuses for which can_payment_transition(status, Voided) is true (pending AND approved), letting the transition predicate gate
- Regression test added that reproduces the defect (fails before, passes after) — unless purely doc/dep
- Any docs/comments describing this surface updated to match (J5)
References
- Source: 2026-06-28 automated code-quality audit — finding
financial-placement-ended-void-incomplete(severity medium, dimension error-handling) - Backlog reconciliation: not-tracked — related: #246 (closed) (closed), #245 (closed) (opened), #614 (closed) (closed)