feat(application): RubroTreeBuilder + GetRubroTree con tieneAvisos (CAT-002)
This commit is contained in:
@@ -8,16 +8,20 @@ namespace SIGCM2.Application.Rubros.GetTree;
|
|||||||
public sealed class GetRubroTreeQueryHandler : ICommandHandler<GetRubroTreeQuery, IReadOnlyList<RubroTreeNodeDto>>
|
public sealed class GetRubroTreeQueryHandler : ICommandHandler<GetRubroTreeQuery, IReadOnlyList<RubroTreeNodeDto>>
|
||||||
{
|
{
|
||||||
private readonly IRubroRepository _repo;
|
private readonly IRubroRepository _repo;
|
||||||
|
private readonly IAvisoQueryRepository _avisoQuery;
|
||||||
|
|
||||||
public GetRubroTreeQueryHandler(IRubroRepository repo)
|
public GetRubroTreeQueryHandler(IRubroRepository repo, IAvisoQueryRepository avisoQuery)
|
||||||
{
|
{
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
|
_avisoQuery = avisoQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IReadOnlyList<RubroTreeNodeDto>> Handle(GetRubroTreeQuery query)
|
public async Task<IReadOnlyList<RubroTreeNodeDto>> Handle(GetRubroTreeQuery query)
|
||||||
{
|
{
|
||||||
var all = await _repo.GetAllAsync(query.IncluirInactivos);
|
var all = await _repo.GetAllAsync(query.IncluirInactivos);
|
||||||
// CAT-002: avisoCounts injected via IAvisoQueryRepository (wired in Batch 6)
|
var ids = all.Select(r => r.Id).ToList();
|
||||||
return RubroTreeBuilder.Build(all, query.IncluirInactivos, new Dictionary<int, int>());
|
// CAT-002: single batch call — avoids N+1 when PRD-002 activates the real implementation
|
||||||
|
var avisoCounts = await _avisoQuery.CountAvisosBatchAsync(ids);
|
||||||
|
return RubroTreeBuilder.Build(all, query.IncluirInactivos, avisoCounts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using FluentAssertions;
|
|||||||
using Microsoft.Extensions.Time.Testing;
|
using Microsoft.Extensions.Time.Testing;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using SIGCM2.Application.Abstractions.Persistence;
|
using SIGCM2.Application.Abstractions.Persistence;
|
||||||
|
using SIGCM2.Application.Avisos;
|
||||||
using SIGCM2.Application.Rubros.GetById;
|
using SIGCM2.Application.Rubros.GetById;
|
||||||
using SIGCM2.Application.Rubros.GetTree;
|
using SIGCM2.Application.Rubros.GetTree;
|
||||||
using SIGCM2.Domain.Entities;
|
using SIGCM2.Domain.Entities;
|
||||||
@@ -13,6 +14,7 @@ public class GetRubroTreeQueryHandlerTests
|
|||||||
{
|
{
|
||||||
private static readonly FakeTimeProvider FakeTime = new(new DateTimeOffset(2026, 4, 18, 12, 0, 0, TimeSpan.Zero));
|
private static readonly FakeTimeProvider FakeTime = new(new DateTimeOffset(2026, 4, 18, 12, 0, 0, TimeSpan.Zero));
|
||||||
private readonly IRubroRepository _repo = Substitute.For<IRubroRepository>();
|
private readonly IRubroRepository _repo = Substitute.For<IRubroRepository>();
|
||||||
|
private readonly IAvisoQueryRepository _avisoQuery = Substitute.For<IAvisoQueryRepository>();
|
||||||
|
|
||||||
private static Rubro MakeRubro(int id, int? parentId = null, bool activo = true)
|
private static Rubro MakeRubro(int id, int? parentId = null, bool activo = true)
|
||||||
=> new(id, parentId, $"Rubro{id}", 0, activo, null, FakeTime.GetUtcNow().UtcDateTime, null);
|
=> new(id, parentId, $"Rubro{id}", 0, activo, null, FakeTime.GetUtcNow().UtcDateTime, null);
|
||||||
@@ -24,8 +26,10 @@ public class GetRubroTreeQueryHandlerTests
|
|||||||
{
|
{
|
||||||
_repo.GetAllAsync(false, Arg.Any<CancellationToken>())
|
_repo.GetAllAsync(false, Arg.Any<CancellationToken>())
|
||||||
.Returns(new[] { MakeRubro(1), MakeRubro(2) });
|
.Returns(new[] { MakeRubro(1), MakeRubro(2) });
|
||||||
|
_avisoQuery.CountAvisosBatchAsync(Arg.Any<IReadOnlyCollection<int>>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(new Dictionary<int, int>());
|
||||||
|
|
||||||
var handler = new GetRubroTreeQueryHandler(_repo);
|
var handler = new GetRubroTreeQueryHandler(_repo, _avisoQuery);
|
||||||
var result = await handler.Handle(new GetRubroTreeQuery(IncluirInactivos: false));
|
var result = await handler.Handle(new GetRubroTreeQuery(IncluirInactivos: false));
|
||||||
|
|
||||||
result.Should().HaveCount(2);
|
result.Should().HaveCount(2);
|
||||||
@@ -36,8 +40,10 @@ public class GetRubroTreeQueryHandlerTests
|
|||||||
{
|
{
|
||||||
_repo.GetAllAsync(true, Arg.Any<CancellationToken>())
|
_repo.GetAllAsync(true, Arg.Any<CancellationToken>())
|
||||||
.Returns(new[] { MakeRubro(1), MakeRubro(2, activo: false) });
|
.Returns(new[] { MakeRubro(1), MakeRubro(2, activo: false) });
|
||||||
|
_avisoQuery.CountAvisosBatchAsync(Arg.Any<IReadOnlyCollection<int>>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(new Dictionary<int, int>());
|
||||||
|
|
||||||
var handler = new GetRubroTreeQueryHandler(_repo);
|
var handler = new GetRubroTreeQueryHandler(_repo, _avisoQuery);
|
||||||
var result = await handler.Handle(new GetRubroTreeQuery(IncluirInactivos: true));
|
var result = await handler.Handle(new GetRubroTreeQuery(IncluirInactivos: true));
|
||||||
|
|
||||||
await _repo.Received(1).GetAllAsync(true, Arg.Any<CancellationToken>());
|
await _repo.Received(1).GetAllAsync(true, Arg.Any<CancellationToken>());
|
||||||
@@ -49,13 +55,77 @@ public class GetRubroTreeQueryHandlerTests
|
|||||||
{
|
{
|
||||||
_repo.GetAllAsync(Arg.Any<bool>(), Arg.Any<CancellationToken>())
|
_repo.GetAllAsync(Arg.Any<bool>(), Arg.Any<CancellationToken>())
|
||||||
.Returns(Array.Empty<Rubro>());
|
.Returns(Array.Empty<Rubro>());
|
||||||
|
_avisoQuery.CountAvisosBatchAsync(Arg.Any<IReadOnlyCollection<int>>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(new Dictionary<int, int>());
|
||||||
|
|
||||||
var handler = new GetRubroTreeQueryHandler(_repo);
|
var handler = new GetRubroTreeQueryHandler(_repo, _avisoQuery);
|
||||||
var result = await handler.Handle(new GetRubroTreeQuery(IncluirInactivos: false));
|
var result = await handler.Handle(new GetRubroTreeQuery(IncluirInactivos: false));
|
||||||
|
|
||||||
result.Should().BeEmpty();
|
result.Should().BeEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── CAT-002: TieneAvisos populated via batch ─────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_PopulatesTieneAvisos_True_WhenBatchResultContainsCount()
|
||||||
|
{
|
||||||
|
_repo.GetAllAsync(false, Arg.Any<CancellationToken>())
|
||||||
|
.Returns(new[] { MakeRubro(1), MakeRubro(2), MakeRubro(3) });
|
||||||
|
_avisoQuery.CountAvisosBatchAsync(Arg.Any<IReadOnlyCollection<int>>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(new Dictionary<int, int> { { 1, 2 } });
|
||||||
|
|
||||||
|
var handler = new GetRubroTreeQueryHandler(_repo, _avisoQuery);
|
||||||
|
var result = await handler.Handle(new GetRubroTreeQuery(IncluirInactivos: false));
|
||||||
|
|
||||||
|
result.Single(n => n.Id == 1).TieneAvisos.Should().BeTrue();
|
||||||
|
result.Single(n => n.Id == 2).TieneAvisos.Should().BeFalse();
|
||||||
|
result.Single(n => n.Id == 3).TieneAvisos.Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_CallsBatchExactlyOnce_WithAllRubroIds()
|
||||||
|
{
|
||||||
|
_repo.GetAllAsync(false, Arg.Any<CancellationToken>())
|
||||||
|
.Returns(new[] { MakeRubro(1), MakeRubro(2), MakeRubro(3) });
|
||||||
|
_avisoQuery.CountAvisosBatchAsync(Arg.Any<IReadOnlyCollection<int>>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(new Dictionary<int, int>());
|
||||||
|
|
||||||
|
var handler = new GetRubroTreeQueryHandler(_repo, _avisoQuery);
|
||||||
|
await handler.Handle(new GetRubroTreeQuery(IncluirInactivos: false));
|
||||||
|
|
||||||
|
await _avisoQuery.Received(1).CountAvisosBatchAsync(
|
||||||
|
Arg.Any<IReadOnlyCollection<int>>(),
|
||||||
|
Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_EmptyTree_CallsBatchWithEmptyList()
|
||||||
|
{
|
||||||
|
_repo.GetAllAsync(Arg.Any<bool>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(Array.Empty<Rubro>());
|
||||||
|
_avisoQuery.CountAvisosBatchAsync(Arg.Any<IReadOnlyCollection<int>>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(new Dictionary<int, int>());
|
||||||
|
|
||||||
|
var handler = new GetRubroTreeQueryHandler(_repo, _avisoQuery);
|
||||||
|
await handler.Handle(new GetRubroTreeQuery(IncluirInactivos: false));
|
||||||
|
|
||||||
|
await _avisoQuery.Received(1).CountAvisosBatchAsync(
|
||||||
|
Arg.Is<IReadOnlyCollection<int>>(ids => ids.Count == 0),
|
||||||
|
Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_StubBehavior_AllNodesTieneAvisosFalse()
|
||||||
|
{
|
||||||
|
_repo.GetAllAsync(false, Arg.Any<CancellationToken>())
|
||||||
|
.Returns(new[] { MakeRubro(1), MakeRubro(2) });
|
||||||
|
|
||||||
|
var handler = new GetRubroTreeQueryHandler(_repo, new NullAvisoQueryRepository());
|
||||||
|
var result = await handler.Handle(new GetRubroTreeQuery(IncluirInactivos: false));
|
||||||
|
|
||||||
|
result.Should().AllSatisfy(n => n.TieneAvisos.Should().BeFalse());
|
||||||
|
}
|
||||||
|
|
||||||
// ── GetRubroByIdQueryHandler ─────────────────────────────────────────────
|
// ── GetRubroByIdQueryHandler ─────────────────────────────────────────────
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
Reference in New Issue
Block a user