-- V013__create_puntos_de_venta.sql
-- ADM-008 Puntos de Venta: DDL + SP de reserva atómica de número de comprobante.
--
-- Cambios:
--   1. dbo.PuntoDeVenta (FK→Medio, UNIQUE(MedioId,NumeroAFIP), SYSTEM_VERSIONING ON, retention 10Y).
--   2. dbo.SecuenciaComprobante (FK→PuntoDeVenta, PK(PuntoDeVentaId,TipoComprobante), SYSTEM_VERSIONING ON, retention 10Y).
--   3. Permiso 'administracion:puntos-de-venta:gestionar' + asignación a rol 'admin'.
--   4. SP dbo.usp_ReservarNumeroComprobante (SERIALIZABLE, UPDATE+OUTPUT, THROW 50001/50002/50003).
--
-- Patrón: V011 (Temporal Tables + Permiso MERGE + PAGE compression en history).
-- Idempotente: seguro para re-ejecutar.
-- Reversa: V013_ROLLBACK.sql.
-- Run on: SIGCM2 (dev) y SIGCM2_Test (integration tests).
--
-- NOTA: el código de permiso usa guion_bajo (_) según CK_Permiso_Codigo_Format del proyecto.
--   Código efectivo: 'administracion:puntos_de_venta:gestionar'
--   El spec menciona 'administracion:puntos-de-venta:gestionar' (guion) pero el CHECK constraint
--   solo permite [a-z0-9_:] — se usa guion_bajo para cumplir la constraint existente.
--   El backend y frontend deben usar el código con guion_bajo.
--
-- Covers: REQ-PDV-001, -003, -009, REQ-SEC-CMB-001, -002, -003, -004
-- Source of truth: Obsidian/02-ARQUITECTURA-y-TECH-STACK/2.10 ADM-008
--
-- NOTA T1.3 — Seeds: NO se seedean PuntoDeVenta.
--   Cada instalación configura sus propios PdVs con el NumeroAFIP real asignado por AFIP/ARCA.
--   Seedear con valores ficticios generaría confusión operativa. El admin los crea manualmente.

SET QUOTED_IDENTIFIER ON;
SET ANSI_NULLS ON;
SET NOCOUNT ON;
GO

-- ═══════════════════════════════════════════════════════════════════════
-- 1. dbo.PuntoDeVenta
-- ═══════════════════════════════════════════════════════════════════════

IF OBJECT_ID(N'dbo.PuntoDeVenta', N'U') IS NULL
BEGIN
    CREATE TABLE dbo.PuntoDeVenta (
        Id                INT IDENTITY(1,1)  NOT NULL CONSTRAINT PK_PuntoDeVenta PRIMARY KEY,
        MedioId           INT                NOT NULL,
        NumeroAFIP        SMALLINT           NOT NULL,
        Nombre            NVARCHAR(100)      NOT NULL,
        Descripcion       NVARCHAR(255)      NULL,
        Activo            BIT                NOT NULL CONSTRAINT DF_PuntoDeVenta_Activo          DEFAULT(1),
        FechaCreacion     DATETIME2(3)       NOT NULL CONSTRAINT DF_PuntoDeVenta_FechaCreacion   DEFAULT(SYSUTCDATETIME()),
        FechaModificacion DATETIME2(3)       NULL,
        CONSTRAINT FK_PuntoDeVenta_Medio    FOREIGN KEY (MedioId) REFERENCES dbo.Medio(Id) ON DELETE NO ACTION,
        CONSTRAINT UQ_PuntoDeVenta_Medio_AFIP UNIQUE (MedioId, NumeroAFIP),
        CONSTRAINT CK_PuntoDeVenta_NumeroAFIP CHECK (NumeroAFIP >= 1)
    );
    PRINT 'Table dbo.PuntoDeVenta created.';
END
ELSE
    PRINT 'Table dbo.PuntoDeVenta already exists — skip.';
GO

IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_PuntoDeVenta_MedioId_Activo' AND object_id = OBJECT_ID('dbo.PuntoDeVenta'))
BEGIN
    CREATE INDEX IX_PuntoDeVenta_MedioId_Activo
        ON dbo.PuntoDeVenta(MedioId, Activo)
        INCLUDE (NumeroAFIP, Nombre);
    PRINT 'Index IX_PuntoDeVenta_MedioId_Activo created.';
END
GO

-- ═══════════════════════════════════════════════════════════════════════
-- 2. dbo.SecuenciaComprobante
-- ═══════════════════════════════════════════════════════════════════════

IF OBJECT_ID(N'dbo.SecuenciaComprobante', N'U') IS NULL
BEGIN
    CREATE TABLE dbo.SecuenciaComprobante (
        PuntoDeVentaId    INT          NOT NULL,
        TipoComprobante   TINYINT      NOT NULL,
        UltimoNumero      INT          NOT NULL CONSTRAINT DF_SecuenciaComprobante_UltimoNumero DEFAULT(0),
        FechaCreacion     DATETIME2(3) NOT NULL CONSTRAINT DF_SecuenciaComprobante_FechaCreacion DEFAULT(SYSUTCDATETIME()),
        FechaModificacion DATETIME2(3) NULL,
        CONSTRAINT PK_SecuenciaComprobante PRIMARY KEY (PuntoDeVentaId, TipoComprobante),
        CONSTRAINT FK_SecuenciaComprobante_PuntoDeVenta FOREIGN KEY (PuntoDeVentaId) REFERENCES dbo.PuntoDeVenta(Id) ON DELETE NO ACTION,
        CONSTRAINT CK_SecuenciaComprobante_TipoComprobante CHECK (TipoComprobante BETWEEN 1 AND 6),
        CONSTRAINT CK_SecuenciaComprobante_UltimoNumero    CHECK (UltimoNumero >= 0)
    );
    PRINT 'Table dbo.SecuenciaComprobante created.';
END
ELSE
    PRINT 'Table dbo.SecuenciaComprobante already exists — skip.';
GO

-- ═══════════════════════════════════════════════════════════════════════
-- 3. SYSTEM_VERSIONING — PuntoDeVenta
-- ═══════════════════════════════════════════════════════════════════════

IF COL_LENGTH('dbo.PuntoDeVenta', 'ValidFrom') IS NULL
BEGIN
    ALTER TABLE dbo.PuntoDeVenta
    ADD
        ValidFrom DATETIME2(3) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL
            CONSTRAINT DF_PuntoDeVenta_ValidFrom DEFAULT(SYSUTCDATETIME()),
        ValidTo   DATETIME2(3) GENERATED ALWAYS AS ROW END   HIDDEN NOT NULL
            CONSTRAINT DF_PuntoDeVenta_ValidTo   DEFAULT(CONVERT(DATETIME2(3), '9999-12-31 23:59:59.999')),
        PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo);
    PRINT 'PuntoDeVenta: PERIOD FOR SYSTEM_TIME added.';
END
GO

IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('dbo.PuntoDeVenta') AND temporal_type = 2)
BEGIN
    ALTER TABLE dbo.PuntoDeVenta
    SET (SYSTEM_VERSIONING = ON (
        HISTORY_TABLE = dbo.PuntoDeVenta_History,
        HISTORY_RETENTION_PERIOD = 10 YEARS
    ));
    PRINT 'PuntoDeVenta: SYSTEM_VERSIONING = ON (history: dbo.PuntoDeVenta_History, retention: 10 years).';
END
ELSE
    PRINT 'PuntoDeVenta: SYSTEM_VERSIONING already ON — skip.';
GO

IF EXISTS (SELECT 1 FROM sys.tables WHERE name = 'PuntoDeVenta_History' AND schema_id = SCHEMA_ID('dbo'))
   AND NOT EXISTS (
       SELECT 1 FROM sys.partitions p
       JOIN sys.tables t ON t.object_id = p.object_id
       WHERE t.name = 'PuntoDeVenta_History' AND p.data_compression = 2
   )
BEGIN
    ALTER TABLE dbo.PuntoDeVenta_History REBUILD WITH (DATA_COMPRESSION = PAGE);
    PRINT 'PuntoDeVenta_History: rebuilt with PAGE compression.';
END
GO

-- ═══════════════════════════════════════════════════════════════════════
-- 4. SYSTEM_VERSIONING — SecuenciaComprobante
-- ═══════════════════════════════════════════════════════════════════════

IF COL_LENGTH('dbo.SecuenciaComprobante', 'ValidFrom') IS NULL
BEGIN
    ALTER TABLE dbo.SecuenciaComprobante
    ADD
        ValidFrom DATETIME2(3) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL
            CONSTRAINT DF_SecuenciaComprobante_ValidFrom DEFAULT(SYSUTCDATETIME()),
        ValidTo   DATETIME2(3) GENERATED ALWAYS AS ROW END   HIDDEN NOT NULL
            CONSTRAINT DF_SecuenciaComprobante_ValidTo   DEFAULT(CONVERT(DATETIME2(3), '9999-12-31 23:59:59.999')),
        PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo);
    PRINT 'SecuenciaComprobante: PERIOD FOR SYSTEM_TIME added.';
END
GO

IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('dbo.SecuenciaComprobante') AND temporal_type = 2)
BEGIN
    ALTER TABLE dbo.SecuenciaComprobante
    SET (SYSTEM_VERSIONING = ON (
        HISTORY_TABLE = dbo.SecuenciaComprobante_History,
        HISTORY_RETENTION_PERIOD = 10 YEARS
    ));
    PRINT 'SecuenciaComprobante: SYSTEM_VERSIONING = ON (history: dbo.SecuenciaComprobante_History, retention: 10 years).';
END
ELSE
    PRINT 'SecuenciaComprobante: SYSTEM_VERSIONING already ON — skip.';
GO

IF EXISTS (SELECT 1 FROM sys.tables WHERE name = 'SecuenciaComprobante_History' AND schema_id = SCHEMA_ID('dbo'))
   AND NOT EXISTS (
       SELECT 1 FROM sys.partitions p
       JOIN sys.tables t ON t.object_id = p.object_id
       WHERE t.name = 'SecuenciaComprobante_History' AND p.data_compression = 2
   )
BEGIN
    ALTER TABLE dbo.SecuenciaComprobante_History REBUILD WITH (DATA_COMPRESSION = PAGE);
    PRINT 'SecuenciaComprobante_History: rebuilt with PAGE compression.';
END
GO

-- ═══════════════════════════════════════════════════════════════════════
-- 5. Permiso: administracion:puntos-de-venta:gestionar
-- ═══════════════════════════════════════════════════════════════════════

MERGE dbo.Permiso AS t
USING (VALUES
    ('administracion:puntos_de_venta:gestionar', N'Gestionar puntos de venta', N'Crear, editar y desactivar puntos de venta AFIP', 'administracion')
) AS s (Codigo, Nombre, Descripcion, Modulo)
ON t.Codigo = s.Codigo
WHEN NOT MATCHED BY TARGET THEN
    INSERT (Codigo, Nombre, Descripcion, Modulo)
    VALUES (s.Codigo, s.Nombre, s.Descripcion, s.Modulo);
GO

MERGE dbo.RolPermiso AS t
USING (
    SELECT r.Id AS RolId, p.Id AS PermisoId
    FROM (VALUES
        ('admin', 'administracion:puntos_de_venta:gestionar')
    ) AS x (RolCodigo, PermisoCodigo)
    JOIN dbo.Rol     r ON r.Codigo = x.RolCodigo
    JOIN dbo.Permiso p ON p.Codigo = x.PermisoCodigo
) AS s ON t.RolId = s.RolId AND t.PermisoId = s.PermisoId
WHEN NOT MATCHED BY TARGET THEN
    INSERT (RolId, PermisoId) VALUES (s.RolId, s.PermisoId);
GO

-- ═══════════════════════════════════════════════════════════════════════
-- 6. SP: dbo.usp_ReservarNumeroComprobante
-- ═══════════════════════════════════════════════════════════════════════
-- Decisión AD1: SP con UPDATE+OUTPUT bajo SERIALIZABLE (lógica crítica en SP;
--   permite validar PdV/Medio activos en DB; atómico sin race).
-- Decisión AD2: patrón "UPDATE si existe, INSERT si @@ROWCOUNT=0" (lazy init;
--   sin trigger; simple; no genera filas huérfanas).
-- El retry ante deadlock (SqlException 1205) vive en el Application handler
--   (ReservarNumeroCommandHandler), NO en este SP. Máx 3 intentos, 50/150/450ms.
-- NO envolver la llamada en TransactionScope externo: el SP ya es atómico.
--   Un TransactionScope externo con otra conexión escalaría a MSDTC innecesariamente.

IF OBJECT_ID(N'dbo.usp_ReservarNumeroComprobante', N'P') IS NOT NULL
    DROP PROCEDURE dbo.usp_ReservarNumeroComprobante;
GO

CREATE PROCEDURE dbo.usp_ReservarNumeroComprobante
    @PuntoDeVentaId INT,
    @TipoComprobante TINYINT,
    @NumeroReservado INT OUTPUT
AS
BEGIN
    SET NOCOUNT ON;
    SET XACT_ABORT ON;
    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

    BEGIN TRAN;

    -- Validar existencia y estado de PdV + Medio padre en una sola lectura.
    -- Con SERIALIZABLE, el lock de rango protege contra inserciones concurrentes
    -- de la misma fila de SecuenciaComprobante entre el SELECT y el UPDATE/INSERT.
    DECLARE @PdvActivo  BIT;
    DECLARE @MedioActivo BIT;

    SELECT
        @PdvActivo   = p.Activo,
        @MedioActivo = m.Activo
    FROM dbo.PuntoDeVenta p
    JOIN dbo.Medio        m ON m.Id = p.MedioId
    WHERE p.Id = @PuntoDeVentaId;

    IF @PdvActivo IS NULL
    BEGIN
        ROLLBACK;
        THROW 50003, 'punto_de_venta_not_found', 1;
    END

    IF @PdvActivo = 0
    BEGIN
        ROLLBACK;
        THROW 50001, 'punto_de_venta_inactivo', 1;
    END

    IF @MedioActivo = 0
    BEGIN
        ROLLBACK;
        THROW 50002, 'medio_inactivo', 1;
    END

    -- Intentar actualizar la fila existente y capturar el nuevo número.
    DECLARE @_out TABLE (n INT NOT NULL);

    UPDATE dbo.SecuenciaComprobante
    SET
        UltimoNumero      = UltimoNumero + 1,
        FechaModificacion = SYSUTCDATETIME()
    OUTPUT inserted.UltimoNumero INTO @_out(n)
    WHERE PuntoDeVentaId = @PuntoDeVentaId
      AND TipoComprobante = @TipoComprobante;

    IF @@ROWCOUNT = 0
    BEGIN
        -- Primera reserva para este par (PdvId, TipoComprobante): inicialización lazy.
        INSERT INTO dbo.SecuenciaComprobante (PuntoDeVentaId, TipoComprobante, UltimoNumero)
        VALUES (@PuntoDeVentaId, @TipoComprobante, 1);
        SET @NumeroReservado = 1;
    END
    ELSE
    BEGIN
        SELECT @NumeroReservado = n FROM @_out;
    END

    COMMIT;
END
GO

PRINT '';
PRINT 'V013 applied successfully.';
PRINT '  - dbo.PuntoDeVenta (temporal, retention 10y, PAGE compression)';
PRINT '  - dbo.SecuenciaComprobante (temporal, retention 10y, PAGE compression)';
PRINT '  - Permiso administracion:puntos-de-venta:gestionar (asignado a admin)';
PRINT '  - SP dbo.usp_ReservarNumeroComprobante';
PRINT 'Next: Batch 2 — Domain (PuntoDeVenta entity + exceptions + TipoComprobante enum).';
GO
