ADM-009: Tablas Fiscales (IVA + IIBB) — append-only versioned ref data #22
208
src/api/SIGCM2.Domain/Entities/TipoDeIva.cs
Normal file
208
src/api/SIGCM2.Domain/Entities/TipoDeIva.cs
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using SIGCM2.Domain.Exceptions;
|
||||||
|
|
||||||
|
namespace SIGCM2.Domain.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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 <see cref="NuevaVersion"/>.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Factory para crear un nuevo TipoDeIva (Id=0 — BD asigna via IDENTITY; Activo=true).
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="ArgumentException">Si Codigo no cumple formato o Porcentaje fuera de rango.</exception>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Factory para reconstruir desde repositorio (Dapper). No aplica validaciones de dominio.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Crea una nueva versión con el porcentaje actualizado.
|
||||||
|
/// 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 nuevoPorcentaje fuera de rango.</exception>
|
||||||
|
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) ─────────────────
|
||||||
|
|
||||||
|
/// <summary>Actualiza la descripción. Porcentaje y vigencias permanecen inmutables.</summary>
|
||||||
|
public TipoDeIva WithDescripcion(string descripcion)
|
||||||
|
=> new(Id, Codigo, descripcion, Porcentaje, AplicaIVA, Activo,
|
||||||
|
VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, DateTime.UtcNow);
|
||||||
|
|
||||||
|
/// <summary>Actualiza el código. Porcentaje y vigencias permanecen inmutables.</summary>
|
||||||
|
public TipoDeIva WithCodigo(string codigo)
|
||||||
|
=> new(Id, codigo, Descripcion, Porcentaje, AplicaIVA, Activo,
|
||||||
|
VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, DateTime.UtcNow);
|
||||||
|
|
||||||
|
/// <summary>Actualiza la bandera AplicaIVA. Porcentaje permanece inmutable.</summary>
|
||||||
|
public TipoDeIva WithAplicaIVA(bool aplicaIVA)
|
||||||
|
=> new(Id, Codigo, Descripcion, Porcentaje, aplicaIVA, Activo,
|
||||||
|
VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, DateTime.UtcNow);
|
||||||
|
|
||||||
|
/// <summary>Retorna instancia con Activo=false.</summary>
|
||||||
|
public TipoDeIva Deactivate()
|
||||||
|
=> new(Id, Codigo, Descripcion, Porcentaje, AplicaIVA, false,
|
||||||
|
VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, DateTime.UtcNow);
|
||||||
|
|
||||||
|
/// <summary>Retorna instancia con Activo=true.</summary>
|
||||||
|
public TipoDeIva Reactivate()
|
||||||
|
=> new(Id, Codigo, Descripcion, Porcentaje, AplicaIVA, true,
|
||||||
|
VigenciaDesde, VigenciaHasta, PredecesorId, FechaCreacion, DateTime.UtcNow);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cierra la vigencia seteando VigenciaHasta. Usado por el handler de NuevaVersion.
|
||||||
|
/// </summary>
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user