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).

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)