- Shipped
- August 7, 2026 at 2:48 AM UTC
- Author
- Kamo
- Commit
- 871bf17
An adversarial review of the money path found the concurrency design was not running at all. @Modifying without @Transactional. Spring Data opens no transaction for such a statement, so every guarded UPDATE this feature relies on — the release claim, the requeue, the void claim, the webhook dedupe insert — was not doing its job. The whole two-pod safety story rested on statements that never executed as claimed. The payout leg had no claim of any kind. Both overlapping pods called Payout.create for the same withdrawal; the loser received a 409 idempotency_error, which the classifier treated as permanent, so it refunded the member's hold while the winner's money was genuinely on its way to their bank. That is a double payment on every rollout with a transferred withdrawal past its 24h delay. Claimed now with a time-based lease, which also lets a pod that dies mid-payout release its own claim instead of stranding the row. RELEASING was a black hole: a pod that died between claiming and hearing back from Stripe left the row there with nothing to move it on and the member's money held indefinitely. Recovered now, but ONLY for rows with no transfer id — one that reached Stripe must be reconciled, never blindly retried. advanceTransferred read page 0 newest-first, permanently starving the oldest rows once the backlog exceeded a page — the member who had waited longest was the one who never got paid. Now ascending. /me/opportunities called findAll() and filtered in Java, pulling every reservation on the platform across every tenant into heap on a page a member can refresh at will. Replaced with an org-and-agent-scoped finder.