ADM-008: Puntos de Venta (CRUD fundacional) #19
@@ -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 ──────────────────────────────────────────────────
|
||||
|
||||
/// <summary>T5.3 — Primera reserva inicializa en 1.</summary>
|
||||
[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<JsonElement>();
|
||||
Assert.Equal(1, json.GetProperty("numeroReservado").GetInt32());
|
||||
}
|
||||
finally
|
||||
{
|
||||
await DeleteMedioIfExistsAsync(medioCodigo);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>T5.3 — 409 punto_de_venta_inactivo al reservar en PdV inactivo.</summary>
|
||||
[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<JsonElement>();
|
||||
Assert.Equal("punto_de_venta_inactivo", json.GetProperty("error").GetString());
|
||||
}
|
||||
finally
|
||||
{
|
||||
await DeleteMedioIfExistsAsync(medioCodigo);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>T5.3 — 409 medio_inactivo al reservar con Medio inactivo.</summary>
|
||||
[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<JsonElement>();
|
||||
Assert.Equal("medio_inactivo", json.GetProperty("error").GetString());
|
||||
}
|
||||
finally
|
||||
{
|
||||
await DeleteMedioIfExistsAsync(medioCodigo);
|
||||
}
|
||||
}
|
||||
|
||||
// ── SECUENCIAS: PROXIMO ───────────────────────────────────────────────────
|
||||
|
||||
/// <summary>T5.3 — GetProximo es read-only: no modifica UltimoNumero.</summary>
|
||||
[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<JsonElement>();
|
||||
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<JsonElement>();
|
||||
Assert.Equal(2, json2.GetProperty("proximoNumero").GetInt32());
|
||||
}
|
||||
finally
|
||||
{
|
||||
await DeleteMedioIfExistsAsync(medioCodigo);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>T5.3 — GetProximo para fila inexistente devuelve 1.</summary>
|
||||
[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<JsonElement>();
|
||||
Assert.Equal(1, json.GetProperty("proximoNumero").GetInt32());
|
||||
}
|
||||
finally
|
||||
{
|
||||
await DeleteMedioIfExistsAsync(medioCodigo);
|
||||
}
|
||||
}
|
||||
|
||||
// ── T5.4 — Concurrencia ───────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// T5.4 — 50 tasks paralelas reservando para mismo PdV + TipoComprobante
|
||||
/// deben producir 50 números distintos cubriendo {1..50}.
|
||||
/// </summary>
|
||||
[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<JsonElement>();
|
||||
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 ─────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// T5.5 — 100 reservas en serie para mismo PdV + TipoComprobante
|
||||
/// deben devolver {1, 2, 3, ..., 100} en orden.
|
||||
/// </summary>
|
||||
[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<int>(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<JsonElement>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<TipoComprobante>())
|
||||
{
|
||||
var seq = Make(tipo: tipo);
|
||||
Assert.Equal(tipo, seq.TipoComprobante);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<IPuntoDeVentaRepository>();
|
||||
private readonly GetProximoNumeroQueryHandler _handler;
|
||||
|
||||
public GetProximoNumeroQueryHandlerTests()
|
||||
{
|
||||
_handler = new GetProximoNumeroQueryHandler(_repo);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ExistingSequence_ReturnsUltimoNumeroMasUno()
|
||||
{
|
||||
_repo.GetUltimoNumeroAsync(10, TipoComprobante.FacturaA, Arg.Any<CancellationToken>())
|
||||
.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<CancellationToken>())
|
||||
.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<CancellationToken>())
|
||||
.Returns(5);
|
||||
|
||||
await _handler.Handle(new GetProximoNumeroQuery(10, TipoComprobante.FacturaA));
|
||||
|
||||
await _repo.DidNotReceive().ReservarNumeroAsync(
|
||||
Arg.Any<int>(), Arg.Any<TipoComprobante>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
}
|
||||
@@ -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<IPuntoDeVentaRepository>();
|
||||
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<CancellationToken>())
|
||||
.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<CancellationToken>())
|
||||
.Returns(
|
||||
_ => Task.FromException<int>(deadlock),
|
||||
_ => Task.FromException<int>(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<CancellationToken>())
|
||||
.Returns(
|
||||
_ => Task.FromException<int>(deadlock),
|
||||
_ => Task.FromException<int>(deadlock),
|
||||
_ => Task.FromException<int>(deadlock));
|
||||
|
||||
await Assert.ThrowsAsync<DeadlockTransientException>(
|
||||
() => _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<CancellationToken>())
|
||||
.Returns(_ => Task.FromException<int>(deadlock));
|
||||
|
||||
try { await _handler.Handle(ValidCommand); } catch (DeadlockTransientException) { }
|
||||
|
||||
await _repo.Received(4).ReservarNumeroAsync(
|
||||
Arg.Any<int>(), Arg.Any<TipoComprobante>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
// ── domain exceptions bubble up without retry ─────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PuntoDeVentaInactivo_BubblesUpImmediately()
|
||||
{
|
||||
_repo.ReservarNumeroAsync(10, TipoComprobante.FacturaA, Arg.Any<CancellationToken>())
|
||||
.Throws(new PuntoDeVentaInactivoException(10));
|
||||
|
||||
await Assert.ThrowsAsync<PuntoDeVentaInactivoException>(
|
||||
() => _handler.Handle(ValidCommand));
|
||||
|
||||
await _repo.Received(1).ReservarNumeroAsync(
|
||||
Arg.Any<int>(), Arg.Any<TipoComprobante>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_MedioInactivo_BubblesUpImmediately()
|
||||
{
|
||||
_repo.ReservarNumeroAsync(10, TipoComprobante.FacturaA, Arg.Any<CancellationToken>())
|
||||
.Throws(new MedioInactivoException(5));
|
||||
|
||||
await Assert.ThrowsAsync<MedioInactivoException>(
|
||||
() => _handler.Handle(ValidCommand));
|
||||
|
||||
await _repo.Received(1).ReservarNumeroAsync(
|
||||
Arg.Any<int>(), Arg.Any<TipoComprobante>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PdvNotFound_BubblesUpImmediately()
|
||||
{
|
||||
_repo.ReservarNumeroAsync(10, TipoComprobante.FacturaA, Arg.Any<CancellationToken>())
|
||||
.Throws(new PuntoDeVentaNotFoundException(10));
|
||||
|
||||
await Assert.ThrowsAsync<PuntoDeVentaNotFoundException>(
|
||||
() => _handler.Handle(ValidCommand));
|
||||
|
||||
await _repo.Received(1).ReservarNumeroAsync(
|
||||
Arg.Any<int>(), Arg.Any<TipoComprobante>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user