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"); + } +}