feat(adm-009): IngresosBrutos sealed entity mirror of TipoDeIva

This commit is contained in:
2026-04-17 17:51:52 -03:00
parent 87364ff8e6
commit 088f2303c1

View File

@@ -0,0 +1,177 @@
using SIGCM2.Domain.Fiscal;
namespace SIGCM2.Domain.Entities;
/// <summary>
/// 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 <see cref="NuevaVersion"/>.
/// </summary>
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;
}
/// <summary>
/// Factory para crear una nueva entrada de IIBB (Id=0 — BD asigna via IDENTITY; Activo=true).
/// </summary>
/// <exception cref="ArgumentException">Si Alicuota fuera de rango 0-100.</exception>
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);
}
/// <summary>
/// Factory para reconstruir desde repositorio (Dapper). No aplica validaciones de dominio.
/// </summary>
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);
/// <summary>
/// Crea una nueva versión con la alícuota actualizada.
/// Retorna la predecesora cerrada y la nueva versión.
/// </summary>
/// <exception cref="InvalidOperationException">Si la predecesora ya está cerrada (VigenciaHasta != null).</exception>
/// <exception cref="ArgumentException">Si vigenciaDesde no es posterior a la predecesora, o nuevaAlicuota fuera de rango.</exception>
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) ─────────────────
/// <summary>Actualiza la descripción. Alicuota y Provincia permanecen inmutables.</summary>
public IngresosBrutos WithDescripcion(string descripcion)
=> new(Id, Provincia, descripcion, Alicuota, Activo,
VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, DateTime.UtcNow);
/// <summary>Retorna instancia con Activo=false.</summary>
public IngresosBrutos Deactivate()
=> new(Id, Provincia, Descripcion, Alicuota, false,
VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, DateTime.UtcNow);
/// <summary>Retorna instancia con Activo=true.</summary>
public IngresosBrutos Reactivate()
=> new(Id, Provincia, Descripcion, Alicuota, true,
VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, DateTime.UtcNow);
/// <summary>
/// Cierra la vigencia seteando VigenciaHasta. Usado por el handler de NuevaVersion.
/// </summary>
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");
}
}