feat(domain): RubroConProductosActivosException + guard en DeactivateRubro (closes #41) #44

Merged
dmolinari merged 4 commits from fix/issue-41-rubro-deactivation-guard into main 2026-04-19 20:09:38 +00:00
2 changed files with 83 additions and 11 deletions
Showing only changes of commit c974e824e0 - Show all commits

View File

@@ -34,4 +34,16 @@ public sealed class ProductQueryRepository : IProductQueryRepository
var result = await connection.ExecuteScalarAsync<int>(sql, new { ProductTypeId = productTypeId }); var result = await connection.ExecuteScalarAsync<int>(sql, new { ProductTypeId = productTypeId });
return result == 1; return result == 1;
} }
public async Task<int> 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<int>(sql, new { RubroId = rubroId });
}
} }

View File

@@ -8,6 +8,7 @@ namespace SIGCM2.Application.Tests.Products.Repository;
/// <summary> /// <summary>
/// PRD-002 — Integration tests for ProductQueryRepository against SIGCM2_Test_App. /// PRD-002 — Integration tests for ProductQueryRepository against SIGCM2_Test_App.
/// These tests verify the real Dapper implementation replaces NullProductQueryRepository. /// These tests verify the real Dapper implementation replaces NullProductQueryRepository.
/// Issue #41: CountActiveByRubroAsync tests added here (same class, same DB, same fixture).
/// </summary> /// </summary>
[Collection("Database")] [Collection("Database")]
public class ProductQueryRepositoryTests : IAsyncLifetime public class ProductQueryRepositoryTests : IAsyncLifetime
@@ -44,7 +45,7 @@ public class ProductQueryRepositoryTests : IAsyncLifetime
{ {
// Arrange: insert a ProductType and an active Product referencing it // Arrange: insert a ProductType and an active Product referencing it
var (medioId, productTypeId) = await InsertMedioAndProductTypeAsync(); var (medioId, productTypeId) = await InsertMedioAndProductTypeAsync();
await InsertActiveProductAsync(medioId, productTypeId); await InsertActiveProductAsync(medioId, productTypeId, rubroId: null);
var result = await _repository.ExistsActiveByProductTypeAsync(productTypeId); var result = await _repository.ExistsActiveByProductTypeAsync(productTypeId);
@@ -56,7 +57,7 @@ public class ProductQueryRepositoryTests : IAsyncLifetime
{ {
// Arrange: insert an inactive product // Arrange: insert an inactive product
var (medioId, productTypeId) = await InsertMedioAndProductTypeAsync(); var (medioId, productTypeId) = await InsertMedioAndProductTypeAsync();
await InsertInactiveProductAsync(medioId, productTypeId); await InsertInactiveProductAsync(medioId, productTypeId, rubroId: null);
var result = await _repository.ExistsActiveByProductTypeAsync(productTypeId); 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 // Arrange: insert active product for productTypeId=A, query for productTypeId=B
var (medioId, productTypeId) = await InsertMedioAndProductTypeAsync(); var (medioId, productTypeId) = await InsertMedioAndProductTypeAsync();
await InsertActiveProductAsync(medioId, productTypeId); await InsertActiveProductAsync(medioId, productTypeId, rubroId: null);
var otherProductTypeId = productTypeId + 100; var otherProductTypeId = productTypeId + 100;
var result = await _repository.ExistsActiveByProductTypeAsync(otherProductTypeId); var result = await _repository.ExistsActiveByProductTypeAsync(otherProductTypeId);
@@ -76,8 +77,65 @@ public class ProductQueryRepositoryTests : IAsyncLifetime
result.Should().BeFalse(); 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 ─────────────────────────────────────────────────────────────── // ── Helpers ───────────────────────────────────────────────────────────────
private async Task<int> InsertRubroAsync(string nombre)
{
await using var conn = new Microsoft.Data.SqlClient.SqlConnection(TestConnectionStrings.AppTestDb);
await conn.OpenAsync();
return await conn.ExecuteScalarAsync<int>("""
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() private async Task<(int MedioId, int ProductTypeId)> InsertMedioAndProductTypeAsync()
{ {
await using var conn = new Microsoft.Data.SqlClient.SqlConnection(TestConnectionStrings.AppTestDb); await using var conn = new Microsoft.Data.SqlClient.SqlConnection(TestConnectionStrings.AppTestDb);
@@ -98,23 +156,25 @@ public class ProductQueryRepositoryTests : IAsyncLifetime
return (medioId, productTypeId); 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 using var conn = new Microsoft.Data.SqlClient.SqlConnection(TestConnectionStrings.AppTestDb);
await conn.OpenAsync(); await conn.OpenAsync();
await conn.ExecuteAsync(""" await conn.ExecuteAsync("""
INSERT INTO dbo.Product (Nombre, MedioId, ProductTypeId, BasePrice, IsActive, FechaCreacion) INSERT INTO dbo.Product (Nombre, MedioId, ProductTypeId, RubroId, BasePrice, IsActive, FechaCreacion)
VALUES ('Producto Activo', @MedioId, @ProductTypeId, 100, 1, SYSUTCDATETIME()) VALUES (@Nombre, @MedioId, @ProductTypeId, @RubroId, 100, 1, SYSUTCDATETIME())
""", new { MedioId = medioId, ProductTypeId = productTypeId }); """, 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 using var conn = new Microsoft.Data.SqlClient.SqlConnection(TestConnectionStrings.AppTestDb);
await conn.OpenAsync(); await conn.OpenAsync();
await conn.ExecuteAsync(""" await conn.ExecuteAsync("""
INSERT INTO dbo.Product (Nombre, MedioId, ProductTypeId, BasePrice, IsActive, FechaCreacion) INSERT INTO dbo.Product (Nombre, MedioId, ProductTypeId, RubroId, BasePrice, IsActive, FechaCreacion)
VALUES ('Producto Inactivo', @MedioId, @ProductTypeId, 100, 0, SYSUTCDATETIME()) VALUES (@Nombre, @MedioId, @ProductTypeId, @RubroId, 100, 0, SYSUTCDATETIME())
""", new { MedioId = medioId, ProductTypeId = productTypeId }); """, new { Nombre = nombre, MedioId = medioId, ProductTypeId = productTypeId, RubroId = rubroId });
} }
} }