feat(infrastructure): RubroRepository Dapper + DI + integration tests (CAT-001)
This commit is contained in:
236
src/api/SIGCM2.Infrastructure/Persistence/RubroRepository.cs
Normal file
236
src/api/SIGCM2.Infrastructure/Persistence/RubroRepository.cs
Normal file
@@ -0,0 +1,236 @@
|
||||
using Dapper;
|
||||
using SIGCM2.Application.Abstractions.Persistence;
|
||||
using SIGCM2.Domain.Entities;
|
||||
|
||||
namespace SIGCM2.Infrastructure.Persistence;
|
||||
|
||||
public sealed class RubroRepository : IRubroRepository
|
||||
{
|
||||
private readonly SqlConnectionFactory _factory;
|
||||
|
||||
public RubroRepository(SqlConnectionFactory factory)
|
||||
{
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
public async Task<int> AddAsync(Rubro rubro, CancellationToken ct = default)
|
||||
{
|
||||
// DF handles: Activo (1), FechaCreacion (SYSUTCDATETIME()), Orden (0 default — overridden if provided).
|
||||
const string sql = """
|
||||
INSERT INTO dbo.Rubro (ParentId, Nombre, Orden, Activo, TarifarioBaseId)
|
||||
OUTPUT INSERTED.Id
|
||||
VALUES (@ParentId, @Nombre, @Orden, @Activo, @TarifarioBaseId)
|
||||
""";
|
||||
|
||||
await using var connection = _factory.CreateConnection();
|
||||
await connection.OpenAsync(ct);
|
||||
|
||||
return await connection.ExecuteScalarAsync<int>(sql, new
|
||||
{
|
||||
rubro.ParentId,
|
||||
rubro.Nombre,
|
||||
rubro.Orden,
|
||||
Activo = rubro.Activo ? 1 : 0,
|
||||
rubro.TarifarioBaseId,
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<Rubro?> GetByIdAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT Id, ParentId, Nombre, Orden, Activo, TarifarioBaseId, FechaCreacion, FechaModificacion
|
||||
FROM dbo.Rubro
|
||||
WHERE Id = @Id
|
||||
""";
|
||||
|
||||
await using var connection = _factory.CreateConnection();
|
||||
await connection.OpenAsync(ct);
|
||||
|
||||
var row = await connection.QuerySingleOrDefaultAsync<RubroRow>(sql, new { Id = id });
|
||||
return row is null ? null : MapRow(row);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Rubro>> GetAllAsync(bool incluirInactivos, CancellationToken ct = default)
|
||||
{
|
||||
var sql = incluirInactivos
|
||||
? """
|
||||
SELECT Id, ParentId, Nombre, Orden, Activo, TarifarioBaseId, FechaCreacion, FechaModificacion
|
||||
FROM dbo.Rubro
|
||||
ORDER BY ParentId, Orden
|
||||
"""
|
||||
: """
|
||||
SELECT Id, ParentId, Nombre, Orden, Activo, TarifarioBaseId, FechaCreacion, FechaModificacion
|
||||
FROM dbo.Rubro
|
||||
WHERE Activo = 1
|
||||
ORDER BY ParentId, Orden
|
||||
""";
|
||||
|
||||
await using var connection = _factory.CreateConnection();
|
||||
await connection.OpenAsync(ct);
|
||||
|
||||
var rows = await connection.QueryAsync<RubroRow>(sql);
|
||||
return rows.Select(MapRow).ToList();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Rubro>> GetDescendantsAsync(int rootId, CancellationToken ct = default)
|
||||
{
|
||||
const string sql = """
|
||||
WITH Descendants AS (
|
||||
SELECT Id, ParentId, Nombre, Orden, Activo, TarifarioBaseId, FechaCreacion, FechaModificacion
|
||||
FROM dbo.Rubro
|
||||
WHERE ParentId = @RootId
|
||||
UNION ALL
|
||||
SELECT r.Id, r.ParentId, r.Nombre, r.Orden, r.Activo, r.TarifarioBaseId, r.FechaCreacion, r.FechaModificacion
|
||||
FROM dbo.Rubro r
|
||||
INNER JOIN Descendants d ON r.ParentId = d.Id
|
||||
)
|
||||
SELECT * FROM Descendants
|
||||
""";
|
||||
|
||||
await using var connection = _factory.CreateConnection();
|
||||
await connection.OpenAsync(ct);
|
||||
|
||||
var rows = await connection.QueryAsync<RubroRow>(sql, new { RootId = rootId });
|
||||
return rows.Select(MapRow).ToList();
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(Rubro rubro, CancellationToken ct = default)
|
||||
{
|
||||
const string sql = """
|
||||
UPDATE dbo.Rubro
|
||||
SET Nombre = @Nombre,
|
||||
ParentId = @ParentId,
|
||||
Orden = @Orden,
|
||||
Activo = @Activo,
|
||||
TarifarioBaseId = @TarifarioBaseId,
|
||||
FechaModificacion = @FechaModificacion
|
||||
WHERE Id = @Id
|
||||
""";
|
||||
|
||||
await using var connection = _factory.CreateConnection();
|
||||
await connection.OpenAsync(ct);
|
||||
|
||||
await connection.ExecuteAsync(sql, new
|
||||
{
|
||||
rubro.Nombre,
|
||||
rubro.ParentId,
|
||||
rubro.Orden,
|
||||
Activo = rubro.Activo ? 1 : 0,
|
||||
rubro.TarifarioBaseId,
|
||||
rubro.FechaModificacion,
|
||||
rubro.Id,
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<int> CountActiveChildrenAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT COUNT(1) FROM dbo.Rubro
|
||||
WHERE ParentId = @Id AND Activo = 1
|
||||
""";
|
||||
|
||||
await using var connection = _factory.CreateConnection();
|
||||
await connection.OpenAsync(ct);
|
||||
|
||||
return await connection.ExecuteScalarAsync<int>(sql, new { Id = id });
|
||||
}
|
||||
|
||||
public async Task<int> GetMaxOrdenAsync(int? parentId, CancellationToken ct = default)
|
||||
{
|
||||
// Returns MAX(Orden) + 1 among siblings, or 0 if no siblings exist.
|
||||
// Handler uses return value directly as the orden for the new Rubro.
|
||||
const string sql = """
|
||||
SELECT ISNULL(MAX(Orden) + 1, 0)
|
||||
FROM dbo.Rubro
|
||||
WHERE (@ParentId IS NULL AND ParentId IS NULL)
|
||||
OR ParentId = @ParentId
|
||||
""";
|
||||
|
||||
await using var connection = _factory.CreateConnection();
|
||||
await connection.OpenAsync(ct);
|
||||
|
||||
return await connection.ExecuteScalarAsync<int>(sql, new { ParentId = parentId });
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsByNombreUnderParentAsync(
|
||||
int? parentId,
|
||||
string nombre,
|
||||
int? excludeId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
// Use UPPER() for explicit case-insensitive comparison.
|
||||
// DB collation is SQL_Latin1_General_CP1_CI_AI on Nombre column (already CI),
|
||||
// but UPPER() makes intent explicit and works regardless of collation.
|
||||
// The WHERE clause handles both root (NULL parent) and non-root cases.
|
||||
const string sql = """
|
||||
SELECT COUNT(1)
|
||||
FROM dbo.Rubro
|
||||
WHERE ((@ParentId IS NULL AND ParentId IS NULL) OR ParentId = @ParentId)
|
||||
AND UPPER(Nombre) = UPPER(@Nombre)
|
||||
AND Activo = 1
|
||||
AND (@ExcludeId IS NULL OR Id <> @ExcludeId)
|
||||
""";
|
||||
|
||||
await using var connection = _factory.CreateConnection();
|
||||
await connection.OpenAsync(ct);
|
||||
|
||||
var count = await connection.ExecuteScalarAsync<int>(sql, new
|
||||
{
|
||||
ParentId = parentId,
|
||||
Nombre = nombre,
|
||||
ExcludeId = excludeId,
|
||||
});
|
||||
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
public async Task<int> GetDepthAsync(int? parentId, CancellationToken ct = default)
|
||||
{
|
||||
// If parentId is null, depth is 0 (creating a root node).
|
||||
if (!parentId.HasValue)
|
||||
return 0;
|
||||
|
||||
// CTE walks up the ancestor chain from parentId to root, counting levels.
|
||||
// Each UNION ALL step goes one level up, so the count of rows = depth of parentId.
|
||||
const string sql = """
|
||||
WITH Ancestors AS (
|
||||
SELECT Id, ParentId, 1 AS Depth
|
||||
FROM dbo.Rubro
|
||||
WHERE Id = @ParentId
|
||||
UNION ALL
|
||||
SELECT r.Id, r.ParentId, a.Depth + 1
|
||||
FROM dbo.Rubro r
|
||||
INNER JOIN Ancestors a ON r.Id = a.ParentId
|
||||
)
|
||||
SELECT ISNULL(MAX(Depth), 0) FROM Ancestors
|
||||
""";
|
||||
|
||||
await using var connection = _factory.CreateConnection();
|
||||
await connection.OpenAsync(ct);
|
||||
|
||||
return await connection.ExecuteScalarAsync<int>(sql, new { ParentId = parentId.Value });
|
||||
}
|
||||
|
||||
// ── mapping ───────────────────────────────────────────────────────────────
|
||||
|
||||
private static Rubro MapRow(RubroRow r)
|
||||
=> new(
|
||||
id: r.Id,
|
||||
parentId: r.ParentId,
|
||||
nombre: r.Nombre,
|
||||
orden: r.Orden,
|
||||
activo: r.Activo,
|
||||
tarifarioBaseId: r.TarifarioBaseId,
|
||||
fechaCreacion: r.FechaCreacion,
|
||||
fechaModificacion: r.FechaModificacion);
|
||||
|
||||
private sealed record RubroRow(
|
||||
int Id,
|
||||
int? ParentId,
|
||||
string Nombre,
|
||||
int Orden,
|
||||
bool Activo,
|
||||
int? TarifarioBaseId,
|
||||
DateTime FechaCreacion,
|
||||
DateTime? FechaModificacion);
|
||||
}
|
||||
Reference in New Issue
Block a user