feat(infra): MedioRepository + SeccionRepository + integration tests — ADM-001 B5

This commit is contained in:
2026-04-16 19:04:09 -03:00
parent a1a8e6e0cb
commit 2f0da2d720
5 changed files with 905 additions and 0 deletions

View File

@@ -32,6 +32,8 @@ public static class DependencyInjection
services.AddScoped<IRolRepository, RolRepository>();
services.AddScoped<IPermisoRepository, PermisoRepository>();
services.AddScoped<IRolPermisoRepository, RolPermisoRepository>();
services.AddScoped<IMedioRepository, MedioRepository>();
services.AddScoped<ISeccionRepository, SeccionRepository>();
// JWT Options — bound lazily via IOptions so tests can override via ConfigureWebHost
services.Configure<JwtOptions>(configuration.GetSection("Jwt"));

View File

@@ -0,0 +1,188 @@
using System.Text;
using Dapper;
using SIGCM2.Application.Abstractions.Persistence;
using SIGCM2.Application.Common;
using SIGCM2.Domain.Entities;
namespace SIGCM2.Infrastructure.Persistence;
public sealed class MedioRepository : IMedioRepository
{
private readonly SqlConnectionFactory _connectionFactory;
public MedioRepository(SqlConnectionFactory connectionFactory)
{
_connectionFactory = connectionFactory;
}
public async Task<int> AddAsync(Medio m, CancellationToken ct = default)
{
// DF handles: Activo (1), FechaCreacion (SYSUTCDATETIME()).
const string sql = """
INSERT INTO dbo.Medio (Codigo, Nombre, Tipo, PlataformaEmpresaId)
OUTPUT INSERTED.Id
VALUES (@Codigo, @Nombre, @Tipo, @PlataformaEmpresaId)
""";
await using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync(ct);
return await connection.ExecuteScalarAsync<int>(sql, new
{
m.Codigo,
m.Nombre,
Tipo = (int)m.Tipo,
m.PlataformaEmpresaId,
});
}
public async Task<Medio?> GetByIdAsync(int id, CancellationToken ct = default)
{
const string sql = """
SELECT Id, Codigo, Nombre, Tipo, PlataformaEmpresaId, Activo, FechaCreacion, FechaModificacion
FROM dbo.Medio
WHERE Id = @Id
""";
await using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync(ct);
var row = await connection.QuerySingleOrDefaultAsync<MedioRow>(sql, new { Id = id });
return row is null ? null : MapRow(row);
}
public async Task<bool> ExistsByCodigoAsync(string codigo, CancellationToken ct = default)
{
const string sql = """
SELECT COUNT(1) FROM dbo.Medio WHERE Codigo = @Codigo
""";
await using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync(ct);
var count = await connection.ExecuteScalarAsync<int>(sql, new { Codigo = codigo });
return count > 0;
}
public async Task UpdateAsync(Medio m, CancellationToken ct = default)
{
const string sql = """
UPDATE dbo.Medio
SET Nombre = @Nombre,
Tipo = @Tipo,
PlataformaEmpresaId = @PlataformaEmpresaId,
Activo = @Activo,
FechaModificacion = @FechaModificacion
WHERE Id = @Id
""";
await using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync(ct);
await connection.ExecuteAsync(sql, new
{
m.Nombre,
Tipo = (int)m.Tipo,
m.PlataformaEmpresaId,
m.Activo,
FechaModificacion = m.FechaModificacion ?? DateTime.UtcNow,
m.Id,
});
}
public async Task<PagedResult<Medio>> GetPagedAsync(MediosQuery 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.Activo.HasValue)
{
where.Append(" AND Activo = @Activo");
parameters.Add("Activo", q.Activo.Value ? 1 : 0);
}
if (q.Tipo.HasValue)
{
where.Append(" AND Tipo = @Tipo");
parameters.Add("Tipo", (int)q.Tipo.Value);
}
if (!string.IsNullOrWhiteSpace(q.Search))
{
where.Append(" AND (Codigo LIKE @Search OR Nombre LIKE @Search)");
parameters.Add("Search", $"%{q.Search}%");
}
var sql = $"""
SELECT
Id, Codigo, Nombre, Tipo, PlataformaEmpresaId, Activo, FechaCreacion, FechaModificacion,
COUNT(*) OVER() AS TotalCount
FROM dbo.Medio
{where}
ORDER BY Codigo
OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY
""";
await using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync(ct);
var rows = await connection.QueryAsync<MedioPagedRow>(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<Medio>(items, page, pageSize, total);
}
// ── mapping ───────────────────────────────────────────────────────────────
private static Medio MapRow(MedioRow r)
=> new(
id: r.Id,
codigo: r.Codigo,
nombre: r.Nombre,
tipo: (TipoMedio)r.Tipo,
plataformaEmpresaId: r.PlataformaEmpresaId,
activo: r.Activo,
fechaCreacion: r.FechaCreacion,
fechaModificacion: r.FechaModificacion);
private static Medio MapRow(MedioPagedRow r)
=> new(
id: r.Id,
codigo: r.Codigo,
nombre: r.Nombre,
tipo: (TipoMedio)r.Tipo,
plataformaEmpresaId: r.PlataformaEmpresaId,
activo: r.Activo,
fechaCreacion: r.FechaCreacion,
fechaModificacion: r.FechaModificacion);
private sealed record MedioRow(
int Id,
string Codigo,
string Nombre,
byte Tipo,
int? PlataformaEmpresaId,
bool Activo,
DateTime FechaCreacion,
DateTime? FechaModificacion);
private sealed record MedioPagedRow(
int Id,
string Codigo,
string Nombre,
byte Tipo,
int? PlataformaEmpresaId,
bool Activo,
DateTime FechaCreacion,
DateTime? FechaModificacion,
int TotalCount);
}

View File

@@ -0,0 +1,197 @@
using System.Text;
using Dapper;
using SIGCM2.Application.Abstractions.Persistence;
using SIGCM2.Application.Common;
using SIGCM2.Domain.Entities;
namespace SIGCM2.Infrastructure.Persistence;
public sealed class SeccionRepository : ISeccionRepository
{
private readonly SqlConnectionFactory _connectionFactory;
public SeccionRepository(SqlConnectionFactory connectionFactory)
{
_connectionFactory = connectionFactory;
}
public async Task<int> AddAsync(Seccion s, CancellationToken ct = default)
{
// DF handles: Activo (1), FechaCreacion (SYSUTCDATETIME()).
// FK_Seccion_Medio: if MedioId does not exist, SQL Server raises FK violation — let it bubble.
const string sql = """
INSERT INTO dbo.Seccion (MedioId, Codigo, Nombre, Tipo)
OUTPUT INSERTED.Id
VALUES (@MedioId, @Codigo, @Nombre, @Tipo)
""";
await using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync(ct);
return await connection.ExecuteScalarAsync<int>(sql, new
{
s.MedioId,
s.Codigo,
s.Nombre,
s.Tipo,
});
}
public async Task<Seccion?> GetByIdAsync(int id, CancellationToken ct = default)
{
const string sql = """
SELECT Id, MedioId, Codigo, Nombre, Tipo, Activo, FechaCreacion, FechaModificacion
FROM dbo.Seccion
WHERE Id = @Id
""";
await using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync(ct);
var row = await connection.QuerySingleOrDefaultAsync<SeccionRow>(sql, new { Id = id });
return row is null ? null : MapRow(row);
}
public async Task<bool> ExistsByCodigoInMedioAsync(int medioId, string codigo, CancellationToken ct = default)
{
const string sql = """
SELECT COUNT(1) FROM dbo.Seccion WHERE MedioId = @MedioId AND Codigo = @Codigo
""";
await using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync(ct);
var count = await connection.ExecuteScalarAsync<int>(sql, new { MedioId = medioId, Codigo = codigo });
return count > 0;
}
public async Task UpdateAsync(Seccion s, CancellationToken ct = default)
{
const string sql = """
UPDATE dbo.Seccion
SET Nombre = @Nombre,
Tipo = @Tipo,
Activo = @Activo,
FechaModificacion = @FechaModificacion
WHERE Id = @Id
""";
await using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync(ct);
await connection.ExecuteAsync(sql, new
{
s.Nombre,
s.Tipo,
s.Activo,
FechaModificacion = s.FechaModificacion ?? DateTime.UtcNow,
s.Id,
});
}
public async Task<PagedResult<Seccion>> GetPagedAsync(SeccionesQuery 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 (!string.IsNullOrWhiteSpace(q.Tipo))
{
where.Append(" AND Tipo = @Tipo");
parameters.Add("Tipo", q.Tipo);
}
if (q.Activo.HasValue)
{
where.Append(" AND Activo = @Activo");
parameters.Add("Activo", q.Activo.Value ? 1 : 0);
}
if (!string.IsNullOrWhiteSpace(q.Search))
{
where.Append(" AND (Codigo LIKE @Search OR Nombre LIKE @Search)");
parameters.Add("Search", $"%{q.Search}%");
}
// ADM-001: filter only Seccion.Activo; Medio.Activo check is left to Application/UI layer.
// Joining Medio to filter on m.Activo would affect performance for large catalogs and is
// not required by the current specs. REQ-SEC-003 (Deactivate Medio hides Secciones) is
// enforced at the Application handler level, not the query level.
var sql = $"""
SELECT
Id, MedioId, Codigo, Nombre, Tipo, Activo, FechaCreacion, FechaModificacion,
COUNT(*) OVER() AS TotalCount
FROM dbo.Seccion
{where}
ORDER BY MedioId, Codigo
OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY
""";
await using var connection = _connectionFactory.CreateConnection();
await connection.OpenAsync(ct);
var rows = await connection.QueryAsync<SeccionPagedRow>(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<Seccion>(items, page, pageSize, total);
}
// ── mapping ───────────────────────────────────────────────────────────────
private static Seccion MapRow(SeccionRow r)
=> new(
id: r.Id,
medioId: r.MedioId,
codigo: r.Codigo,
nombre: r.Nombre,
tipo: r.Tipo,
activo: r.Activo,
fechaCreacion: r.FechaCreacion,
fechaModificacion: r.FechaModificacion);
private static Seccion MapRow(SeccionPagedRow r)
=> new(
id: r.Id,
medioId: r.MedioId,
codigo: r.Codigo,
nombre: r.Nombre,
tipo: r.Tipo,
activo: r.Activo,
fechaCreacion: r.FechaCreacion,
fechaModificacion: r.FechaModificacion);
private sealed record SeccionRow(
int Id,
int MedioId,
string Codigo,
string Nombre,
string Tipo,
bool Activo,
DateTime FechaCreacion,
DateTime? FechaModificacion);
private sealed record SeccionPagedRow(
int Id,
int MedioId,
string Codigo,
string Nombre,
string Tipo,
bool Activo,
DateTime FechaCreacion,
DateTime? FechaModificacion,
int TotalCount);
}