diff --git a/src/api/SIGCM2.Application/Abstractions/Persistence/IProductQueryRepository.cs b/src/api/SIGCM2.Application/Abstractions/Persistence/IProductQueryRepository.cs index e4418ba..53bee93 100644 --- a/src/api/SIGCM2.Application/Abstractions/Persistence/IProductQueryRepository.cs +++ b/src/api/SIGCM2.Application/Abstractions/Persistence/IProductQueryRepository.cs @@ -12,4 +12,10 @@ public interface IProductQueryRepository /// Used by DeactivateProductTypeCommandHandler to guard against orphaning active products. /// Task ExistsActiveByProductTypeAsync(int productTypeId, CancellationToken ct = default); + + /// + /// Returns the count of active Products where RubroId = rubroId. + /// Used by DeactivateRubroCommandHandler to guard against orphaning active products. (issue #41) + /// + Task CountActiveByRubroAsync(int rubroId, CancellationToken ct = default); } diff --git a/src/api/SIGCM2.Application/Rubros/Deactivate/DeactivateRubroCommandHandler.cs b/src/api/SIGCM2.Application/Rubros/Deactivate/DeactivateRubroCommandHandler.cs index 0099b13..1a15bf0 100644 --- a/src/api/SIGCM2.Application/Rubros/Deactivate/DeactivateRubroCommandHandler.cs +++ b/src/api/SIGCM2.Application/Rubros/Deactivate/DeactivateRubroCommandHandler.cs @@ -10,15 +10,18 @@ public sealed class DeactivateRubroCommandHandler : ICommandHandler 0) throw new RubroTieneHijosActivosException(command.Id, activeChildren); + var productosActivos = await _productQuery.CountActiveByRubroAsync(command.Id); + if (productosActivos > 0) + throw new RubroConProductosActivosException(command.Id, productosActivos); + var deactivated = target.WithActivo(false, _timeProvider); using var tx = new TransactionScope( diff --git a/tests/SIGCM2.Application.Tests/Rubros/Deactivate/DeactivateRubroCommandHandlerTests.cs b/tests/SIGCM2.Application.Tests/Rubros/Deactivate/DeactivateRubroCommandHandlerTests.cs index 68b5ddd..2ca3040 100644 --- a/tests/SIGCM2.Application.Tests/Rubros/Deactivate/DeactivateRubroCommandHandlerTests.cs +++ b/tests/SIGCM2.Application.Tests/Rubros/Deactivate/DeactivateRubroCommandHandlerTests.cs @@ -13,6 +13,7 @@ public class DeactivateRubroCommandHandlerTests { private readonly IRubroRepository _repo = Substitute.For(); private readonly IAuditLogger _audit = Substitute.For(); + private readonly IProductQueryRepository _productQuery = Substitute.For(); private readonly FakeTimeProvider _timeProvider = new(new DateTimeOffset(2026, 4, 18, 12, 0, 0, TimeSpan.Zero)); private readonly DeactivateRubroCommandHandler _handler; @@ -22,7 +23,8 @@ public class DeactivateRubroCommandHandlerTests public DeactivateRubroCommandHandlerTests() { _repo.CountActiveChildrenAsync(Arg.Any(), Arg.Any()).Returns(0); - _handler = new DeactivateRubroCommandHandler(_repo, _audit, _timeProvider); + _productQuery.CountActiveByRubroAsync(Arg.Any(), Arg.Any()).Returns(0); + _handler = new DeactivateRubroCommandHandler(_repo, _audit, _productQuery, _timeProvider); } // ── Happy path: leaf soft-delete ───────────────────────────────────────── @@ -103,4 +105,43 @@ public class DeactivateRubroCommandHandlerTests await act.Should().ThrowAsync(); } + + // ── Active Products guard (issue #41) ──────────────────────────────────── + + [Fact] + public async Task Deactivate_WithActiveProducts_ThrowsRubroConProductosActivosException() + { + _repo.GetByIdAsync(10, Arg.Any()).Returns(LeafRubro(10)); + _productQuery.CountActiveByRubroAsync(10, Arg.Any()).Returns(3); + + var act = () => _handler.Handle(new DeactivateRubroCommand(Id: 10)); + + await act.Should().ThrowAsync() + .Where(ex => ex.RubroId == 10 && ex.ProductosActivosCount == 3); + } + + [Fact] + public async Task Deactivate_WithZeroActiveProducts_Succeeds() + { + _repo.GetByIdAsync(10, Arg.Any()).Returns(LeafRubro(10)); + _productQuery.CountActiveByRubroAsync(10, Arg.Any()).Returns(0); + + var result = await _handler.Handle(new DeactivateRubroCommand(Id: 10)); + + result.Activo.Should().BeFalse(); + } + + [Fact] + public async Task Deactivate_WithActiveChildrenAndProducts_ThrowsChildrenFirst() + { + // Children guard fires BEFORE products guard — order of checks must be stable. + _repo.GetByIdAsync(5, Arg.Any()).Returns(LeafRubro(5)); + _repo.CountActiveChildrenAsync(5, Arg.Any()).Returns(2); + _productQuery.CountActiveByRubroAsync(5, Arg.Any()).Returns(3); + + var act = () => _handler.Handle(new DeactivateRubroCommand(Id: 5)); + + // Children exception must win — products guard is never reached + await act.Should().ThrowAsync(); + } }