diff --git a/src/api/SIGCM2.Api/Controllers/PuntosDeVentaController.cs b/src/api/SIGCM2.Api/Controllers/PuntosDeVentaController.cs index cc3b2c2..0ebffb9 100644 --- a/src/api/SIGCM2.Api/Controllers/PuntosDeVentaController.cs +++ b/src/api/SIGCM2.Api/Controllers/PuntosDeVentaController.cs @@ -7,11 +7,8 @@ using SIGCM2.Application.PuntosDeVenta.Create; using SIGCM2.Application.PuntosDeVenta.Deactivate; using SIGCM2.Application.PuntosDeVenta.GetById; using SIGCM2.Application.PuntosDeVenta.List; -using SIGCM2.Application.PuntosDeVenta.ProximoNumero; using SIGCM2.Application.PuntosDeVenta.Reactivate; -using SIGCM2.Application.PuntosDeVenta.Reservar; using SIGCM2.Application.PuntosDeVenta.Update; -using SIGCM2.Domain.Enums; namespace SIGCM2.Api.Controllers; @@ -160,34 +157,6 @@ public sealed class PuntosDeVentaController : ControllerBase return NoContent(); } - /// Reserves the next sequential number for a given PdV and TipoComprobante. - [HttpPost("{id:int}/secuencias/{tipoComprobante}/reservar")] - [RequirePermission("administracion:puntos_de_venta:gestionar")] - [ProducesResponseType(typeof(ReservaNumeroDto), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task ReservarNumero([FromRoute] int id, [FromRoute] TipoComprobante tipoComprobante) - { - var command = new ReservarNumeroCommand(id, tipoComprobante); - var result = await _dispatcher.Send(command); - return Ok(result); - } - - /// Returns the next available number (read-only, no reservation). - [HttpGet("{id:int}/secuencias/{tipoComprobante}/proximo")] - [RequirePermission("administracion:puntos_de_venta:gestionar")] - [ProducesResponseType(typeof(ProximoNumeroDto), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task GetProximoNumero([FromRoute] int id, [FromRoute] TipoComprobante tipoComprobante) - { - var query = new GetProximoNumeroQuery(id, tipoComprobante); - var result = await _dispatcher.Send(query); - return Ok(result); - } } // ── Request body records ────────────────────────────────────────────────────── diff --git a/src/api/SIGCM2.Api/Filters/ExceptionFilter.cs b/src/api/SIGCM2.Api/Filters/ExceptionFilter.cs index 35fb5be..b86c0e2 100644 --- a/src/api/SIGCM2.Api/Filters/ExceptionFilter.cs +++ b/src/api/SIGCM2.Api/Filters/ExceptionFilter.cs @@ -244,18 +244,6 @@ public sealed class ExceptionFilter : IExceptionFilter context.ExceptionHandled = true; break; - case PuntoDeVentaInactivoException puntoDeVentaInactivoEx: - context.Result = new ObjectResult(new - { - error = "punto_de_venta_inactivo", - message = puntoDeVentaInactivoEx.Message - }) - { - StatusCode = StatusCodes.Status409Conflict - }; - context.ExceptionHandled = true; - break; - case NumeroAFIPDuplicadoException numeroAFIPDupEx: context.Result = new ObjectResult(new { diff --git a/src/api/SIGCM2.Application/Abstractions/Persistence/IPuntoDeVentaRepository.cs b/src/api/SIGCM2.Application/Abstractions/Persistence/IPuntoDeVentaRepository.cs index 5d5cabd..eb6cf04 100644 --- a/src/api/SIGCM2.Application/Abstractions/Persistence/IPuntoDeVentaRepository.cs +++ b/src/api/SIGCM2.Application/Abstractions/Persistence/IPuntoDeVentaRepository.cs @@ -1,6 +1,5 @@ using SIGCM2.Application.Common; using SIGCM2.Domain.Entities; -using SIGCM2.Domain.Enums; namespace SIGCM2.Application.Abstractions.Persistence; @@ -11,6 +10,4 @@ public interface IPuntoDeVentaRepository Task ExistsByNumeroAFIPInMedioAsync(int medioId, short numeroAFIP, int? excludeId = null, CancellationToken ct = default); Task UpdateAsync(PuntoDeVenta pdv, CancellationToken ct = default); Task> GetPagedAsync(PuntosDeVentaQuery q, CancellationToken ct = default); - Task ReservarNumeroAsync(int puntoDeVentaId, TipoComprobante tipo, CancellationToken ct = default); - Task GetUltimoNumeroAsync(int puntoDeVentaId, TipoComprobante tipo, CancellationToken ct = default); } diff --git a/src/api/SIGCM2.Application/DependencyInjection.cs b/src/api/SIGCM2.Application/DependencyInjection.cs index 83e0276..cda65d3 100644 --- a/src/api/SIGCM2.Application/DependencyInjection.cs +++ b/src/api/SIGCM2.Application/DependencyInjection.cs @@ -25,9 +25,7 @@ using SIGCM2.Application.PuntosDeVenta.Create; using SIGCM2.Application.PuntosDeVenta.Deactivate; using SIGCM2.Application.PuntosDeVenta.GetById; using SIGCM2.Application.PuntosDeVenta.List; -using SIGCM2.Application.PuntosDeVenta.ProximoNumero; using SIGCM2.Application.PuntosDeVenta.Reactivate; -using SIGCM2.Application.PuntosDeVenta.Reservar; using SIGCM2.Application.PuntosDeVenta.Update; using SIGCM2.Application.Secciones.Create; using SIGCM2.Application.Secciones.Deactivate; @@ -105,8 +103,6 @@ public static class DependencyInjection services.AddScoped, ReactivatePuntoDeVentaCommandHandler>(); services.AddScoped>, ListPuntosDeVentaQueryHandler>(); services.AddScoped, GetPuntoDeVentaByIdQueryHandler>(); - services.AddScoped, ReservarNumeroCommandHandler>(); - services.AddScoped, GetProximoNumeroQueryHandler>(); // FluentValidation validators (scans entire Application assembly) services.AddValidatorsFromAssemblyContaining(); diff --git a/src/api/SIGCM2.Application/PuntosDeVenta/ProximoNumero/GetProximoNumeroQuery.cs b/src/api/SIGCM2.Application/PuntosDeVenta/ProximoNumero/GetProximoNumeroQuery.cs deleted file mode 100644 index 7fb0ada..0000000 --- a/src/api/SIGCM2.Application/PuntosDeVenta/ProximoNumero/GetProximoNumeroQuery.cs +++ /dev/null @@ -1,5 +0,0 @@ -using SIGCM2.Domain.Enums; - -namespace SIGCM2.Application.PuntosDeVenta.ProximoNumero; - -public sealed record GetProximoNumeroQuery(int PuntoDeVentaId, TipoComprobante TipoComprobante); diff --git a/src/api/SIGCM2.Application/PuntosDeVenta/ProximoNumero/GetProximoNumeroQueryHandler.cs b/src/api/SIGCM2.Application/PuntosDeVenta/ProximoNumero/GetProximoNumeroQueryHandler.cs deleted file mode 100644 index e0c930c..0000000 --- a/src/api/SIGCM2.Application/PuntosDeVenta/ProximoNumero/GetProximoNumeroQueryHandler.cs +++ /dev/null @@ -1,27 +0,0 @@ -using SIGCM2.Application.Abstractions; -using SIGCM2.Application.Abstractions.Persistence; - -namespace SIGCM2.Application.PuntosDeVenta.ProximoNumero; - -/// -/// Consulta el próximo número disponible sin reservarlo (read-only, REQ-SEC-CMB-005). -/// Retorna UltimoNumero+1; si no existe fila devuelve 1. -/// -public sealed class GetProximoNumeroQueryHandler : ICommandHandler -{ - private readonly IPuntoDeVentaRepository _repo; - - public GetProximoNumeroQueryHandler(IPuntoDeVentaRepository repo) - { - _repo = repo; - } - - public async Task Handle(GetProximoNumeroQuery query) - { - var ultimoNumero = await _repo.GetUltimoNumeroAsync(query.PuntoDeVentaId, query.TipoComprobante); - - var proximo = ultimoNumero.HasValue ? ultimoNumero.Value + 1 : 1; - - return new ProximoNumeroDto(query.TipoComprobante, proximo); - } -} diff --git a/src/api/SIGCM2.Application/PuntosDeVenta/ProximoNumero/ProximoNumeroDto.cs b/src/api/SIGCM2.Application/PuntosDeVenta/ProximoNumero/ProximoNumeroDto.cs deleted file mode 100644 index 3b2fb8f..0000000 --- a/src/api/SIGCM2.Application/PuntosDeVenta/ProximoNumero/ProximoNumeroDto.cs +++ /dev/null @@ -1,5 +0,0 @@ -using SIGCM2.Domain.Enums; - -namespace SIGCM2.Application.PuntosDeVenta.ProximoNumero; - -public sealed record ProximoNumeroDto(TipoComprobante TipoComprobante, int ProximoNumero); diff --git a/src/api/SIGCM2.Application/PuntosDeVenta/Reservar/ReservaNumeroDto.cs b/src/api/SIGCM2.Application/PuntosDeVenta/Reservar/ReservaNumeroDto.cs deleted file mode 100644 index 1544c7e..0000000 --- a/src/api/SIGCM2.Application/PuntosDeVenta/Reservar/ReservaNumeroDto.cs +++ /dev/null @@ -1,5 +0,0 @@ -using SIGCM2.Domain.Enums; - -namespace SIGCM2.Application.PuntosDeVenta.Reservar; - -public sealed record ReservaNumeroDto(TipoComprobante TipoComprobante, int NumeroReservado); diff --git a/src/api/SIGCM2.Application/PuntosDeVenta/Reservar/ReservarNumeroCommand.cs b/src/api/SIGCM2.Application/PuntosDeVenta/Reservar/ReservarNumeroCommand.cs deleted file mode 100644 index 44d4918..0000000 --- a/src/api/SIGCM2.Application/PuntosDeVenta/Reservar/ReservarNumeroCommand.cs +++ /dev/null @@ -1,5 +0,0 @@ -using SIGCM2.Domain.Enums; - -namespace SIGCM2.Application.PuntosDeVenta.Reservar; - -public sealed record ReservarNumeroCommand(int PuntoDeVentaId, TipoComprobante TipoComprobante); diff --git a/src/api/SIGCM2.Application/PuntosDeVenta/Reservar/ReservarNumeroCommandHandler.cs b/src/api/SIGCM2.Application/PuntosDeVenta/Reservar/ReservarNumeroCommandHandler.cs deleted file mode 100644 index 86896e7..0000000 --- a/src/api/SIGCM2.Application/PuntosDeVenta/Reservar/ReservarNumeroCommandHandler.cs +++ /dev/null @@ -1,53 +0,0 @@ -using SIGCM2.Application.Abstractions; -using SIGCM2.Application.Abstractions.Persistence; -using SIGCM2.Domain.Exceptions; - -namespace SIGCM2.Application.PuntosDeVenta.Reservar; - -/// -/// Reserva el próximo número correlativo para (PdvId × TipoComprobante) ejecutando -/// usp_ReservarNumeroComprobante vía el repositorio. -/// -/// NOTAS DE DISEÑO (AD4, AD9): -/// - NO se envuelve en TransactionScope: el SP ya es atómico bajo SERIALIZABLE. -/// Un TransactionScope ambiente aquí escalaría a DTC → innecesario. -/// - NO usa Polly: no está en el proyecto. Retry deadlock con bucle simple. -/// - Infrastructure traduce SqlException 1205 → DeadlockTransientException. -/// - Backoff en ms: [25, 75, 200, 500, 1200] — 5 retries máximo (6 intentos totales). -/// - La auditoría de reservas corre solo vía Temporal Tables (AD8). -/// -public sealed class ReservarNumeroCommandHandler : ICommandHandler -{ - private readonly IPuntoDeVentaRepository _repo; - private readonly int[] _deadlockBackoffMs; - - private static readonly int[] DefaultBackoffMs = [25, 75, 200, 500, 1200]; - - public ReservarNumeroCommandHandler(IPuntoDeVentaRepository repo) - : this(repo, DefaultBackoffMs) { } - - /// Constructor with custom backoff for testing (e.g., [0,0,0] for fast tests). - public ReservarNumeroCommandHandler(IPuntoDeVentaRepository repo, int[] deadlockBackoffMs) - { - _repo = repo; - _deadlockBackoffMs = deadlockBackoffMs; - } - - public async Task Handle(ReservarNumeroCommand command) - { - for (var i = 0; ; i++) - { - try - { - var numero = await _repo.ReservarNumeroAsync(command.PuntoDeVentaId, command.TipoComprobante); - return new ReservaNumeroDto(command.TipoComprobante, numero); - } - catch (DeadlockTransientException) when (i < _deadlockBackoffMs.Length) - { - // Deadlock — retry with backoff - await Task.Delay(_deadlockBackoffMs[i]); - } - // All other exceptions bubble up immediately - } - } -} diff --git a/src/api/SIGCM2.Domain/Entities/SecuenciaComprobante.cs b/src/api/SIGCM2.Domain/Entities/SecuenciaComprobante.cs deleted file mode 100644 index c936c67..0000000 --- a/src/api/SIGCM2.Domain/Entities/SecuenciaComprobante.cs +++ /dev/null @@ -1,34 +0,0 @@ -using SIGCM2.Domain.Enums; - -namespace SIGCM2.Domain.Entities; - -/// -/// Lleva el correlativo de números de comprobante por (PuntoDeVentaId × TipoComprobante). -/// La reserva atómica la ejecuta usp_ReservarNumeroComprobante directamente en BD. -/// Este objeto es un helper de lectura/proyección. -/// -public sealed class SecuenciaComprobante -{ - public int PuntoDeVentaId { get; } - public TipoComprobante TipoComprobante { get; } - public int UltimoNumero { get; } - public DateTime FechaCreacion { get; } - public DateTime? FechaModificacion { get; } - - /// El próximo número disponible (read-only, sin modificar el estado). - public int ProximoNumero => UltimoNumero + 1; - - public SecuenciaComprobante( - int puntoDeVentaId, - TipoComprobante tipoComprobante, - int ultimoNumero, - DateTime fechaCreacion, - DateTime? fechaModificacion) - { - PuntoDeVentaId = puntoDeVentaId; - TipoComprobante = tipoComprobante; - UltimoNumero = ultimoNumero; - FechaCreacion = fechaCreacion; - FechaModificacion = fechaModificacion; - } -} diff --git a/src/api/SIGCM2.Domain/Enums/TipoComprobante.cs b/src/api/SIGCM2.Domain/Enums/TipoComprobante.cs deleted file mode 100644 index e1be111..0000000 --- a/src/api/SIGCM2.Domain/Enums/TipoComprobante.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace SIGCM2.Domain.Enums; - -/// -/// Tipos de comprobante AFIP soportados por ADM-008. -/// Valor TINYINT persistido en BD (CHECK TipoComprobante BETWEEN 1 AND 6). -/// Migración a tabla maestra diferida a FAC-001. -/// -public enum TipoComprobante : byte -{ - FacturaA = 1, - FacturaB = 2, - FacturaC = 3, - NotaCreditoA = 4, - NotaCreditoB = 5, - NotaCreditoC = 6, -} diff --git a/src/api/SIGCM2.Domain/Exceptions/DeadlockTransientException.cs b/src/api/SIGCM2.Domain/Exceptions/DeadlockTransientException.cs deleted file mode 100644 index fef02a6..0000000 --- a/src/api/SIGCM2.Domain/Exceptions/DeadlockTransientException.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace SIGCM2.Domain.Exceptions; - -/// -/// Thrown by Infrastructure when a database deadlock (SQL 1205) is detected. -/// Allows Application handlers to retry without referencing SqlClient. -/// -public sealed class DeadlockTransientException : DomainException -{ - public DeadlockTransientException() - : base("Se detectó un deadlock en la base de datos. Reintentando operación.") { } - - public DeadlockTransientException(Exception innerException) - : base("Se detectó un deadlock en la base de datos. Reintentando operación.", innerException) { } -} diff --git a/src/api/SIGCM2.Domain/Exceptions/PuntoDeVentaInactivoException.cs b/src/api/SIGCM2.Domain/Exceptions/PuntoDeVentaInactivoException.cs deleted file mode 100644 index 3405365..0000000 --- a/src/api/SIGCM2.Domain/Exceptions/PuntoDeVentaInactivoException.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace SIGCM2.Domain.Exceptions; - -/// -/// Thrown when a mutation (reserva) is attempted on an inactive PuntoDeVenta. -/// -public sealed class PuntoDeVentaInactivoException : DomainException -{ - public int PuntoDeVentaId { get; } - - public PuntoDeVentaInactivoException(int puntoDeVentaId) - : base($"El punto de venta {puntoDeVentaId} está inactivo. No se pueden realizar operaciones hasta reactivarlo.") - { - PuntoDeVentaId = puntoDeVentaId; - } -} diff --git a/src/api/SIGCM2.Infrastructure/Persistence/PuntoDeVentaRepository.cs b/src/api/SIGCM2.Infrastructure/Persistence/PuntoDeVentaRepository.cs index 657f573..d276536 100644 --- a/src/api/SIGCM2.Infrastructure/Persistence/PuntoDeVentaRepository.cs +++ b/src/api/SIGCM2.Infrastructure/Persistence/PuntoDeVentaRepository.cs @@ -1,11 +1,9 @@ -using System.Data; using System.Text; using Dapper; using Microsoft.Data.SqlClient; using SIGCM2.Application.Abstractions.Persistence; using SIGCM2.Application.Common; using SIGCM2.Domain.Entities; -using SIGCM2.Domain.Enums; using SIGCM2.Domain.Exceptions; namespace SIGCM2.Infrastructure.Persistence; @@ -153,56 +151,6 @@ public sealed class PuntoDeVentaRepository : IPuntoDeVentaRepository return new PagedResult(items, page, pageSize, total); } - public async Task ReservarNumeroAsync(int puntoDeVentaId, TipoComprobante tipo, CancellationToken ct = default) - { - var parameters = new DynamicParameters(); - parameters.Add("PuntoDeVentaId", puntoDeVentaId, DbType.Int32); - parameters.Add("TipoComprobante", (byte)tipo, DbType.Byte); - parameters.Add("NumeroReservado", dbType: DbType.Int32, direction: ParameterDirection.Output); - - await using var connection = _connectionFactory.CreateConnection(); - await connection.OpenAsync(ct); - - try - { - await connection.ExecuteAsync( - "dbo.usp_ReservarNumeroComprobante", - parameters, - commandType: CommandType.StoredProcedure); - } - catch (SqlException ex) - { - throw ex.Number switch - { - 50001 => new PuntoDeVentaInactivoException(puntoDeVentaId), - 50002 => new MedioInactivoException(puntoDeVentaId), - 50003 => new PuntoDeVentaNotFoundException(puntoDeVentaId), - 1205 => new DeadlockTransientException(ex), - _ => ex, - }; - } - - return parameters.Get("NumeroReservado"); - } - - public async Task GetUltimoNumeroAsync(int puntoDeVentaId, TipoComprobante tipo, CancellationToken ct = default) - { - const string sql = """ - SELECT UltimoNumero - FROM dbo.SecuenciaComprobante - WHERE PuntoDeVentaId = @PuntoDeVentaId AND TipoComprobante = @TipoComprobante - """; - - await using var connection = _connectionFactory.CreateConnection(); - await connection.OpenAsync(ct); - - return await connection.QuerySingleOrDefaultAsync(sql, new - { - PuntoDeVentaId = puntoDeVentaId, - TipoComprobante = (byte)tipo, - }); - } - // ── mapping ─────────────────────────────────────────────────────────────── private static PuntoDeVenta MapRow(PdvRow r)