Entrada 13 (Integración de sp_GetError)

Fecha: 26/04/2026

Inicio: [23:07] | Fin: [23:53] || Total: [46 minutos ]

Presentes: Sebastián Ramírez Abarca

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

¿QUÉ HICIMOS HOY?

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

- Se integró el sp_GetError a las funciones de capa lógica, para esto:

    + Se creó errorHelper.ts para que contenga la función getErrorMessage que recibe el código de error y llama a sp_GetError.

   + Se modificó empleadoController.ts para que importe errorHelper y llame getErrorMessage si se detecta un error en todas las funciones excepto que los errores sean de espacios de datos vacíos o  los del catch porque son errores para la capa de presentación.

 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

PROBLEMAS ENCONTRADOS Y CÓMO SE RESOLVIERON

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

- Hubo un error de comunicación entre mi compañero y yo, resulta que ambos trabajamos el mismo archivo a la vez aunque con propósitos diferentes, él hizo el commit antes que yo pero solo me di cuenta cuando al intentar hacer mi commit me dio error con el merge.

Solución: Volví a hacer los cambios pero desde el nuevo commit de mi compañero, por eso getErrorMessage está en un archivo aparte.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

DUDAS Y DIVERGENCIAS DE CRITERIO

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

- Debido a que los errores de los catch usan el código 50008 no se decidió si se debía usar getErrorMessage en ellos.

 

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

AVANCE DEL CÓDIGO

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

errorHelper.ts:

import { getPool, sql } from './db/connection';

export async function getErrorMessage(codigo: number): Promise<string> {
    try {
        const pool = await getPool();

        const result = await pool
            .request()
            .input('inCodigo', sql.Int, codigo)
            .output('outResultCode', sql.Int)
            .execute('sp_GetError');

        return result.recordset?.[0]?.Descripcion ?? 'Error desconocido';
       
    } catch (error) {
        console.error('Error al obtener el mensaje de error:', error);
        return 'Error desconocido';
    }
}

versión actualizada de empleadoController.ts:

import { Request, Response } from 'express';
import { getPool, sql } from '../db/connection';
import { getErrorMessage } from '../utils/errorHelper';

// Resolver el id del usuario a partir del username.
// Se usa en insert, update y delete porque los SPs guardan trazabilidad en BitacoraEvento.
async function resolveUsuarioId(pool: Awaited<ReturnType<typeof getPool>>, username: string): Promise<number | null> {
    const usuarioResult = await pool
        .request()
        .input('inUsername', sql.VarChar(128), username)
        .query('SELECT id FROM Usuario WHERE Username = @inUsername');

    return usuarioResult.recordset?.[0]?.id ?? null;
}

// GET /api/empleados
// Invoca sp_GetEmpleados y retorna el listado filtrado
export async function getEmpleados(req: Request, res: Response): Promise<void> {
    try {
        const filtro = String(req.query.filtro ?? '').trim();

        // Temporal para pruebas; luego se toma desde token/sesion
        const username = String(req.headers['x-username'] ?? 'UsuarioScripts');
        const ipPostIn = req.ip ?? '';
        const postTime = new Date();

        const pool = await getPool();
        let usernameForLog = username;
        const usuarioId = await resolveUsuarioId(pool, username);

        if (!usuarioId) {
            const fallbackResult = await pool
                .request()
                .query('SELECT TOP 1 Username FROM Usuario ORDER BY Id');

            usernameForLog = String(fallbackResult.recordset?.[0]?.Username ?? 'UsuarioScripts');
        }

        const result = await pool
            .request()
            .input('inFiltro', sql.VarChar(128), filtro)
            .input('inUsername', sql.VarChar(128), usernameForLog)
            .input('inIpPostIn', sql.VarChar(64), ipPostIn)
            .input('inPostTime', sql.DateTime, postTime)
            .output('outResultCode', sql.Int)
            .execute('sp_GetEmpleados');

        const outResultCode: number = result.output.outResultCode;

        if (outResultCode !== 0) {
            res.status(400).json({
                success: false,
                outResultCode,
                message: await getErrorMessage(outResultCode)
            });
            return;
        }

        res.status(200).json({
            success: true,
            outResultCode,
            data: result.recordset
        });
    } catch (error) {
        console.error('Error en getEmpleados:', error);
        res.status(500).json({
            success: false,
            outResultCode: 50008,
            message: 'Error interno del servidor'
        });
    }
}

// POST /api/empleados
// Invoca sp_InsertarEmpleado para crear un nuevo empleado
export async function insertEmpleado(req: Request, res: Response): Promise<void> {
    const { valorDocumentoIdentidad, nombre, idPuesto } = req.body;

    if (!valorDocumentoIdentidad || !nombre || idPuesto === undefined) {
        res.status(400).json({
            success: false,
            message: 'valorDocumentoIdentidad, nombre e idPuesto son requeridos'
        });
        return;
    }

    try {
        // Temporal para pruebas; luego se toma desde token/sesion
        const username = String(req.headers['x-username'] ?? 'UsuarioScripts');
        const ipPostIn = req.ip ?? '';
        const postTime = new Date();

        const pool = await getPool();

        // El SP necesita el id del usuario para dejar la traza en la bitácora.
        const idUsuario = await resolveUsuarioId(pool, username);

        if (!idUsuario) {
            res.status(400).json({
                success: false,
                outResultCode: 50001,
                message: await getErrorMessage(50001)
            });
            return;
        }

        const result = await pool
            .request()
            .input('inValorDocumentoIdentidad', sql.VarChar(32), String(valorDocumentoIdentidad))
            .input('inNombre', sql.VarChar(128), String(nombre))
            .input('inIdPuesto', sql.Int, Number(idPuesto))
            .input('inIdUsuario', sql.Int, idUsuario)
            .input('inIpPostIn', sql.VarChar(64), ipPostIn)
            .input('inPostTime', sql.DateTime, postTime)
            .output('outResultCode', sql.Int)
            .execute('sp_InsertarEmpleado');

        const outResultCode: number = result.output.outResultCode;

        if (outResultCode !== 0) {
            res.status(400).json({
                success: false,
                outResultCode,
                message: await getErrorMessage(outResultCode)
            });
            return;
        }

        res.status(201).json({
            success: true,
            outResultCode,
            message: 'Empleado creado exitosamente'
        });
    } catch (error) {
        console.error('Error en insertEmpleado:', error);
        res.status(500).json({
            success: false,
            outResultCode: 50008,
            message: 'Error interno del servidor'
        });
    }
}

// GET /api/empleados/:valorDocumentoIdentidad
// Invoca sp_GetEmpleadoById y retorna un empleado activo.
// Este endpoint nos sirve para cargar los formularios de consulta, edición y borrado.
export async function getEmpleadoById(req: Request, res: Response): Promise<void> {
    try {
        const valorDocumentoIdentidad = String(req.params.valorDocumentoIdentidad ?? '').trim();

        if (!valorDocumentoIdentidad) {
            res.status(400).json({
                success: false,
                message: 'valorDocumentoIdentidad es requerido'
            });
            return;
        }

        const pool = await getPool();

        const result = await pool
            .request()
            .input('inValorDocumentoIdentidad', sql.VarChar(32), valorDocumentoIdentidad)
            .output('outResultCode', sql.Int)
            .execute('sp_GetEmpleadoById');

        const outResultCode: number = result.output.outResultCode;

        if (outResultCode !== 0) {
            res.status(404).json({
                success: false,
                outResultCode,
                message: await getErrorMessage(outResultCode)
            });
            return;
        }

        res.status(200).json({
            success: true,
            outResultCode,
            data: result.recordset?.[0] ?? null
        });
    } catch (error) {
        console.error('Error en getEmpleadoById:', error);
        res.status(500).json({
            success: false,
            outResultCode: 50008,
            message: 'Error interno del servidor'
        });
    }
}

// PATCH /api/empleados/:valorDocumentoIdentidad
// Invoca sp_UpdateEmpleado.
// El SP recibe valores "antes" y "después" para validar duplicados y dejar la bitácora completa.
export async function updateEmpleado(req: Request, res: Response): Promise<void> {
    const valorDocumentoIdentidadAntes = String(req.params.valorDocumentoIdentidad ?? '').trim();
    const {
        valorDocumentoIdentidadDespues,
        nombreAntes,
        nombreDespues,
        idPuestoAntes,
        idPuestoDespues
    } = req.body;

    if (!valorDocumentoIdentidadAntes || !valorDocumentoIdentidadDespues || !nombreAntes || !nombreDespues || idPuestoAntes === undefined || idPuestoDespues === undefined) {
        res.status(400).json({
            success: false,
            message: 'Faltan datos para actualizar el empleado'
        });
        return;
    }

    try {
        // Igual que en insert, obtenemos el usuario para que el SP registre la operación.
        const username = String(req.headers['x-username'] ?? 'UsuarioScripts');
        const ipPostIn = req.ip ?? '';
        const postTime = new Date();

        const pool = await getPool();
        const idUsuario = await resolveUsuarioId(pool, username);

        if (!idUsuario) {
            res.status(400).json({
                success: false,
                outResultCode: 50001,
                message: await getErrorMessage(50001)
            });
            return;
        }

        const result = await pool
            .request()
            .input('inValorDocumentoIdentidadAntes', sql.VarChar(32), valorDocumentoIdentidadAntes)
            .input('inValorDocumentoIdentidadDespues', sql.VarChar(32), String(valorDocumentoIdentidadDespues))
            .input('inNombreAntes', sql.VarChar(128), String(nombreAntes))
            .input('inNombreDespues', sql.VarChar(128), String(nombreDespues))
            .input('inIdPuestoAntes', sql.Int, Number(idPuestoAntes))
            .input('inIdPuestoDespues', sql.Int, Number(idPuestoDespues))
            .input('inIdUsuario', sql.Int, idUsuario)
            .input('inIpPostIn', sql.VarChar(64), ipPostIn)
            .input('inPostTime', sql.DateTime, postTime)
            .output('outResultCode', sql.Int)
            .execute('sp_UpdateEmpleado');

        const outResultCode: number = result.output.outResultCode;

        if (outResultCode !== 0) {
            res.status(400).json({
                success: false,
                outResultCode,
                message: await getErrorMessage(outResultCode)
            });
            return;
        }

        res.status(200).json({
            success: true,
            outResultCode,
            message: 'Empleado actualizado exitosamente'
        });
    } catch (error) {
        console.error('Error en updateEmpleado:', error);
        res.status(500).json({
            success: false,
            outResultCode: 50008,
            message: 'Error interno del servidor'
        });
    }
}

// DELETE /api/empleados/:valorDocumentoIdentidad
// Invoca sp_DeleteEmpleado.
// El SP maneja tanto el intento como el borrado real según el flag confirmado.
export async function deleteEmpleado(req: Request, res: Response): Promise<void> {
    const valorDocumentoIdentidad = String(req.params.valorDocumentoIdentidad ?? '').trim();
    const confirmado = Boolean(req.body?.confirmado);

    if (!valorDocumentoIdentidad) {
        res.status(400).json({
            success: false,
            message: 'valorDocumentoIdentidad es requerido'
        });
        return;
    }

    try {
        // Igual que en las demás operaciones, capturamos contexto para la bitácora.
        const username = String(req.headers['x-username'] ?? 'UsuarioScripts');
        const ipPostIn = req.ip ?? '';
        const postTime = new Date();

        const pool = await getPool();
        const idUsuario = await resolveUsuarioId(pool, username);

        if (!idUsuario) {
            res.status(400).json({
                success: false,
                outResultCode: 50001,
                message: await getErrorMessage(50001)
            });
            return;
        }

        const result = await pool
            .request()
            .input('inValorDocumentoIdentidad', sql.VarChar(32), valorDocumentoIdentidad)
            .input('inIdUsuario', sql.Int, idUsuario)
            .input('inIpPostIn', sql.VarChar(64), ipPostIn)
            .input('inPostTime', sql.DateTime, postTime)
            .input('inConfirmado', sql.Bit, confirmado)
            .output('outResultCode', sql.Int)
            .execute('sp_DeleteEmpleado');

        const outResultCode: number = result.output.outResultCode;

        if (outResultCode !== 0) {
            res.status(400).json({
                success: false,
                outResultCode,
                message: await getErrorMessage(outResultCode)
            });
            return;
        }

        res.status(200).json({
            success: true,
            outResultCode,
            message: confirmado ? 'Empleado eliminado exitosamente' : 'Intento de borrado registrado'
        });
    } catch (error) {
        console.error('Error en deleteEmpleado:', error);
        res.status(500).json({
            success: false,
            outResultCode: 50008,
            message: 'Error interno del servidor'
        });
    }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

MORALEJAS / BUENAS PRÁCTICAS

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

- No mezclar errores de BD con errores de aplicación: Los catch con 'Error interno del servidor' son errores de conexión o excepciones inesperadas, no tienen código en la tabla Error y por eso se manejan diferente.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

PRÓXIMA SESIÓN: ¿QUÉ SIGUE?

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

- Hacer el controller de movimientos.

Comentarios

Entradas más populares de este blog

Entrada 16 (Controlador de movimientos)

Entrada 20 (Lógica de insertarMovimientos, conectarla y probarla)

Entrada 21 (Análisis de resultados)