feat(infra): MedioRepository + SeccionRepository + integration tests — ADM-001 B5
This commit is contained in:
188
src/api/SIGCM2.Infrastructure/Persistence/MedioRepository.cs
Normal file
188
src/api/SIGCM2.Infrastructure/Persistence/MedioRepository.cs
Normal 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);
|
||||
}
|
||||
197
src/api/SIGCM2.Infrastructure/Persistence/SeccionRepository.cs
Normal file
197
src/api/SIGCM2.Infrastructure/Persistence/SeccionRepository.cs
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user