From 4c9b7eabaf7778a681b8698b8b1fe3708eec30a5 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Sat, 18 Apr 2026 19:17:33 -0300 Subject: [PATCH] feat(domain): Rubro entity + domain exceptions (CAT-001) --- src/api/SIGCM2.Api/Filters/ExceptionFilter.cs | 73 ++++++++ src/api/SIGCM2.Domain/Entities/Rubro.cs | 139 ++++++++++++++ .../Exceptions/RubroCycleDetectedException.cs | 17 ++ .../RubroMaxDepthExceededException.cs | 17 ++ .../RubroNombreDuplicadoEnPadreException.cs | 19 ++ .../Exceptions/RubroNotFoundException.cs | 15 ++ .../Exceptions/RubroPadreInactivoException.cs | 15 ++ .../RubroTieneHijosActivosException.cs | 17 ++ .../Domain/Rubros/RubroExceptionsTests.cs | 137 ++++++++++++++ .../Domain/Rubros/RubroTests.cs | 171 ++++++++++++++++++ 10 files changed, 620 insertions(+) create mode 100644 src/api/SIGCM2.Domain/Entities/Rubro.cs create mode 100644 src/api/SIGCM2.Domain/Exceptions/RubroCycleDetectedException.cs create mode 100644 src/api/SIGCM2.Domain/Exceptions/RubroMaxDepthExceededException.cs create mode 100644 src/api/SIGCM2.Domain/Exceptions/RubroNombreDuplicadoEnPadreException.cs create mode 100644 src/api/SIGCM2.Domain/Exceptions/RubroNotFoundException.cs create mode 100644 src/api/SIGCM2.Domain/Exceptions/RubroPadreInactivoException.cs create mode 100644 src/api/SIGCM2.Domain/Exceptions/RubroTieneHijosActivosException.cs create mode 100644 tests/SIGCM2.Application.Tests/Domain/Rubros/RubroExceptionsTests.cs create mode 100644 tests/SIGCM2.Application.Tests/Domain/Rubros/RubroTests.cs diff --git a/src/api/SIGCM2.Api/Filters/ExceptionFilter.cs b/src/api/SIGCM2.Api/Filters/ExceptionFilter.cs index bf0f054..f43eac9 100644 --- a/src/api/SIGCM2.Api/Filters/ExceptionFilter.cs +++ b/src/api/SIGCM2.Api/Filters/ExceptionFilter.cs @@ -169,6 +169,79 @@ public sealed class ExceptionFilter : IExceptionFilter context.ExceptionHandled = true; 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 case MedioCodigoDuplicadoException medioCodDupEx: context.Result = new ObjectResult(new diff --git a/src/api/SIGCM2.Domain/Entities/Rubro.cs b/src/api/SIGCM2.Domain/Entities/Rubro.cs new file mode 100644 index 0000000..582a7a0 --- /dev/null +++ b/src/api/SIGCM2.Domain/Entities/Rubro.cs @@ -0,0 +1,139 @@ +namespace SIGCM2.Domain.Entities; + +/// +/// Immutable N-ary tree node for the commercial catalog taxonomy. +/// Follows the same sealed-class + factory + with-methods pattern as Medio.cs. +/// +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; } + + /// + /// Full hydration constructor — used by the repository to reconstruct from DB rows. + /// + 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; + } + + /// + /// 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. + /// + 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); + } + + /// + /// Returns a new Rubro instance with an updated Nombre and FechaModificacion. + /// Does NOT mutate the current instance. + /// + 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); + } + + /// + /// Returns a new Rubro instance with updated ParentId and Orden. + /// Does NOT mutate the current instance. + /// + 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); + } + + /// + /// Returns a new Rubro instance with updated Activo flag. + /// Use Deactivate (false) or Reactivate (true). + /// Does NOT mutate the current instance. + /// + 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)); + } +} diff --git a/src/api/SIGCM2.Domain/Exceptions/RubroCycleDetectedException.cs b/src/api/SIGCM2.Domain/Exceptions/RubroCycleDetectedException.cs new file mode 100644 index 0000000..c86a387 --- /dev/null +++ b/src/api/SIGCM2.Domain/Exceptions/RubroCycleDetectedException.cs @@ -0,0 +1,17 @@ +namespace SIGCM2.Domain.Exceptions; + +/// +/// Thrown when moving a Rubro to one of its own descendants would create a cycle. → HTTP 400 +/// +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; + } +} diff --git a/src/api/SIGCM2.Domain/Exceptions/RubroMaxDepthExceededException.cs b/src/api/SIGCM2.Domain/Exceptions/RubroMaxDepthExceededException.cs new file mode 100644 index 0000000..d993955 --- /dev/null +++ b/src/api/SIGCM2.Domain/Exceptions/RubroMaxDepthExceededException.cs @@ -0,0 +1,17 @@ +namespace SIGCM2.Domain.Exceptions; + +/// +/// Thrown when creating or moving a Rubro would exceed the configured maximum tree depth. → HTTP 422 +/// +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; + } +} diff --git a/src/api/SIGCM2.Domain/Exceptions/RubroNombreDuplicadoEnPadreException.cs b/src/api/SIGCM2.Domain/Exceptions/RubroNombreDuplicadoEnPadreException.cs new file mode 100644 index 0000000..1b0b284 --- /dev/null +++ b/src/api/SIGCM2.Domain/Exceptions/RubroNombreDuplicadoEnPadreException.cs @@ -0,0 +1,19 @@ +namespace SIGCM2.Domain.Exceptions; + +/// +/// Thrown when a Rubro with the same Nombre (CI) already exists under the same parent. → HTTP 409 +/// +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; + } +} diff --git a/src/api/SIGCM2.Domain/Exceptions/RubroNotFoundException.cs b/src/api/SIGCM2.Domain/Exceptions/RubroNotFoundException.cs new file mode 100644 index 0000000..c1b4f28 --- /dev/null +++ b/src/api/SIGCM2.Domain/Exceptions/RubroNotFoundException.cs @@ -0,0 +1,15 @@ +namespace SIGCM2.Domain.Exceptions; + +/// +/// Thrown when a requested Rubro does not exist in the system. → HTTP 404 +/// +public sealed class RubroNotFoundException : DomainException +{ + public int Id { get; } + + public RubroNotFoundException(int id) + : base($"El rubro con id '{id}' no existe.") + { + Id = id; + } +} diff --git a/src/api/SIGCM2.Domain/Exceptions/RubroPadreInactivoException.cs b/src/api/SIGCM2.Domain/Exceptions/RubroPadreInactivoException.cs new file mode 100644 index 0000000..08bf642 --- /dev/null +++ b/src/api/SIGCM2.Domain/Exceptions/RubroPadreInactivoException.cs @@ -0,0 +1,15 @@ +namespace SIGCM2.Domain.Exceptions; + +/// +/// Thrown when attempting to create or move a Rubro under an inactive parent. → HTTP 400 +/// +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; + } +} diff --git a/src/api/SIGCM2.Domain/Exceptions/RubroTieneHijosActivosException.cs b/src/api/SIGCM2.Domain/Exceptions/RubroTieneHijosActivosException.cs new file mode 100644 index 0000000..4df437f --- /dev/null +++ b/src/api/SIGCM2.Domain/Exceptions/RubroTieneHijosActivosException.cs @@ -0,0 +1,17 @@ +namespace SIGCM2.Domain.Exceptions; + +/// +/// Thrown when attempting to soft-delete a Rubro that still has active children. → HTTP 409 +/// +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; + } +} diff --git a/tests/SIGCM2.Application.Tests/Domain/Rubros/RubroExceptionsTests.cs b/tests/SIGCM2.Application.Tests/Domain/Rubros/RubroExceptionsTests.cs new file mode 100644 index 0000000..e55a283 --- /dev/null +++ b/tests/SIGCM2.Application.Tests/Domain/Rubros/RubroExceptionsTests.cs @@ -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(); + } + + // ── 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(); + } + + // ── 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(); + } + + // ── 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(); + } + + // ── 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(); + } + + // ── 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(); + } +} diff --git a/tests/SIGCM2.Application.Tests/Domain/Rubros/RubroTests.cs b/tests/SIGCM2.Application.Tests/Domain/Rubros/RubroTests.cs new file mode 100644 index 0000000..2688bfd --- /dev/null +++ b/tests/SIGCM2.Application.Tests/Domain/Rubros/RubroTests.cs @@ -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(); + } + + [Fact] + public void Create_con_nombre_solo_whitespace_lanza_ArgumentException() + { + var act = () => Rubro.ForCreation(" ", parentId: null, orden: 0, tarifarioBaseId: null, FakeTime); + + act.Should().Throw(); + } + + [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(); + } + + [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(); + } + + [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(); + } + + // ── 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(); + } + + [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(); + } +}