feat(domain): Rubro entity + domain exceptions (CAT-001)
This commit is contained in:
139
src/api/SIGCM2.Domain/Entities/Rubro.cs
Normal file
139
src/api/SIGCM2.Domain/Entities/Rubro.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
namespace SIGCM2.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Immutable N-ary tree node for the commercial catalog taxonomy.
|
||||
/// Follows the same sealed-class + factory + with-methods pattern as Medio.cs.
|
||||
/// </summary>
|
||||
public sealed class Rubro
|
||||
{
|
||||
private const int NombreMaxLength = 200;
|
||||
|
||||
public int Id { get; }
|
||||
public int? ParentId { get; }
|
||||
public string Nombre { get; }
|
||||
public int Orden { get; }
|
||||
public bool Activo { get; }
|
||||
public int? TarifarioBaseId { get; }
|
||||
public DateTime FechaCreacion { get; }
|
||||
public DateTime? FechaModificacion { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Full hydration constructor — used by the repository to reconstruct from DB rows.
|
||||
/// </summary>
|
||||
public Rubro(
|
||||
int id,
|
||||
int? parentId,
|
||||
string nombre,
|
||||
int orden,
|
||||
bool activo,
|
||||
int? tarifarioBaseId,
|
||||
DateTime fechaCreacion,
|
||||
DateTime? fechaModificacion)
|
||||
{
|
||||
Id = id;
|
||||
ParentId = parentId;
|
||||
Nombre = nombre;
|
||||
Orden = orden;
|
||||
Activo = activo;
|
||||
TarifarioBaseId = tarifarioBaseId;
|
||||
FechaCreacion = fechaCreacion;
|
||||
FechaModificacion = fechaModificacion;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating a new Rubro.
|
||||
/// Id=0 — DB assigns via IDENTITY.
|
||||
/// Activo=true, FechaModificacion=null by default.
|
||||
/// FechaCreacion is set from TimeProvider so it is testable.
|
||||
/// </summary>
|
||||
public static Rubro ForCreation(
|
||||
string nombre,
|
||||
int? parentId,
|
||||
int orden,
|
||||
int? tarifarioBaseId,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
ValidateNombre(nombre);
|
||||
|
||||
if (parentId.HasValue && parentId.Value <= 0)
|
||||
throw new ArgumentException("parentId debe ser un entero positivo cuando no es nulo.", nameof(parentId));
|
||||
|
||||
if (tarifarioBaseId.HasValue && tarifarioBaseId.Value < 0)
|
||||
throw new ArgumentException("tarifarioBaseId no puede ser negativo.", nameof(tarifarioBaseId));
|
||||
|
||||
return new Rubro(
|
||||
id: 0,
|
||||
parentId: parentId,
|
||||
nombre: nombre,
|
||||
orden: orden,
|
||||
activo: true,
|
||||
tarifarioBaseId: tarifarioBaseId,
|
||||
fechaCreacion: timeProvider.GetUtcNow().UtcDateTime,
|
||||
fechaModificacion: null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new Rubro instance with an updated Nombre and FechaModificacion.
|
||||
/// Does NOT mutate the current instance.
|
||||
/// </summary>
|
||||
public Rubro WithRenamed(string nuevoNombre, TimeProvider timeProvider)
|
||||
{
|
||||
ValidateNombre(nuevoNombre);
|
||||
|
||||
return new Rubro(
|
||||
id: Id,
|
||||
parentId: ParentId,
|
||||
nombre: nuevoNombre,
|
||||
orden: Orden,
|
||||
activo: Activo,
|
||||
tarifarioBaseId: TarifarioBaseId,
|
||||
fechaCreacion: FechaCreacion,
|
||||
fechaModificacion: timeProvider.GetUtcNow().UtcDateTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new Rubro instance with updated ParentId and Orden.
|
||||
/// Does NOT mutate the current instance.
|
||||
/// </summary>
|
||||
public Rubro WithMoved(int? nuevoParentId, int nuevoOrden, TimeProvider timeProvider)
|
||||
{
|
||||
return new Rubro(
|
||||
id: Id,
|
||||
parentId: nuevoParentId,
|
||||
nombre: Nombre,
|
||||
orden: nuevoOrden,
|
||||
activo: Activo,
|
||||
tarifarioBaseId: TarifarioBaseId,
|
||||
fechaCreacion: FechaCreacion,
|
||||
fechaModificacion: timeProvider.GetUtcNow().UtcDateTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new Rubro instance with updated Activo flag.
|
||||
/// Use Deactivate (false) or Reactivate (true).
|
||||
/// Does NOT mutate the current instance.
|
||||
/// </summary>
|
||||
public Rubro WithActivo(bool activo, TimeProvider timeProvider)
|
||||
{
|
||||
return new Rubro(
|
||||
id: Id,
|
||||
parentId: ParentId,
|
||||
nombre: Nombre,
|
||||
orden: Orden,
|
||||
activo: activo,
|
||||
tarifarioBaseId: TarifarioBaseId,
|
||||
fechaCreacion: FechaCreacion,
|
||||
fechaModificacion: timeProvider.GetUtcNow().UtcDateTime);
|
||||
}
|
||||
|
||||
private static void ValidateNombre(string nombre)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(nombre))
|
||||
throw new ArgumentException("El nombre del rubro no puede estar vacío o ser solo espacios.", nameof(nombre));
|
||||
|
||||
if (nombre.Length > NombreMaxLength)
|
||||
throw new ArgumentException(
|
||||
$"El nombre del rubro no puede superar los {NombreMaxLength} caracteres.",
|
||||
nameof(nombre));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace SIGCM2.Domain.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Thrown when moving a Rubro to one of its own descendants would create a cycle. → HTTP 400
|
||||
/// </summary>
|
||||
public sealed class RubroCycleDetectedException : DomainException
|
||||
{
|
||||
public int RubroId { get; }
|
||||
public int NuevoParentId { get; }
|
||||
|
||||
public RubroCycleDetectedException(int rubroId, int nuevoParentId)
|
||||
: base($"Mover el rubro '{rubroId}' al padre '{nuevoParentId}' crearía un ciclo en el árbol.")
|
||||
{
|
||||
RubroId = rubroId;
|
||||
NuevoParentId = nuevoParentId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace SIGCM2.Domain.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Thrown when creating or moving a Rubro would exceed the configured maximum tree depth. → HTTP 422
|
||||
/// </summary>
|
||||
public sealed class RubroMaxDepthExceededException : DomainException
|
||||
{
|
||||
public int Intentada { get; }
|
||||
public int Max { get; }
|
||||
|
||||
public RubroMaxDepthExceededException(int intentada, int max)
|
||||
: base($"La profundidad intentada ({intentada}) excede el máximo permitido ({max}).")
|
||||
{
|
||||
Intentada = intentada;
|
||||
Max = max;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace SIGCM2.Domain.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Thrown when a Rubro with the same Nombre (CI) already exists under the same parent. → HTTP 409
|
||||
/// </summary>
|
||||
public sealed class RubroNombreDuplicadoEnPadreException : DomainException
|
||||
{
|
||||
public string Nombre { get; }
|
||||
public int? ParentId { get; }
|
||||
|
||||
public RubroNombreDuplicadoEnPadreException(string nombre, int? parentId)
|
||||
: base(parentId.HasValue
|
||||
? $"Ya existe un rubro con el nombre '{nombre}' bajo el padre con id '{parentId}'."
|
||||
: $"Ya existe un rubro raíz con el nombre '{nombre}'.")
|
||||
{
|
||||
Nombre = nombre;
|
||||
ParentId = parentId;
|
||||
}
|
||||
}
|
||||
15
src/api/SIGCM2.Domain/Exceptions/RubroNotFoundException.cs
Normal file
15
src/api/SIGCM2.Domain/Exceptions/RubroNotFoundException.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace SIGCM2.Domain.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Thrown when a requested Rubro does not exist in the system. → HTTP 404
|
||||
/// </summary>
|
||||
public sealed class RubroNotFoundException : DomainException
|
||||
{
|
||||
public int Id { get; }
|
||||
|
||||
public RubroNotFoundException(int id)
|
||||
: base($"El rubro con id '{id}' no existe.")
|
||||
{
|
||||
Id = id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace SIGCM2.Domain.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Thrown when attempting to create or move a Rubro under an inactive parent. → HTTP 400
|
||||
/// </summary>
|
||||
public sealed class RubroPadreInactivoException : DomainException
|
||||
{
|
||||
public int ParentId { get; }
|
||||
|
||||
public RubroPadreInactivoException(int parentId)
|
||||
: base($"El padre con id '{parentId}' está inactivo y no puede tener hijos.")
|
||||
{
|
||||
ParentId = parentId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace SIGCM2.Domain.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Thrown when attempting to soft-delete a Rubro that still has active children. → HTTP 409
|
||||
/// </summary>
|
||||
public sealed class RubroTieneHijosActivosException : DomainException
|
||||
{
|
||||
public int Id { get; }
|
||||
public int Count { get; }
|
||||
|
||||
public RubroTieneHijosActivosException(int id, int count)
|
||||
: base($"El rubro con id '{id}' tiene {count} subrubros activos.")
|
||||
{
|
||||
Id = id;
|
||||
Count = count;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user