-- V014__create_tablas_fiscales.sql -- ADM-009 Tablas Fiscales: DDL para dbo.TipoDeIva + dbo.IngresosBrutos + permisos. -- -- Patron: append-only versioned ref data. -- Porcentaje/Alicuota son INMUTABLES post-creacion; cambiar el valor = nueva fila + cierre de predecesora. -- PredecesorId (FK self) establece la cadena de versiones (historial de negocio). -- SYSTEM_VERSIONING ON para historial tecnico (auditoria temporal de SQL Server). -- -- Idempotente: seguro para re-ejecutar. -- Reversa: V014_ROLLBACK.sql. -- Run on: SIGCM2 (dev) y SIGCM2_Test (integration tests). -- -- Covers: REQ-SEED-001, REQ-SEED-002, REQ-SEED-003, REQ-TEMPORAL-001, REQ-FISCAL-AUTH-002 SET QUOTED_IDENTIFIER ON; SET ANSI_NULLS ON; SET NOCOUNT ON; GO -- ═══════════════════════════════════════════════════════════════════════ -- 1. dbo.TipoDeIva -- ═══════════════════════════════════════════════════════════════════════ IF OBJECT_ID(N'dbo.TipoDeIva', N'U') IS NULL BEGIN CREATE TABLE dbo.TipoDeIva ( Id INT IDENTITY(1,1) NOT NULL CONSTRAINT PK_TipoDeIva PRIMARY KEY, Codigo VARCHAR(32) NOT NULL, Descripcion NVARCHAR(100) NOT NULL, Porcentaje DECIMAL(5,2) NOT NULL, AplicaIVA BIT NOT NULL, Activo BIT NOT NULL CONSTRAINT DF_TipoDeIva_Activo DEFAULT(1), VigenciaDesde DATE NOT NULL, VigenciaHasta DATE NULL, PredecesorId INT NULL, FechaCreacion DATETIME2(3) NOT NULL CONSTRAINT DF_TipoDeIva_FechaCreacion DEFAULT(SYSUTCDATETIME()), FechaModificacion DATETIME2(3) NULL, CONSTRAINT CK_TipoDeIva_Porcentaje CHECK (Porcentaje >= 0 AND Porcentaje <= 100), CONSTRAINT CK_TipoDeIva_Vigencia CHECK (VigenciaHasta IS NULL OR VigenciaHasta >= VigenciaDesde), CONSTRAINT UQ_TipoDeIva_Codigo_Vigencia UNIQUE (Codigo, VigenciaDesde), CONSTRAINT FK_TipoDeIva_Predecesor FOREIGN KEY (PredecesorId) REFERENCES dbo.TipoDeIva(Id) ); PRINT 'Table dbo.TipoDeIva created.'; END ELSE PRINT 'Table dbo.TipoDeIva already exists — skip.'; GO -- Indices TipoDeIva IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_TipoDeIva_Codigo_VigenciaDesde' AND object_id = OBJECT_ID('dbo.TipoDeIva')) BEGIN CREATE INDEX IX_TipoDeIva_Codigo_VigenciaDesde ON dbo.TipoDeIva(Codigo, VigenciaDesde DESC); PRINT 'Index IX_TipoDeIva_Codigo_VigenciaDesde created.'; END GO IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_TipoDeIva_PredecesorId' AND object_id = OBJECT_ID('dbo.TipoDeIva')) BEGIN CREATE INDEX IX_TipoDeIva_PredecesorId ON dbo.TipoDeIva(PredecesorId) WHERE PredecesorId IS NOT NULL; PRINT 'Index IX_TipoDeIva_PredecesorId created.'; END GO -- SYSTEM_VERSIONING — TipoDeIva IF COL_LENGTH('dbo.TipoDeIva', 'ValidFrom') IS NULL BEGIN ALTER TABLE dbo.TipoDeIva ADD ValidFrom DATETIME2(3) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL CONSTRAINT DF_TipoDeIva_ValidFrom DEFAULT(SYSUTCDATETIME()), ValidTo DATETIME2(3) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL CONSTRAINT DF_TipoDeIva_ValidTo DEFAULT(CONVERT(DATETIME2(3), '9999-12-31 23:59:59.999')), PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo); PRINT 'TipoDeIva: PERIOD FOR SYSTEM_TIME added.'; END GO IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('dbo.TipoDeIva') AND temporal_type = 2) BEGIN ALTER TABLE dbo.TipoDeIva SET (SYSTEM_VERSIONING = ON ( HISTORY_TABLE = dbo.TipoDeIva_History, HISTORY_RETENTION_PERIOD = 10 YEARS )); PRINT 'TipoDeIva: SYSTEM_VERSIONING = ON (history: dbo.TipoDeIva_History, retention: 10 years).'; END ELSE PRINT 'TipoDeIva: SYSTEM_VERSIONING already ON — skip.'; GO IF EXISTS (SELECT 1 FROM sys.tables WHERE name = 'TipoDeIva_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 = 'TipoDeIva_History' AND p.data_compression = 2 ) BEGIN ALTER TABLE dbo.TipoDeIva_History REBUILD WITH (DATA_COMPRESSION = PAGE); PRINT 'TipoDeIva_History: rebuilt with PAGE compression.'; END GO -- ═══════════════════════════════════════════════════════════════════════ -- 2. dbo.IngresosBrutos -- ═══════════════════════════════════════════════════════════════════════ IF OBJECT_ID(N'dbo.IngresosBrutos', N'U') IS NULL BEGIN CREATE TABLE dbo.IngresosBrutos ( Id INT IDENTITY(1,1) NOT NULL CONSTRAINT PK_IngresosBrutos PRIMARY KEY, Provincia VARCHAR(50) NOT NULL, Descripcion NVARCHAR(100) NOT NULL, Alicuota DECIMAL(5,2) NOT NULL, Activo BIT NOT NULL CONSTRAINT DF_IIBB_Activo DEFAULT(1), VigenciaDesde DATE NOT NULL, VigenciaHasta DATE NULL, PredecesorId INT NULL, FechaCreacion DATETIME2(3) NOT NULL CONSTRAINT DF_IIBB_FechaCreacion DEFAULT(SYSUTCDATETIME()), FechaModificacion DATETIME2(3) NULL, CONSTRAINT CK_IIBB_Alicuota CHECK (Alicuota >= 0 AND Alicuota <= 100), CONSTRAINT CK_IIBB_Vigencia CHECK (VigenciaHasta IS NULL OR VigenciaHasta >= VigenciaDesde), CONSTRAINT UQ_IIBB_Provincia_Vigencia UNIQUE (Provincia, VigenciaDesde), CONSTRAINT FK_IIBB_Predecesor FOREIGN KEY (PredecesorId) REFERENCES dbo.IngresosBrutos(Id) ); PRINT 'Table dbo.IngresosBrutos created.'; END ELSE PRINT 'Table dbo.IngresosBrutos already exists — skip.'; GO -- Indices IngresosBrutos IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_IIBB_Provincia_VigenciaDesde' AND object_id = OBJECT_ID('dbo.IngresosBrutos')) BEGIN CREATE INDEX IX_IIBB_Provincia_VigenciaDesde ON dbo.IngresosBrutos(Provincia, VigenciaDesde DESC); PRINT 'Index IX_IIBB_Provincia_VigenciaDesde created.'; END GO IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_IIBB_PredecesorId' AND object_id = OBJECT_ID('dbo.IngresosBrutos')) BEGIN CREATE INDEX IX_IIBB_PredecesorId ON dbo.IngresosBrutos(PredecesorId) WHERE PredecesorId IS NOT NULL; PRINT 'Index IX_IIBB_PredecesorId created.'; END GO -- SYSTEM_VERSIONING — IngresosBrutos IF COL_LENGTH('dbo.IngresosBrutos', 'ValidFrom') IS NULL BEGIN ALTER TABLE dbo.IngresosBrutos ADD ValidFrom DATETIME2(3) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL CONSTRAINT DF_IIBB_ValidFrom DEFAULT(SYSUTCDATETIME()), ValidTo DATETIME2(3) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL CONSTRAINT DF_IIBB_ValidTo DEFAULT(CONVERT(DATETIME2(3), '9999-12-31 23:59:59.999')), PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo); PRINT 'IngresosBrutos: PERIOD FOR SYSTEM_TIME added.'; END GO IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('dbo.IngresosBrutos') AND temporal_type = 2) BEGIN ALTER TABLE dbo.IngresosBrutos SET (SYSTEM_VERSIONING = ON ( HISTORY_TABLE = dbo.IngresosBrutos_History, HISTORY_RETENTION_PERIOD = 10 YEARS )); PRINT 'IngresosBrutos: SYSTEM_VERSIONING = ON (history: dbo.IngresosBrutos_History, retention: 10 years).'; END ELSE PRINT 'IngresosBrutos: SYSTEM_VERSIONING already ON — skip.'; GO IF EXISTS (SELECT 1 FROM sys.tables WHERE name = 'IngresosBrutos_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 = 'IngresosBrutos_History' AND p.data_compression = 2 ) BEGIN ALTER TABLE dbo.IngresosBrutos_History REBUILD WITH (DATA_COMPRESSION = PAGE); PRINT 'IngresosBrutos_History: rebuilt with PAGE compression.'; END GO -- ═══════════════════════════════════════════════════════════════════════ -- 3. Seed TipoDeIva — 4 filas canonicas (REQ-SEED-001) -- MERGE garantiza idempotencia (REQ-SEED-003) -- EXENTO y NO_GRAVADO no aplican IVA; IVA_105 e IVA_21 si aplican. -- ═══════════════════════════════════════════════════════════════════════ MERGE dbo.TipoDeIva AS t USING (VALUES ('EXENTO', N'Exento de IVA', CAST(0 AS DECIMAL(5,2)), CAST(0 AS BIT), CAST('2020-01-01' AS DATE)), ('NO_GRAVADO', N'No gravado', CAST(0 AS DECIMAL(5,2)), CAST(0 AS BIT), CAST('2020-01-01' AS DATE)), ('IVA_105', N'IVA alicuota diferencial 10.5%', CAST(10.5 AS DECIMAL(5,2)), CAST(1 AS BIT), CAST('2020-01-01' AS DATE)), ('IVA_21', N'IVA alicuota general 21%', CAST(21 AS DECIMAL(5,2)), CAST(1 AS BIT), CAST('2020-01-01' AS DATE)) ) AS s (Codigo, Descripcion, Porcentaje, AplicaIVA, VigenciaDesde) ON t.Codigo = s.Codigo AND t.PredecesorId IS NULL WHEN NOT MATCHED BY TARGET THEN INSERT (Codigo, Descripcion, Porcentaje, AplicaIVA, Activo, VigenciaDesde, VigenciaHasta, PredecesorId) VALUES (s.Codigo, s.Descripcion, s.Porcentaje, s.AplicaIVA, 1, s.VigenciaDesde, NULL, NULL); GO PRINT 'TipoDeIva: 4 canonical rows seeded (EXENTO, NO_GRAVADO, IVA_105, IVA_21).'; GO -- ═══════════════════════════════════════════════════════════════════════ -- 4. Seed IngresosBrutos — 24 filas (23 provincias INDEC + CABA) (REQ-SEED-002) -- Alicuota=0 placeholder — el operador cargara las alicuotas reales via UI. -- MERGE garantiza idempotencia (REQ-SEED-003). -- Provincias almacenadas como nombre de enum ProvinciaArgentina PascalCase (VARCHAR(50)). -- DISCOVERY: spec dice 25 filas pero lista canonica del design tiene 24 entradas -- (23 provincias INDEC + CABA). Implementado con 24. Ver apply-progress. -- T700 cleanup: valores cambiados de UPPER_SNAKE_CASE a PascalCase (matching enum.ToString()). -- ═══════════════════════════════════════════════════════════════════════ MERGE dbo.IngresosBrutos AS t USING (VALUES ('BuenosAires', N'Ingresos Brutos - Buenos Aires'), ('CiudadAutonomaDeBuenosAires', N'Ingresos Brutos - Ciudad Autonoma de Buenos Aires'), ('Catamarca', N'Ingresos Brutos - Catamarca'), ('Chaco', N'Ingresos Brutos - Chaco'), ('Chubut', N'Ingresos Brutos - Chubut'), ('Cordoba', N'Ingresos Brutos - Cordoba'), ('Corrientes', N'Ingresos Brutos - Corrientes'), ('EntreRios', N'Ingresos Brutos - Entre Rios'), ('Formosa', N'Ingresos Brutos - Formosa'), ('Jujuy', N'Ingresos Brutos - Jujuy'), ('LaPampa', N'Ingresos Brutos - La Pampa'), ('LaRioja', N'Ingresos Brutos - La Rioja'), ('Mendoza', N'Ingresos Brutos - Mendoza'), ('Misiones', N'Ingresos Brutos - Misiones'), ('Neuquen', N'Ingresos Brutos - Neuquen'), ('RioNegro', N'Ingresos Brutos - Rio Negro'), ('Salta', N'Ingresos Brutos - Salta'), ('SanJuan', N'Ingresos Brutos - San Juan'), ('SanLuis', N'Ingresos Brutos - San Luis'), ('SantaCruz', N'Ingresos Brutos - Santa Cruz'), ('SantaFe', N'Ingresos Brutos - Santa Fe'), ('SantiagoDelEstero', N'Ingresos Brutos - Santiago del Estero'), ('TierraDelFuego', N'Ingresos Brutos - Tierra del Fuego'), ('Tucuman', N'Ingresos Brutos - Tucuman') ) AS s (Provincia, Descripcion) ON t.Provincia = s.Provincia AND t.PredecesorId IS NULL WHEN NOT MATCHED BY TARGET THEN INSERT (Provincia, Descripcion, Alicuota, Activo, VigenciaDesde, VigenciaHasta, PredecesorId) VALUES (s.Provincia, s.Descripcion, CAST(0 AS DECIMAL(5,2)), 1, CAST('2020-01-01' AS DATE), NULL, NULL); GO PRINT 'IngresosBrutos: 24 canonical rows seeded (23 provincias INDEC + CABA, Alicuota=0 placeholder, PascalCase).'; GO -- ═══════════════════════════════════════════════════════════════════════ -- 5. Permiso: administracion:fiscal:gestionar (REQ-FISCAL-AUTH-002) -- ═══════════════════════════════════════════════════════════════════════ MERGE dbo.Permiso AS t USING (VALUES ('administracion:fiscal:gestionar', N'Gestionar tablas fiscales', N'Gestionar tablas fiscales (IVA, IIBB)', '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:fiscal: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 PRINT ''; PRINT 'V014 applied successfully.'; PRINT ' - dbo.TipoDeIva (temporal, retention 10y, PAGE compression)'; PRINT ' - dbo.IngresosBrutos (temporal, retention 10y, PAGE compression)'; PRINT ' - TipoDeIva: 4 canonical rows (EXENTO, NO_GRAVADO, IVA_105, IVA_21)'; PRINT ' - IngresosBrutos: 24 rows (23 provincias INDEC + CABA, Alicuota=0 placeholder)'; PRINT ' - Permiso administracion:fiscal:gestionar (asignado a admin)'; GO