From 6458ee0106bdcfad7cb4678eab9934b02ad1cfd0 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Fri, 17 Apr 2026 14:16:21 -0300 Subject: [PATCH] revert(tests): eliminar tests de reserva/concurrencia/secuencialidad ADM-008 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eliminar SecuenciaComprobanteTests, ReservarNumeroCommandHandlerTests, GetProximoNumeroQueryHandlerTests y 7 tests de integración en PuntosDeVentaControllerTests (reserva/proximo/concurrencia/secuencialidad). SqlTestFixture ahora limpia SecuenciaComprobante+SP si existen (drops idempotentes) y solo crea PuntoDeVenta + temporal table. --- .../Admin/PuntosDeVentaControllerTests.cs | 241 ------------------ .../Domain/SecuenciaComprobanteTests.cs | 57 ----- .../GetProximoNumeroQueryHandlerTests.cs | 52 ---- .../ReservarNumeroCommandHandlerTests.cs | 126 --------- tests/SIGCM2.TestSupport/SqlTestFixture.cs | 175 +++---------- 5 files changed, 39 insertions(+), 612 deletions(-) delete mode 100644 tests/SIGCM2.Application.Tests/Domain/SecuenciaComprobanteTests.cs delete mode 100644 tests/SIGCM2.Application.Tests/PuntosDeVenta/ProximoNumero/GetProximoNumeroQueryHandlerTests.cs delete mode 100644 tests/SIGCM2.Application.Tests/PuntosDeVenta/Reservar/ReservarNumeroCommandHandlerTests.cs diff --git a/tests/SIGCM2.Api.Tests/Admin/PuntosDeVentaControllerTests.cs b/tests/SIGCM2.Api.Tests/Admin/PuntosDeVentaControllerTests.cs index 2ec816c..5bffba0 100644 --- a/tests/SIGCM2.Api.Tests/Admin/PuntosDeVentaControllerTests.cs +++ b/tests/SIGCM2.Api.Tests/Admin/PuntosDeVentaControllerTests.cs @@ -125,13 +125,6 @@ public sealed class PuntosDeVentaControllerTests : IAsyncLifetime "SELECT Id FROM dbo.Medio WHERE Codigo = @Codigo", new { Codigo = codigo }); if (id is null) return; - // Delete SecuenciaComprobante for PuntosDeVenta of this Medio (no versioning) - await conn.ExecuteAsync(""" - DELETE sc FROM dbo.SecuenciaComprobante sc - INNER JOIN dbo.PuntoDeVenta pdv ON pdv.Id = sc.PuntoDeVentaId - WHERE pdv.MedioId = @id - """, new { id }); - // Delete dependent PuntosDeVenta (disable versioning to also clear history) await conn.ExecuteAsync("ALTER TABLE dbo.PuntoDeVenta SET (SYSTEM_VERSIONING = OFF)"); await conn.ExecuteAsync("DELETE FROM dbo.PuntoDeVenta_History WHERE MedioId = @id", new { id }); @@ -604,238 +597,4 @@ public sealed class PuntosDeVentaControllerTests : IAsyncLifetime } } - // ── SECUENCIAS: RESERVAR ────────────────────────────────────────────────── - - /// T5.3 — Primera reserva inicializa en 1. - [Fact] - public async Task ReservarNumero_FirstReservation_Returns1() - { - const string medioCodigo = "ADMS08_MED_RSV1"; - var token = await GetAdminTokenAsync(); - - try - { - var medioId = await CreateMedioAsync(medioCodigo, "Medio PDV Reservar 1", token); - var pdvId = await CreatePdvAsync(medioId, 1, "PdV Reservar First", token); - - using var req = BuildRequest(HttpMethod.Post, $"{Endpoint}/{pdvId}/secuencias/FacturaA/reservar", bearerToken: token); - var resp = await _client.SendAsync(req); - - Assert.Equal(HttpStatusCode.OK, resp.StatusCode); - var json = await resp.Content.ReadFromJsonAsync(); - Assert.Equal(1, json.GetProperty("numeroReservado").GetInt32()); - } - finally - { - await DeleteMedioIfExistsAsync(medioCodigo); - } - } - - /// T5.3 — 409 punto_de_venta_inactivo al reservar en PdV inactivo. - [Fact] - public async Task ReservarNumero_WhenPdvInactive_Returns409PdvInactivo() - { - const string medioCodigo = "ADMS08_MED_RSVI"; - var token = await GetAdminTokenAsync(); - - try - { - var medioId = await CreateMedioAsync(medioCodigo, "Medio PDV Reservar Inactivo", token); - var pdvId = await CreatePdvAsync(medioId, 1, "PdV Reservar Inactivo", token); - - // Deactivate PdV - using var deactReq = BuildRequest(HttpMethod.Post, $"{Endpoint}/{pdvId}/deactivate", bearerToken: token); - await _client.SendAsync(deactReq); - - using var req = BuildRequest(HttpMethod.Post, $"{Endpoint}/{pdvId}/secuencias/FacturaA/reservar", bearerToken: token); - var resp = await _client.SendAsync(req); - - Assert.Equal(HttpStatusCode.Conflict, resp.StatusCode); - var json = await resp.Content.ReadFromJsonAsync(); - Assert.Equal("punto_de_venta_inactivo", json.GetProperty("error").GetString()); - } - finally - { - await DeleteMedioIfExistsAsync(medioCodigo); - } - } - - /// T5.3 — 409 medio_inactivo al reservar con Medio inactivo. - [Fact] - public async Task ReservarNumero_WhenMedioInactive_Returns409MedioInactivo() - { - const string medioCodigo = "ADMS08_MED_RSVMI"; - var token = await GetAdminTokenAsync(); - - try - { - var medioId = await CreateMedioAsync(medioCodigo, "Medio PDV Reservar MedioInactivo", token); - var pdvId = await CreatePdvAsync(medioId, 1, "PdV Reservar MedioInact", token); - - // Deactivate medio - using var deactMedioReq = BuildRequest(HttpMethod.Post, $"{MediosEndpoint}/{medioId}/deactivate", bearerToken: token); - await _client.SendAsync(deactMedioReq); - - using var req = BuildRequest(HttpMethod.Post, $"{Endpoint}/{pdvId}/secuencias/FacturaA/reservar", bearerToken: token); - var resp = await _client.SendAsync(req); - - Assert.Equal(HttpStatusCode.Conflict, resp.StatusCode); - var json = await resp.Content.ReadFromJsonAsync(); - Assert.Equal("medio_inactivo", json.GetProperty("error").GetString()); - } - finally - { - await DeleteMedioIfExistsAsync(medioCodigo); - } - } - - // ── SECUENCIAS: PROXIMO ─────────────────────────────────────────────────── - - /// T5.3 — GetProximo es read-only: no modifica UltimoNumero. - [Fact] - public async Task GetProximoNumero_DoesNotChangeState() - { - const string medioCodigo = "ADMS08_MED_PROX"; - var token = await GetAdminTokenAsync(); - - try - { - var medioId = await CreateMedioAsync(medioCodigo, "Medio PDV Proximo", token); - var pdvId = await CreatePdvAsync(medioId, 1, "PdV Proximo", token); - - // Reserve once to establish state - using var rsv = BuildRequest(HttpMethod.Post, $"{Endpoint}/{pdvId}/secuencias/FacturaA/reservar", bearerToken: token); - await _client.SendAsync(rsv); - - // GetProximo twice — should return 2 both times - using var req1 = BuildRequest(HttpMethod.Get, $"{Endpoint}/{pdvId}/secuencias/FacturaA/proximo", bearerToken: token); - var resp1 = await _client.SendAsync(req1); - Assert.Equal(HttpStatusCode.OK, resp1.StatusCode); - var json1 = await resp1.Content.ReadFromJsonAsync(); - Assert.Equal(2, json1.GetProperty("proximoNumero").GetInt32()); - - using var req2 = BuildRequest(HttpMethod.Get, $"{Endpoint}/{pdvId}/secuencias/FacturaA/proximo", bearerToken: token); - var resp2 = await _client.SendAsync(req2); - var json2 = await resp2.Content.ReadFromJsonAsync(); - Assert.Equal(2, json2.GetProperty("proximoNumero").GetInt32()); - } - finally - { - await DeleteMedioIfExistsAsync(medioCodigo); - } - } - - /// T5.3 — GetProximo para fila inexistente devuelve 1. - [Fact] - public async Task GetProximoNumero_WhenNoSequenceExists_Returns1() - { - const string medioCodigo = "ADMS08_MED_PROX1"; - var token = await GetAdminTokenAsync(); - - try - { - var medioId = await CreateMedioAsync(medioCodigo, "Medio PDV Proximo 1", token); - var pdvId = await CreatePdvAsync(medioId, 1, "PdV Proximo First", token); - - using var req = BuildRequest(HttpMethod.Get, $"{Endpoint}/{pdvId}/secuencias/FacturaB/proximo", bearerToken: token); - var resp = await _client.SendAsync(req); - - Assert.Equal(HttpStatusCode.OK, resp.StatusCode); - var json = await resp.Content.ReadFromJsonAsync(); - Assert.Equal(1, json.GetProperty("proximoNumero").GetInt32()); - } - finally - { - await DeleteMedioIfExistsAsync(medioCodigo); - } - } - - // ── T5.4 — Concurrencia ─────────────────────────────────────────────────── - - /// - /// T5.4 — 50 tasks paralelas reservando para mismo PdV + TipoComprobante - /// deben producir 50 números distintos cubriendo {1..50}. - /// - [Fact] - public async Task ReservarNumero_50ConcurrentReservations_ProducesNoDuplicates() - { - const string medioCodigo = "ADMS08_CONC50"; - var token = await GetAdminTokenAsync(); - - try - { - var medioId = await CreateMedioAsync(medioCodigo, "Medio PDV Concurrencia 50", token); - var pdvId = await CreatePdvAsync(medioId, 1, "PdV Concurrencia 50", token); - - const int taskCount = 50; - var tasks = Enumerable.Range(0, taskCount) - .Select(_ => Task.Run(async () => - { - // Each task creates its own HttpClient to avoid sharing - // the shared _client which is not thread-safe for concurrent requests. - // Use BuildRequest on a shared client IS safe since HttpClient is thread-safe - // for concurrent operations as long as each message is distinct. - using var req = new HttpRequestMessage(HttpMethod.Post, $"{Endpoint}/{pdvId}/secuencias/FacturaA/reservar"); - req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); - var resp = await _client.SendAsync(req); - resp.EnsureSuccessStatusCode(); - var json = await resp.Content.ReadFromJsonAsync(); - return json.GetProperty("numeroReservado").GetInt32(); - })) - .ToList(); - - var results = await Task.WhenAll(tasks); - - // All 50 numbers must be present exactly once - Assert.Equal(taskCount, results.Length); - Assert.Equal(taskCount, results.Distinct().Count()); - - var expected = Enumerable.Range(1, taskCount).ToHashSet(); - var actual = results.ToHashSet(); - Assert.Equal(expected, actual); - } - finally - { - await DeleteMedioIfExistsAsync(medioCodigo); - } - } - - // ── T5.5 — Secuencialidad ───────────────────────────────────────────────── - - /// - /// T5.5 — 100 reservas en serie para mismo PdV + TipoComprobante - /// deben devolver {1, 2, 3, ..., 100} en orden. - /// - [Fact] - public async Task ReservarNumero_100SerialReservations_ProducesSequentialNumbers() - { - const string medioCodigo = "ADMS08_SEQ100"; - var token = await GetAdminTokenAsync(); - - try - { - var medioId = await CreateMedioAsync(medioCodigo, "Medio PDV Secuencial 100", token); - var pdvId = await CreatePdvAsync(medioId, 1, "PdV Secuencial 100", token); - - const int count = 100; - var results = new List(count); - - for (int i = 0; i < count; i++) - { - using var req = BuildRequest(HttpMethod.Post, $"{Endpoint}/{pdvId}/secuencias/FacturaA/reservar", bearerToken: token); - var resp = await _client.SendAsync(req); - resp.EnsureSuccessStatusCode(); - var json = await resp.Content.ReadFromJsonAsync(); - results.Add(json.GetProperty("numeroReservado").GetInt32()); - } - - // Verify sequential: {1, 2, 3, ..., 100} - var expected = Enumerable.Range(1, count).ToList(); - Assert.Equal(expected, results); - } - finally - { - await DeleteMedioIfExistsAsync(medioCodigo); - } - } } diff --git a/tests/SIGCM2.Application.Tests/Domain/SecuenciaComprobanteTests.cs b/tests/SIGCM2.Application.Tests/Domain/SecuenciaComprobanteTests.cs deleted file mode 100644 index 39c2fa7..0000000 --- a/tests/SIGCM2.Application.Tests/Domain/SecuenciaComprobanteTests.cs +++ /dev/null @@ -1,57 +0,0 @@ -using SIGCM2.Domain.Entities; -using SIGCM2.Domain.Enums; - -namespace SIGCM2.Application.Tests.Domain; - -public class SecuenciaComprobanteTests -{ - private static SecuenciaComprobante Make( - int puntoDeVentaId = 1, - TipoComprobante tipo = TipoComprobante.FacturaA, - int ultimoNumero = 0) - => new(puntoDeVentaId, tipo, ultimoNumero, DateTime.UtcNow, null); - - [Fact] - public void Constructor_SetsAllProperties() - { - var now = DateTime.UtcNow; - var seq = new SecuenciaComprobante( - puntoDeVentaId: 3, - tipoComprobante: TipoComprobante.FacturaB, - ultimoNumero: 42, - fechaCreacion: now, - fechaModificacion: null); - - Assert.Equal(3, seq.PuntoDeVentaId); - Assert.Equal(TipoComprobante.FacturaB, seq.TipoComprobante); - Assert.Equal(42, seq.UltimoNumero); - Assert.Equal(now, seq.FechaCreacion); - Assert.Null(seq.FechaModificacion); - } - - [Fact] - public void ProximoNumero_WhenUltimoNumeroZero_ReturnsOne() - { - var seq = Make(ultimoNumero: 0); - - Assert.Equal(1, seq.ProximoNumero); - } - - [Fact] - public void ProximoNumero_WhenUltimoNumeroN_ReturnsNPlusOne() - { - var seq = Make(ultimoNumero: 7); - - Assert.Equal(8, seq.ProximoNumero); - } - - [Fact] - public void AllTipoComprobanteValues_CanBeUsedInConstructor() - { - foreach (TipoComprobante tipo in Enum.GetValues()) - { - var seq = Make(tipo: tipo); - Assert.Equal(tipo, seq.TipoComprobante); - } - } -} diff --git a/tests/SIGCM2.Application.Tests/PuntosDeVenta/ProximoNumero/GetProximoNumeroQueryHandlerTests.cs b/tests/SIGCM2.Application.Tests/PuntosDeVenta/ProximoNumero/GetProximoNumeroQueryHandlerTests.cs deleted file mode 100644 index 4adc03b..0000000 --- a/tests/SIGCM2.Application.Tests/PuntosDeVenta/ProximoNumero/GetProximoNumeroQueryHandlerTests.cs +++ /dev/null @@ -1,52 +0,0 @@ -using NSubstitute; -using SIGCM2.Application.Abstractions.Persistence; -using SIGCM2.Application.PuntosDeVenta.ProximoNumero; -using SIGCM2.Domain.Enums; - -namespace SIGCM2.Application.Tests.PuntosDeVenta.ProximoNumero; - -public class GetProximoNumeroQueryHandlerTests -{ - private readonly IPuntoDeVentaRepository _repo = Substitute.For(); - private readonly GetProximoNumeroQueryHandler _handler; - - public GetProximoNumeroQueryHandlerTests() - { - _handler = new GetProximoNumeroQueryHandler(_repo); - } - - [Fact] - public async Task Handle_ExistingSequence_ReturnsUltimoNumeroMasUno() - { - _repo.GetUltimoNumeroAsync(10, TipoComprobante.FacturaA, Arg.Any()) - .Returns(7); - - var result = await _handler.Handle(new GetProximoNumeroQuery(10, TipoComprobante.FacturaA)); - - Assert.Equal(TipoComprobante.FacturaA, result.TipoComprobante); - Assert.Equal(8, result.ProximoNumero); - } - - [Fact] - public async Task Handle_NoExistingSequence_ReturnsOne() - { - _repo.GetUltimoNumeroAsync(10, TipoComprobante.FacturaB, Arg.Any()) - .Returns((int?)null); - - var result = await _handler.Handle(new GetProximoNumeroQuery(10, TipoComprobante.FacturaB)); - - Assert.Equal(1, result.ProximoNumero); - } - - [Fact] - public async Task Handle_DoesNotCallReservarNumero() - { - _repo.GetUltimoNumeroAsync(10, TipoComprobante.FacturaA, Arg.Any()) - .Returns(5); - - await _handler.Handle(new GetProximoNumeroQuery(10, TipoComprobante.FacturaA)); - - await _repo.DidNotReceive().ReservarNumeroAsync( - Arg.Any(), Arg.Any(), Arg.Any()); - } -} diff --git a/tests/SIGCM2.Application.Tests/PuntosDeVenta/Reservar/ReservarNumeroCommandHandlerTests.cs b/tests/SIGCM2.Application.Tests/PuntosDeVenta/Reservar/ReservarNumeroCommandHandlerTests.cs deleted file mode 100644 index a3cf99f..0000000 --- a/tests/SIGCM2.Application.Tests/PuntosDeVenta/Reservar/ReservarNumeroCommandHandlerTests.cs +++ /dev/null @@ -1,126 +0,0 @@ -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using SIGCM2.Application.Abstractions.Persistence; -using SIGCM2.Application.PuntosDeVenta.Reservar; -using SIGCM2.Domain.Enums; -using SIGCM2.Domain.Exceptions; - -namespace SIGCM2.Application.Tests.PuntosDeVenta.Reservar; - -public class ReservarNumeroCommandHandlerTests -{ - private readonly IPuntoDeVentaRepository _repo = Substitute.For(); - private readonly ReservarNumeroCommandHandler _handler; - - private static readonly ReservarNumeroCommand ValidCommand = - new(PuntoDeVentaId: 10, TipoComprobante: TipoComprobante.FacturaA); - - public ReservarNumeroCommandHandlerTests() - { - // Use delay = 0 for fast tests - _handler = new ReservarNumeroCommandHandler(_repo, deadlockBackoffMs: [0, 0, 0]); - } - - // ── happy path ─────────────────────────────────────────────────────────── - - [Fact] - public async Task Handle_HappyPath_ReturnsNumeroReservado() - { - _repo.ReservarNumeroAsync(10, TipoComprobante.FacturaA, Arg.Any()) - .Returns(7); - - var result = await _handler.Handle(ValidCommand); - - Assert.Equal(TipoComprobante.FacturaA, result.TipoComprobante); - Assert.Equal(7, result.NumeroReservado); - } - - // ── retry deadlock ──────────────────────────────────────────────────────── - - [Fact] - public async Task Handle_DeadlockTwiceThenSucceeds_ReturnsResult() - { - var deadlock = new DeadlockTransientException(); - - _repo.ReservarNumeroAsync(10, TipoComprobante.FacturaA, Arg.Any()) - .Returns( - _ => Task.FromException(deadlock), - _ => Task.FromException(deadlock), - _ => Task.FromResult(3)); - - var result = await _handler.Handle(ValidCommand); - - Assert.Equal(3, result.NumeroReservado); - } - - [Fact] - public async Task Handle_DeadlockThreeTimes_BubblesUpDeadlockException() - { - var deadlock = new DeadlockTransientException(); - - _repo.ReservarNumeroAsync(10, TipoComprobante.FacturaA, Arg.Any()) - .Returns( - _ => Task.FromException(deadlock), - _ => Task.FromException(deadlock), - _ => Task.FromException(deadlock)); - - await Assert.ThrowsAsync( - () => _handler.Handle(ValidCommand)); - } - - [Fact] - public async Task Handle_DeadlockExhaustsBackoff_TriedFourTimesTotal() - { - // backoff = [0,0,0] → 3 retries → 4 total attempts (1 initial + 3 retries) - var deadlock = new DeadlockTransientException(); - - _repo.ReservarNumeroAsync(10, TipoComprobante.FacturaA, Arg.Any()) - .Returns(_ => Task.FromException(deadlock)); - - try { await _handler.Handle(ValidCommand); } catch (DeadlockTransientException) { } - - await _repo.Received(4).ReservarNumeroAsync( - Arg.Any(), Arg.Any(), Arg.Any()); - } - - // ── domain exceptions bubble up without retry ───────────────────────────── - - [Fact] - public async Task Handle_PuntoDeVentaInactivo_BubblesUpImmediately() - { - _repo.ReservarNumeroAsync(10, TipoComprobante.FacturaA, Arg.Any()) - .Throws(new PuntoDeVentaInactivoException(10)); - - await Assert.ThrowsAsync( - () => _handler.Handle(ValidCommand)); - - await _repo.Received(1).ReservarNumeroAsync( - Arg.Any(), Arg.Any(), Arg.Any()); - } - - [Fact] - public async Task Handle_MedioInactivo_BubblesUpImmediately() - { - _repo.ReservarNumeroAsync(10, TipoComprobante.FacturaA, Arg.Any()) - .Throws(new MedioInactivoException(5)); - - await Assert.ThrowsAsync( - () => _handler.Handle(ValidCommand)); - - await _repo.Received(1).ReservarNumeroAsync( - Arg.Any(), Arg.Any(), Arg.Any()); - } - - [Fact] - public async Task Handle_PdvNotFound_BubblesUpImmediately() - { - _repo.ReservarNumeroAsync(10, TipoComprobante.FacturaA, Arg.Any()) - .Throws(new PuntoDeVentaNotFoundException(10)); - - await Assert.ThrowsAsync( - () => _handler.Handle(ValidCommand)); - - await _repo.Received(1).ReservarNumeroAsync( - Arg.Any(), Arg.Any(), Arg.Any()); - } -} diff --git a/tests/SIGCM2.TestSupport/SqlTestFixture.cs b/tests/SIGCM2.TestSupport/SqlTestFixture.cs index 54c698d..45dbb9c 100644 --- a/tests/SIGCM2.TestSupport/SqlTestFixture.cs +++ b/tests/SIGCM2.TestSupport/SqlTestFixture.cs @@ -171,7 +171,7 @@ 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 y reservar numeros','administracion') + ('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 @@ -382,13 +382,38 @@ public sealed class SqlTestFixture : IAsyncLifetime } /// - /// ADM-008 (V013): applies PuntoDeVenta / SecuenciaComprobante schema + temporal tables + - /// permiso 'administracion:puntos_de_venta:gestionar' + SP usp_ReservarNumeroComprobante. - /// Idempotent — mirrors V013__create_puntos_de_venta.sql. Permiso y asignación se siembran - /// desde SeedPermisosCanonicalAsync / SeedRolPermisosCanonicalAsync (post-respawn). + /// ADM-008 (V013): applies dbo.PuntoDeVenta schema + temporal table. + /// NOTE: SecuenciaComprobante y SP usp_ReservarNumeroComprobante fueron eliminados + /// post-smoke (Batch 9) — IMAC/Infogestión asigna los números AFIP externamente. + /// Este método también hace DROP idempotente de esos artefactos en caso de que + /// SIGCM2_Test los tuviera de una versión previa de la migración V013. + /// Permiso y asignación se siembran desde SeedPermisosCanonicalAsync / SeedRolPermisosCanonicalAsync. /// private async Task EnsureV013SchemaAsync() { + // ── Drops idempotentes de artefactos eliminados (cirugía post-smoke) ── + // Si SIGCM2_Test tiene SecuenciaComprobante o el SP de una versión previa, se limpian. + const string dropSp = """ + IF OBJECT_ID(N'dbo.usp_ReservarNumeroComprobante', N'P') IS NOT NULL + DROP PROCEDURE dbo.usp_ReservarNumeroComprobante; + """; + + const string disableSecuenciaVersioning = """ + IF EXISTS (SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('dbo.SecuenciaComprobante') AND temporal_type = 2) + ALTER TABLE dbo.SecuenciaComprobante SET (SYSTEM_VERSIONING = OFF); + """; + + const string dropSecuenciaHistory = """ + IF OBJECT_ID(N'dbo.SecuenciaComprobante_History', N'U') IS NOT NULL + DROP TABLE dbo.SecuenciaComprobante_History; + """; + + const string dropSecuencia = """ + IF OBJECT_ID(N'dbo.SecuenciaComprobante', N'U') IS NOT NULL + DROP TABLE dbo.SecuenciaComprobante; + """; + + // ── PuntoDeVenta: crear si no existe ────────────────────────────────── const string createPdv = """ IF OBJECT_ID(N'dbo.PuntoDeVenta', N'U') IS NULL BEGIN @@ -417,23 +442,6 @@ public sealed class SqlTestFixture : IAsyncLifetime END """; - const string createSecuencia = """ - 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) - ); - END - """; - const string addPdvPeriod = """ IF COL_LENGTH('dbo.PuntoDeVenta', 'ValidFrom') IS NULL BEGIN @@ -458,122 +466,17 @@ public sealed class SqlTestFixture : IAsyncLifetime END """; - // SecuenciaComprobante: sin SYSTEM_VERSIONING (AD8 revisitado — ver comentario en V013). - // Si una version previa del fixture activo SYSTEM_VERSIONING, lo desactiva + drop history. - const string disableSecuenciaVersioning = """ - IF 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 = OFF); - END - """; - - const string dropSecuenciaHistory = """ - IF OBJECT_ID(N'dbo.SecuenciaComprobante_History', N'U') IS NOT NULL - BEGIN - DROP TABLE dbo.SecuenciaComprobante_History; - END - """; - - const string dropSecuenciaPeriod = """ - IF EXISTS (SELECT 1 FROM sys.periods WHERE object_id = OBJECT_ID('dbo.SecuenciaComprobante')) - BEGIN - ALTER TABLE dbo.SecuenciaComprobante DROP PERIOD FOR SYSTEM_TIME; - END - """; - - const string dropSecuenciaValidCols = """ - IF COL_LENGTH('dbo.SecuenciaComprobante', 'ValidFrom') IS NOT NULL - BEGIN - IF EXISTS (SELECT 1 FROM sys.default_constraints WHERE name = 'DF_SecuenciaComprobante_ValidFrom' AND parent_object_id = OBJECT_ID('dbo.SecuenciaComprobante')) - ALTER TABLE dbo.SecuenciaComprobante DROP CONSTRAINT DF_SecuenciaComprobante_ValidFrom; - IF EXISTS (SELECT 1 FROM sys.default_constraints WHERE name = 'DF_SecuenciaComprobante_ValidTo' AND parent_object_id = OBJECT_ID('dbo.SecuenciaComprobante')) - ALTER TABLE dbo.SecuenciaComprobante DROP CONSTRAINT DF_SecuenciaComprobante_ValidTo; - ALTER TABLE dbo.SecuenciaComprobante DROP COLUMN ValidFrom, ValidTo; - END - """; - - const string dropSp = """ - IF OBJECT_ID(N'dbo.usp_ReservarNumeroComprobante', N'P') IS NOT NULL - DROP PROCEDURE dbo.usp_ReservarNumeroComprobante; - """; - - const string createSp = """ - 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; - - 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 - - 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 - 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 - """; - - await _connection.ExecuteAsync(createPdv); - await _connection.ExecuteAsync(createPdvIndex); - await _connection.ExecuteAsync(createSecuencia); - await _connection.ExecuteAsync(addPdvPeriod); - await _connection.ExecuteAsync(setPdvVersioning); + // Drops primero (limpieza de versión previa) + await _connection.ExecuteAsync(dropSp); await _connection.ExecuteAsync(disableSecuenciaVersioning); await _connection.ExecuteAsync(dropSecuenciaHistory); - await _connection.ExecuteAsync(dropSecuenciaPeriod); - await _connection.ExecuteAsync(dropSecuenciaValidCols); - await _connection.ExecuteAsync(dropSp); - await _connection.ExecuteAsync(createSp); + await _connection.ExecuteAsync(dropSecuencia); + + // Luego crear PuntoDeVenta + Temporal Table + await _connection.ExecuteAsync(createPdv); + await _connection.ExecuteAsync(createPdvIndex); + await _connection.ExecuteAsync(addPdvPeriod); + await _connection.ExecuteAsync(setPdvVersioning); } ///