using FluentAssertions; using Microsoft.Extensions.Time.Testing; using NSubstitute; using NSubstitute.ExceptionExtensions; using SIGCM2.Application.Abstractions.Persistence; using SIGCM2.Application.Audit; using SIGCM2.Application.Pricing.ChargeableChars.Deactivate; using SIGCM2.Domain.Pricing.ChargeableChars; namespace SIGCM2.Application.Tests.Pricing.ChargeableChars; /// /// PRC-001 — DeactivateChargeableCharConfigCommandHandler tests. /// Covers: happy path, not-found, audit emit, audit fail → rollback. /// public class DeactivateChargeableCharConfigHandlerTests { private readonly FakeTimeProvider _time = new(new DateTimeOffset(2026, 4, 20, 12, 0, 0, TimeSpan.Zero)); private readonly IChargeableCharConfigRepository _repo = Substitute.For(); private readonly IAuditLogger _audit = Substitute.For(); private readonly DeactivateChargeableCharConfigCommandHandler _handler; private static readonly DateOnly Today = new(2026, 4, 20); private static ChargeableCharConfig ActiveConfig() => ChargeableCharConfig.Rehydrate(1L, null, "$", ChargeableCharCategories.Currency, 1.0m, Today, null, true); public DeactivateChargeableCharConfigHandlerTests() { _repo.GetByIdAsync(1L, Arg.Any()) .Returns(ActiveConfig()); _handler = new DeactivateChargeableCharConfigCommandHandler(_repo, _audit, _time); } private static DeactivateChargeableCharConfigCommand ValidCmd() => new(Id: 1L); // ── Happy path ────────────────────────────────────────────────────────────── [Fact] public async Task Handle_HappyPath_ReturnsDeactivateResponse() { var result = await _handler.Handle(ValidCmd()); result.Should().NotBeNull(); result.Id.Should().Be(1L); result.ValidTo.Should().Be(Today); } [Fact] public async Task Handle_HappyPath_CallsDeactivateAsync() { await _handler.Handle(ValidCmd()); await _repo.Received(1).DeactivateAsync(1L, Today, Arg.Any()); } [Fact] public async Task Handle_HappyPath_EmitsAuditDeactivate() { await _handler.Handle(ValidCmd()); await _audit.Received(1).LogAsync( action: "tasacion.chargeable_char.deactivate", targetType: "ChargeableCharConfig", targetId: "1", metadata: Arg.Any(), ct: Arg.Any()); } // ── Not found ─────────────────────────────────────────────────────────────── [Fact] public async Task Handle_ConfigNotFound_ThrowsKeyNotFoundException() { _repo.GetByIdAsync(99L, Arg.Any()) .Returns((ChargeableCharConfig?)null); var act = async () => await _handler.Handle(ValidCmd() with { Id = 99L }); await act.Should().ThrowAsync(); } // ── Audit fail → rollback ─────────────────────────────────────────────────── [Fact] public async Task Handle_AuditThrows_ExceptionPropagates() { _audit.LogAsync( Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .ThrowsAsync(new InvalidOperationException("Audit error")); var act = async () => await _handler.Handle(ValidCmd()); await act.Should().ThrowAsync() .WithMessage("Audit error"); } [Fact] public async Task Handle_AuditThrows_DeactivateWasCalled_TransactionNotCompleted() { _audit.LogAsync( Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .ThrowsAsync(new InvalidOperationException("Audit error")); var act = async () => await _handler.Handle(ValidCmd()); await act.Should().ThrowAsync(); // Repo.DeactivateAsync was called but TX not completed (exception propagated) await _repo.Received(1).DeactivateAsync(1L, Today, Arg.Any()); } }