327 lines
9.0 KiB
PL/PgSQL
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';
|