feat: CAT-001 Árbol N-ario de Rubros #30
@@ -169,6 +169,79 @@ public sealed class ExceptionFilter : IExceptionFilter
|
|||||||
context.ExceptionHandled = true;
|
context.ExceptionHandled = true;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// CAT-001: Rubro exceptions
|
||||||
|
case RubroNotFoundException rubroNotFoundEx:
|
||||||
|
context.Result = new ObjectResult(new
|
||||||
|
{
|
||||||
|
error = "rubro_not_found",
|
||||||
|
message = rubroNotFoundEx.Message
|
||||||
|
})
|
||||||
|
{
|
||||||
|
StatusCode = StatusCodes.Status404NotFound
|
||||||
|
};
|
||||||
|
context.ExceptionHandled = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RubroNombreDuplicadoEnPadreException rubroDupEx:
|
||||||
|
context.Result = new ObjectResult(new
|
||||||
|
{
|
||||||
|
error = "rubro_nombre_duplicado",
|
||||||
|
message = rubroDupEx.Message
|
||||||
|
})
|
||||||
|
{
|
||||||
|
StatusCode = StatusCodes.Status409Conflict
|
||||||
|
};
|
||||||
|
context.ExceptionHandled = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RubroTieneHijosActivosException rubroHijosEx:
|
||||||
|
context.Result = new ObjectResult(new
|
||||||
|
{
|
||||||
|
error = "rubro_tiene_hijos_activos",
|
||||||
|
message = rubroHijosEx.Message
|
||||||
|
})
|
||||||
|
{
|
||||||
|
StatusCode = StatusCodes.Status409Conflict
|
||||||
|
};
|
||||||
|
context.ExceptionHandled = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RubroPadreInactivoException rubroPadreEx:
|
||||||
|
context.Result = new ObjectResult(new
|
||||||
|
{
|
||||||
|
error = "rubro_padre_inactivo",
|
||||||
|
message = rubroPadreEx.Message
|
||||||
|
})
|
||||||
|
{
|
||||||
|
StatusCode = StatusCodes.Status400BadRequest
|
||||||
|
};
|
||||||
|
context.ExceptionHandled = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RubroMaxDepthExceededException rubroDepthEx:
|
||||||
|
context.Result = new ObjectResult(new
|
||||||
|
{
|
||||||
|
error = "rubro_max_depth_exceeded",
|
||||||
|
message = rubroDepthEx.Message
|
||||||
|
})
|
||||||
|
{
|
||||||
|
StatusCode = StatusCodes.Status422UnprocessableEntity
|
||||||
|
};
|
||||||
|
context.ExceptionHandled = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RubroCycleDetectedException rubroCycleEx:
|
||||||
|
context.Result = new ObjectResult(new
|
||||||
|
{
|
||||||
|
error = "rubro_cycle_detected",
|
||||||
|
message = rubroCycleEx.Message
|
||||||
|
})
|
||||||
|
{
|
||||||
|
StatusCode = StatusCodes.Status400BadRequest
|
||||||
|
};
|
||||||
|
context.ExceptionHandled = true;
|
||||||
|
break;
|
||||||
|
|
||||||
// ADM-001: Medio exceptions
|
// ADM-001: Medio exceptions
|
||||||
case MedioCodigoDuplicadoException medioCodDupEx:
|
case MedioCodigoDuplicadoException medioCodDupEx:
|
||||||
context.Result = new ObjectResult(new
|
context.Result = new ObjectResult(new
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
using FluentAssertions;
|
||||||
|
using SIGCM2.Domain.Exceptions;
|
||||||
|
|
||||||
|
namespace SIGCM2.Application.Tests.Domain.Rubros;
|
||||||
|
|
||||||
|
public class RubroExceptionsTests
|
||||||
|
{
|
||||||
|
// ── RubroNotFoundException ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RubroNotFoundException_ContainsId()
|
||||||
|
{
|
||||||
|
var ex = new RubroNotFoundException(42);
|
||||||
|
|
||||||
|
ex.Id.Should().Be(42);
|
||||||
|
ex.Message.Should().Contain("42");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RubroNotFoundException_InheritsFromDomainException()
|
||||||
|
{
|
||||||
|
var ex = new RubroNotFoundException(1);
|
||||||
|
|
||||||
|
ex.Should().BeAssignableTo<DomainException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── RubroNombreDuplicadoEnPadreException ─────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RubroNombreDuplicadoEnPadreException_ContainsNombreAndParentId()
|
||||||
|
{
|
||||||
|
var ex = new RubroNombreDuplicadoEnPadreException("Autos", parentId: 5);
|
||||||
|
|
||||||
|
ex.Nombre.Should().Be("Autos");
|
||||||
|
ex.ParentId.Should().Be(5);
|
||||||
|
ex.Message.Should().Contain("Autos");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RubroNombreDuplicadoEnPadreException_WithNullParent_IsValid()
|
||||||
|
{
|
||||||
|
var ex = new RubroNombreDuplicadoEnPadreException("Autos", parentId: null);
|
||||||
|
|
||||||
|
ex.Nombre.Should().Be("Autos");
|
||||||
|
ex.ParentId.Should().BeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RubroNombreDuplicadoEnPadreException_InheritsFromDomainException()
|
||||||
|
{
|
||||||
|
var ex = new RubroNombreDuplicadoEnPadreException("x", null);
|
||||||
|
|
||||||
|
ex.Should().BeAssignableTo<DomainException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── RubroMaxDepthExceededException ───────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RubroMaxDepthExceededException_ContainsDepthInfo()
|
||||||
|
{
|
||||||
|
var ex = new RubroMaxDepthExceededException(intentada: 11, max: 10);
|
||||||
|
|
||||||
|
ex.Intentada.Should().Be(11);
|
||||||
|
ex.Max.Should().Be(10);
|
||||||
|
ex.Message.Should().Contain("11");
|
||||||
|
ex.Message.Should().Contain("10");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RubroMaxDepthExceededException_InheritsFromDomainException()
|
||||||
|
{
|
||||||
|
var ex = new RubroMaxDepthExceededException(11, 10);
|
||||||
|
|
||||||
|
ex.Should().BeAssignableTo<DomainException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── RubroCycleDetectedException ──────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RubroCycleDetectedException_ContainsRubroIdAndIntendedParentId()
|
||||||
|
{
|
||||||
|
var ex = new RubroCycleDetectedException(rubroId: 5, nuevoParentId: 10);
|
||||||
|
|
||||||
|
ex.RubroId.Should().Be(5);
|
||||||
|
ex.NuevoParentId.Should().Be(10);
|
||||||
|
ex.Message.Should().Contain("5");
|
||||||
|
ex.Message.Should().Contain("10");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RubroCycleDetectedException_InheritsFromDomainException()
|
||||||
|
{
|
||||||
|
var ex = new RubroCycleDetectedException(1, 2);
|
||||||
|
|
||||||
|
ex.Should().BeAssignableTo<DomainException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── RubroTieneHijosActivosException ─────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RubroTieneHijosActivosException_ContainsIdAndCount()
|
||||||
|
{
|
||||||
|
var ex = new RubroTieneHijosActivosException(id: 7, count: 3);
|
||||||
|
|
||||||
|
ex.Id.Should().Be(7);
|
||||||
|
ex.Count.Should().Be(3);
|
||||||
|
ex.Message.Should().Contain("3");
|
||||||
|
ex.Message.Should().Contain("subrubros");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RubroTieneHijosActivosException_InheritsFromDomainException()
|
||||||
|
{
|
||||||
|
var ex = new RubroTieneHijosActivosException(1, 2);
|
||||||
|
|
||||||
|
ex.Should().BeAssignableTo<DomainException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── RubroPadreInactivoException ──────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RubroPadreInactivoException_ContainsParentId()
|
||||||
|
{
|
||||||
|
var ex = new RubroPadreInactivoException(parentId: 9);
|
||||||
|
|
||||||
|
ex.ParentId.Should().Be(9);
|
||||||
|
ex.Message.Should().Contain("9");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RubroPadreInactivoException_InheritsFromDomainException()
|
||||||
|
{
|
||||||
|
var ex = new RubroPadreInactivoException(1);
|
||||||
|
|
||||||
|
ex.Should().BeAssignableTo<DomainException>();
|
||||||
|
}
|
||||||
|
}
|
||||||
171
tests/SIGCM2.Application.Tests/Domain/Rubros/RubroTests.cs
Normal file
171
tests/SIGCM2.Application.Tests/Domain/Rubros/RubroTests.cs
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
using FluentAssertions;
|
||||||
|
using Microsoft.Extensions.Time.Testing;
|
||||||
|
using SIGCM2.Domain.Entities;
|
||||||
|
|
||||||
|
namespace SIGCM2.Application.Tests.Domain.Rubros;
|
||||||
|
|
||||||
|
public class RubroTests
|
||||||
|
{
|
||||||
|
private static readonly FakeTimeProvider FakeTime = new(new DateTimeOffset(2026, 4, 18, 12, 0, 0, TimeSpan.Zero));
|
||||||
|
|
||||||
|
// ── ForCreation: happy path ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Create_con_datos_validos_crea_rubro_activo_con_orden_cero_como_default()
|
||||||
|
{
|
||||||
|
var rubro = Rubro.ForCreation("Autos", parentId: null, orden: 0, tarifarioBaseId: null, FakeTime);
|
||||||
|
|
||||||
|
rubro.Nombre.Should().Be("Autos");
|
||||||
|
rubro.ParentId.Should().BeNull();
|
||||||
|
rubro.Orden.Should().Be(0);
|
||||||
|
rubro.Activo.Should().BeTrue();
|
||||||
|
rubro.TarifarioBaseId.Should().BeNull();
|
||||||
|
rubro.Id.Should().Be(0);
|
||||||
|
rubro.FechaCreacion.Should().Be(FakeTime.GetUtcNow().UtcDateTime);
|
||||||
|
rubro.FechaModificacion.Should().BeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Create_root_con_parentId_null_es_valido()
|
||||||
|
{
|
||||||
|
var rubro = Rubro.ForCreation("Autos", parentId: null, orden: 0, tarifarioBaseId: null, FakeTime);
|
||||||
|
|
||||||
|
rubro.ParentId.Should().BeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ForCreation: validations ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Create_con_nombre_vacio_lanza_ArgumentException()
|
||||||
|
{
|
||||||
|
var act = () => Rubro.ForCreation("", parentId: null, orden: 0, tarifarioBaseId: null, FakeTime);
|
||||||
|
|
||||||
|
act.Should().Throw<ArgumentException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Create_con_nombre_solo_whitespace_lanza_ArgumentException()
|
||||||
|
{
|
||||||
|
var act = () => Rubro.ForCreation(" ", parentId: null, orden: 0, tarifarioBaseId: null, FakeTime);
|
||||||
|
|
||||||
|
act.Should().Throw<ArgumentException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Create_con_nombre_excediendo_200_chars_lanza_ArgumentException()
|
||||||
|
{
|
||||||
|
var nombre = new string('A', 201);
|
||||||
|
|
||||||
|
var act = () => Rubro.ForCreation(nombre, parentId: null, orden: 0, tarifarioBaseId: null, FakeTime);
|
||||||
|
|
||||||
|
act.Should().Throw<ArgumentException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Create_con_parentId_menor_o_igual_a_cero_lanza_ArgumentException()
|
||||||
|
{
|
||||||
|
var act = () => Rubro.ForCreation("Autos", parentId: 0, orden: 0, tarifarioBaseId: null, FakeTime);
|
||||||
|
|
||||||
|
act.Should().Throw<ArgumentException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Create_con_tarifarioBaseId_menor_a_cero_lanza_ArgumentException()
|
||||||
|
{
|
||||||
|
var act = () => Rubro.ForCreation("Autos", parentId: null, orden: 0, tarifarioBaseId: -1, FakeTime);
|
||||||
|
|
||||||
|
act.Should().Throw<ArgumentException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── WithRenamed ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Rename_con_nombre_valido_devuelve_nueva_instancia_con_FechaModificacion()
|
||||||
|
{
|
||||||
|
var rubro = Rubro.ForCreation("Autos", parentId: null, orden: 0, tarifarioBaseId: null, FakeTime);
|
||||||
|
var laterTime = new FakeTimeProvider(new DateTimeOffset(2026, 4, 18, 14, 0, 0, TimeSpan.Zero));
|
||||||
|
|
||||||
|
var renamed = rubro.WithRenamed("Vehiculos", laterTime);
|
||||||
|
|
||||||
|
renamed.Nombre.Should().Be("Vehiculos");
|
||||||
|
renamed.FechaModificacion.Should().Be(laterTime.GetUtcNow().UtcDateTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Rename_con_nombre_invalido_lanza_ArgumentException()
|
||||||
|
{
|
||||||
|
var rubro = Rubro.ForCreation("Autos", parentId: null, orden: 0, tarifarioBaseId: null, FakeTime);
|
||||||
|
|
||||||
|
var act = () => rubro.WithRenamed("", FakeTime);
|
||||||
|
|
||||||
|
act.Should().Throw<ArgumentException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Rename_no_muta_la_instancia_original()
|
||||||
|
{
|
||||||
|
var rubro = Rubro.ForCreation("Autos", parentId: null, orden: 0, tarifarioBaseId: null, FakeTime);
|
||||||
|
|
||||||
|
rubro.WithRenamed("Vehiculos", FakeTime);
|
||||||
|
|
||||||
|
rubro.Nombre.Should().Be("Autos");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── WithMoved ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Move_a_nuevo_parent_devuelve_nueva_instancia_con_parentId_actualizado()
|
||||||
|
{
|
||||||
|
var rubro = Rubro.ForCreation("Autos", parentId: null, orden: 0, tarifarioBaseId: null, FakeTime);
|
||||||
|
|
||||||
|
var moved = rubro.WithMoved(nuevoParentId: 5, nuevoOrden: 2, FakeTime);
|
||||||
|
|
||||||
|
moved.ParentId.Should().Be(5);
|
||||||
|
moved.Orden.Should().Be(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Move_a_root_permite_parentId_null()
|
||||||
|
{
|
||||||
|
var rubro = Rubro.ForCreation("Autos", parentId: 3, orden: 0, tarifarioBaseId: null, FakeTime);
|
||||||
|
|
||||||
|
var moved = rubro.WithMoved(nuevoParentId: null, nuevoOrden: 0, FakeTime);
|
||||||
|
|
||||||
|
moved.ParentId.Should().BeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Move_no_muta_la_instancia_original()
|
||||||
|
{
|
||||||
|
var rubro = Rubro.ForCreation("Autos", parentId: null, orden: 0, tarifarioBaseId: null, FakeTime);
|
||||||
|
|
||||||
|
rubro.WithMoved(nuevoParentId: 5, nuevoOrden: 1, FakeTime);
|
||||||
|
|
||||||
|
rubro.ParentId.Should().BeNull();
|
||||||
|
rubro.Orden.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── WithActivo (Deactivate / Reactivate) ────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Deactivate_flip_Activo_a_false()
|
||||||
|
{
|
||||||
|
var rubro = Rubro.ForCreation("Autos", parentId: null, orden: 0, tarifarioBaseId: null, FakeTime);
|
||||||
|
|
||||||
|
var deactivated = rubro.WithActivo(false, FakeTime);
|
||||||
|
|
||||||
|
deactivated.Activo.Should().BeFalse();
|
||||||
|
deactivated.FechaModificacion.Should().Be(FakeTime.GetUtcNow().UtcDateTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Reactivate_flip_Activo_a_true()
|
||||||
|
{
|
||||||
|
var rubro = Rubro.ForCreation("Autos", parentId: null, orden: 0, tarifarioBaseId: null, FakeTime);
|
||||||
|
var deactivated = rubro.WithActivo(false, FakeTime);
|
||||||
|
|
||||||
|
var reactivated = deactivated.WithActivo(true, FakeTime);
|
||||||
|
|
||||||
|
reactivated.Activo.Should().BeTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user