diff --git a/tests/SIGCM2.Api.Tests/Auth/AuthControllerTests.cs b/tests/SIGCM2.Api.Tests/Auth/AuthControllerTests.cs index 8176d06..13c6faf 100644 --- a/tests/SIGCM2.Api.Tests/Auth/AuthControllerTests.cs +++ b/tests/SIGCM2.Api.Tests/Auth/AuthControllerTests.cs @@ -48,8 +48,9 @@ public class AuthControllerTests Assert.False(string.IsNullOrWhiteSpace(rol.GetString()), "'usuario.rol' must not be empty"); Assert.Equal(JsonValueKind.Array, permisos.ValueKind); // V011 (ADM-001) adds 'administracion:secciones:gestionar' → 22 - // V013 (ADM-008) adds 'administracion:puntos_de_venta:gestionar' → 23 total - Assert.Equal(23, permisos.GetArrayLength()); + // V013 (ADM-008) adds 'administracion:puntos_de_venta:gestionar' → 23 + // V014 (ADM-009) adds 'administracion:fiscal:gestionar' → 24 total + Assert.Equal(24, permisos.GetArrayLength()); } // Scenario: invalid credentials return 401 with opaque error diff --git a/tests/SIGCM2.Api.Tests/Permisos/PermisosEndpointTests.cs b/tests/SIGCM2.Api.Tests/Permisos/PermisosEndpointTests.cs index f386d6f..2936a27 100644 --- a/tests/SIGCM2.Api.Tests/Permisos/PermisosEndpointTests.cs +++ b/tests/SIGCM2.Api.Tests/Permisos/PermisosEndpointTests.cs @@ -130,7 +130,7 @@ public sealed class PermisosEndpointTests : IAsyncLifetime // ── GET /api/v1/permisos — catalog ─────────────────────────────────────── [Fact] - public async Task GetPermisos_WithAdmin_Returns200With23Items() + public async Task GetPermisos_WithAdmin_Returns200With24Items() { var token = await GetBearerTokenAsync(AdminUsername, AdminPassword); using var req = BuildRequest(HttpMethod.Get, "/api/v1/permisos", bearerToken: token); @@ -139,8 +139,9 @@ public sealed class PermisosEndpointTests : IAsyncLifetime Assert.Equal(HttpStatusCode.OK, resp.StatusCode); var list = await resp.Content.ReadFromJsonAsync(); // V011 (ADM-001) adds 'administracion:secciones:gestionar' → 22 - // V013 (ADM-008) adds 'administracion:puntos_de_venta:gestionar' → 23 total - Assert.Equal(23, list.GetArrayLength()); + // V013 (ADM-008) adds 'administracion:puntos_de_venta:gestionar' → 23 + // V014 (ADM-009) adds 'administracion:fiscal:gestionar' → 24 total + Assert.Equal(24, list.GetArrayLength()); } [Fact] @@ -183,7 +184,7 @@ public sealed class PermisosEndpointTests : IAsyncLifetime // ── GET /api/v1/roles/{codigo}/permisos ────────────────────────────────── [Fact] - public async Task GetRolPermisos_AdminRol_Returns200With23Items() + public async Task GetRolPermisos_AdminRol_Returns200With24Items() { var token = await GetBearerTokenAsync(AdminUsername, AdminPassword); using var req = BuildRequest(HttpMethod.Get, "/api/v1/roles/admin/permisos", bearerToken: token); @@ -192,8 +193,9 @@ public sealed class PermisosEndpointTests : IAsyncLifetime Assert.Equal(HttpStatusCode.OK, resp.StatusCode); var list = await resp.Content.ReadFromJsonAsync(); // V011 (ADM-001) adds 'administracion:secciones:gestionar' → 22 - // V013 (ADM-008) adds 'administracion:puntos_de_venta:gestionar' → 23 total - Assert.Equal(23, list.GetArrayLength()); + // V013 (ADM-008) adds 'administracion:puntos_de_venta:gestionar' → 23 + // V014 (ADM-009) adds 'administracion:fiscal:gestionar' → 24 total + Assert.Equal(24, list.GetArrayLength()); } [Fact] diff --git a/tests/SIGCM2.TestSupport/SqlTestFixture.cs b/tests/SIGCM2.TestSupport/SqlTestFixture.cs index 995414b..99fde0b 100644 --- a/tests/SIGCM2.TestSupport/SqlTestFixture.cs +++ b/tests/SIGCM2.TestSupport/SqlTestFixture.cs @@ -44,6 +44,9 @@ public sealed class SqlTestFixture : IAsyncLifetime // IMAC asigna numeros AFIP, no nosotros — ver memoria architecture/facturacion-imac-numeracion). await EnsureV013SchemaAsync(); + // V014 (ADM-009): ensure dbo.TipoDeIva + dbo.IngresosBrutos + temporal + seed + permiso fiscal. + await EnsureV014SchemaAsync(); + _respawner = await Respawner.CreateAsync(_connection, new RespawnerOptions { DbAdapter = DbAdapter.SqlServer, @@ -62,6 +65,12 @@ public sealed class SqlTestFixture : IAsyncLifetime new Respawn.Graph.Table("dbo", "Medio_History"), new Respawn.Graph.Table("dbo", "Seccion_History"), new Respawn.Graph.Table("dbo", "PuntoDeVenta_History"), + // ADM-009 (V014): TipoDeIva + IngresosBrutos son temporales. + new Respawn.Graph.Table("dbo", "TipoDeIva_History"), + new Respawn.Graph.Table("dbo", "IngresosBrutos_History"), + // Seed de TipoDeIva e IngresosBrutos son datos de referencia — no limpiar con Respawn. + new Respawn.Graph.Table("dbo", "TipoDeIva"), + new Respawn.Graph.Table("dbo", "IngresosBrutos"), ] }); @@ -173,7 +182,9 @@ public sealed class SqlTestFixture : IAsyncLifetime -- V011 (ADM-001): permiso para CRUD de Secciones ('administracion:secciones:gestionar', N'Gestionar secciones por medio', N'Crear, editar y desactivar secciones de un medio','administracion'), -- V013 (ADM-008): permiso para CRUD de Puntos de Venta - ('administracion:puntos_de_venta:gestionar', N'Gestionar puntos de venta', N'Crear, editar y desactivar puntos de venta AFIP','administracion') + ('administracion:puntos_de_venta:gestionar', N'Gestionar puntos de venta', N'Crear, editar y desactivar puntos de venta AFIP','administracion'), + -- V014 (ADM-009): permiso para tablas fiscales + ('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 @@ -217,6 +228,8 @@ public sealed class SqlTestFixture : IAsyncLifetime ('admin', 'administracion:secciones:gestionar'), -- V013 (ADM-008) ('admin', 'administracion:puntos_de_venta:gestionar'), + -- V014 (ADM-009) + ('admin', 'administracion:fiscal:gestionar'), ('cajero', 'ventas:contado:crear'), ('cajero', 'ventas:contado:modificar'), ('cajero', 'ventas:contado:cobrar'), @@ -567,4 +580,197 @@ public sealed class SqlTestFixture : IAsyncLifetime await _connection.ExecuteAsync(addConstraint); await _connection.ExecuteAsync(migrateRows); } + + /// + /// ADM-009 (V014): applies dbo.TipoDeIva + dbo.IngresosBrutos schema + temporal tables + seed + permiso fiscal. + /// Mirrors V014__create_tablas_fiscales.sql (idempotente). + /// Permiso y asignacion se siembran desde SeedPermisosCanonicalAsync / SeedRolPermisosCanonicalAsync. + /// Seed de TipoDeIva e IngresosBrutos se aplica aqui (datos de referencia estables). + /// + private async Task EnsureV014SchemaAsync() + { + // ── 1. dbo.TipoDeIva ───────────────────────────────────────────────── + const string createTipoDeIva = """ + 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) + ); + END + """; + + const string createTipoDeIvaIdx1 = """ + IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_TipoDeIva_Codigo_VigenciaDesde' AND object_id = OBJECT_ID('dbo.TipoDeIva')) + CREATE INDEX IX_TipoDeIva_Codigo_VigenciaDesde ON dbo.TipoDeIva(Codigo, VigenciaDesde DESC); + """; + + const string createTipoDeIvaIdx2 = """ + IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_TipoDeIva_PredecesorId' AND object_id = OBJECT_ID('dbo.TipoDeIva')) + CREATE INDEX IX_TipoDeIva_PredecesorId ON dbo.TipoDeIva(PredecesorId) WHERE PredecesorId IS NOT NULL; + """; + + const string addTipoDeIvaPeriod = """ + 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); + END + """; + + const string setTipoDeIvaVersioning = """ + 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 + )); + END + """; + + // ── 2. dbo.IngresosBrutos ──────────────────────────────────────────── + const string createIIBB = """ + 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) + ); + END + """; + + const string createIIBBIdx1 = """ + IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_IIBB_Provincia_VigenciaDesde' AND object_id = OBJECT_ID('dbo.IngresosBrutos')) + CREATE INDEX IX_IIBB_Provincia_VigenciaDesde ON dbo.IngresosBrutos(Provincia, VigenciaDesde DESC); + """; + + const string createIIBBIdx2 = """ + IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_IIBB_PredecesorId' AND object_id = OBJECT_ID('dbo.IngresosBrutos')) + CREATE INDEX IX_IIBB_PredecesorId ON dbo.IngresosBrutos(PredecesorId) WHERE PredecesorId IS NOT NULL; + """; + + const string addIIBBPeriod = """ + 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); + END + """; + + const string setIIBBVersioning = """ + 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 + )); + END + """; + + // ── 3. Seed TipoDeIva ──────────────────────────────────────────────── + const string seedTipoDeIva = """ + SET QUOTED_IDENTIFIER ON; + 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); + """; + + // ── 4. Seed IngresosBrutos ──────────────────────────────────────────── + // 24 filas: 23 provincias INDEC + CABA. Alicuota=0 placeholder. + const string seedIIBB = """ + SET QUOTED_IDENTIFIER ON; + MERGE dbo.IngresosBrutos AS t + USING (VALUES + ('BUENOS_AIRES', N'Ingresos Brutos - Buenos Aires'), + ('CABA', 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'), + ('ENTRE_RIOS', N'Ingresos Brutos - Entre Rios'), + ('FORMOSA', N'Ingresos Brutos - Formosa'), + ('JUJUY', N'Ingresos Brutos - Jujuy'), + ('LA_PAMPA', N'Ingresos Brutos - La Pampa'), + ('LA_RIOJA', N'Ingresos Brutos - La Rioja'), + ('MENDOZA', N'Ingresos Brutos - Mendoza'), + ('MISIONES', N'Ingresos Brutos - Misiones'), + ('NEUQUEN', N'Ingresos Brutos - Neuquen'), + ('RIO_NEGRO', N'Ingresos Brutos - Rio Negro'), + ('SALTA', N'Ingresos Brutos - Salta'), + ('SAN_JUAN', N'Ingresos Brutos - San Juan'), + ('SAN_LUIS', N'Ingresos Brutos - San Luis'), + ('SANTA_CRUZ', N'Ingresos Brutos - Santa Cruz'), + ('SANTA_FE', N'Ingresos Brutos - Santa Fe'), + ('SANTIAGO_DEL_ESTERO', N'Ingresos Brutos - Santiago del Estero'), + ('TIERRA_DEL_FUEGO', 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); + """; + + // Apply in order + await _connection.ExecuteAsync(createTipoDeIva); + await _connection.ExecuteAsync(createTipoDeIvaIdx1); + await _connection.ExecuteAsync(createTipoDeIvaIdx2); + await _connection.ExecuteAsync(addTipoDeIvaPeriod); + await _connection.ExecuteAsync(setTipoDeIvaVersioning); + await _connection.ExecuteAsync(createIIBB); + await _connection.ExecuteAsync(createIIBBIdx1); + await _connection.ExecuteAsync(createIIBBIdx2); + await _connection.ExecuteAsync(addIIBBPeriod); + await _connection.ExecuteAsync(setIIBBVersioning); + await _connection.ExecuteAsync(seedTipoDeIva); + await _connection.ExecuteAsync(seedIIBB); + // Permiso 'administracion:fiscal:gestionar' y asignacion a admin se siembran + // desde SeedPermisosCanonicalAsync / SeedRolPermisosCanonicalAsync (post-respawn). + } }