-- He separado el procedimiento de activacion en otro archivo por comodidad de desarrollo -- creo que es mas seguro meter la lógica de activaciones directamente en la bdd que en -- un server node y asi evitamos problemas de consistencia entre versiones. -- CREATE OR REPLACE FUNCTION activate_payment_card( p_card_id UUID, -- uuidv7 de la tarjeta p_input_code TEXT, -- El código que mete el usuario, puede ser incorrecto en este punto p_ip_address INET, -- Datos de origen de la activación p_device_info JSONB -- Datos del dispositivo de origen ) RETURNS TABLE ( success BOOLEAN, message TEXT ) AS $$ DECLARE v_code_hash TEXT; v_is_used BOOLEAN; v_is_blocked BOOLEAN; v_expires_at TIMESTAMPTZ; v_failed_attempts INT; v_max_attempts CONSTANT INT := 3; BEGIN -- 1. Obtener datos del código y bloquear la fila para actualización (FOR UPDATE) -- sería raro que 2 clientes intentasen modificar la tarjeta a la vez. SELECT code_hash, is_used, is_blocked, expires_at, failed_attempts INTO v_code_hash, v_is_used, v_is_blocked, v_expires_at, v_failed_attempts FROM activation_codes WHERE card_id = p_card_id FOR UPDATE; -- 2. Validaciones: -- 2.1 Si no existe ninguna tarjeta con ese card_id IF NOT FOUND THEN RETURN QUERY SELECT FALSE, 'CARD_NOT_FOUND'; RETURN; END IF; -- 2.2 Si el código introducido ya ha sido usado IF v_is_used THEN RETURN QUERY SELECT FALSE, 'ALREADY_ACTIVATED'; RETURN; END IF; -- 2.3 Si el código ya ha sido bloqueado o se ha intentado demasiadas veces IF v_is_blocked OR v_failed_attempts >= v_max_attempts THEN RETURN QUERY SELECT FALSE, 'CODE_BLOCKED'; RETURN; END IF; -- 2.4 Si el código es demasiado viejo IF v_expires_at < NOW() THEN RETURN QUERY SELECT FALSE, 'CODE_EXPIRED'; RETURN; END IF; -- 3. Verificación del Hash del codigo que ha introducido el usuario -- En la bdd guardo el hash, al procedimiento se introduce el codigo per-se -- Si el código NO coincide: IF v_code_hash != crypt(p_input_code, v_code_hash) THEN UPDATE activation_codes -- 3.1 Control de intentos SET failed_attempts = failed_attempts + 1, is_blocked = (failed_attempts + 1 >= v_max_attempts) WHERE card_id = p_card_id; -- 3.2 Se loguea el fallo INSERT INTO activation_logs (card_id, action_type, ip_address, device_info) VALUES (p_card_id, 'FAILED_ATTEMPT', p_ip_address, p_device_info); RETURN QUERY SELECT FALSE, 'INVALID_CODE'; RETURN; END IF; -- 4. Si el código ES correcto: -- Marcar código como usado UPDATE activation_codes SET is_used = TRUE WHERE card_id = p_card_id; -- Activar la tarjeta UPDATE payment_cards SET status = 'ACTIVE', activated_at = NOW() WHERE card_id = p_card_id; -- Registrar éxito en auditoría INSERT INTO activation_logs (card_id, action_type, ip_address, device_info) VALUES (p_card_id, 'ACTIVATION_SUCCESS', p_ip_address, p_device_info); RETURN QUERY SELECT TRUE, 'SUCCESS'; EXCEPTION WHEN OTHERS THEN -- En caso de error inesperado, Postgres hace rollback automático RETURN QUERY SELECT FALSE, 'INTERNAL_ERROR: ' || SQLERRM; END; $$ LANGUAGE plpgsql;