diff --git a/src/api/SIGCM2.Domain/Entities/IngresosBrutos.cs b/src/api/SIGCM2.Domain/Entities/IngresosBrutos.cs
new file mode 100644
index 0000000..db35ce6
--- /dev/null
+++ b/src/api/SIGCM2.Domain/Entities/IngresosBrutos.cs
@@ -0,0 +1,177 @@
+using SIGCM2.Domain.Fiscal;
+
+namespace SIGCM2.Domain.Entities;
+
+///
+/// Entrada de Ingresos Brutos por provincia con versionado append-only.
+/// Alicuota es INMUTABLE post-creación; cambiar el valor requiere crear una nueva versión
+/// vía .
+///
+public sealed class IngresosBrutos
+{
+ public int Id { get; }
+ public ProvinciaArgentina Provincia { get; } // INMUTABLE
+ public string Descripcion { get; }
+ public decimal Alicuota { get; } // INMUTABLE — usar NuevaVersion para cambiar
+ 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 IngresosBrutos(
+ int id,
+ ProvinciaArgentina provincia,
+ string descripcion,
+ decimal alicuota,
+ bool activo,
+ DateOnly vigenciaDesde,
+ DateOnly? vigenciaHasta,
+ int? predecesorId,
+ DateTime fechaCreacion,
+ DateTime? fechaModificacion)
+ {
+ Id = id;
+ Provincia = provincia;
+ Descripcion = descripcion;
+ Alicuota = alicuota;
+ Activo = activo;
+ VigenciaDesde = vigenciaDesde;
+ VigenciaHasta = vigenciaHasta;
+ PredecesorId = predecesorId;
+ FechaCreacion = fechaCreacion;
+ FechaModificacion = fechaModificacion;
+ }
+
+ ///
+ /// Factory para crear una nueva entrada de IIBB (Id=0 — BD asigna via IDENTITY; Activo=true).
+ ///
+ /// Si Alicuota fuera de rango 0-100.
+ public static IngresosBrutos ForCreation(
+ ProvinciaArgentina provincia,
+ string descripcion,
+ decimal alicuota,
+ DateOnly vigenciaDesde,
+ DateOnly? vigenciaHasta = null,
+ int? predecesorId = null)
+ {
+ ValidateAlicuota(alicuota, nameof(alicuota));
+ ValidateVigencias(vigenciaDesde, vigenciaHasta);
+
+ return new(
+ id: 0,
+ provincia: provincia,
+ descripcion: descripcion,
+ alicuota: alicuota,
+ 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 IngresosBrutos FromDb(
+ int id,
+ ProvinciaArgentina provincia,
+ string descripcion,
+ decimal alicuota,
+ bool activo,
+ DateOnly vigenciaDesde,
+ DateOnly? vigenciaHasta,
+ int? predecesorId,
+ DateTime fechaCreacion,
+ DateTime? fechaModificacion)
+ => new(id, provincia, descripcion, alicuota, activo,
+ vigenciaDesde, vigenciaHasta, predecesorId, fechaCreacion, fechaModificacion);
+
+ ///
+ /// Crea una nueva versión con la alícuota actualizada.
+ /// 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 nuevaAlicuota fuera de rango.
+ public (IngresosBrutos predecesoraCerrada, IngresosBrutos nuevaVersion) NuevaVersion(
+ decimal nuevaAlicuota,
+ 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));
+
+ ValidateAlicuota(nuevaAlicuota, nameof(nuevaAlicuota));
+
+ var cerrada = new IngresosBrutos(
+ id: Id,
+ provincia: Provincia,
+ descripcion: Descripcion,
+ alicuota: Alicuota,
+ activo: Activo,
+ vigenciaDesde: VigenciaDesde,
+ vigenciaHasta: vigenciaDesde.AddDays(-1),
+ predecesorId: PredecesorId,
+ fechaCreacion: FechaCreacion,
+ fechaModificacion: DateTime.UtcNow);
+
+ var nueva = ForCreation(
+ provincia: Provincia,
+ descripcion: Descripcion,
+ alicuota: nuevaAlicuota,
+ vigenciaDesde: vigenciaDesde,
+ vigenciaHasta: null,
+ predecesorId: Id);
+
+ return (cerrada, nueva);
+ }
+
+ // ── Cosmetic mutators (NO WithAlicuota, NO WithProvincia) ─────────────────
+
+ /// Actualiza la descripción. Alicuota y Provincia permanecen inmutables.
+ public IngresosBrutos WithDescripcion(string descripcion)
+ => new(Id, Provincia, descripcion, Alicuota, Activo,
+ VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, DateTime.UtcNow);
+
+ /// Retorna instancia con Activo=false.
+ public IngresosBrutos Deactivate()
+ => new(Id, Provincia, Descripcion, Alicuota, false,
+ VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, DateTime.UtcNow);
+
+ /// Retorna instancia con Activo=true.
+ public IngresosBrutos Reactivate()
+ => new(Id, Provincia, Descripcion, Alicuota, true,
+ VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, DateTime.UtcNow);
+
+ ///
+ /// Cierra la vigencia seteando VigenciaHasta. Usado por el handler de NuevaVersion.
+ ///
+ public IngresosBrutos CerrarVigencia(DateOnly vigenciaHasta)
+ => new(Id, Provincia, Descripcion, Alicuota, Activo,
+ VigenciaDesde, vigenciaHasta, PredecesorId, FechaCreacion, DateTime.UtcNow);
+
+ // ── Private helpers ───────────────────────────────────────────────────────
+
+ private static void ValidateAlicuota(decimal alicuota, string paramName)
+ {
+ if (alicuota < 0m || alicuota > 100m)
+ throw new ArgumentException(
+ $"Alicuota ({alicuota}) 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");
+ }
+}