-- ===================================================== -- 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';