trading-platform-database-v2/ddl/schemas/financial/functions/01-update_wallet_balance.sql
rckrdmrd 45e77e9a9c feat: Initial commit - Database schemas and scripts
DDL schemas for Trading Platform:
- User management
- Authentication
- Payments
- Education
- ML predictions
- Trading data

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 04:30:23 -06:00

284 lines
7.7 KiB
PL/PgSQL

-- =====================================================
-- ORBIQUANT IA - UPDATE WALLET BALANCE FUNCTION
-- =====================================================
-- Description: Safely update wallet balance with audit trail
-- Schema: financial
-- =====================================================
CREATE OR REPLACE FUNCTION financial.update_wallet_balance(
p_wallet_id UUID,
p_amount DECIMAL(20,8),
p_operation VARCHAR(20), -- 'add', 'subtract', 'set'
p_transaction_id UUID DEFAULT NULL,
p_actor_id UUID DEFAULT NULL,
p_actor_type VARCHAR(50) DEFAULT 'system',
p_reason TEXT DEFAULT NULL,
p_metadata JSONB DEFAULT '{}'
)
RETURNS TABLE (
success BOOLEAN,
new_balance DECIMAL(20,8),
new_available DECIMAL(20,8),
error_message TEXT
)
LANGUAGE plpgsql
AS $$
DECLARE
v_wallet RECORD;
v_old_balance DECIMAL(20,8);
v_old_available DECIMAL(20,8);
v_new_balance DECIMAL(20,8);
v_new_available DECIMAL(20,8);
BEGIN
-- Lock wallet row for update
SELECT * INTO v_wallet
FROM financial.wallets
WHERE id = p_wallet_id
FOR UPDATE;
-- Validar que existe
IF NOT FOUND THEN
RETURN QUERY SELECT false, 0::DECIMAL, 0::DECIMAL, 'Wallet not found';
RETURN;
END IF;
-- Validar que está activa
IF v_wallet.status != 'active' THEN
RETURN QUERY SELECT false, v_wallet.balance, v_wallet.available_balance,
'Wallet is not active (status: ' || v_wallet.status::TEXT || ')';
RETURN;
END IF;
-- Guardar valores antiguos
v_old_balance := v_wallet.balance;
v_old_available := v_wallet.available_balance;
-- Calcular nuevo balance según operación
CASE p_operation
WHEN 'add' THEN
v_new_balance := v_old_balance + p_amount;
v_new_available := v_old_available + p_amount;
WHEN 'subtract' THEN
v_new_balance := v_old_balance - p_amount;
v_new_available := v_old_available - p_amount;
WHEN 'set' THEN
v_new_balance := p_amount;
v_new_available := p_amount - v_wallet.pending_balance;
ELSE
RETURN QUERY SELECT false, v_old_balance, v_old_available,
'Invalid operation: ' || p_operation;
RETURN;
END CASE;
-- Validar que no quede negativo
IF v_new_balance < 0 THEN
RETURN QUERY SELECT false, v_old_balance, v_old_available,
'Insufficient balance (current: ' || v_old_balance::TEXT || ', required: ' || p_amount::TEXT || ')';
RETURN;
END IF;
IF v_new_available < 0 THEN
RETURN QUERY SELECT false, v_old_balance, v_old_available,
'Insufficient available balance (current: ' || v_old_available::TEXT || ')';
RETURN;
END IF;
-- Validar min_balance si existe
IF v_wallet.min_balance IS NOT NULL AND v_new_available < v_wallet.min_balance THEN
RETURN QUERY SELECT false, v_old_balance, v_old_available,
'Would violate minimum balance requirement (min: ' || v_wallet.min_balance::TEXT || ')';
RETURN;
END IF;
-- Actualizar wallet
UPDATE financial.wallets
SET
balance = v_new_balance,
available_balance = v_new_available,
last_transaction_at = NOW(),
updated_at = NOW()
WHERE id = p_wallet_id;
-- Registrar en audit log
INSERT INTO financial.wallet_audit_log (
wallet_id,
action,
actor_id,
actor_type,
old_values,
new_values,
balance_before,
balance_after,
transaction_id,
reason,
metadata
) VALUES (
p_wallet_id,
'balance_updated',
p_actor_id,
p_actor_type,
jsonb_build_object(
'balance', v_old_balance,
'available_balance', v_old_available
),
jsonb_build_object(
'balance', v_new_balance,
'available_balance', v_new_available
),
v_old_balance,
v_new_balance,
p_transaction_id,
p_reason,
p_metadata
);
-- Retornar éxito
RETURN QUERY SELECT true, v_new_balance, v_new_available, NULL::TEXT;
END;
$$;
COMMENT ON FUNCTION financial.update_wallet_balance IS 'Safely update wallet balance with validation and audit trail';
-- Función helper para reservar fondos (pending balance)
CREATE OR REPLACE FUNCTION financial.reserve_wallet_funds(
p_wallet_id UUID,
p_amount DECIMAL(20,8),
p_reason TEXT DEFAULT NULL
)
RETURNS TABLE (
success BOOLEAN,
new_available DECIMAL(20,8),
new_pending DECIMAL(20,8),
error_message TEXT
)
LANGUAGE plpgsql
AS $$
DECLARE
v_wallet RECORD;
BEGIN
-- Lock wallet
SELECT * INTO v_wallet
FROM financial.wallets
WHERE id = p_wallet_id
FOR UPDATE;
IF NOT FOUND THEN
RETURN QUERY SELECT false, 0::DECIMAL, 0::DECIMAL, 'Wallet not found';
RETURN;
END IF;
IF v_wallet.status != 'active' THEN
RETURN QUERY SELECT false, v_wallet.available_balance, v_wallet.pending_balance,
'Wallet is not active';
RETURN;
END IF;
IF v_wallet.available_balance < p_amount THEN
RETURN QUERY SELECT false, v_wallet.available_balance, v_wallet.pending_balance,
'Insufficient available balance';
RETURN;
END IF;
-- Mover de available a pending
UPDATE financial.wallets
SET
available_balance = available_balance - p_amount,
pending_balance = pending_balance + p_amount,
updated_at = NOW()
WHERE id = p_wallet_id;
-- Audit log
INSERT INTO financial.wallet_audit_log (
wallet_id, action, actor_type, reason,
old_values, new_values
) VALUES (
p_wallet_id, 'balance_updated', 'system', p_reason,
jsonb_build_object('available', v_wallet.available_balance, 'pending', v_wallet.pending_balance),
jsonb_build_object('available', v_wallet.available_balance - p_amount, 'pending', v_wallet.pending_balance + p_amount)
);
RETURN QUERY SELECT
true,
v_wallet.available_balance - p_amount,
v_wallet.pending_balance + p_amount,
NULL::TEXT;
END;
$$;
COMMENT ON FUNCTION financial.reserve_wallet_funds IS 'Reserve funds by moving from available to pending balance';
-- Función helper para liberar fondos reservados
CREATE OR REPLACE FUNCTION financial.release_wallet_funds(
p_wallet_id UUID,
p_amount DECIMAL(20,8),
p_to_available BOOLEAN DEFAULT true,
p_reason TEXT DEFAULT NULL
)
RETURNS TABLE (
success BOOLEAN,
new_available DECIMAL(20,8),
new_pending DECIMAL(20,8),
error_message TEXT
)
LANGUAGE plpgsql
AS $$
DECLARE
v_wallet RECORD;
v_new_balance DECIMAL(20,8);
BEGIN
-- Lock wallet
SELECT * INTO v_wallet
FROM financial.wallets
WHERE id = p_wallet_id
FOR UPDATE;
IF NOT FOUND THEN
RETURN QUERY SELECT false, 0::DECIMAL, 0::DECIMAL, 'Wallet not found';
RETURN;
END IF;
IF v_wallet.pending_balance < p_amount THEN
RETURN QUERY SELECT false, v_wallet.available_balance, v_wallet.pending_balance,
'Insufficient pending balance';
RETURN;
END IF;
-- Liberar fondos
IF p_to_available THEN
-- Devolver a available
v_new_balance := v_wallet.balance;
UPDATE financial.wallets
SET
available_balance = available_balance + p_amount,
pending_balance = pending_balance - p_amount,
updated_at = NOW()
WHERE id = p_wallet_id;
ELSE
-- Remover completamente (ej: después de withdrawal exitoso)
v_new_balance := v_wallet.balance - p_amount;
UPDATE financial.wallets
SET
balance = balance - p_amount,
pending_balance = pending_balance - p_amount,
updated_at = NOW()
WHERE id = p_wallet_id;
END IF;
-- Audit log
INSERT INTO financial.wallet_audit_log (
wallet_id, action, actor_type, reason, metadata
) VALUES (
p_wallet_id, 'balance_updated', 'system', p_reason,
jsonb_build_object('released_amount', p_amount, 'to_available', p_to_available)
);
SELECT available_balance, pending_balance INTO v_wallet
FROM financial.wallets
WHERE id = p_wallet_id;
RETURN QUERY SELECT true, v_wallet.available_balance, v_wallet.pending_balance, NULL::TEXT;
END;
$$;
COMMENT ON FUNCTION financial.release_wallet_funds IS 'Release reserved funds back to available or remove from balance';