diff --git a/src/api/SIGCM2.Application/Abstractions/Persistence/IAvisoQueryRepository.cs b/src/api/SIGCM2.Application/Abstractions/Persistence/IAvisoQueryRepository.cs new file mode 100644 index 0000000..a077780 --- /dev/null +++ b/src/api/SIGCM2.Application/Abstractions/Persistence/IAvisoQueryRepository.cs @@ -0,0 +1,24 @@ +namespace SIGCM2.Application.Abstractions.Persistence; + +/// +/// Query-only access to Aviso counts by Rubro. +/// CAT-002 introduces the contract. The real Dapper-based impl lands in PRD-002 +/// (when dbo.Aviso exists). Until then, NullAvisoQueryRepository is the binding. +/// +public interface IAvisoQueryRepository +{ + /// + /// Returns the count of avisos (active, non-archived) assigned to the given rubro. + /// + Task CountAvisosEnRubroAsync(int rubroId, CancellationToken ct = default); + + /// + /// Returns a dictionary of { rubroId → count } for the provided ids. + /// Used by GetRubroTreeQueryHandler to avoid N+1 when populating TieneAvisos per node. + /// The implementation MUST do a single query; the stub returns an empty dictionary + /// (every rubro gets 0 via dictionary.GetValueOrDefault). + /// + Task> CountAvisosBatchAsync( + IReadOnlyCollection rubroIds, + CancellationToken ct = default); +} diff --git a/src/api/SIGCM2.Application/Avisos/NullAvisoQueryRepository.cs b/src/api/SIGCM2.Application/Avisos/NullAvisoQueryRepository.cs new file mode 100644 index 0000000..223c146 --- /dev/null +++ b/src/api/SIGCM2.Application/Avisos/NullAvisoQueryRepository.cs @@ -0,0 +1,22 @@ +using SIGCM2.Application.Abstractions.Persistence; + +namespace SIGCM2.Application.Avisos; + +/// +/// STUB — PRD-002 reemplaza con AvisoQueryRepository contra dbo.Aviso. +/// Returns 0 / empty dictionary so every handler guard passes and every tree node shows TieneAvisos=false. +/// This is intentional for CAT-002: the mechanism is installed; the data feed arrives in PRD-002. +/// +public sealed class NullAvisoQueryRepository : IAvisoQueryRepository +{ + private static readonly IReadOnlyDictionary Empty = + new Dictionary(capacity: 0); + + public Task CountAvisosEnRubroAsync(int rubroId, CancellationToken ct = default) + => Task.FromResult(0); + + public Task> CountAvisosBatchAsync( + IReadOnlyCollection rubroIds, + CancellationToken ct = default) + => Task.FromResult(Empty); +} diff --git a/src/api/SIGCM2.Application/DependencyInjection.cs b/src/api/SIGCM2.Application/DependencyInjection.cs index 7d7f85f..2b10fbb 100644 --- a/src/api/SIGCM2.Application/DependencyInjection.cs +++ b/src/api/SIGCM2.Application/DependencyInjection.cs @@ -67,6 +67,8 @@ using SIGCM2.Application.Rubros.Move; using SIGCM2.Application.Rubros.GetTree; using SIGCM2.Application.Rubros.GetById; using SIGCM2.Application.Rubros.Dtos; +using SIGCM2.Application.Abstractions.Persistence; +using SIGCM2.Application.Avisos; namespace SIGCM2.Application; @@ -152,7 +154,10 @@ public static class DependencyInjection services.AddScoped>, ListIngresosBrutosQueryHandler>(); services.AddScoped>, GetHistorialIngresosBrutosQueryHandler>(); - // Rubros (CAT-001) + // Rubros (CAT-001 + CAT-002) + // CAT-002: Regla de Oro Rama vs Hoja — stub binding until PRD-002 provides real impl + services.AddScoped(); + services.AddScoped, CreateRubroCommandHandler>(); services.AddScoped, UpdateRubroCommandHandler>(); services.AddScoped, DeactivateRubroCommandHandler>(); diff --git a/tests/SIGCM2.Application.Tests/Avisos/NullAvisoQueryRepositoryTests.cs b/tests/SIGCM2.Application.Tests/Avisos/NullAvisoQueryRepositoryTests.cs new file mode 100644 index 0000000..4afa33d --- /dev/null +++ b/tests/SIGCM2.Application.Tests/Avisos/NullAvisoQueryRepositoryTests.cs @@ -0,0 +1,35 @@ +using FluentAssertions; +using SIGCM2.Application.Avisos; + +namespace SIGCM2.Application.Tests.Avisos; + +public class NullAvisoQueryRepositoryTests +{ + private readonly NullAvisoQueryRepository _repo = new(); + + [Fact] + public async Task CountAvisosEnRubroAsync_Always_ReturnsZero() + { + var result = await _repo.CountAvisosEnRubroAsync(rubroId: 99); + + result.Should().Be(0); + } + + [Fact] + public async Task CountAvisosBatchAsync_WithIds_ReturnsDictionaryAllZero() + { + var result = await _repo.CountAvisosBatchAsync([1, 2, 3]); + + result.GetValueOrDefault(1, 0).Should().Be(0); + result.GetValueOrDefault(2, 0).Should().Be(0); + result.GetValueOrDefault(3, 0).Should().Be(0); + } + + [Fact] + public async Task CountAvisosBatchAsync_EmptyIds_ReturnsEmptyDictionary() + { + var result = await _repo.CountAvisosBatchAsync([]); + + result.Should().HaveCount(0); + } +}