-- Migration 978: Refund state machine for Hyperswitch reverse-charge (v1.0.6) -- Before v1.0.6, RefundOrder marked orders as `refunded` immediately after -- POSTing to /refunds — treating Hyperswitch's synchronous API ack as -- confirmation that the money had moved. In reality the PSP returns `pending` -- and only confirms via webhook (`refund_succeeded` / `refund_failed`). -- Customers could see "refunded" in the UI while their bank account still -- hadn't been credited. -- -- This migration introduces an auditable refund row that tracks the full -- lifecycle: pending → (succeeded | failed). Uniqueness on -- hyperswitch_refund_id is the load-bearing constraint — it guarantees that -- the webhook handler can safely retry (idempotent 200) because a duplicate -- PSP notification lands on the same DB row. CREATE TABLE IF NOT EXISTS public.refunds ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), order_id UUID NOT NULL REFERENCES public.orders(id) ON DELETE CASCADE, initiator_id UUID NOT NULL REFERENCES public.users(id) ON DELETE SET NULL, hyperswitch_payment_id TEXT NOT NULL, hyperswitch_refund_id TEXT UNIQUE, amount_cents BIGINT NOT NULL, currency VARCHAR(3) NOT NULL DEFAULT 'EUR', reason TEXT NOT NULL DEFAULT '', status TEXT NOT NULL DEFAULT 'pending', error_message TEXT, succeeded_at TIMESTAMPTZ, failed_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_refunds_order_id ON public.refunds(order_id); CREATE INDEX IF NOT EXISTS idx_refunds_status ON public.refunds(status); CREATE INDEX IF NOT EXISTS idx_refunds_hyperswitch_payment_id ON public.refunds(hyperswitch_payment_id);