Hotfix surfaced by the v1.0.6 refund smoke test. Migration 978's plain
UNIQUE constraint on hyperswitch_refund_id collided on empty strings
— two refunds in the same post-Phase-1 / pre-Phase-2 state (or a
previous Phase-2 failure leaving '') would violate the constraint at
INSERT time on the second attempt, even though the refunds were for
different orders.
* Migration 979_refunds_unique_partial.sql replaces the plain
UNIQUE with a partial index excluding empty and NULL values.
Idempotency for successful refunds is preserved — duplicate
Hyperswitch webhooks land on the same row because the PSP-
assigned refund_id is non-empty.
* No Go code change. The bug was purely in the DB constraint shape.
Smoke test that caught it — 5/5 scenarios re-verified end-to-end:
happy path, idempotent replay (succeeded_at + balance strictly
invariant), PSP error rollback, webhook refund.failed, double-submit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
30 lines
1.4 KiB
SQL
30 lines
1.4 KiB
SQL
-- Migration 979: partial UNIQUE on refunds.hyperswitch_refund_id (v1.0.6.1 hotfix)
|
|
--
|
|
-- The v1.0.6 migration 978 used a plain UNIQUE constraint on
|
|
-- hyperswitch_refund_id. That broke when two refunds in the same DB
|
|
-- stayed in their post-Phase-1 / pre-Phase-2 state: both rows have
|
|
-- hyperswitch_refund_id='' (empty string, because Go's zero-value for
|
|
-- string writes '' rather than NULL), and PostgreSQL treats two empty
|
|
-- strings as colliding under a regular UNIQUE constraint.
|
|
--
|
|
-- Surfaced by the v1.0.6 refund smoke test (scenario S4, triggered
|
|
-- after the S3 PSP-error path left one row with refund_id=''): the
|
|
-- second refund attempt on a different order got a UNIQUE violation
|
|
-- at INSERT time.
|
|
--
|
|
-- Fix: make the UNIQUE partial — only enforce uniqueness on rows that
|
|
-- have actually received a PSP-assigned refund_id. Empty strings and
|
|
-- NULLs are ignored. This preserves the load-bearing idempotency
|
|
-- guarantee for successful refunds (duplicate webhook lands on the
|
|
-- same row) without rejecting legitimate second attempts after a PSP
|
|
-- failure on a different order.
|
|
|
|
ALTER TABLE public.refunds
|
|
DROP CONSTRAINT IF EXISTS refunds_hyperswitch_refund_id_key;
|
|
|
|
DROP INDEX IF EXISTS refunds_hyperswitch_refund_id_unique;
|
|
|
|
CREATE UNIQUE INDEX refunds_hyperswitch_refund_id_unique
|
|
ON public.refunds (hyperswitch_refund_id)
|
|
WHERE hyperswitch_refund_id IS NOT NULL
|
|
AND hyperswitch_refund_id <> '';
|