using NSubstitute; using SIGCM2.Application.Abstractions.Persistence; using SIGCM2.Application.Audit; using SIGCM2.Application.PuntosDeVenta.Create; using SIGCM2.Domain.Entities; using SIGCM2.Domain.Exceptions; namespace SIGCM2.Application.Tests.PuntosDeVenta.Create; public class CreatePuntoDeVentaCommandHandlerTests { private readonly IPuntoDeVentaRepository _repo = Substitute.For(); private readonly IMedioRepository _medioRepo = Substitute.For(); private readonly IAuditLogger _audit = Substitute.For(); private readonly CreatePuntoDeVentaCommandHandler _handler; private static Medio MakeMedio(int id = 5, bool activo = true) => new(id, "COD" + id, "Medio " + id, TipoMedio.Diario, null, activo, DateTime.UtcNow, null); private static CreatePuntoDeVentaCommand ValidCommand() => new( MedioId: 5, NumeroAFIP: 1, Nombre: "PdV Central", Descripcion: null); public CreatePuntoDeVentaCommandHandlerTests() { _handler = new CreatePuntoDeVentaCommandHandler(_repo, _medioRepo, _audit); _medioRepo.GetByIdAsync(5, Arg.Any()).Returns(MakeMedio(5)); _repo.ExistsByNumeroAFIPInMedioAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()).Returns(false); _repo.AddAsync(Arg.Any(), Arg.Any()).Returns(10); } // ── medio not found → throws ───────────────────────────────────────────── [Fact] public async Task Handle_MedioNotFound_ThrowsMedioNotFoundException() { _medioRepo.GetByIdAsync(5, Arg.Any()).Returns((Medio?)null); await Assert.ThrowsAsync( () => _handler.Handle(ValidCommand())); } // ── medio inactivo → throws ────────────────────────────────────────────── [Fact] public async Task Handle_MedioInactivo_ThrowsMedioInactivoException() { _medioRepo.GetByIdAsync(5, Arg.Any()).Returns(MakeMedio(5, false)); await Assert.ThrowsAsync( () => _handler.Handle(ValidCommand())); } // ── NumeroAFIP duplicado → throws ──────────────────────────────────────── [Fact] public async Task Handle_NumeroAFIPDuplicado_ThrowsNumeroAFIPDuplicadoException() { _repo.ExistsByNumeroAFIPInMedioAsync(5, 1, null, Arg.Any()).Returns(true); await Assert.ThrowsAsync( () => _handler.Handle(ValidCommand())); } [Fact] public async Task Handle_NumeroAFIPDuplicado_DoesNotCallAddAsync() { _repo.ExistsByNumeroAFIPInMedioAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()).Returns(true); try { await _handler.Handle(ValidCommand()); } catch (NumeroAFIPDuplicadoException) { } await _repo.DidNotReceive().AddAsync(Arg.Any(), Arg.Any()); } // ── happy path ─────────────────────────────────────────────────────────── [Fact] public async Task Handle_HappyPath_ReturnsDtoWithIdFromRepository() { var result = await _handler.Handle(ValidCommand()); Assert.Equal(10, result.Id); } [Fact] public async Task Handle_HappyPath_DtoContainsCorrectFields() { var result = await _handler.Handle(ValidCommand()); Assert.Equal(5, result.MedioId); Assert.Equal(1, result.NumeroAFIP); Assert.Equal("PdV Central", result.Nombre); Assert.True(result.Activo); } [Fact] public async Task Handle_HappyPath_CallsAuditWithCreateAction() { await _handler.Handle(ValidCommand()); await _audit.Received(1).LogAsync( action: "punto_de_venta.create", targetType: "PuntoDeVenta", targetId: "10", metadata: Arg.Any(), ct: Arg.Any()); } // ── audit fail-closed ──────────────────────────────────────────────────── [Fact] public async Task Handle_AuditLoggerThrows_ExceptionBubblesUpAndAddNotCommitted() { _audit.LogAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(Task.FromException(new InvalidOperationException("audit fail"))); await Assert.ThrowsAsync( () => _handler.Handle(ValidCommand())); } }