diff --git a/src/api/SIGCM2.Domain/Entities/TipoDeIva.cs b/src/api/SIGCM2.Domain/Entities/TipoDeIva.cs
new file mode 100644
index 0000000..3514bdc
--- /dev/null
+++ b/src/api/SIGCM2.Domain/Entities/TipoDeIva.cs
@@ -0,0 +1,208 @@
+using System.Text.RegularExpressions;
+using SIGCM2.Domain.Exceptions;
+
+namespace SIGCM2.Domain.Entities;
+
+///
+/// Tipo de IVA de referencia con versionado append-only.
+/// Porcentaje es INMUTABLE post-creación; cambiar el valor requiere crear una nueva versión
+/// vía .
+///
+public sealed class TipoDeIva
+{
+ private static readonly Regex CodigoRegex =
+ new(@"^(EXENTO|NO_GRAVADO|IVA_\d+)$", RegexOptions.Compiled | RegexOptions.CultureInvariant);
+
+ public int Id { get; }
+ public string Codigo { get; }
+ public string Descripcion { get; }
+ public decimal Porcentaje { get; } // INMUTABLE — usar NuevaVersion para cambiar
+ public bool AplicaIVA { get; }
+ public bool Activo { get; }
+ public DateOnly VigenciaDesde { get; }
+ public DateOnly? VigenciaHasta { get; }
+ public int? PredecesorId { get; }
+ public DateTime FechaCreacion { get; }
+ public DateTime? FechaModificacion { get; }
+
+ private TipoDeIva(
+ int id,
+ string codigo,
+ string descripcion,
+ decimal porcentaje,
+ bool aplicaIVA,
+ bool activo,
+ DateOnly vigenciaDesde,
+ DateOnly? vigenciaHasta,
+ int? predecesorId,
+ DateTime fechaCreacion,
+ DateTime? fechaModificacion)
+ {
+ Id = id;
+ Codigo = codigo;
+ Descripcion = descripcion;
+ Porcentaje = porcentaje;
+ AplicaIVA = aplicaIVA;
+ Activo = activo;
+ VigenciaDesde = vigenciaDesde;
+ VigenciaHasta = vigenciaHasta;
+ PredecesorId = predecesorId;
+ FechaCreacion = fechaCreacion;
+ FechaModificacion = fechaModificacion;
+ }
+
+ ///
+ /// Factory para crear un nuevo TipoDeIva (Id=0 — BD asigna via IDENTITY; Activo=true).
+ ///
+ /// Si Codigo no cumple formato o Porcentaje fuera de rango.
+ public static TipoDeIva ForCreation(
+ string codigo,
+ string descripcion,
+ decimal porcentaje,
+ bool aplicaIVA,
+ DateOnly vigenciaDesde,
+ DateOnly? vigenciaHasta = null,
+ int? predecesorId = null)
+ {
+ ValidateCodigo(codigo);
+ ValidatePorcentaje(porcentaje, nameof(porcentaje));
+ ValidateVigencias(vigenciaDesde, vigenciaHasta);
+
+ return new(
+ id: 0,
+ codigo: codigo,
+ descripcion: descripcion,
+ porcentaje: porcentaje,
+ aplicaIVA: aplicaIVA,
+ activo: true,
+ vigenciaDesde: vigenciaDesde,
+ vigenciaHasta: vigenciaHasta,
+ predecesorId: predecesorId,
+ fechaCreacion: default,
+ fechaModificacion: null);
+ }
+
+ ///
+ /// Factory para reconstruir desde repositorio (Dapper). No aplica validaciones de dominio.
+ ///
+ public static TipoDeIva FromDb(
+ int id,
+ string codigo,
+ string descripcion,
+ decimal porcentaje,
+ bool aplicaIVA,
+ bool activo,
+ DateOnly vigenciaDesde,
+ DateOnly? vigenciaHasta,
+ int? predecesorId,
+ DateTime fechaCreacion,
+ DateTime? fechaModificacion)
+ => new(id, codigo, descripcion, porcentaje, aplicaIVA, activo,
+ vigenciaDesde, vigenciaHasta, predecesorId, fechaCreacion, fechaModificacion);
+
+ ///
+ /// Crea una nueva versión con el porcentaje actualizado.
+ /// Retorna la predecesora cerrada y la nueva versión.
+ ///
+ /// Si la predecesora ya está cerrada (VigenciaHasta != null).
+ /// Si vigenciaDesde no es posterior a la predecesora, o nuevoPorcentaje fuera de rango.
+ public (TipoDeIva predecesoraCerrada, TipoDeIva nuevaVersion) NuevaVersion(
+ decimal nuevoPorcentaje,
+ DateOnly vigenciaDesde)
+ {
+ if (VigenciaHasta is not null)
+ throw new InvalidOperationException(
+ $"La versión {Id} ya está cerrada (VigenciaHasta={VigenciaHasta}). No puede generar nueva versión.");
+
+ if (vigenciaDesde <= VigenciaDesde)
+ throw new ArgumentException(
+ $"vigenciaDesde ({vigenciaDesde}) debe ser posterior a VigenciaDesde de la predecesora ({VigenciaDesde}).",
+ nameof(vigenciaDesde));
+
+ ValidatePorcentaje(nuevoPorcentaje, nameof(nuevoPorcentaje));
+
+ var cerrada = new TipoDeIva(
+ id: Id,
+ codigo: Codigo,
+ descripcion: Descripcion,
+ porcentaje: Porcentaje,
+ aplicaIVA: AplicaIVA,
+ activo: Activo,
+ vigenciaDesde: VigenciaDesde,
+ vigenciaHasta: vigenciaDesde.AddDays(-1),
+ predecesorId: PredecesorId,
+ fechaCreacion: FechaCreacion,
+ fechaModificacion: DateTime.UtcNow);
+
+ var nueva = ForCreation(
+ codigo: Codigo,
+ descripcion: Descripcion,
+ porcentaje: nuevoPorcentaje,
+ aplicaIVA: AplicaIVA,
+ vigenciaDesde: vigenciaDesde,
+ vigenciaHasta: null,
+ predecesorId: Id);
+
+ return (cerrada, nueva);
+ }
+
+ // ── Cosmetic mutators (sealed With* — NOT WithPorcentaje) ─────────────────
+
+ /// Actualiza la descripción. Porcentaje y vigencias permanecen inmutables.
+ public TipoDeIva WithDescripcion(string descripcion)
+ => new(Id, Codigo, descripcion, Porcentaje, AplicaIVA, Activo,
+ VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, DateTime.UtcNow);
+
+ /// Actualiza el código. Porcentaje y vigencias permanecen inmutables.
+ public TipoDeIva WithCodigo(string codigo)
+ => new(Id, codigo, Descripcion, Porcentaje, AplicaIVA, Activo,
+ VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, DateTime.UtcNow);
+
+ /// Actualiza la bandera AplicaIVA. Porcentaje permanece inmutable.
+ public TipoDeIva WithAplicaIVA(bool aplicaIVA)
+ => new(Id, Codigo, Descripcion, Porcentaje, aplicaIVA, Activo,
+ VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, DateTime.UtcNow);
+
+ /// Retorna instancia con Activo=false.
+ public TipoDeIva Deactivate()
+ => new(Id, Codigo, Descripcion, Porcentaje, AplicaIVA, false,
+ VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, DateTime.UtcNow);
+
+ /// Retorna instancia con Activo=true.
+ public TipoDeIva Reactivate()
+ => new(Id, Codigo, Descripcion, Porcentaje, AplicaIVA, true,
+ VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, DateTime.UtcNow);
+
+ ///
+ /// Cierra la vigencia seteando VigenciaHasta. Usado por el handler de NuevaVersion.
+ ///
+ public TipoDeIva CerrarVigencia(DateOnly vigenciaHasta)
+ => new(Id, Codigo, Descripcion, Porcentaje, AplicaIVA, Activo,
+ VigenciaDesde, vigenciaHasta, PredecesorId, FechaCreacion, DateTime.UtcNow);
+
+ // ── Private helpers ───────────────────────────────────────────────────────
+
+ private static void ValidateCodigo(string codigo)
+ {
+ if (string.IsNullOrWhiteSpace(codigo) || !CodigoRegex.IsMatch(codigo))
+ throw new ArgumentException(
+ $"Codigo '{codigo}' inválido. Debe cumplir ^(EXENTO|NO_GRAVADO|IVA_\\d+)$.",
+ nameof(codigo));
+ }
+
+ private static void ValidatePorcentaje(decimal porcentaje, string paramName)
+ {
+ if (porcentaje < 0m || porcentaje > 100m)
+ throw new ArgumentException(
+ $"Porcentaje ({porcentaje}) debe estar entre 0 y 100.",
+ paramName);
+ }
+
+ private static void ValidateVigencias(DateOnly desde, DateOnly? hasta)
+ {
+ if (hasta.HasValue && hasta.Value < desde)
+ throw new ArgumentException(
+ $"VigenciaHasta ({hasta}) no puede ser anterior a VigenciaDesde ({desde}).",
+ "vigenciaHasta");
+ }
+}