ADM-009: Tablas Fiscales (IVA + IIBB) — append-only versioned ref data #22

Merged
dmolinari merged 36 commits from feature/ADM-009 into main 2026-04-18 11:45:13 +00:00
Showing only changes of commit b16dd313ed - Show all commits

View File

@@ -0,0 +1,351 @@
using System.Reflection;
using FluentAssertions;
using SIGCM2.Domain.Entities;
using SIGCM2.Domain.Exceptions;
namespace SIGCM2.Application.Tests.Domain.Fiscal;
public class TipoDeIvaTests
{
private static readonly DateOnly Desde2020 = new(2020, 1, 1);
private static readonly DateOnly Desde2026 = new(2026, 6, 1);
private static TipoDeIva MakeTipoDeIva(
int id = 1,
string codigo = "IVA_21",
string descripcion = "IVA 21%",
decimal porcentaje = 21m,
bool aplicaIVA = true,
bool activo = true,
DateOnly? vigenciaDesde = null,
DateOnly? vigenciaHasta = null,
int? predecesorId = null)
=> TipoDeIva.FromDb(
id: id,
codigo: codigo,
descripcion: descripcion,
porcentaje: porcentaje,
aplicaIVA: aplicaIVA,
activo: activo,
vigenciaDesde: vigenciaDesde ?? Desde2020,
vigenciaHasta: vigenciaHasta,
predecesorId: predecesorId,
fechaCreacion: DateTime.UtcNow,
fechaModificacion: null);
// ── T200.10: ForCreation validations (RED) ────────────────────────────────
[Fact]
public void ForCreation_ValidArgs_ReturnsEntity()
{
var iva = TipoDeIva.ForCreation("IVA_21", "IVA 21%", 21m, true, Desde2020);
iva.Codigo.Should().Be("IVA_21");
iva.Descripcion.Should().Be("IVA 21%");
iva.Porcentaje.Should().Be(21m);
iva.AplicaIVA.Should().BeTrue();
iva.Activo.Should().BeTrue();
iva.Id.Should().Be(0);
iva.PredecesorId.Should().BeNull();
iva.VigenciaHasta.Should().BeNull();
}
[Theory]
[InlineData("INVALIDO")]
[InlineData("IVA21")]
[InlineData("iva_21")]
[InlineData("IVA-21")]
[InlineData("")]
[InlineData(" ")]
public void ForCreation_CodigoInvalido_ThrowsArgumentException(string codigoInvalido)
{
var act = () => TipoDeIva.ForCreation(codigoInvalido, "desc", 21m, true, Desde2020);
act.Should().Throw<ArgumentException>()
.WithParameterName("codigo");
}
[Theory]
[InlineData("EXENTO")]
[InlineData("NO_GRAVADO")]
[InlineData("IVA_21")]
[InlineData("IVA_105")]
[InlineData("IVA_0")]
public void ForCreation_CodigoValido_NoLanza(string codigoValido)
{
var act = () => TipoDeIva.ForCreation(codigoValido, "desc", 0m, false, Desde2020);
act.Should().NotThrow();
}
[Fact]
public void ForCreation_PorcentajeNegativo_ThrowsArgumentException()
{
var act = () => TipoDeIva.ForCreation("IVA_21", "desc", -5m, true, Desde2020);
act.Should().Throw<ArgumentException>()
.WithParameterName("porcentaje");
}
[Fact]
public void ForCreation_PorcentajeMayorA100_ThrowsArgumentException()
{
var act = () => TipoDeIva.ForCreation("IVA_21", "desc", 150m, true, Desde2020);
act.Should().Throw<ArgumentException>()
.WithParameterName("porcentaje");
}
[Theory]
[InlineData(0)]
[InlineData(10.5)]
[InlineData(21)]
[InlineData(100)]
public void ForCreation_PorcentajeEnRango_NoLanza(double porcentaje)
{
var act = () => TipoDeIva.ForCreation("IVA_21", "desc", (decimal)porcentaje, true, Desde2020);
act.Should().NotThrow();
}
[Fact]
public void ForCreation_VigenciaHastaMenorQueDesde_ThrowsArgumentException()
{
var desde = new DateOnly(2026, 6, 1);
var hasta = new DateOnly(2026, 1, 1); // antes que desde
var act = () => TipoDeIva.ForCreation("IVA_21", "desc", 21m, true, desde, hasta);
act.Should().Throw<ArgumentException>()
.WithParameterName("vigenciaHasta");
}
[Fact]
public void ForCreation_VigenciaHastaIgualQueDesde_NoLanza()
{
var fecha = new DateOnly(2026, 1, 1);
var act = () => TipoDeIva.ForCreation("IVA_21", "desc", 21m, true, fecha, fecha);
act.Should().NotThrow();
}
// ── T200.12: ForCreation sets all properties correctly ────────────────────
[Fact]
public void ForCreation_SetsAllProps_Correctly()
{
var iva = TipoDeIva.ForCreation("EXENTO", "Exento de IVA", 0m, false, Desde2020);
iva.Id.Should().Be(0);
iva.Activo.Should().BeTrue();
iva.VigenciaHasta.Should().BeNull();
iva.VigenciaDesde.Should().Be(Desde2020);
iva.PredecesorId.Should().BeNull();
}
// ── T200.11: With* methods ────────────────────────────────────────────────
[Fact]
public void WithDescripcion_ReturnsNewInstanceWithUpdatedDescripcion()
{
var original = MakeTipoDeIva(descripcion: "Original");
var updated = original.WithDescripcion("Nueva descripcion");
updated.Should().NotBeSameAs(original);
updated.Descripcion.Should().Be("Nueva descripcion");
updated.Porcentaje.Should().Be(original.Porcentaje, "Porcentaje es inmutable");
}
[Fact]
public void WithCodigo_ReturnsNewInstanceWithUpdatedCodigo()
{
var original = MakeTipoDeIva(codigo: "IVA_21");
var updated = original.WithCodigo("NO_GRAVADO");
updated.Codigo.Should().Be("NO_GRAVADO");
updated.Porcentaje.Should().Be(original.Porcentaje);
updated.Id.Should().Be(original.Id);
}
[Fact]
public void WithAplicaIVA_ReturnsNewInstanceWithUpdatedAplicaIVA()
{
var original = MakeTipoDeIva(aplicaIVA: true);
var updated = original.WithAplicaIVA(false);
updated.AplicaIVA.Should().BeFalse();
updated.Porcentaje.Should().Be(original.Porcentaje);
}
[Fact]
public void Deactivate_ReturnsNewInstanceWithActivoFalse()
{
var original = MakeTipoDeIva(activo: true);
var deactivated = original.Deactivate();
deactivated.Activo.Should().BeFalse();
deactivated.Porcentaje.Should().Be(original.Porcentaje);
deactivated.Id.Should().Be(original.Id);
}
[Fact]
public void Reactivate_ReturnsNewInstanceWithActivoTrue()
{
var original = MakeTipoDeIva(activo: false);
var reactivated = original.Reactivate();
reactivated.Activo.Should().BeTrue();
}
[Fact]
public void CerrarVigencia_SetsVigenciaHasta()
{
var original = MakeTipoDeIva(vigenciaHasta: null);
var hasta = new DateOnly(2026, 5, 31);
var cerrado = original.CerrarVigencia(hasta);
cerrado.VigenciaHasta.Should().Be(hasta);
cerrado.Porcentaje.Should().Be(original.Porcentaje);
cerrado.Id.Should().Be(original.Id);
}
// ── T200.13 & T200.15: NuevaVersion tuple (RED then GREEN) ───────────────
[Fact]
public void NuevaVersion_ReturnsPredecesoraCerradaYNuevaVersion()
{
var predecesora = MakeTipoDeIva(id: 5, porcentaje: 21m, vigenciaDesde: Desde2020, vigenciaHasta: null);
var (cerrada, nueva) = predecesora.NuevaVersion(23.5m, Desde2026);
cerrada.Id.Should().Be(5);
cerrada.VigenciaHasta.Should().Be(Desde2026.AddDays(-1), "predecesora queda cerrada el día anterior");
cerrada.Porcentaje.Should().Be(21m, "porcentaje predecesora no cambia");
nueva.Id.Should().Be(0, "ID lo asigna la BD");
nueva.PredecesorId.Should().Be(5);
nueva.Porcentaje.Should().Be(23.5m);
nueva.VigenciaDesde.Should().Be(Desde2026);
nueva.VigenciaHasta.Should().BeNull("nueva versión nace abierta");
nueva.Codigo.Should().Be(predecesora.Codigo, "hereda el código");
nueva.Descripcion.Should().Be(predecesora.Descripcion, "hereda la descripción");
nueva.Activo.Should().BeTrue();
}
[Fact]
public void NuevaVersion_PorcentajeDistinto_EsIndependiente()
{
var predecesora = MakeTipoDeIva(porcentaje: 10.5m, vigenciaDesde: Desde2020);
var nuevaVigencia = new DateOnly(2025, 1, 1);
var (_, nueva) = predecesora.NuevaVersion(21m, nuevaVigencia);
nueva.Porcentaje.Should().Be(21m);
predecesora.Porcentaje.Should().Be(10.5m, "predecesora no muta");
}
// ── T200.13: NuevaVersion validations ────────────────────────────────────
[Fact]
public void NuevaVersion_PredecesoraConVigenciaHasta_ThrowsInvalidOperationException()
{
var predecesora = MakeTipoDeIva(
vigenciaDesde: Desde2020,
vigenciaHasta: new DateOnly(2025, 12, 31)); // ya cerrada
var act = () => predecesora.NuevaVersion(23.5m, Desde2026);
act.Should().Throw<InvalidOperationException>();
}
[Fact]
public void NuevaVersion_VigenciaDesdeIgualAPredecesora_ThrowsArgumentException()
{
var predecesora = MakeTipoDeIva(vigenciaDesde: Desde2020, vigenciaHasta: null);
var act = () => predecesora.NuevaVersion(23.5m, Desde2020); // igual a VigenciaDesde predecesora
act.Should().Throw<ArgumentException>()
.WithParameterName("vigenciaDesde");
}
[Fact]
public void NuevaVersion_VigenciaDesDeMenorQuePredecesora_ThrowsArgumentException()
{
var predecesora = MakeTipoDeIva(vigenciaDesde: Desde2026, vigenciaHasta: null);
var vigenciaAnterior = new DateOnly(2020, 1, 1);
var act = () => predecesora.NuevaVersion(23.5m, vigenciaAnterior);
act.Should().Throw<ArgumentException>()
.WithParameterName("vigenciaDesde");
}
[Fact]
public void NuevaVersion_NuevoPorcentajeNegativo_ThrowsArgumentException()
{
var predecesora = MakeTipoDeIva(vigenciaDesde: Desde2020, vigenciaHasta: null);
var act = () => predecesora.NuevaVersion(-1m, Desde2026);
act.Should().Throw<ArgumentException>()
.WithParameterName("nuevoPorcentaje");
}
[Fact]
public void NuevaVersion_NuevoPorcentajeMayorA100_ThrowsArgumentException()
{
var predecesora = MakeTipoDeIva(vigenciaDesde: Desde2020, vigenciaHasta: null);
var act = () => predecesora.NuevaVersion(101m, Desde2026);
act.Should().Throw<ArgumentException>()
.WithParameterName("nuevoPorcentaje");
}
// ── T200.16: reflection — NO debe existir WithPorcentaje ─────────────────
[Fact]
public void TipoDeIva_No_Debe_Exponer_WithPorcentaje()
{
var method = typeof(TipoDeIva).GetMethod("WithPorcentaje", BindingFlags.Public | BindingFlags.Instance);
method.Should().BeNull("Porcentaje es inmutable — usar NuevaVersion");
}
// ── Immutable fields preserved across With* ───────────────────────────────
[Fact]
public void WithDescripcion_PreservesImmutableFields()
{
var original = MakeTipoDeIva(id: 99, porcentaje: 21m, vigenciaDesde: Desde2020);
var updated = original.WithDescripcion("Nueva");
updated.Id.Should().Be(99);
updated.Porcentaje.Should().Be(21m);
updated.VigenciaDesde.Should().Be(Desde2020);
}
[Fact]
public void FromDb_SetsAllProperties()
{
var fechaCreacion = DateTime.UtcNow;
var iva = TipoDeIva.FromDb(
id: 7, codigo: "IVA_21", descripcion: "IVA 21%",
porcentaje: 21m, aplicaIVA: true, activo: true,
vigenciaDesde: Desde2020, vigenciaHasta: null,
predecesorId: null, fechaCreacion: fechaCreacion, fechaModificacion: null);
iva.Id.Should().Be(7);
iva.Codigo.Should().Be("IVA_21");
iva.Porcentaje.Should().Be(21m);
iva.FechaCreacion.Should().Be(fechaCreacion);
}
}