Entrada 9: Primeros SPs

Bitacora de Sesion

Fecha: 04/06/2026

Inicio: [13:00] | Fin: [15:30] || Total: [2 horas 30 minutos]

Presente: Matías Benavides Sandoval

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

¿QUÉ HICIMOS HOY?

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

Se reescribieron 5 SPs de Tarea2-BD que podíamos aprovechar con el modelo actual de PlanillaDB: sp_Login, sp_Logout, sp_GetError, sp_GetEmpleados y sp_GetEmpleadoById.

Se alinearon todos con las convenciones del proyecto: USE [PlanillaDB], IF OBJECT_ID DROP, SET XACT_ABORT ON, nombres de columnas correctos (Activo, ValorDocumento, PasswordHash), DBError con dbo., y TRY/CATCH.

Se validaron 6 escenarios contra localhost\SQLEXPRESS: sp_GetError (valid/invalid), sp_Login (success/wrong password), sp_GetEmpleados (all/filtered) y sp_GetEmpleadoById (found/not found).

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

PROBLEMAS DETECTADOS Y CÓMO SE RESOLVIERON

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

Problema: Los 5 SPs usaban USE VacacionesDB, DROP PROCEDURE IF EXISTS, y columnas del modelo viejo (EsActivo, ValorDocumentoIdentidad, PasswordHash como VARBINARY).

Causa: Eran las copias de Tarea2-BD no habiamos cambiado nada para adaptarlos a PlanillaDB.

Solución: Se reescribieron todos con USE [PlanillaDB], IF OBJECT_ID DROP, y los nombres de columna reales del esquema actual.

Problema: sp_Login y sp_Logout usaban INSERT INTO BitacoraEvento VALUES(...) directo, sin patrón de tabla variable + INSERT...SELECT.

Causa: No seguían la convención del profesor de preparar datos en una tabla variable y hacer el INSERT en una sola operación.

Solución: Se reescribieron con DECLARE @bitacoraData TABLE, INSERT INTO @bitacoraData, y luego BEGIN TRANSACTION → INSERT INTO BitacoraEvento SELECT FROM @bitacoraData → COMMIT.

Problema: sp_Login fallaba con Msg 3930 ("The current transaction cannot be committed") en el path de "usuario no encontrado" cuando se combinaba SET XACT_ABORT ON con BEGIN TRANSACTION/COMMIT explícito.

Causa: SET XACT_ABORT ON interactúa de forma incompatible con transacciones explícitas en ciertos flujos del SP.

Solución: Se documentó el bug  y se dejó pendiente para resolver en la próxima sesión — el SP funciona correctamente en los paths de login exitoso y password incorrecto.

Problema: sp_GetEmpleados y sp_GetEmpleadoById tenían columnas incorrectas y dynamic SQL innecesario.

Causa: sp_GetEmpleados filtraba por EsActivo y ValorDocumentoIdentidad; sp_GetEmpleadoById usaba dynamic SQL para resolver FechaContratacion.

Solución: Se corrigieron los nombres de columna y se eliminó el dynamic SQL (FechaContratacion ya existe en el esquema). Se agregó NOT EXISTS en sp_GetEmpleadoById para retornar 50012 si no se encuentra.

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

DUDAS Y DIVERGENCIAS DE CRITERIO

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

Se confirmó que los SPs de solo lectura (sp_GetError, sp_GetEmpleados, sp_GetEmpleadoById) no necesitan INSERT en BitacoraEvento — la trazabilidad de consultas se puede hacer desde la capa de aplicación.

El código de error 50012 para "empleado no encontrado" se definió como convención propia del SP, no está en la tabla Error del XML.

El bug de Msg 3930 en sp_Login se documentó pero no se resolvió — afecta solo al path de "usuario no encontrado" con lockout, no al login exitoso ni al password incorrecto.

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

AVANCE DEL CÓDIGO

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

USE [PlanillaDB];
GO

-- =====================================================
-- SP: Login de usuario
-- =====================================================
IF OBJECT_ID(N'dbo.sp_Login', N'P') IS NOT NULL
    DROP PROCEDURE [dbo].[sp_Login];
GO

CREATE PROCEDURE [dbo].[sp_Login]
    @inUsername VARCHAR(128),
    @inPassword VARCHAR(128),
    @inIpPostIn VARCHAR(64),
    @inPostTime DATETIME,
    @outResultCode INT OUTPUT
AS
BEGIN
    SET NOCOUNT ON;
    SET XACT_ABORT ON;

    SET @outResultCode = 0;

    DECLARE @idUsuario INT,
        @password VARCHAR(128),
        @idEventoSuccess INT,
        @idEventoFail INT,
        @idEventoDisabled INT,
        @intentosFallidos INT,
        @descripcion VARCHAR(512);

    DECLARE @bitacoraData TABLE (
        idTipoEvento INT,
        Descripcion VARCHAR(512),
        idUsuario INT,
        IpPostIn VARCHAR(64),
        PostTime DATETIME
    );

    BEGIN TRY

        SELECT @idEventoSuccess = t.id
        FROM dbo.TipoEvento t
        WHERE t.Nombre = 'Login Exitoso';

        SELECT @idEventoFail = t.id
        FROM dbo.TipoEvento t
        WHERE t.Nombre = 'Login No Exitoso';

        SELECT @idEventoDisabled = t.id
        FROM dbo.TipoEvento t
        WHERE t.Nombre = 'Login deshabilitado';

        SELECT @idUsuario = u.id, @password = u.PasswordHash
        FROM dbo.Usuario u
        WHERE u.Username = @inUsername;

        IF @idUsuario IS NULL
        BEGIN
            SELECT @intentosFallidos = COUNT(*)
            FROM dbo.BitacoraEvento b
            WHERE b.idTipoEvento = @idEventoFail
                AND b.IpPostIn = @inIpPostIn
                AND b.PostTime >= DATEADD(MINUTE, -20, @inPostTime);

            IF @intentosFallidos >= 5
            BEGIN
                SET @outResultCode = 50003;

                INSERT INTO @bitacoraData
                SELECT @idEventoDisabled, NULL, NULL, @inIpPostIn, @inPostTime;

                BEGIN TRANSACTION
                    INSERT INTO dbo.BitacoraEvento (idTipoEvento, Descripcion, idUsuario, IpPostIn, PostTime)
                    SELECT idTipoEvento, Descripcion, idUsuario, IpPostIn, PostTime
                    FROM @bitacoraData;
                COMMIT TRANSACTION;

                RETURN;
            END

            SET @descripcion = 'Intento: ' + CAST(@intentosFallidos + 1 AS VARCHAR(10))
                + ' | Error: ' + (SELECT Descripcion FROM dbo.Error WHERE Codigo = 50001);

            INSERT INTO @bitacoraData
            SELECT @idEventoFail, @descripcion, NULL, @inIpPostIn, @inPostTime;

            BEGIN TRANSACTION
                INSERT INTO dbo.BitacoraEvento (idTipoEvento, Descripcion, idUsuario, IpPostIn, PostTime)
                SELECT idTipoEvento, Descripcion, idUsuario, IpPostIn, PostTime
                FROM @bitacoraData;
            COMMIT TRANSACTION;

            SET @outResultCode = 50001;
            RETURN;
        END

        SELECT @intentosFallidos = COUNT(*)
        FROM dbo.BitacoraEvento b
        WHERE b.idTipoEvento = @idEventoFail
            AND b.idUsuario = @idUsuario
            AND b.IpPostIn = @inIpPostIn
            AND b.PostTime >= DATEADD(MINUTE, -20, @inPostTime);

        IF @intentosFallidos >= 5
        BEGIN
            SET @outResultCode = 50003;

            INSERT INTO @bitacoraData
            SELECT @idEventoDisabled, NULL, @idUsuario, @inIpPostIn, @inPostTime;

            BEGIN TRANSACTION
                INSERT INTO dbo.BitacoraEvento (idTipoEvento, Descripcion, idUsuario, IpPostIn, PostTime)
                SELECT idTipoEvento, Descripcion, idUsuario, IpPostIn, PostTime
                FROM @bitacoraData;
            COMMIT TRANSACTION;

            RETURN;
        END

        IF @password <> @inPassword
        BEGIN
            SET @outResultCode = 50002;

            SET @descripcion = 'Intento: ' + CAST(@intentosFallidos + 1 AS VARCHAR(10))
                + ' | Error: ' + (SELECT Descripcion FROM dbo.Error WHERE Codigo = 50002);

            INSERT INTO @bitacoraData
            SELECT @idEventoFail, @descripcion, @idUsuario, @inIpPostIn, @inPostTime;

            BEGIN TRANSACTION
                INSERT INTO dbo.BitacoraEvento (idTipoEvento, Descripcion, idUsuario, IpPostIn, PostTime)
                SELECT idTipoEvento, Descripcion, idUsuario, IpPostIn, PostTime
                FROM @bitacoraData;
            COMMIT TRANSACTION;

            RETURN;
        END

        INSERT INTO @bitacoraData
        SELECT @idEventoSuccess, 'Exitoso', @idUsuario, @inIpPostIn, @inPostTime;

        BEGIN TRANSACTION
            INSERT INTO dbo.BitacoraEvento (idTipoEvento, Descripcion, idUsuario, IpPostIn, PostTime)
            SELECT idTipoEvento, Descripcion, idUsuario, IpPostIn, PostTime
            FROM @bitacoraData;
        COMMIT TRANSACTION;

        SET @outResultCode = 0;

    END TRY
    BEGIN CATCH

        INSERT INTO DBError (UserName, Number, State, Severity, Line, [Procedure], Message, DateTime)
        VALUES (
            SYSTEM_USER,
            ERROR_NUMBER(),
            CAST(ERROR_STATE()    AS VARCHAR(32)),
            CAST(ERROR_SEVERITY() AS VARCHAR(32)),
            ERROR_LINE(),
            ISNULL(ERROR_PROCEDURE(), 'sp_Login'),
            ERROR_MESSAGE(),
            GETDATE()
        );

        SET @outResultCode = 50008;

    END CATCH
END;
GO


USE [PlanillaDB];
GO

-- =====================================================
-- SP: Logout de usuario
-- =====================================================
IF OBJECT_ID(N'dbo.sp_Logout', N'P') IS NOT NULL
    DROP PROCEDURE [dbo].[sp_Logout];
GO

CREATE PROCEDURE [dbo].[sp_Logout]
    @inIdUsuario INT,
    @inIpPostIn VARCHAR(64),
    @inPostTime DATETIME,
    @outResultCode INT OUTPUT
AS
BEGIN
    SET NOCOUNT ON;
    SET XACT_ABORT ON;

    SET @outResultCode = 0;

    DECLARE @idTipoEventoLogout INT;

    DECLARE @bitacoraData TABLE (
        idTipoEvento INT,
        Descripcion VARCHAR(512),
        idUsuario INT,
        IpPostIn VARCHAR(64),
        PostTime DATETIME
    );

    BEGIN TRY

        SELECT @idTipoEventoLogout = t.id
        FROM dbo.TipoEvento t
        WHERE t.Nombre = 'Logout';

        INSERT INTO @bitacoraData
        SELECT @idTipoEventoLogout, NULL, @inIdUsuario, @inIpPostIn, @inPostTime;

        BEGIN TRANSACTION
            INSERT INTO dbo.BitacoraEvento (idTipoEvento, Descripcion, idUsuario, IpPostIn, PostTime)
            SELECT idTipoEvento, Descripcion, idUsuario, IpPostIn, PostTime
            FROM @bitacoraData;
        COMMIT TRANSACTION;

        SET @outResultCode = 0;

    END TRY
    BEGIN CATCH

        INSERT INTO DBError (UserName, Number, State, Severity, Line, [Procedure], Message, DateTime)
        VALUES (
            SYSTEM_USER,
            ERROR_NUMBER(),
            CAST(ERROR_STATE()    AS VARCHAR(32)),
            CAST(ERROR_SEVERITY() AS VARCHAR(32)),
            ERROR_LINE(),
            ISNULL(ERROR_PROCEDURE(), 'sp_Logout'),
            ERROR_MESSAGE(),
            GETDATE()
        );

        SET @outResultCode = 50008;

    END CATCH
END;
GO

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

MORALEJAS / BUENAS PRÁCTICAS

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

Los SPs de solo lectura no necesitan transacciones explícitas ni logging a BitacoraEvento — simplifica el código y evita bugs de sp_Login.

Antes de reescribir un SP, comparar los nombres de columna del viejo contra Tablas.sql para detectar todas las diferencias de una vez.

El patrón table variable + INSERT...SELECT + transacción explícita funciona bien para SPs que escriben en BitacoraEvento, pero hay que tener cuidado con SET XACT_ABORT ON en paths donde no se hace INSERT (como el return temprano de sp_Login).

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

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

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

Corregir el bug de en sp_Login (path de usuario no encontrado con lockout).

Continuar con la Fase 1: sp_InsertarEmpleado, sp_UpdateEmpleado, sp_DeleteEmpleado (Matías).

Definir el mecanismo de generación del PDF del Comprobante (si es que si se tiene que hacer claro).

Comentarios