Prevent duplicate credit wallets for one account

SI
7d ago
+197-0

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.