trading-platform-database/ddl/schemas/financial/functions/02-process_transaction.sql

327 lines
9.0 KiB
PL/PgSQL

-- =====================================================
-- ORBIQUANT IA - PROCESS TRANSACTION FUNCTION
-- =====================================================
-- Description: Create and process wallet transactions atomically
-- Schema: financial
-- =====================================================
CREATE OR REPLACE FUNCTION financial.process_transaction(
p_wallet_id UUID,
p_transaction_type financial.transaction_type,
p_amount DECIMAL(20,8),
p_currency financial.currency_code,
p_fee DECIMAL(15,8) DEFAULT 0,
p_description TEXT DEFAULT NULL,
p_reference_id VARCHAR(100) DEFAULT NULL,
p_destination_wallet_id UUID DEFAULT NULL,
p_idempotency_key VARCHAR(255) DEFAULT NULL,
p_metadata JSONB DEFAULT '{}',
p_auto_complete BOOLEAN DEFAULT false
)
RETURNS TABLE (
success BOOLEAN,
transaction_id UUID,
new_balance DECIMAL(20,8),
error_message TEXT
)
LANGUAGE plpgsql
AS $$
DECLARE
v_wallet RECORD;
v_tx_id UUID;
v_balance_before DECIMAL(20,8);
v_balance_after DECIMAL(20,8);
v_update_result RECORD;
v_dest_tx_id UUID;
BEGIN
-- Validar idempotency
IF p_idempotency_key IS NOT NULL THEN
SELECT id, wallet_transactions.status INTO v_tx_id, v_wallet
FROM financial.wallet_transactions
WHERE idempotency_key = p_idempotency_key;
IF FOUND THEN
-- Transacción ya existe
SELECT balance INTO v_balance_after
FROM financial.wallets
WHERE id = p_wallet_id;
RETURN QUERY SELECT
true,
v_tx_id,
v_balance_after,
'Transaction already exists (idempotent)'::TEXT;
RETURN;
END IF;
END IF;
-- Lock wallet
SELECT * INTO v_wallet
FROM financial.wallets
WHERE id = p_wallet_id
FOR UPDATE;
IF NOT FOUND THEN
RETURN QUERY SELECT false, NULL::UUID, 0::DECIMAL, 'Wallet not found';
RETURN;
END IF;
IF v_wallet.status != 'active' THEN
RETURN QUERY SELECT false, NULL::UUID, v_wallet.balance,
'Wallet is not active (status: ' || v_wallet.status::TEXT || ')';
RETURN;
END IF;
-- Validar currency match
IF v_wallet.currency != p_currency THEN
RETURN QUERY SELECT false, NULL::UUID, v_wallet.balance,
'Currency mismatch (wallet: ' || v_wallet.currency::TEXT || ', transaction: ' || p_currency::TEXT || ')';
RETURN;
END IF;
-- Validar destination para transfers
IF p_transaction_type IN ('transfer_out', 'transfer_in') AND p_destination_wallet_id IS NULL THEN
RETURN QUERY SELECT false, NULL::UUID, v_wallet.balance,
'Transfer requires destination_wallet_id';
RETURN;
END IF;
-- No permitir self-transfers
IF p_destination_wallet_id = p_wallet_id THEN
RETURN QUERY SELECT false, NULL::UUID, v_wallet.balance,
'Cannot transfer to same wallet';
RETURN;
END IF;
v_balance_before := v_wallet.balance;
-- Crear transacción
INSERT INTO financial.wallet_transactions (
wallet_id,
transaction_type,
status,
amount,
fee,
currency,
balance_before,
destination_wallet_id,
reference_id,
description,
metadata,
idempotency_key,
processed_at
) VALUES (
p_wallet_id,
p_transaction_type,
CASE WHEN p_auto_complete THEN 'completed'::financial.transaction_status
ELSE 'pending'::financial.transaction_status END,
p_amount,
p_fee,
p_currency,
v_balance_before,
p_destination_wallet_id,
p_reference_id,
p_description,
p_metadata,
p_idempotency_key,
CASE WHEN p_auto_complete THEN NOW() ELSE NULL END
)
RETURNING id INTO v_tx_id;
-- Si es auto_complete, procesar inmediatamente
IF p_auto_complete THEN
-- Determinar operación de balance
CASE p_transaction_type
WHEN 'deposit', 'transfer_in', 'earning', 'distribution', 'bonus', 'refund' THEN
-- Aumentar balance
SELECT * INTO v_update_result
FROM financial.update_wallet_balance(
p_wallet_id,
p_amount - p_fee,
'add',
v_tx_id,
NULL,
'system',
'Transaction: ' || p_transaction_type::TEXT
);
WHEN 'withdrawal', 'transfer_out', 'fee' THEN
-- Disminuir balance
SELECT * INTO v_update_result
FROM financial.update_wallet_balance(
p_wallet_id,
p_amount + p_fee,
'subtract',
v_tx_id,
NULL,
'system',
'Transaction: ' || p_transaction_type::TEXT
);
ELSE
RETURN QUERY SELECT false, v_tx_id, v_balance_before,
'Unknown transaction type: ' || p_transaction_type::TEXT;
RETURN;
END CASE;
-- Verificar éxito de actualización
IF NOT v_update_result.success THEN
-- Marcar transacción como fallida
UPDATE financial.wallet_transactions
SET
status = 'failed',
failed_reason = v_update_result.error_message,
failed_at = NOW()
WHERE id = v_tx_id;
RETURN QUERY SELECT false, v_tx_id, v_balance_before, v_update_result.error_message;
RETURN;
END IF;
v_balance_after := v_update_result.new_balance;
-- Actualizar balance_after en transacción
UPDATE financial.wallet_transactions
SET
balance_after = v_balance_after,
completed_at = NOW()
WHERE id = v_tx_id;
-- Si es transfer_out, crear transfer_in en destino
IF p_transaction_type = 'transfer_out' AND p_destination_wallet_id IS NOT NULL THEN
SELECT * INTO v_update_result
FROM financial.process_transaction(
p_destination_wallet_id,
'transfer_in',
p_amount - p_fee, -- El fee lo paga el origen
p_currency,
0, -- Sin fee adicional en destino
'Transfer from wallet ' || p_wallet_id::TEXT,
p_reference_id,
p_wallet_id, -- Origen como destino inverso
p_idempotency_key || '_dest', -- Idempotency para destino
p_metadata,
true -- Auto-complete
);
IF v_update_result.success THEN
v_dest_tx_id := v_update_result.transaction_id;
-- Vincular transacciones
UPDATE financial.wallet_transactions
SET related_transaction_id = v_dest_tx_id
WHERE id = v_tx_id;
UPDATE financial.wallet_transactions
SET related_transaction_id = v_tx_id
WHERE id = v_dest_tx_id;
END IF;
END IF;
-- Actualizar totals en wallet
IF p_transaction_type IN ('deposit', 'transfer_in') THEN
UPDATE financial.wallets
SET total_deposits = total_deposits + p_amount
WHERE id = p_wallet_id;
ELSIF p_transaction_type IN ('withdrawal', 'transfer_out') THEN
UPDATE financial.wallets
SET total_withdrawals = total_withdrawals + p_amount
WHERE id = p_wallet_id;
END IF;
ELSE
-- Transaction pending, no balance update yet
v_balance_after := v_balance_before;
END IF;
RETURN QUERY SELECT true, v_tx_id, v_balance_after, NULL::TEXT;
END;
$$;
COMMENT ON FUNCTION financial.process_transaction IS 'Create and optionally complete a wallet transaction atomically';
-- Función para completar transacción pendiente
CREATE OR REPLACE FUNCTION financial.complete_transaction(
p_transaction_id UUID
)
RETURNS TABLE (
success BOOLEAN,
new_balance DECIMAL(20,8),
error_message TEXT
)
LANGUAGE plpgsql
AS $$
DECLARE
v_tx RECORD;
v_update_result RECORD;
BEGIN
-- Lock transaction
SELECT * INTO v_tx
FROM financial.wallet_transactions
WHERE id = p_transaction_id
FOR UPDATE;
IF NOT FOUND THEN
RETURN QUERY SELECT false, 0::DECIMAL, 'Transaction not found';
RETURN;
END IF;
IF v_tx.status != 'pending' THEN
RETURN QUERY SELECT false, 0::DECIMAL,
'Transaction is not pending (status: ' || v_tx.status::TEXT || ')';
RETURN;
END IF;
-- Procesar según tipo
CASE v_tx.transaction_type
WHEN 'deposit', 'transfer_in', 'earning', 'distribution', 'bonus', 'refund' THEN
SELECT * INTO v_update_result
FROM financial.update_wallet_balance(
v_tx.wallet_id,
v_tx.amount - v_tx.fee,
'add',
p_transaction_id
);
WHEN 'withdrawal', 'transfer_out', 'fee' THEN
SELECT * INTO v_update_result
FROM financial.update_wallet_balance(
v_tx.wallet_id,
v_tx.amount + v_tx.fee,
'subtract',
p_transaction_id
);
ELSE
RETURN QUERY SELECT false, 0::DECIMAL, 'Unknown transaction type';
RETURN;
END CASE;
IF NOT v_update_result.success THEN
-- Marcar como fallida
UPDATE financial.wallet_transactions
SET
status = 'failed',
failed_reason = v_update_result.error_message,
failed_at = NOW()
WHERE id = p_transaction_id;
RETURN QUERY SELECT false, 0::DECIMAL, v_update_result.error_message;
RETURN;
END IF;
-- Marcar como completada
UPDATE financial.wallet_transactions
SET
status = 'completed',
balance_after = v_update_result.new_balance,
completed_at = NOW(),
processed_at = COALESCE(processed_at, NOW())
WHERE id = p_transaction_id;
RETURN QUERY SELECT true, v_update_result.new_balance, NULL::TEXT;
END;
$$;
COMMENT ON FUNCTION financial.complete_transaction IS 'Complete a pending wallet transaction';