ADM-008: Puntos de Venta (CRUD fundacional) #19
@@ -34,6 +34,7 @@ public static class DependencyInjection
|
||||
services.AddScoped<IRolPermisoRepository, RolPermisoRepository>();
|
||||
services.AddScoped<IMedioRepository, MedioRepository>();
|
||||
services.AddScoped<ISeccionRepository, SeccionRepository>();
|
||||
services.AddScoped<IPuntoDeVentaRepository, PuntoDeVentaRepository>();
|
||||
|
||||
// JWT Options — bound lazily via IOptions so tests can override via ConfigureWebHost
|
||||
services.Configure<JwtOptions>(configuration.GetSection("Jwt"));
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
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;
|
||||
|
||||
public sealed class PuntoDeVentaRepository : IPuntoDeVentaRepository
|
||||
{
|
||||
private readonly SqlConnectionFactory _connectionFactory;
|
||||
|
||||
public PuntoDeVentaRepository(SqlConnectionFactory connectionFactory)
|
||||
{
|
||||
_connectionFactory = connectionFactory;
|
||||
}
|
||||
|
||||
public async Task<int> AddAsync(PuntoDeVenta pdv, CancellationToken ct = default)
|
||||
{
|
||||
// DF handles: Activo (1), FechaCreacion (SYSUTCDATETIME()).
|
||||
const string sql = """
|
||||
INSERT INTO dbo.PuntoDeVenta (MedioId, NumeroAFIP, Nombre, Descripcion)
|
||||
OUTPUT INSERTED.Id
|
||||
VALUES (@MedioId, @NumeroAFIP, @Nombre, @Descripcion)
|
||||
""";
|
||||
|
||||
await using var connection = _connectionFactory.CreateConnection();
|
||||
await connection.OpenAsync(ct);
|
||||
|
||||
try
|
||||
{
|
||||
return await connection.ExecuteScalarAsync<int>(sql, new
|
||||
{
|
||||
pdv.MedioId,
|
||||
pdv.NumeroAFIP,
|
||||
pdv.Nombre,
|
||||
pdv.Descripcion,
|
||||
});
|
||||
}
|
||||
catch (SqlException ex) when (IsUniqueViolation(ex) && ex.Message.Contains("UQ_PuntoDeVenta_MedioId_NumeroAFIP"))
|
||||
{
|
||||
throw new NumeroAFIPDuplicadoException(pdv.MedioId, pdv.NumeroAFIP);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<PuntoDeVenta?> GetByIdAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT Id, MedioId, NumeroAFIP, Nombre, Descripcion, Activo, FechaCreacion, FechaModificacion
|
||||
FROM dbo.PuntoDeVenta
|
||||
WHERE Id = @Id
|
||||
""";
|
||||
|
||||
await using var connection = _connectionFactory.CreateConnection();
|
||||
await connection.OpenAsync(ct);
|
||||
|
||||
var row = await connection.QuerySingleOrDefaultAsync<PdvRow>(sql, new { Id = id });
|
||||
return row is null ? null : MapRow(row);
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsByNumeroAFIPInMedioAsync(int medioId, short numeroAFIP, int? excludeId = null, CancellationToken ct = default)
|
||||
{
|
||||
var sql = excludeId.HasValue
|
||||
? "SELECT COUNT(1) FROM dbo.PuntoDeVenta WHERE MedioId = @MedioId AND NumeroAFIP = @NumeroAFIP AND Id <> @ExcludeId"
|
||||
: "SELECT COUNT(1) FROM dbo.PuntoDeVenta WHERE MedioId = @MedioId AND NumeroAFIP = @NumeroAFIP";
|
||||
|
||||
await using var connection = _connectionFactory.CreateConnection();
|
||||
await connection.OpenAsync(ct);
|
||||
|
||||
var count = await connection.ExecuteScalarAsync<int>(sql, new { MedioId = medioId, NumeroAFIP = numeroAFIP, ExcludeId = excludeId });
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(PuntoDeVenta pdv, CancellationToken ct = default)
|
||||
{
|
||||
const string sql = """
|
||||
UPDATE dbo.PuntoDeVenta
|
||||
SET NumeroAFIP = @NumeroAFIP,
|
||||
Nombre = @Nombre,
|
||||
Descripcion = @Descripcion,
|
||||
Activo = @Activo,
|
||||
FechaModificacion = @FechaModificacion
|
||||
WHERE Id = @Id
|
||||
""";
|
||||
|
||||
await using var connection = _connectionFactory.CreateConnection();
|
||||
await connection.OpenAsync(ct);
|
||||
|
||||
try
|
||||
{
|
||||
await connection.ExecuteAsync(sql, new
|
||||
{
|
||||
pdv.NumeroAFIP,
|
||||
pdv.Nombre,
|
||||
pdv.Descripcion,
|
||||
pdv.Activo,
|
||||
FechaModificacion = pdv.FechaModificacion ?? DateTime.UtcNow,
|
||||
pdv.Id,
|
||||
});
|
||||
}
|
||||
catch (SqlException ex) when (IsUniqueViolation(ex) && ex.Message.Contains("UQ_PuntoDeVenta_MedioId_NumeroAFIP"))
|
||||
{
|
||||
throw new NumeroAFIPDuplicadoException(pdv.MedioId, pdv.NumeroAFIP);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<PagedResult<PuntoDeVenta>> GetPagedAsync(PuntosDeVentaQuery q, CancellationToken ct = default)
|
||||
{
|
||||
var page = Math.Max(1, q.Page);
|
||||
var pageSize = Math.Clamp(q.PageSize, 1, 100);
|
||||
var offset = (page - 1) * pageSize;
|
||||
|
||||
var where = new StringBuilder("WHERE 1=1");
|
||||
var parameters = new DynamicParameters();
|
||||
parameters.Add("PageSize", pageSize);
|
||||
parameters.Add("Offset", offset);
|
||||
|
||||
if (q.MedioId.HasValue)
|
||||
{
|
||||
where.Append(" AND MedioId = @MedioId");
|
||||
parameters.Add("MedioId", q.MedioId.Value);
|
||||
}
|
||||
|
||||
if (q.Activo.HasValue)
|
||||
{
|
||||
where.Append(" AND Activo = @Activo");
|
||||
parameters.Add("Activo", q.Activo.Value ? 1 : 0);
|
||||
}
|
||||
|
||||
var sql = $"""
|
||||
SELECT
|
||||
Id, MedioId, NumeroAFIP, Nombre, Descripcion, Activo, FechaCreacion, FechaModificacion,
|
||||
COUNT(*) OVER() AS TotalCount
|
||||
FROM dbo.PuntoDeVenta
|
||||
{where}
|
||||
ORDER BY MedioId, NumeroAFIP
|
||||
OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY
|
||||
""";
|
||||
|
||||
await using var connection = _connectionFactory.CreateConnection();
|
||||
await connection.OpenAsync(ct);
|
||||
|
||||
var rows = await connection.QueryAsync<PdvPagedRow>(sql, parameters);
|
||||
var list = rows.ToList();
|
||||
|
||||
var total = list.Count > 0 ? list[0].TotalCount : 0;
|
||||
var items = list.Select(r => MapRow(r)).ToList();
|
||||
|
||||
return new PagedResult<PuntoDeVenta>(items, page, pageSize, total);
|
||||
}
|
||||
|
||||
public async Task<int> 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<int>("NumeroReservado");
|
||||
}
|
||||
|
||||
public async Task<int?> 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<int?>(sql, new
|
||||
{
|
||||
PuntoDeVentaId = puntoDeVentaId,
|
||||
TipoComprobante = (byte)tipo,
|
||||
});
|
||||
}
|
||||
|
||||
// ── mapping ───────────────────────────────────────────────────────────────
|
||||
|
||||
private static PuntoDeVenta MapRow(PdvRow r)
|
||||
=> new(
|
||||
id: r.Id,
|
||||
medioId: r.MedioId,
|
||||
numeroAFIP: r.NumeroAFIP,
|
||||
nombre: r.Nombre,
|
||||
descripcion: r.Descripcion,
|
||||
activo: r.Activo,
|
||||
fechaCreacion: r.FechaCreacion,
|
||||
fechaModificacion: r.FechaModificacion);
|
||||
|
||||
private static PuntoDeVenta MapRow(PdvPagedRow r)
|
||||
=> new(
|
||||
id: r.Id,
|
||||
medioId: r.MedioId,
|
||||
numeroAFIP: r.NumeroAFIP,
|
||||
nombre: r.Nombre,
|
||||
descripcion: r.Descripcion,
|
||||
activo: r.Activo,
|
||||
fechaCreacion: r.FechaCreacion,
|
||||
fechaModificacion: r.FechaModificacion);
|
||||
|
||||
private static bool IsUniqueViolation(SqlException ex)
|
||||
=> ex.Number is 2627 or 2601;
|
||||
|
||||
// ── private rows ──────────────────────────────────────────────────────────
|
||||
|
||||
private sealed record PdvRow(
|
||||
int Id,
|
||||
int MedioId,
|
||||
short NumeroAFIP,
|
||||
string Nombre,
|
||||
string? Descripcion,
|
||||
bool Activo,
|
||||
DateTime FechaCreacion,
|
||||
DateTime? FechaModificacion);
|
||||
|
||||
private sealed record PdvPagedRow(
|
||||
int Id,
|
||||
int MedioId,
|
||||
short NumeroAFIP,
|
||||
string Nombre,
|
||||
string? Descripcion,
|
||||
bool Activo,
|
||||
DateTime FechaCreacion,
|
||||
DateTime? FechaModificacion,
|
||||
int TotalCount);
|
||||
}
|
||||
Reference in New Issue
Block a user