fix(financial): make payment issue and clear transitions reachable with truthful events
Summary
The payment state machine defines pending -> approved -> issued -> cleared, but the live API exposes only pending -> approved. No production writer moves a payment to issued or cleared, while claim generation aggregates only rows whose status is issued.
The approval handler also emits financial.payment_created a second time instead of an approval event. Runtime-created payments therefore cannot enter a real claim without direct database manipulation, and event consumers receive a false duplicate-create signal.
Evidence
services/craig-financial/src/api/mod.rs:220-263mounts calculate/list/get/approve routes but no issue or clear route.services/craig-financial/src/transitions.rs:17-30defines approved-to-issued and issued-to-cleared as valid transitions.services/craig-financial/src/store/claims.rs:157-180aggregates onlystatus = issued.- The only production call to
update_payment_statusis approval inservices/craig-financial/src/api/payments.rs:305-335. - That approval transaction calls
publish_payment_createdat lines 327-334. financial.payment_issuedis documented and parsed by craig-security, but no financial publisher emits it.
Why it matters
This blocks the normal Title IV-E payment-to-claim workflow and makes the audit/event stream misrepresent approval as resource creation.
Recommended fix
Add explicit, authorization-gated issue and clear transitions (and a deliberate manual-void path if required), each using an atomic expected-state update plus transactional outbox event. Emit distinct payment_approved, payment_issued, and payment_cleared events. Carry the endpoints through typed clients, CLI, and web so the documented API/CLI/Web parity remains true.
Acceptance criteria
-
pending -> approved -> issued -> clearedis reachable through supported service APIs without direct SQL. - Every transition uses
WHERE status = <expected>or a row lock so concurrent callers cannot double-transition or double-publish. - Approval no longer emits
financial.payment_created. - Distinct lifecycle events are staged in the same transaction as each state change.
- Claim generation includes payments issued through the supported workflow.
- REST, typed-client, CLI, and web coverage exercise the full lifecycle and invalid/concurrent transitions.
- Event documentation and craig-security event parsing remain synchronized.
References
- Source: 2026-07-09 repository review.
- Backlog reconciliation: not tracked. #773 (closed)-#775 (closed) cover rounding, decimal wire representation, and input validation; none covers lifecycle reachability or approval event semantics.