diff --git a/src/api/SIGCM2.Infrastructure/Persistence/ProductQueryRepository.cs b/src/api/SIGCM2.Infrastructure/Persistence/ProductQueryRepository.cs index 4ee619b..0615f39 100644 --- a/src/api/SIGCM2.Infrastructure/Persistence/ProductQueryRepository.cs +++ b/src/api/SIGCM2.Infrastructure/Persistence/ProductQueryRepository.cs @@ -34,4 +34,16 @@ public sealed class ProductQueryRepository : IProductQueryRepository var result = await connection.ExecuteScalarAsync(sql, new { ProductTypeId = productTypeId }); return result == 1; } + + public async Task CountActiveByRubroAsync(int rubroId, CancellationToken ct = default) + { + const string sql = """ + SELECT COUNT(1) FROM dbo.Product WHERE RubroId = @RubroId AND IsActive = 1 + """; + + await using var connection = _factory.CreateConnection(); + await connection.OpenAsync(ct); + + return await connection.ExecuteScalarAsync(sql, new { RubroId = rubroId }); + } } diff --git a/tests/SIGCM2.Application.Tests/Products/Repository/ProductQueryRepositoryTests.cs b/tests/SIGCM2.Application.Tests/Products/Repository/ProductQueryRepositoryTests.cs index 309e8e3..e557810 100644 --- a/tests/SIGCM2.Application.Tests/Products/Repository/ProductQueryRepositoryTests.cs +++ b/tests/SIGCM2.Application.Tests/Products/Repository/ProductQueryRepositoryTests.cs @@ -8,6 +8,7 @@ namespace SIGCM2.Application.Tests.Products.Repository; /// /// PRD-002 — Integration tests for ProductQueryRepository against SIGCM2_Test_App. /// These tests verify the real Dapper implementation replaces NullProductQueryRepository. +/// Issue #41: CountActiveByRubroAsync tests added here (same class, same DB, same fixture). /// [Collection("Database")] public class ProductQueryRepositoryTests : IAsyncLifetime @@ -44,7 +45,7 @@ public class ProductQueryRepositoryTests : IAsyncLifetime { // Arrange: insert a ProductType and an active Product referencing it var (medioId, productTypeId) = await InsertMedioAndProductTypeAsync(); - await InsertActiveProductAsync(medioId, productTypeId); + await InsertActiveProductAsync(medioId, productTypeId, rubroId: null); var result = await _repository.ExistsActiveByProductTypeAsync(productTypeId); @@ -56,7 +57,7 @@ public class ProductQueryRepositoryTests : IAsyncLifetime { // Arrange: insert an inactive product var (medioId, productTypeId) = await InsertMedioAndProductTypeAsync(); - await InsertInactiveProductAsync(medioId, productTypeId); + await InsertInactiveProductAsync(medioId, productTypeId, rubroId: null); var result = await _repository.ExistsActiveByProductTypeAsync(productTypeId); @@ -68,7 +69,7 @@ public class ProductQueryRepositoryTests : IAsyncLifetime { // Arrange: insert active product for productTypeId=A, query for productTypeId=B var (medioId, productTypeId) = await InsertMedioAndProductTypeAsync(); - await InsertActiveProductAsync(medioId, productTypeId); + await InsertActiveProductAsync(medioId, productTypeId, rubroId: null); var otherProductTypeId = productTypeId + 100; var result = await _repository.ExistsActiveByProductTypeAsync(otherProductTypeId); @@ -76,8 +77,65 @@ public class ProductQueryRepositoryTests : IAsyncLifetime result.Should().BeFalse(); } + // ── CountActiveByRubroAsync (issue #41) ────────────────────────────────── + + [Fact] + public async Task CountActiveByRubroAsync_NoProducts_ReturnsZero() + { + var result = await _repository.CountActiveByRubroAsync(rubroId: 99999); + + result.Should().Be(0); + } + + [Fact] + public async Task CountActiveByRubroAsync_Returns_CorrectCount() + { + // Arrange: insert a Rubro and products in various states + var rubroId = await InsertRubroAsync("Rubro Clasificados"); + var (medioId, productTypeId) = await InsertMedioAndProductTypeAsync(); + + // 2 active products referencing the rubro + await InsertActiveProductAsync(medioId, productTypeId, rubroId: rubroId); + await InsertActiveProductAsync(medioId, productTypeId, rubroId: rubroId); + + // 1 inactive product referencing the same rubro (should NOT count) + await InsertInactiveProductAsync(medioId, productTypeId, rubroId: rubroId); + + // 1 active product with a DIFFERENT rubroId (should NOT count) + var otherRubroId = await InsertRubroAsync("Otro Rubro"); + await InsertActiveProductAsync(medioId, productTypeId, rubroId: otherRubroId); + + var result = await _repository.CountActiveByRubroAsync(rubroId); + + result.Should().Be(2); + } + + [Fact] + public async Task CountActiveByRubroAsync_WithOnlyInactiveProducts_ReturnsZero() + { + var rubroId = await InsertRubroAsync("Rubro Solo Inactivos"); + var (medioId, productTypeId) = await InsertMedioAndProductTypeAsync(); + + await InsertInactiveProductAsync(medioId, productTypeId, rubroId: rubroId); + + var result = await _repository.CountActiveByRubroAsync(rubroId); + + result.Should().Be(0); + } + // ── Helpers ─────────────────────────────────────────────────────────────── + private async Task InsertRubroAsync(string nombre) + { + await using var conn = new Microsoft.Data.SqlClient.SqlConnection(TestConnectionStrings.AppTestDb); + await conn.OpenAsync(); + return await conn.ExecuteScalarAsync(""" + INSERT INTO dbo.Rubro (Nombre, ParentId, Orden, Activo, TarifarioBaseId, FechaCreacion) + OUTPUT INSERTED.Id + VALUES (@Nombre, NULL, 0, 1, NULL, SYSUTCDATETIME()) + """, new { Nombre = nombre }); + } + private async Task<(int MedioId, int ProductTypeId)> InsertMedioAndProductTypeAsync() { await using var conn = new Microsoft.Data.SqlClient.SqlConnection(TestConnectionStrings.AppTestDb); @@ -98,23 +156,25 @@ public class ProductQueryRepositoryTests : IAsyncLifetime return (medioId, productTypeId); } - private async Task InsertActiveProductAsync(int medioId, int productTypeId) + private async Task InsertActiveProductAsync(int medioId, int productTypeId, int? rubroId) { + var nombre = $"ProdActivo-{Guid.NewGuid():N}"; await using var conn = new Microsoft.Data.SqlClient.SqlConnection(TestConnectionStrings.AppTestDb); await conn.OpenAsync(); await conn.ExecuteAsync(""" - INSERT INTO dbo.Product (Nombre, MedioId, ProductTypeId, BasePrice, IsActive, FechaCreacion) - VALUES ('Producto Activo', @MedioId, @ProductTypeId, 100, 1, SYSUTCDATETIME()) - """, new { MedioId = medioId, ProductTypeId = productTypeId }); + INSERT INTO dbo.Product (Nombre, MedioId, ProductTypeId, RubroId, BasePrice, IsActive, FechaCreacion) + VALUES (@Nombre, @MedioId, @ProductTypeId, @RubroId, 100, 1, SYSUTCDATETIME()) + """, new { Nombre = nombre, MedioId = medioId, ProductTypeId = productTypeId, RubroId = rubroId }); } - private async Task InsertInactiveProductAsync(int medioId, int productTypeId) + private async Task InsertInactiveProductAsync(int medioId, int productTypeId, int? rubroId) { + var nombre = $"ProdInactivo-{Guid.NewGuid():N}"; await using var conn = new Microsoft.Data.SqlClient.SqlConnection(TestConnectionStrings.AppTestDb); await conn.OpenAsync(); await conn.ExecuteAsync(""" - INSERT INTO dbo.Product (Nombre, MedioId, ProductTypeId, BasePrice, IsActive, FechaCreacion) - VALUES ('Producto Inactivo', @MedioId, @ProductTypeId, 100, 0, SYSUTCDATETIME()) - """, new { MedioId = medioId, ProductTypeId = productTypeId }); + INSERT INTO dbo.Product (Nombre, MedioId, ProductTypeId, RubroId, BasePrice, IsActive, FechaCreacion) + VALUES (@Nombre, @MedioId, @ProductTypeId, @RubroId, 100, 0, SYSUTCDATETIME()) + """, new { Nombre = nombre, MedioId = medioId, ProductTypeId = productTypeId, RubroId = rubroId }); } }