veza/veza-backend-api/migrations/981_seller_transfers_stripe_reversal_id.sql
senke eedaad9f83 refactor(connect): persist stripe_transfer_id on create + retry — v1.0.7 item A
TransferService.CreateTransfer signature changes from (...) error to
(...) (string, error) — the caller now captures the Stripe transfer
identifier and persists it on the SellerTransfer row. Pre-v1.0.7 the
stripe_transfer_id column was declared on the model and table but
never written to, which blocked the reversal worker (v1.0.7 item B)
from identifying which transfer to reverse on refund.

Changes:
  * `TransferService` interface and `StripeConnectService.CreateTransfer`
    both return the Stripe transfer id alongside the error.
  * `processSellerTransfers` (marketplace service) persists the id on
    success before `tx.Create(&st)` so a crash between Stripe ACK and
    DB commit leaves no inconsistency.
  * `TransferRetryWorker.retryOne` persists on retry success — a row
    that failed on first attempt and succeeded via the worker is
    reversal-ready all the same.
  * `admin_transfer_handler.RetryTransfer` (manual retry) persists too.
  * `SellerPayout.ExternalPayoutID` is populated by the Connect payout
    flow (`payout.go`) — the field existed but was never written.
  * Four test mocks updated; two tests assert the id is persisted on
    the happy path, one on the failure path confirms we don't write a
    fake id when the provider errors.

Migration `981_seller_transfers_stripe_reversal_id.sql`:
  * Adds nullable `stripe_reversal_id` column for item B.
  * Partial UNIQUE indexes on both stripe_transfer_id and
    stripe_reversal_id (WHERE IS NOT NULL AND <> ''), mirroring the
    v1.0.6.1 pattern for refunds.hyperswitch_refund_id.
  * Logs a count of historical completed transfers that lack an id —
    these are candidates for the backfill CLI follow-up task.

Backfill for historical rows is a separate follow-up (cmd/tools/
backfill_stripe_transfer_ids, calling Stripe's transfers.List with
Destination + Metadata[order_id]). Pre-v1.0.7 transfers without a
backfilled id cannot be auto-reversed on refund — document in P2.9
admin-recovery when it lands. Acceptable scope per v107-plan.

Migration number bumped 980 → 981 because v1.0.6.2 used 980 for the
unpaid-subscription cleanup; v107-plan updated with the note.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 13:08:39 +02:00

47 lines
2.4 KiB
SQL

-- v1.0.7 item A: add stripe_reversal_id column to seller_transfers.
-- Prepares ground for item B (async reversal worker): when a refund-driven
-- reversal succeeds, the worker persists the Stripe reversal id here so
-- the operation is idempotent (a replayed webhook skips the reversal call
-- if this column is populated) and auditable against the Stripe dashboard.
--
-- This migration ships with item A because item B's worker is the next
-- commit and we want the column in place before the code that writes it.
-- Nullable — pre-v1.0.7 transfers will never have a reversal id.
--
-- Companion column stripe_transfer_id already exists (pre-v1.0.7), but was
-- never written to until item A: the TransferService.CreateTransfer
-- signature changed to return the Stripe transfer id, which is now
-- persisted by processSellerTransfers, TransferRetryWorker, and
-- admin_transfer_handler.
ALTER TABLE seller_transfers
ADD COLUMN IF NOT EXISTS stripe_reversal_id VARCHAR(255);
-- Partial UNIQUE index so a given Stripe reversal id cannot collide across
-- rows, while still allowing many NULL/empty rows (the common case: only
-- refunded transfers carry a reversal id). Mirrors the pattern landed in
-- v1.0.6.1 for refunds.hyperswitch_refund_id.
CREATE UNIQUE INDEX IF NOT EXISTS idx_seller_transfers_stripe_reversal_id
ON seller_transfers(stripe_reversal_id)
WHERE stripe_reversal_id IS NOT NULL AND stripe_reversal_id <> '';
-- Same pattern for stripe_transfer_id — previously declared without an
-- index, now populated by item A so worth indexing for reconciliation
-- lookups. Partial because pre-v1.0.7 rows carry empty values.
CREATE UNIQUE INDEX IF NOT EXISTS idx_seller_transfers_stripe_transfer_id
ON seller_transfers(stripe_transfer_id)
WHERE stripe_transfer_id IS NOT NULL AND stripe_transfer_id <> '';
-- Visibility: how many historical rows lack a stripe_transfer_id? These
-- are the rows that the backfill CLI (cmd/tools/backfill_stripe_transfer_ids)
-- will target. Acceptable to leave NULL where Stripe has no match — see
-- axis-1 P2.9 for the admin-triggered recovery path.
DO $$
DECLARE v_count INTEGER;
BEGIN
SELECT COUNT(*) INTO v_count
FROM seller_transfers
WHERE status = 'completed'
AND (stripe_transfer_id IS NULL OR stripe_transfer_id = '');
RAISE NOTICE 'v1.0.7 item A: % completed seller_transfer(s) have no stripe_transfer_id and need backfill (see cmd/tools/backfill_stripe_transfer_ids)', v_count;
END $$;