ADM-009: Tablas Fiscales (IVA + IIBB) — append-only versioned ref data #22
345
tests/SIGCM2.Api.Tests/Admin/V014MigrationTests.cs
Normal file
345
tests/SIGCM2.Api.Tests/Admin/V014MigrationTests.cs
Normal file
@@ -0,0 +1,345 @@
|
||||
using Dapper;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Xunit;
|
||||
|
||||
namespace SIGCM2.Api.Tests.Admin;
|
||||
|
||||
/// <summary>
|
||||
/// ADM-009 Batch 1 — V014 migration integration tests.
|
||||
/// Validates:
|
||||
/// REQ-SEED-001 : TipoDeIva seed genera exactamente 4 filas canónicas (EXENTO, NO_GRAVADO, IVA_105, IVA_21).
|
||||
/// REQ-SEED-002 : IngresosBrutos seed genera exactamente 25 filas (24 provincias INDEC + CABA).
|
||||
/// REQ-SEED-003 : Re-ejecución de V014 NO duplica filas (idempotencia MERGE).
|
||||
/// REQ-TEMPORAL-001 : SYSTEM_VERSIONING ON en TipoDeIva e IngresosBrutos (temporal_type = 2).
|
||||
/// REQ-FISCAL-AUTH-002 : Permiso 'administracion:fiscal:gestionar' existe y está asignado al rol admin.
|
||||
///
|
||||
/// NOTA: Esta suite opera directamente sobre SIGCM2_Test con Dapper.
|
||||
/// NO usa WebApplicationFactory (es test de migración pura, no API).
|
||||
/// La migración debe haberse aplicado previamente a SIGCM2_Test via sqlcmd o SqlTestFixture.
|
||||
/// </summary>
|
||||
[Collection("ApiIntegration")]
|
||||
public sealed class V014MigrationTests : IClassFixture<SIGCM2.TestSupport.TestWebAppFactory>
|
||||
{
|
||||
private const string ConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
public V014MigrationTests(SIGCM2.TestSupport.TestWebAppFactory _)
|
||||
{
|
||||
// Depend on the factory so SqlTestFixture.InitializeAsync runs
|
||||
// (ensures V014 schema is present via EnsureV014SchemaAsync).
|
||||
}
|
||||
|
||||
// ── REQ-TEMPORAL-001 ──────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task TipoDeIva_SystemVersioning_IsActive()
|
||||
{
|
||||
await using var conn = new SqlConnection(ConnectionString);
|
||||
await conn.OpenAsync();
|
||||
|
||||
var temporalType = await conn.ExecuteScalarAsync<int>("""
|
||||
SELECT temporal_type
|
||||
FROM sys.tables
|
||||
WHERE object_id = OBJECT_ID('dbo.TipoDeIva')
|
||||
""");
|
||||
|
||||
temporalType.Should().Be(2, "TipoDeIva debe tener SYSTEM_VERSIONING = ON (temporal_type = 2)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TipoDeIva_History_Exists()
|
||||
{
|
||||
await using var conn = new SqlConnection(ConnectionString);
|
||||
await conn.OpenAsync();
|
||||
|
||||
var exists = await conn.ExecuteScalarAsync<int>("""
|
||||
SELECT COUNT(1) FROM sys.tables
|
||||
WHERE name = 'TipoDeIva_History'
|
||||
AND schema_id = SCHEMA_ID('dbo')
|
||||
""");
|
||||
|
||||
exists.Should().Be(1, "dbo.TipoDeIva_History debe existir como tabla de historial temporal");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IngresosBrutos_SystemVersioning_IsActive()
|
||||
{
|
||||
await using var conn = new SqlConnection(ConnectionString);
|
||||
await conn.OpenAsync();
|
||||
|
||||
var temporalType = await conn.ExecuteScalarAsync<int>("""
|
||||
SELECT temporal_type
|
||||
FROM sys.tables
|
||||
WHERE object_id = OBJECT_ID('dbo.IngresosBrutos')
|
||||
""");
|
||||
|
||||
temporalType.Should().Be(2, "IngresosBrutos debe tener SYSTEM_VERSIONING = ON (temporal_type = 2)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IngresosBrutos_History_Exists()
|
||||
{
|
||||
await using var conn = new SqlConnection(ConnectionString);
|
||||
await conn.OpenAsync();
|
||||
|
||||
var exists = await conn.ExecuteScalarAsync<int>("""
|
||||
SELECT COUNT(1) FROM sys.tables
|
||||
WHERE name = 'IngresosBrutos_History'
|
||||
AND schema_id = SCHEMA_ID('dbo')
|
||||
""");
|
||||
|
||||
exists.Should().Be(1, "dbo.IngresosBrutos_History debe existir como tabla de historial temporal");
|
||||
}
|
||||
|
||||
// ── REQ-SEED-001 ──────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task TipoDeIva_Seed_HasExactly4Rows()
|
||||
{
|
||||
await using var conn = new SqlConnection(ConnectionString);
|
||||
await conn.OpenAsync();
|
||||
|
||||
var count = await conn.ExecuteScalarAsync<int>(
|
||||
"SELECT COUNT(1) FROM dbo.TipoDeIva");
|
||||
|
||||
count.Should().Be(4, "El seed de V014 debe generar exactamente 4 TipoDeIva canónicos");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TipoDeIva_Seed_HasCorrectCodigos()
|
||||
{
|
||||
await using var conn = new SqlConnection(ConnectionString);
|
||||
await conn.OpenAsync();
|
||||
|
||||
var codigos = (await conn.QueryAsync<string>(
|
||||
"SELECT Codigo FROM dbo.TipoDeIva ORDER BY Codigo")).ToList();
|
||||
|
||||
codigos.Should().BeEquivalentTo(
|
||||
new[] { "EXENTO", "IVA_105", "IVA_21", "NO_GRAVADO" },
|
||||
"Los 4 códigos canónicos deben existir en TipoDeIva");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TipoDeIva_Seed_Porcentajes_AreCorrect()
|
||||
{
|
||||
await using var conn = new SqlConnection(ConnectionString);
|
||||
await conn.OpenAsync();
|
||||
|
||||
var rows = (await conn.QueryAsync<(string Codigo, decimal Porcentaje)>(
|
||||
"SELECT Codigo, Porcentaje FROM dbo.TipoDeIva ORDER BY Codigo")).ToList();
|
||||
|
||||
rows.Should().ContainSingle(r => r.Codigo == "EXENTO" && r.Porcentaje == 0m);
|
||||
rows.Should().ContainSingle(r => r.Codigo == "NO_GRAVADO" && r.Porcentaje == 0m);
|
||||
rows.Should().ContainSingle(r => r.Codigo == "IVA_105" && r.Porcentaje == 10.5m);
|
||||
rows.Should().ContainSingle(r => r.Codigo == "IVA_21" && r.Porcentaje == 21m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TipoDeIva_Seed_AllRowsActive_PredecesorNull_VigenciaHastaNull()
|
||||
{
|
||||
await using var conn = new SqlConnection(ConnectionString);
|
||||
await conn.OpenAsync();
|
||||
|
||||
var invalidRows = await conn.ExecuteScalarAsync<int>("""
|
||||
SELECT COUNT(1) FROM dbo.TipoDeIva
|
||||
WHERE Activo = 0
|
||||
OR PredecesorId IS NOT NULL
|
||||
OR VigenciaHasta IS NOT NULL
|
||||
""");
|
||||
|
||||
invalidRows.Should().Be(0,
|
||||
"Todas las filas seed deben tener Activo=1, PredecesorId=NULL, VigenciaHasta=NULL");
|
||||
}
|
||||
|
||||
// ── REQ-SEED-002 ──────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task IngresosBrutos_Seed_HasExactly24Rows()
|
||||
{
|
||||
await using var conn = new SqlConnection(ConnectionString);
|
||||
await conn.OpenAsync();
|
||||
|
||||
var count = await conn.ExecuteScalarAsync<int>(
|
||||
"SELECT COUNT(1) FROM dbo.IngresosBrutos");
|
||||
|
||||
// Design canónico: 23 provincias INDEC + CABA = 24 jurisdicciones.
|
||||
// La lista del design incluye CABA como elemento propio junto a BUENOS_AIRES (provincia).
|
||||
// REQ-SEED-002 especifica "25" pero la lista canónica del design tiene 24 entradas únicas.
|
||||
// DISCOVERY: posible discrepancia spec vs. design — anotado en apply-progress.
|
||||
// Implementamos lo que la lista del design establece explícitamente: 24 filas.
|
||||
count.Should().Be(24, "El seed de V014 debe generar 24 IngresosBrutos (23 provincias INDEC + CABA)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IngresosBrutos_Seed_HasAllProvincias()
|
||||
{
|
||||
await using var conn = new SqlConnection(ConnectionString);
|
||||
await conn.OpenAsync();
|
||||
|
||||
var provincias = (await conn.QueryAsync<string>(
|
||||
"SELECT Provincia FROM dbo.IngresosBrutos ORDER BY Provincia")).ToList();
|
||||
|
||||
// Lista canónica del design ADM-009: 23 provincias argentinas INDEC + CABA = 24
|
||||
var expectedCanonical = new[]
|
||||
{
|
||||
"BUENOS_AIRES", "CABA", "CATAMARCA", "CHACO", "CHUBUT",
|
||||
"CORDOBA", "CORRIENTES", "ENTRE_RIOS", "FORMOSA", "JUJUY",
|
||||
"LA_PAMPA", "LA_RIOJA", "MENDOZA", "MISIONES", "NEUQUEN",
|
||||
"RIO_NEGRO", "SALTA", "SAN_JUAN", "SAN_LUIS", "SANTA_CRUZ",
|
||||
"SANTA_FE", "SANTIAGO_DEL_ESTERO", "TIERRA_DEL_FUEGO", "TUCUMAN"
|
||||
};
|
||||
|
||||
provincias.Should().Contain("CABA", "CABA debe estar entre las provincias");
|
||||
provincias.Should().Contain("BUENOS_AIRES", "Buenos Aires (provincia) debe estar como BUENOS_AIRES");
|
||||
foreach (var prov in expectedCanonical)
|
||||
provincias.Should().Contain(prov, $"Provincia {prov} debe estar en el seed");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IngresosBrutos_Seed_AlicuotaZero_AllRows()
|
||||
{
|
||||
await using var conn = new SqlConnection(ConnectionString);
|
||||
await conn.OpenAsync();
|
||||
|
||||
var nonZero = await conn.ExecuteScalarAsync<int>(
|
||||
"SELECT COUNT(1) FROM dbo.IngresosBrutos WHERE Alicuota <> 0");
|
||||
|
||||
nonZero.Should().Be(0, "Todas las filas seed de IngresosBrutos deben tener Alicuota=0 (placeholder)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IngresosBrutos_Seed_AllRowsActive_PredecesorNull_VigenciaHastaNull()
|
||||
{
|
||||
await using var conn = new SqlConnection(ConnectionString);
|
||||
await conn.OpenAsync();
|
||||
|
||||
var invalidRows = await conn.ExecuteScalarAsync<int>("""
|
||||
SELECT COUNT(1) FROM dbo.IngresosBrutos
|
||||
WHERE Activo = 0
|
||||
OR PredecesorId IS NOT NULL
|
||||
OR VigenciaHasta IS NOT NULL
|
||||
""");
|
||||
|
||||
invalidRows.Should().Be(0,
|
||||
"Todas las filas seed deben tener Activo=1, PredecesorId=NULL, VigenciaHasta=NULL");
|
||||
}
|
||||
|
||||
// ── REQ-FISCAL-AUTH-002 ───────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task Permiso_AdministracionFiscalGestionar_Exists()
|
||||
{
|
||||
await using var conn = new SqlConnection(ConnectionString);
|
||||
await conn.OpenAsync();
|
||||
|
||||
var count = await conn.ExecuteScalarAsync<int>("""
|
||||
SELECT COUNT(1) FROM dbo.Permiso
|
||||
WHERE Codigo = 'administracion:fiscal:gestionar'
|
||||
""");
|
||||
|
||||
count.Should().Be(1, "El permiso 'administracion:fiscal:gestionar' debe existir en dbo.Permiso");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Permiso_AdministracionFiscalGestionar_AsignadoARolAdmin()
|
||||
{
|
||||
await using var conn = new SqlConnection(ConnectionString);
|
||||
await conn.OpenAsync();
|
||||
|
||||
var count = await conn.ExecuteScalarAsync<int>("""
|
||||
SELECT COUNT(1)
|
||||
FROM dbo.RolPermiso rp
|
||||
JOIN dbo.Rol r ON r.Id = rp.RolId
|
||||
JOIN dbo.Permiso p ON p.Id = rp.PermisoId
|
||||
WHERE r.Codigo = 'admin'
|
||||
AND p.Codigo = 'administracion:fiscal:gestionar'
|
||||
""");
|
||||
|
||||
count.Should().Be(1,
|
||||
"El permiso 'administracion:fiscal:gestionar' debe estar asignado al rol 'admin'");
|
||||
}
|
||||
|
||||
// ── REQ-SEED-003 — Idempotencia ───────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task V014_Idempotencia_TipoDeIva_NoSeDuplica()
|
||||
{
|
||||
await using var conn = new SqlConnection(ConnectionString);
|
||||
await conn.OpenAsync();
|
||||
|
||||
// Aplicar el MERGE seed manualmente una segunda vez (simula re-ejecución de V014)
|
||||
await conn.ExecuteAsync("""
|
||||
MERGE dbo.TipoDeIva AS t
|
||||
USING (VALUES
|
||||
('EXENTO', N'Exento de IVA', CAST(0 AS DECIMAL(5,2)), CAST(0 AS BIT), '2020-01-01'),
|
||||
('NO_GRAVADO', N'No gravado', CAST(0 AS DECIMAL(5,2)), CAST(0 AS BIT), '2020-01-01'),
|
||||
('IVA_105', N'IVA 10.5%', CAST(10.5 AS DECIMAL(5,2)), CAST(1 AS BIT), '2020-01-01'),
|
||||
('IVA_21', N'IVA 21%', CAST(21 AS DECIMAL(5,2)), CAST(1 AS BIT), '2020-01-01')
|
||||
) 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);
|
||||
""");
|
||||
|
||||
var count = await conn.ExecuteScalarAsync<int>(
|
||||
"SELECT COUNT(1) FROM dbo.TipoDeIva");
|
||||
|
||||
count.Should().Be(4, "Re-ejecutar el seed MERGE no debe duplicar filas en TipoDeIva");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task V014_Idempotencia_IngresosBrutos_NoSeDuplica()
|
||||
{
|
||||
await using var conn = new SqlConnection(ConnectionString);
|
||||
await conn.OpenAsync();
|
||||
|
||||
// Re-aplicar el MERGE de provincias (simula re-ejecución de V014)
|
||||
// Las 25 provincias canónicas: 24 INDEC + CABA (CABA es la #25)
|
||||
await conn.ExecuteAsync("""
|
||||
MERGE dbo.IngresosBrutos AS t
|
||||
USING (VALUES
|
||||
('BUENOS_AIRES'),('CABA'),('CATAMARCA'),('CHACO'),('CHUBUT'),
|
||||
('CORDOBA'),('CORRIENTES'),('ENTRE_RIOS'),('FORMOSA'),('JUJUY'),
|
||||
('LA_PAMPA'),('LA_RIOJA'),('MENDOZA'),('MISIONES'),('NEUQUEN'),
|
||||
('RIO_NEGRO'),('SALTA'),('SAN_JUAN'),('SAN_LUIS'),('SANTA_CRUZ'),
|
||||
('SANTA_FE'),('SANTIAGO_DEL_ESTERO'),('TIERRA_DEL_FUEGO'),('TUCUMAN')
|
||||
) AS s (Provincia)
|
||||
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, N'Ingresos Brutos ' + s.Provincia, CAST(0 AS DECIMAL(5,2)), 1, '2020-01-01', NULL, NULL);
|
||||
""");
|
||||
|
||||
var count = await conn.ExecuteScalarAsync<int>(
|
||||
"SELECT COUNT(1) FROM dbo.IngresosBrutos");
|
||||
|
||||
count.Should().Be(24, "Re-ejecutar el seed MERGE no debe duplicar filas en IngresosBrutos");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task V014_Idempotencia_Permiso_NoSeDuplica()
|
||||
{
|
||||
await using var conn = new SqlConnection(ConnectionString);
|
||||
await conn.OpenAsync();
|
||||
|
||||
// Re-aplicar MERGE permiso (simula re-ejecución de V014)
|
||||
await conn.ExecuteAsync("""
|
||||
MERGE dbo.Permiso AS t
|
||||
USING (VALUES ('administracion:fiscal:gestionar', N'Gestionar tablas fiscales (IVA, IIBB)', 'administracion'))
|
||||
AS s (Codigo, Descripcion, Modulo)
|
||||
ON t.Codigo = s.Codigo
|
||||
WHEN NOT MATCHED BY TARGET THEN
|
||||
INSERT (Codigo, Nombre, Descripcion, Modulo)
|
||||
VALUES (s.Codigo, N'Gestionar tablas fiscales', s.Descripcion, s.Modulo);
|
||||
""");
|
||||
|
||||
var count = await conn.ExecuteScalarAsync<int>("""
|
||||
SELECT COUNT(1) FROM dbo.Permiso
|
||||
WHERE Codigo = 'administracion:fiscal:gestionar'
|
||||
""");
|
||||
|
||||
count.Should().Be(1, "Re-ejecutar el MERGE de Permiso no debe crear duplicados");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user