-- ===================================================================== -- PAYMENT BROKER SCHEMA — generic multi-client ZarinPal gateway -- Served on pay.flatrender.ir (the single ZarinPal-verified callback domain). -- Other sites (meezi.ir, bargevasat.ir, FlatRender) register as client_apps -- and route payments through this broker. -- -- NOTE: migrations auto-run only on FIRST volume creation. On an existing -- DB volume, apply this manually: -- docker exec -i fr2-postgres psql -U postgres -d flatrender < 31_payment_broker.sql -- ===================================================================== CREATE SCHEMA IF NOT EXISTS payment; SET search_path TO payment, public; -- --------------------------------------------------------------------- -- client_apps — each site that pays through the broker -- --------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS payment.client_apps ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID, -- optional link to identity.tenants name TEXT NOT NULL, -- "meezi.ir", "FlatRender" slug TEXT NOT NULL UNIQUE, -- "meezi" api_key TEXT NOT NULL UNIQUE, -- public id (pk_...) secret TEXT NOT NULL, -- shared HMAC secret (sk_...) — signs in+out -- ZarinPal: per-client override; NULL → broker default merchant/sandbox zarinpal_merchant_id TEXT, zarinpal_sandbox BOOLEAN, allowed_return_origins TEXT[] NOT NULL DEFAULT '{}', -- e.g. {'https://meezi.ir'}; empty = permissive webhook_url TEXT, -- server-to-server result notification is_active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- --------------------------------------------------------------------- -- transactions — one row per payment attempt -- --------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS payment.transactions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), client_app_id UUID NOT NULL REFERENCES payment.client_apps(id) ON DELETE RESTRICT, status TEXT NOT NULL DEFAULT 'Created', -- Created|Pending|Paid|Failed|Cancelled|Expired gateway TEXT NOT NULL DEFAULT 'ZarinPal', amount_rial BIGINT NOT NULL, -- canonical Rial currency TEXT NOT NULL DEFAULT 'IRR', description TEXT, client_ref TEXT, -- the client's own order id return_url TEXT NOT NULL, -- where the user is sent back metadata JSONB, -- echoed back to the client payer_mobile TEXT, payer_email TEXT, authority TEXT, -- ZarinPal authority token ref_id TEXT, -- ZarinPal ref_id (receipt) card_pan TEXT, fee_rial BIGINT, gateway_response JSONB, failure_reason TEXT, paid_at TIMESTAMPTZ, failed_at TIMESTAMPTZ, expires_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_pay_txn_client ON payment.transactions(client_app_id, created_at DESC); CREATE UNIQUE INDEX IF NOT EXISTS idx_pay_txn_authority ON payment.transactions(authority) WHERE authority IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_pay_txn_clientref ON payment.transactions(client_app_id, client_ref) WHERE client_ref IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_pay_txn_status ON payment.transactions(status); -- --------------------------------------------------------------------- -- webhook_deliveries — outbound signed notifications with retry -- --------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS payment.webhook_deliveries ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), transaction_id UUID NOT NULL REFERENCES payment.transactions(id) ON DELETE CASCADE, url TEXT NOT NULL, payload JSONB NOT NULL, signature TEXT NOT NULL, attempts INT NOT NULL DEFAULT 0, delivered BOOLEAN NOT NULL DEFAULT FALSE, last_status INT, last_error TEXT, next_attempt_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_pay_wh_pending ON payment.webhook_deliveries(delivered, next_attempt_at) WHERE delivered = FALSE; -- --------------------------------------------------------------------- -- updated_at triggers (helper tg_set_updated_at() created in 00_setup.sql) -- --------------------------------------------------------------------- DROP TRIGGER IF EXISTS tg_pay_client_apps_updated ON payment.client_apps; CREATE TRIGGER tg_pay_client_apps_updated BEFORE UPDATE ON payment.client_apps FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at(); DROP TRIGGER IF EXISTS tg_pay_transactions_updated ON payment.transactions; CREATE TRIGGER tg_pay_transactions_updated BEFORE UPDATE ON payment.transactions FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at(); DROP TRIGGER IF EXISTS tg_pay_webhook_updated ON payment.webhook_deliveries; CREATE TRIGGER tg_pay_webhook_updated BEFORE UPDATE ON payment.webhook_deliveries FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();