DDL schemas for Trading Platform: - User management - Authentication - Payments - Education - ML predictions - Trading data Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
284 lines
7.7 KiB
PL/PgSQL
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';
|