using System.Transactions; using Microsoft.Extensions.Options; using SIGCM2.Application.Abstractions; using SIGCM2.Application.Abstractions.Persistence; using SIGCM2.Application.Audit; using SIGCM2.Domain.Exceptions; namespace SIGCM2.Application.Rubros.Move; public sealed class MoveRubroCommandHandler : ICommandHandler { private readonly IRubroRepository _repo; private readonly IAuditLogger _audit; private readonly TimeProvider _timeProvider; private readonly RubrosOptions _options; public MoveRubroCommandHandler( IRubroRepository repo, IAuditLogger audit, TimeProvider timeProvider, IOptions options) { _repo = repo; _audit = audit; _timeProvider = timeProvider; _options = options.Value; } public async Task Handle(MoveRubroCommand command) { var target = await _repo.GetByIdAsync(command.Id) ?? throw new RubroNotFoundException(command.Id); var anteriorParentId = target.ParentId; // Cycle check: nuevoParentId must not be in descendants of target if (command.NuevoParentId.HasValue) { var descendants = await _repo.GetDescendantsAsync(command.Id); if (descendants.Any(d => d.Id == command.NuevoParentId.Value)) throw new RubroCycleDetectedException(command.Id, command.NuevoParentId.Value); // New parent must exist and be active var newParent = await _repo.GetByIdAsync(command.NuevoParentId.Value) ?? throw new RubroNotFoundException(command.NuevoParentId.Value); if (!newParent.Activo) throw new RubroPadreInactivoException(command.NuevoParentId.Value); // Depth check var parentDepth = await _repo.GetDepthAsync(command.NuevoParentId); var newDepth = parentDepth + 1; if (newDepth > _options.MaxDepth) throw new RubroMaxDepthExceededException(newDepth, _options.MaxDepth); } // Duplicate name check under new parent (excluding self) var exists = await _repo.ExistsByNombreUnderParentAsync(command.NuevoParentId, target.Nombre, excludeId: command.Id); if (exists) throw new RubroNombreDuplicadoEnPadreException(target.Nombre, command.NuevoParentId); var moved = target.WithMoved(command.NuevoParentId, command.NuevoOrden, _timeProvider); using var tx = new TransactionScope( TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted }, TransactionScopeAsyncFlowOption.Enabled); await _repo.UpdateAsync(moved); await _audit.LogAsync( action: "rubro.moved", targetType: "Rubro", targetId: command.Id.ToString(), metadata: new { anteriorParentId, nuevoParentId = command.NuevoParentId, anteriorOrden = target.Orden, nuevoOrden = command.NuevoOrden, }); tx.Complete(); return new RubroMovedDto( Id: moved.Id, Nombre: moved.Nombre, ParentId: moved.ParentId, Orden: moved.Orden, Activo: moved.Activo); } }