ADM-008: Puntos de Venta (CRUD fundacional) #19
@@ -34,6 +34,7 @@ public static class DependencyInjection
|
|||||||
services.AddScoped<IRolPermisoRepository, RolPermisoRepository>();
|
services.AddScoped<IRolPermisoRepository, RolPermisoRepository>();
|
||||||
services.AddScoped<IMedioRepository, MedioRepository>();
|
services.AddScoped<IMedioRepository, MedioRepository>();
|
||||||
services.AddScoped<ISeccionRepository, SeccionRepository>();
|
services.AddScoped<ISeccionRepository, SeccionRepository>();
|
||||||
|
services.AddScoped<IPuntoDeVentaRepository, PuntoDeVentaRepository>();
|
||||||
|
|
||||||
// JWT Options — bound lazily via IOptions so tests can override via ConfigureWebHost
|
// JWT Options — bound lazily via IOptions so tests can override via ConfigureWebHost
|
||||||
services.Configure<JwtOptions>(configuration.GetSection("Jwt"));
|
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