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