diff --git a/tests/SIGCM2.Application.Tests/Infrastructure/IngresosBrutosRepositoryTests.cs b/tests/SIGCM2.Application.Tests/Infrastructure/IngresosBrutosRepositoryTests.cs
new file mode 100644
index 0000000..0b8a7f2
--- /dev/null
+++ b/tests/SIGCM2.Application.Tests/Infrastructure/IngresosBrutosRepositoryTests.cs
@@ -0,0 +1,288 @@
+using FluentAssertions;
+using Microsoft.Data.SqlClient;
+using SIGCM2.Application.Abstractions.Persistence;
+using SIGCM2.Application.Common;
+using SIGCM2.Domain.Exceptions;
+using SIGCM2.Domain.Fiscal;
+using SIGCM2.Infrastructure.Persistence;
+using IibbEntity = SIGCM2.Domain.Entities.IngresosBrutos;
+
+namespace SIGCM2.Application.Tests.Infrastructure;
+
+///
+/// Integration tests for IngresosBrutosRepository against SIGCM2_Test.
+/// NOTE: IngresosBrutos is in Respawn TablesToIgnore (seed data must survive resets).
+/// Tests insert their own rows and identify them by the returned Id.
+/// Provincia + VigenciaDesde combos chosen to be unique per test to avoid UQ violations.
+///
+[Collection("Database")]
+public class IngresosBrutosRepositoryTests : IAsyncLifetime
+{
+ private const string ConnectionString =
+ "Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
+
+ private SqlConnection _connection = null!;
+ private IIngresosBrutosRepository _repo = null!;
+
+ private static int _testCounter = 0;
+
+ private static DateOnly NextUniqueDate()
+ {
+ var counter = Interlocked.Increment(ref _testCounter);
+ // Start at 2091-01-01 to avoid clashing with TipoDeIvaRepositoryTests (2090-01-01)
+ return new DateOnly(2091, 1, 1).AddDays(counter);
+ }
+
+ public async Task InitializeAsync()
+ {
+ _connection = new SqlConnection(ConnectionString);
+ await _connection.OpenAsync();
+
+ var factory = new SqlConnectionFactory(ConnectionString);
+ _repo = new IngresosBrutosRepository(factory);
+ }
+
+ public async Task DisposeAsync()
+ {
+ await _connection.CloseAsync();
+ await _connection.DisposeAsync();
+ }
+
+ // ── T400.10 / T400.12: InsertAsync + GetByIdAsync ─────────────────────────
+
+ [Fact]
+ public async Task InsertAsync_ReturnsPositiveId_AndRowIsVisibleInGetById()
+ {
+ var vigencia = NextUniqueDate();
+ // Use a province not in the seed or use a date far enough to avoid UQ clash
+ var entity = IibbEntity.ForCreation(
+ provincia: ProvinciaArgentina.Cordoba,
+ descripcion: "Test IIBB Cordoba",
+ alicuota: 3.5m,
+ vigenciaDesde: vigencia);
+
+ var id = await _repo.InsertAsync(entity);
+
+ id.Should().BeGreaterThan(0);
+
+ var fetched = await _repo.GetByIdAsync(id);
+ fetched.Should().NotBeNull();
+ fetched!.Id.Should().Be(id);
+ fetched.Provincia.Should().Be(ProvinciaArgentina.Cordoba);
+ fetched.Descripcion.Should().Be("Test IIBB Cordoba");
+ fetched.Alicuota.Should().Be(3.5m);
+ fetched.Activo.Should().BeTrue();
+ fetched.VigenciaDesde.Should().Be(vigencia);
+ fetched.VigenciaHasta.Should().BeNull();
+ fetched.PredecesorId.Should().BeNull();
+ }
+
+ [Fact]
+ public async Task GetByIdAsync_NonExistentId_ReturnsNull()
+ {
+ var result = await _repo.GetByIdAsync(999_999_998);
+
+ result.Should().BeNull();
+ }
+
+ // ── InsertAsync con Provincia duplicada en misma VigenciaDesde ────────────
+
+ [Fact]
+ public async Task InsertAsync_DuplicateProvincia_SameVigenciaDesde_ThrowsDuplicateProvinciaException()
+ {
+ var vigencia = NextUniqueDate();
+ var first = IibbEntity.ForCreation(ProvinciaArgentina.Mendoza, "Primero", 2m, vigencia);
+ await _repo.InsertAsync(first);
+
+ var second = IibbEntity.ForCreation(ProvinciaArgentina.Mendoza, "Segundo", 3m, vigencia);
+ var act = async () => await _repo.InsertAsync(second);
+
+ await act.Should().ThrowAsync();
+ }
+
+ // ── UpdateCosmeticoAsync ──────────────────────────────────────────────────
+
+ [Fact]
+ public async Task UpdateCosmeticoAsync_ChangesOnlyCosmeticFields_NotAlicuotaOrProvincia()
+ {
+ var vigencia = NextUniqueDate();
+ var entity = IibbEntity.ForCreation(ProvinciaArgentina.Salta, "Original Salta", 1.5m, vigencia);
+ var id = await _repo.InsertAsync(entity);
+
+ var result = await _repo.UpdateCosmeticoAsync(id, "Updated Salta", true);
+
+ result.Should().BeTrue();
+
+ var fetched = await _repo.GetByIdAsync(id);
+ fetched!.Descripcion.Should().Be("Updated Salta");
+ fetched.Activo.Should().BeTrue();
+ // Immutable fields must NOT change
+ fetched.Alicuota.Should().Be(1.5m);
+ fetched.Provincia.Should().Be(ProvinciaArgentina.Salta);
+ fetched.VigenciaDesde.Should().Be(vigencia);
+ fetched.VigenciaHasta.Should().BeNull();
+ }
+
+ // ── UpdateCierreVigenciaAsync ─────────────────────────────────────────────
+
+ [Fact]
+ public async Task UpdateCierreVigenciaAsync_SetsVigenciaHasta_WhenRowIsOpen()
+ {
+ var vigencia = NextUniqueDate();
+ var entity = IibbEntity.ForCreation(ProvinciaArgentina.Tucuman, "Cierre test", 2m, vigencia);
+ var id = await _repo.InsertAsync(entity);
+
+ var closeDate = vigencia.AddMonths(6);
+ var result = await _repo.UpdateCierreVigenciaAsync(id, closeDate);
+
+ result.Should().BeTrue();
+
+ var fetched = await _repo.GetByIdAsync(id);
+ fetched!.VigenciaHasta.Should().Be(closeDate);
+ }
+
+ [Fact]
+ public async Task UpdateCierreVigenciaAsync_DoubleClose_ReturnsFalseOnSecondCall()
+ {
+ var vigencia = NextUniqueDate();
+ var entity = IibbEntity.ForCreation(ProvinciaArgentina.Jujuy, "Double close", 1m, vigencia);
+ var id = await _repo.InsertAsync(entity);
+
+ var closeDate = vigencia.AddMonths(6);
+ var first = await _repo.UpdateCierreVigenciaAsync(id, closeDate);
+ var second = await _repo.UpdateCierreVigenciaAsync(id, closeDate.AddMonths(1));
+
+ first.Should().BeTrue();
+ second.Should().BeFalse("the guard prevents double-close: WHERE VigenciaHasta IS NULL");
+ }
+
+ // ── SetActivoAsync ────────────────────────────────────────────────────────
+
+ [Fact]
+ public async Task SetActivoAsync_FalseAndBack_TogglesActivoCorrectly()
+ {
+ var vigencia = NextUniqueDate();
+ var entity = IibbEntity.ForCreation(ProvinciaArgentina.Chaco, "Toggle test", 0m, vigencia);
+ var id = await _repo.InsertAsync(entity);
+
+ await _repo.SetActivoAsync(id, false);
+ var inactive = await _repo.GetByIdAsync(id);
+ inactive!.Activo.Should().BeFalse();
+
+ await _repo.SetActivoAsync(id, true);
+ var active = await _repo.GetByIdAsync(id);
+ active!.Activo.Should().BeTrue();
+ }
+
+ // ── ListAsync con paginación + filtros ────────────────────────────────────
+
+ [Fact]
+ public async Task ListAsync_WithActivoFilter_ReturnsOnlyMatchingRows()
+ {
+ var d1 = NextUniqueDate();
+ var d2 = NextUniqueDate();
+
+ var activeId = await _repo.InsertAsync(
+ IibbEntity.ForCreation(ProvinciaArgentina.Misiones, "Active row", 1m, d1));
+ var inactiveId = await _repo.InsertAsync(
+ IibbEntity.ForCreation(ProvinciaArgentina.Misiones, "Inactive row", 1m, d2));
+ await _repo.SetActivoAsync(inactiveId, false);
+
+ var result = await _repo.ListAsync(new IngresosBrutosQuery(
+ Page: 1,
+ PageSize: 100,
+ Activo: true,
+ Provincia: null));
+
+ result.Items.Should().Contain(x => x.Id == activeId);
+ result.Items.Should().NotContain(x => x.Id == inactiveId);
+ }
+
+ [Fact]
+ public async Task ListAsync_WithProvinciaFilter_ReturnsOnlyMatchingRows()
+ {
+ var vigencia = NextUniqueDate();
+ var id = await _repo.InsertAsync(
+ IibbEntity.ForCreation(ProvinciaArgentina.Formosa, "Formosa row", 0m, vigencia));
+
+ var result = await _repo.ListAsync(new IngresosBrutosQuery(
+ Page: 1,
+ PageSize: 100,
+ Activo: null,
+ Provincia: ProvinciaArgentina.Formosa));
+
+ result.Total.Should().BeGreaterThan(0);
+ result.Items.Should().Contain(x => x.Id == id);
+ result.Items.Should().AllSatisfy(x => x.Provincia.Should().Be(ProvinciaArgentina.Formosa));
+ }
+
+ // ── Cadena de 3 versiones ─────────────────────────────────────────────────
+
+ [Fact]
+ public async Task VersionChain_ThreeVersions_PredecesorIdChainIsCorrect()
+ {
+ var v1Date = NextUniqueDate();
+ var v1 = IibbEntity.ForCreation(ProvinciaArgentina.LaPampa, "v1", 1m, v1Date);
+ var v1Id = await _repo.InsertAsync(v1);
+
+ var v2Date = v1Date.AddMonths(1);
+ await _repo.UpdateCierreVigenciaAsync(v1Id, v2Date.AddDays(-1));
+ var v2 = IibbEntity.ForCreation(ProvinciaArgentina.LaPampa, "v2", 2m, v2Date, null, v1Id);
+ var v2Id = await _repo.InsertAsync(v2);
+
+ var v3Date = v2Date.AddMonths(1);
+ await _repo.UpdateCierreVigenciaAsync(v2Id, v3Date.AddDays(-1));
+ var v3 = IibbEntity.ForCreation(ProvinciaArgentina.LaPampa, "v3", 3m, v3Date, null, v2Id);
+ var v3Id = await _repo.InsertAsync(v3);
+
+ var fv2 = await _repo.GetByIdAsync(v2Id);
+ var fv3 = await _repo.GetByIdAsync(v3Id);
+
+ fv2!.PredecesorId.Should().Be(v1Id);
+ fv3!.PredecesorId.Should().Be(v2Id);
+ }
+
+ // ── GetHistorialAsync ─────────────────────────────────────────────────────
+
+ [Fact]
+ public async Task GetHistorialAsync_ReturnsChainFromRootToId_OrderedByVigenciaDesdeAsc()
+ {
+ var v1Date = NextUniqueDate();
+ var v1 = IibbEntity.ForCreation(ProvinciaArgentina.LaRioja, "Hist v1", 1m, v1Date);
+ var v1Id = await _repo.InsertAsync(v1);
+
+ var v2Date = v1Date.AddMonths(1);
+ await _repo.UpdateCierreVigenciaAsync(v1Id, v2Date.AddDays(-1));
+ var v2 = IibbEntity.ForCreation(ProvinciaArgentina.LaRioja, "Hist v2", 2m, v2Date, null, v1Id);
+ var v2Id = await _repo.InsertAsync(v2);
+
+ var v3Date = v2Date.AddMonths(1);
+ await _repo.UpdateCierreVigenciaAsync(v2Id, v3Date.AddDays(-1));
+ var v3 = IibbEntity.ForCreation(ProvinciaArgentina.LaRioja, "Hist v3", 3m, v3Date, null, v2Id);
+ var v3Id = await _repo.InsertAsync(v3);
+
+ var historial = await _repo.GetHistorialAsync(v3Id);
+
+ historial.Should().HaveCount(3);
+ historial[0].Id.Should().Be(v1Id, "root is first");
+ historial[1].Id.Should().Be(v2Id);
+ historial[2].Id.Should().Be(v3Id, "requested Id is last");
+ historial[0].VigenciaDesde.Should().BeBefore(historial[1].VigenciaDesde);
+ historial[1].VigenciaDesde.Should().BeBefore(historial[2].VigenciaDesde);
+ // Provincia preserved correctly across mapping
+ historial.Should().AllSatisfy(x => x.Provincia.Should().Be(ProvinciaArgentina.LaRioja));
+ }
+
+ [Fact]
+ public async Task GetHistorialAsync_SingleVersion_ReturnsListWithOneItem()
+ {
+ var vigencia = NextUniqueDate();
+ var entity = IibbEntity.ForCreation(ProvinciaArgentina.Neuquen, "Solo", 1m, vigencia);
+ var id = await _repo.InsertAsync(entity);
+
+ var historial = await _repo.GetHistorialAsync(id);
+
+ historial.Should().HaveCount(1);
+ historial[0].Id.Should().Be(id);
+ }
+}
diff --git a/tests/SIGCM2.Application.Tests/Infrastructure/TipoDeIvaRepositoryTests.cs b/tests/SIGCM2.Application.Tests/Infrastructure/TipoDeIvaRepositoryTests.cs
new file mode 100644
index 0000000..7e8c684
--- /dev/null
+++ b/tests/SIGCM2.Application.Tests/Infrastructure/TipoDeIvaRepositoryTests.cs
@@ -0,0 +1,302 @@
+using Dapper;
+using FluentAssertions;
+using Microsoft.Data.SqlClient;
+using Respawn;
+using SIGCM2.Application.Abstractions.Persistence;
+using SIGCM2.Application.Common;
+using SIGCM2.Domain.Entities;
+using SIGCM2.Domain.Exceptions;
+using SIGCM2.Infrastructure.Persistence;
+
+namespace SIGCM2.Application.Tests.Infrastructure;
+
+///
+/// Integration tests for TipoDeIvaRepository against SIGCM2_Test.
+/// NOTE: TipoDeIva is in Respawn TablesToIgnore (seed data must survive resets).
+/// Tests insert their own rows and identify them by the returned Id.
+/// VigenciaDesde dates are chosen to be unique per test class to avoid UQ violations.
+///
+[Collection("Database")]
+public class TipoDeIvaRepositoryTests : IAsyncLifetime
+{
+ private const string ConnectionString =
+ "Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
+
+ private SqlConnection _connection = null!;
+ private ITipoDeIvaRepository _repo = null!;
+
+ // Use a unique date range for this test class to avoid UQ constraint clashes with seed
+ // seed uses VigenciaDesde = '2020-01-01'; we use a far-future range per test.
+ private static int _testCounter = 0;
+
+ private static DateOnly NextUniqueDate()
+ {
+ var counter = Interlocked.Increment(ref _testCounter);
+ // Start at 2090-01-01 + counter days so each test gets its own unique date
+ return new DateOnly(2090, 1, 1).AddDays(counter);
+ }
+
+ public async Task InitializeAsync()
+ {
+ _connection = new SqlConnection(ConnectionString);
+ await _connection.OpenAsync();
+
+ var factory = new SqlConnectionFactory(ConnectionString);
+ _repo = new TipoDeIvaRepository(factory);
+ }
+
+ public async Task DisposeAsync()
+ {
+ await _connection.CloseAsync();
+ await _connection.DisposeAsync();
+ }
+
+ // ── T400.1 / T400.4: InsertAsync + GetByIdAsync ───────────────────────────
+
+ [Fact]
+ public async Task InsertAsync_ReturnsPositiveId_AndRowIsVisibleInGetById()
+ {
+ var vigencia = NextUniqueDate();
+ var entity = TipoDeIva.ForCreation(
+ codigo: "IVA_21",
+ descripcion: "Test IVA 21%",
+ porcentaje: 21m,
+ aplicaIVA: true,
+ vigenciaDesde: vigencia);
+
+ var id = await _repo.InsertAsync(entity);
+
+ id.Should().BeGreaterThan(0);
+
+ var fetched = await _repo.GetByIdAsync(id);
+ fetched.Should().NotBeNull();
+ fetched!.Id.Should().Be(id);
+ fetched.Codigo.Should().Be("IVA_21");
+ fetched.Descripcion.Should().Be("Test IVA 21%");
+ fetched.Porcentaje.Should().Be(21m);
+ fetched.AplicaIVA.Should().BeTrue();
+ fetched.Activo.Should().BeTrue();
+ fetched.VigenciaDesde.Should().Be(vigencia);
+ fetched.VigenciaHasta.Should().BeNull();
+ fetched.PredecesorId.Should().BeNull();
+ }
+
+ [Fact]
+ public async Task GetByIdAsync_NonExistentId_ReturnsNull()
+ {
+ var result = await _repo.GetByIdAsync(999_999_999);
+
+ result.Should().BeNull();
+ }
+
+ // ── T400.7: InsertAsync con Codigo duplicado en misma VigenciaDesde ───────
+
+ [Fact]
+ public async Task InsertAsync_DuplicateCodigo_SameVigenciaDesde_ThrowsDuplicateCodigoException()
+ {
+ var vigencia = NextUniqueDate();
+ var first = TipoDeIva.ForCreation("IVA_21", "Primero", 21m, true, vigencia);
+ await _repo.InsertAsync(first);
+
+ var second = TipoDeIva.ForCreation("IVA_21", "Segundo", 21m, true, vigencia);
+ var act = async () => await _repo.InsertAsync(second);
+
+ await act.Should().ThrowAsync();
+ }
+
+ // ── UpdateCosmeticoAsync ──────────────────────────────────────────────────
+
+ [Fact]
+ public async Task UpdateCosmeticoAsync_ChangesOnlyCosmeticFields_NotPorcentajeOrVigencias()
+ {
+ var vigencia = NextUniqueDate();
+ var entity = TipoDeIva.ForCreation("IVA_21", "Original Desc", 21m, true, vigencia);
+ var id = await _repo.InsertAsync(entity);
+
+ var result = await _repo.UpdateCosmeticoAsync(id, "IVA_21", "Updated Desc", false, true);
+
+ result.Should().BeTrue();
+
+ var fetched = await _repo.GetByIdAsync(id);
+ fetched!.Descripcion.Should().Be("Updated Desc");
+ fetched.AplicaIVA.Should().BeFalse();
+ // Immutable fields must NOT change
+ fetched.Porcentaje.Should().Be(21m);
+ fetched.VigenciaDesde.Should().Be(vigencia);
+ fetched.VigenciaHasta.Should().BeNull();
+ fetched.PredecesorId.Should().BeNull();
+ }
+
+ // ── UpdateCierreVigenciaAsync ─────────────────────────────────────────────
+
+ [Fact]
+ public async Task UpdateCierreVigenciaAsync_SetsVigenciaHasta_WhenRowIsOpen()
+ {
+ var vigencia = NextUniqueDate();
+ var entity = TipoDeIva.ForCreation("IVA_21", "Cierre test", 21m, true, vigencia);
+ var id = await _repo.InsertAsync(entity);
+
+ var closeDate = vigencia.AddMonths(6);
+ var result = await _repo.UpdateCierreVigenciaAsync(id, closeDate);
+
+ result.Should().BeTrue();
+
+ var fetched = await _repo.GetByIdAsync(id);
+ fetched!.VigenciaHasta.Should().Be(closeDate);
+ }
+
+ // ── T400.6: Race double-close guard ──────────────────────────────────────
+
+ [Fact]
+ public async Task UpdateCierreVigenciaAsync_DoubleClose_ReturnsFalseOnSecondCall()
+ {
+ var vigencia = NextUniqueDate();
+ var entity = TipoDeIva.ForCreation("IVA_21", "Double close", 21m, true, vigencia);
+ var id = await _repo.InsertAsync(entity);
+
+ var closeDate = vigencia.AddMonths(6);
+ var first = await _repo.UpdateCierreVigenciaAsync(id, closeDate);
+ var second = await _repo.UpdateCierreVigenciaAsync(id, closeDate.AddMonths(1));
+
+ first.Should().BeTrue();
+ second.Should().BeFalse("the guard prevents double-close: WHERE VigenciaHasta IS NULL");
+ }
+
+ // ── SetActivoAsync ────────────────────────────────────────────────────────
+
+ [Fact]
+ public async Task SetActivoAsync_FalseAndBack_TogglesActivoCorrectly()
+ {
+ var vigencia = NextUniqueDate();
+ var entity = TipoDeIva.ForCreation("IVA_21", "Toggle test", 21m, true, vigencia);
+ var id = await _repo.InsertAsync(entity);
+
+ await _repo.SetActivoAsync(id, false);
+ var inactive = await _repo.GetByIdAsync(id);
+ inactive!.Activo.Should().BeFalse();
+
+ await _repo.SetActivoAsync(id, true);
+ var active = await _repo.GetByIdAsync(id);
+ active!.Activo.Should().BeTrue();
+ }
+
+ // ── ListAsync con paginación + filtros ────────────────────────────────────
+
+ [Fact]
+ public async Task ListAsync_WithActivoFilter_ReturnsOnlyMatchingRows()
+ {
+ // Insert one active and one inactive in their own date range
+ var d1 = NextUniqueDate();
+ var d2 = NextUniqueDate();
+
+ var activeId = await _repo.InsertAsync(
+ TipoDeIva.ForCreation("IVA_21", "Active row", 21m, true, d1));
+ var inactiveId = await _repo.InsertAsync(
+ TipoDeIva.ForCreation("IVA_21", "Inactive row", 21m, true, d2));
+ await _repo.SetActivoAsync(inactiveId, false);
+
+ var result = await _repo.ListAsync(new TiposDeIvaQuery(
+ Page: 1,
+ PageSize: 100,
+ Activo: true,
+ Codigo: null));
+
+ result.Items.Should().Contain(x => x.Id == activeId);
+ result.Items.Should().NotContain(x => x.Id == inactiveId);
+ }
+
+ [Fact]
+ public async Task ListAsync_ReturnsCorrectTotalCount()
+ {
+ // Insert a row with a unique Codigo prefix we can filter on
+ var vigencia = NextUniqueDate();
+ await _repo.InsertAsync(
+ TipoDeIva.ForCreation("EXENTO", "Total count test", 0m, false, vigencia));
+
+ var result = await _repo.ListAsync(new TiposDeIvaQuery(
+ Page: 1,
+ PageSize: 10,
+ Activo: null,
+ Codigo: "EXENTO"));
+
+ result.Total.Should().BeGreaterThan(0);
+ result.Items.Should().AllSatisfy(x => x.Codigo.Should().StartWith("EXENTO"));
+ }
+
+ // ── T400.5: Cadena de 3 versiones ────────────────────────────────────────
+
+ [Fact]
+ public async Task VersionChain_ThreeVersions_PredecesorIdChainIsCorrect()
+ {
+ // v1: IVA_21 at 21%
+ var v1Date = NextUniqueDate();
+ var v1 = TipoDeIva.ForCreation("IVA_21", "Version 1", 21m, true, v1Date);
+ var v1Id = await _repo.InsertAsync(v1);
+
+ // Close v1 vigencia
+ var v2Date = v1Date.AddMonths(1);
+ await _repo.UpdateCierreVigenciaAsync(v1Id, v2Date.AddDays(-1));
+
+ // v2: 23%
+ var v2 = TipoDeIva.ForCreation("IVA_21", "Version 2", 23m, true, v2Date, null, v1Id);
+ var v2Id = await _repo.InsertAsync(v2);
+
+ // Close v2 vigencia
+ var v3Date = v2Date.AddMonths(1);
+ await _repo.UpdateCierreVigenciaAsync(v2Id, v3Date.AddDays(-1));
+
+ // v3: 25%
+ var v3 = TipoDeIva.ForCreation("IVA_21", "Version 3", 25m, true, v3Date, null, v2Id);
+ var v3Id = await _repo.InsertAsync(v3);
+
+ // Verify chain
+ var fv2 = await _repo.GetByIdAsync(v2Id);
+ var fv3 = await _repo.GetByIdAsync(v3Id);
+
+ fv2!.PredecesorId.Should().Be(v1Id);
+ fv3!.PredecesorId.Should().Be(v2Id);
+ }
+
+ // ── T400.8: GetHistorialAsync ─────────────────────────────────────────────
+
+ [Fact]
+ public async Task GetHistorialAsync_ReturnsChainFromRootToId_OrderedByVigenciaDesdeAsc()
+ {
+ var v1Date = NextUniqueDate();
+ var v1 = TipoDeIva.ForCreation("IVA_21", "Hist v1", 21m, true, v1Date);
+ var v1Id = await _repo.InsertAsync(v1);
+
+ var v2Date = v1Date.AddMonths(1);
+ await _repo.UpdateCierreVigenciaAsync(v1Id, v2Date.AddDays(-1));
+ var v2 = TipoDeIva.ForCreation("IVA_21", "Hist v2", 23m, true, v2Date, null, v1Id);
+ var v2Id = await _repo.InsertAsync(v2);
+
+ var v3Date = v2Date.AddMonths(1);
+ await _repo.UpdateCierreVigenciaAsync(v2Id, v3Date.AddDays(-1));
+ var v3 = TipoDeIva.ForCreation("IVA_21", "Hist v3", 25m, true, v3Date, null, v2Id);
+ var v3Id = await _repo.InsertAsync(v3);
+
+ // Get historial from v3Id — should return chain v1, v2, v3
+ var historial = await _repo.GetHistorialAsync(v3Id);
+
+ historial.Should().HaveCount(3);
+ historial[0].Id.Should().Be(v1Id, "root is first");
+ historial[1].Id.Should().Be(v2Id);
+ historial[2].Id.Should().Be(v3Id, "requested Id is last");
+ historial[0].VigenciaDesde.Should().BeBefore(historial[1].VigenciaDesde);
+ historial[1].VigenciaDesde.Should().BeBefore(historial[2].VigenciaDesde);
+ }
+
+ [Fact]
+ public async Task GetHistorialAsync_SingleVersion_ReturnsListWithOneItem()
+ {
+ var vigencia = NextUniqueDate();
+ var entity = TipoDeIva.ForCreation("IVA_21", "Solo", 21m, true, vigencia);
+ var id = await _repo.InsertAsync(entity);
+
+ var historial = await _repo.GetHistorialAsync(id);
+
+ historial.Should().HaveCount(1);
+ historial[0].Id.Should().Be(id);
+ }
+}