Prevent duplicate credit wallets for one account
Concurrent account setup can currently create multiple credit balances for the same account. Add a unique constraint on credits_usage.account_id so every writer must use one assigned wallet. Existing balances and charge receipts remain unchanged; pre-existing duplicates fail the migration instead of being merged or deleted. Installation has bounded lock/work timeouts.
This is a shared accounting prerequisite for recoupable/app#2105 and recoupable/app#2123, tracked under recoupable/app#2116. It does not enable paid collection, implement reservations or fix stale refill/top-up writes. Existing nullable-owner behavior is preserved. Concurrent legacy grant creation can now fail and roll back instead of creating a second wallet; no automatic grant retry is introduced.
Validation: five regression assertions failed before the migration. All 18 new/existing transaction tests pass on disposable PostgreSQL 15 and 17; CI covers 15/16. Tests cover simultaneous setup, conflicting reassignment, rollback, unchanged existing balances, duplicate-data installation failure, and existing audited debit/stable receipt compatibility. Read-only production preflight found zero duplicate account groups; this is a point-in-time check, and installation validates again. No production migration or financial mutation performed.
Deployment and remaining writer audit: tests/CREDIT-WALLET-IDENTITY.md. Requires explicit merge approval after CI/review; verify the production constraint after the normal Supabase release.
Summary by cubic
Concurrent account setup can currently create multiple credit balances for the same account. This adds a unique constraint on credits_usage.account_id so each billing account gets exactly one wallet; concurrent inserts and conflicting reassignments now fail and roll back instead of creating a second wallet. Existing balances, charge receipts, and nullable-owner behavior are unchanged.
This is a shared accounting prerequisite for recoupable/app#2105 and recoupable/app#2123, tracked under recoupable/app#2116. It does not enable paid collection, implement reservations, or fix stale refill/top-up writes, and no automatic grant retry is introduced.
- Pre-existing duplicates fail the migration instead of being merged or deleted.
- The migration takes a table lock with a 5s lock timeout and 30s statement timeout, rolling back on timeout.
Validation
- All 18 new/existing transaction tests pass on PostgreSQL 15 and 17 (CI covers 15 and 16), including simultaneous setup, conflict rollback, and existing debit/receipt compatibility.
- A read-only production preflight found zero duplicate account groups; installation validates again, and no production migration or financial mutation was performed.
- Requires explicit merge approval after CI/review; deployment and remaining writer audit steps are in
tests/CREDIT-WALLET-IDENTITY.md.