Files
pwap/web/supabase/migrations/archive/20260223150000_p2p_visa_system.sql
T
pezkuwichain e18ba679be db: replace the migration history with a baseline of the live schema
The migrations did not describe this database, and could not be made to.

Three findings, in the order they surfaced:

  * 25 functions declared across six migrations — all recorded as applied — did
    not exist. Their tables did. Two were reached by the app, so merchant tier
    upgrades and post-trade reputation updates had been quietly dead. This only
    came to light because a user hit "Could not find the function
    public.upsert_user_profile(...)" while toggling a notification setting.

  * admin_roles has three conflicting definitions across the set and production
    matches none of them. 001 says (id, user_id, role, granted_by, granted_at),
    COMBINED says (user_id, role, created_at), production has
    (id, user_id, role, permissions, created_at, updated_at).

  * Applied to an empty database, five migrations fail. The legacy 0NN filenames
    sort before the 14-digit timestamps they depend on — "013" < "20241117054600"
    — so 013 runs before the migration creating the table it alters. The set
    could never have been replayed from scratch.

So it could not be tested, could not rebuild the database, and did not match what
was running. Widening or patching it would have been dressing up a history that
was already fiction.

The baseline is a pg_dump of the live public schema, which matches production by
construction. Privileges are included deliberately: the REVOKEs on
lock_escrow_internal, release_escrow_internal, refund_escrow_internal and
request_withdraw are the 20260725030000 hardening, and dropping them would hand
fund movement back to anon.

Verified step by step against production before committing:
  - applies to an empty database cleanly, after three real obstacles were fixed
    (extensions live in the `extensions` schema, supabase_admin membership,
    platform-level ALTER DEFAULT PRIVILEGES that cannot apply outside Supabase)
  - produces 83 tables / 39 functions / 222 indexes / 360 policies — identical
    counts to production
  - recorded as applied in supabase_migrations without touching the 37 existing
    rows, then dry-run confirmed "up to date — no pending migrations", so the
    next deploy will not try to replay it over live tables
  - drift check now reports "every declared function is present"; the known-gaps
    list drops from 22 entries to zero

The old files move to migrations/archive/ rather than being deleted — they are
the only record of why parts of this look the way they do. Their README says
plainly not to run them, and why.

CI now applies migrations to an empty Postgres on every PR. That check was
impossible while the old set was the starting point; it is the thing that stops
this class of drift from being discovered by a user again.
2026-07-31 18:49:59 -07:00

86 lines
2.4 KiB
PL/PgSQL

-- P2P Visa System
-- Provides identity for non-citizen P2P traders
-- Citizens use their on-chain Citizen Number (from NFT)
-- Non-citizens apply for a Visa (off-chain, stored in Supabase)
CREATE TABLE IF NOT EXISTS public.p2p_visa (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
visa_number TEXT UNIQUE NOT NULL,
wallet_address TEXT UNIQUE NOT NULL,
status TEXT NOT NULL DEFAULT 'active',
trust_level INTEGER NOT NULL DEFAULT 1,
issued_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expires_at TIMESTAMPTZ DEFAULT (now() + interval '1 year'),
metadata JSONB DEFAULT '{}'
);
CREATE INDEX IF NOT EXISTS idx_visa_wallet ON public.p2p_visa(wallet_address);
CREATE INDEX IF NOT EXISTS idx_visa_status ON public.p2p_visa(status);
-- Generate unique visa number: V-XXXXXX (6 digits)
CREATE OR REPLACE FUNCTION generate_visa_number()
RETURNS TEXT
LANGUAGE plpgsql
AS $$
DECLARE
num TEXT;
BEGIN
LOOP
num := 'V-' || lpad(floor(random() * 1000000)::text, 6, '0');
EXIT WHEN NOT EXISTS (SELECT 1 FROM public.p2p_visa WHERE visa_number = num);
END LOOP;
RETURN num;
END;
$$;
-- Issue a visa for a wallet address (returns the visa record)
CREATE OR REPLACE FUNCTION issue_p2p_visa(p_wallet_address TEXT)
RETURNS JSONB
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
v_visa_number TEXT;
v_result JSONB;
BEGIN
-- Check if wallet already has a visa
IF EXISTS (SELECT 1 FROM public.p2p_visa WHERE wallet_address = p_wallet_address AND status = 'active') THEN
SELECT jsonb_build_object(
'success', true,
'visa_number', visa_number,
'already_exists', true
) INTO v_result
FROM public.p2p_visa
WHERE wallet_address = p_wallet_address AND status = 'active';
RETURN v_result;
END IF;
-- Generate unique visa number
v_visa_number := generate_visa_number();
-- Insert new visa
INSERT INTO public.p2p_visa (visa_number, wallet_address)
VALUES (v_visa_number, p_wallet_address);
RETURN jsonb_build_object(
'success', true,
'visa_number', v_visa_number,
'already_exists', false
);
END;
$$;
-- RLS: service role only (P2P operations go through edge functions)
ALTER TABLE public.p2p_visa ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Service role full access on p2p_visa"
ON public.p2p_visa
FOR ALL
USING (auth.role() = 'service_role');
-- Allow anon/authenticated to read their own visa by wallet address
CREATE POLICY "Users can read own visa"
ON public.p2p_visa
FOR SELECT
USING (true);