refactor+feat(backend): ChargeableCharConfig por ProductType + Reactivate + Delete endpoints (PRC-001)
Part A — MedioId → ProductTypeId rename across all C# layers:
Domain, Application, Infrastructure, API, all test projects.
Solution was non-compilable after BD refactor (5c1675e); now compiles clean (0 errors).
Part B — PATCH /api/v1/admin/chargeable-chars/{id}/reactivate:
ReactivateChargeableCharConfigCommand/Handler, SP guard maps 50410/50411/50412
→ ChargeableCharConfigReactivationNotAllowedException(Reason) → HTTP 409.
Part C — DELETE /api/v1/admin/chargeable-chars/{id}:
DeleteChargeableCharConfigCommand/Handler, physical DELETE on SYSTEM_VERSIONED table.
KeyNotFoundException → 404 via ExceptionFilter.
Tests: +30 unit tests (TDD RED→GREEN). All 1266 unit tests pass.
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
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.Delete;
|
||||
using SIGCM2.Domain.Pricing.ChargeableChars;
|
||||
|
||||
namespace SIGCM2.Application.Tests.Pricing.ChargeableChars;
|
||||
|
||||
/// <summary>
|
||||
/// PRC-001 — DeleteChargeableCharConfigCommandHandler tests.
|
||||
/// Strict TDD — RED written before implementation.
|
||||
/// Covers: happy path, not-found, audit emission, audit fail-closed.
|
||||
/// </summary>
|
||||
public class DeleteChargeableCharConfigHandlerTests
|
||||
{
|
||||
private readonly FakeTimeProvider _time = new(new DateTimeOffset(2026, 4, 20, 12, 0, 0, TimeSpan.Zero));
|
||||
private readonly IChargeableCharConfigRepository _repo = Substitute.For<IChargeableCharConfigRepository>();
|
||||
private readonly IAuditLogger _audit = Substitute.For<IAuditLogger>();
|
||||
private readonly DeleteChargeableCharConfigCommandHandler _handler;
|
||||
|
||||
private static ChargeableCharConfig SomeConfig() =>
|
||||
ChargeableCharConfig.Rehydrate(
|
||||
id: 1L, productTypeId: null, symbol: "$", category: ChargeableCharCategories.Currency,
|
||||
price: 1.5m, validFrom: new DateOnly(2026, 1, 1), validTo: null, isActive: true);
|
||||
|
||||
public DeleteChargeableCharConfigHandlerTests()
|
||||
{
|
||||
_repo.GetByIdAsync(1L, Arg.Any<CancellationToken>())
|
||||
.Returns(SomeConfig());
|
||||
|
||||
_handler = new DeleteChargeableCharConfigCommandHandler(_repo, _audit, _time);
|
||||
}
|
||||
|
||||
private static DeleteChargeableCharConfigCommand ValidCmd() => new(Id: 1L);
|
||||
|
||||
// ── Happy path ──────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_HappyPath_ReturnsResponse()
|
||||
{
|
||||
var result = await _handler.Handle(ValidCmd());
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result.Id.Should().Be(1L);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_HappyPath_CallsDeleteAsync()
|
||||
{
|
||||
await _handler.Handle(ValidCmd());
|
||||
|
||||
await _repo.Received(1).DeleteAsync(1L, Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_HappyPath_EmitsAuditDelete()
|
||||
{
|
||||
await _handler.Handle(ValidCmd());
|
||||
|
||||
await _audit.Received(1).LogAsync(
|
||||
action: "tasacion.chargeable_char.delete",
|
||||
targetType: "ChargeableCharConfig",
|
||||
targetId: "1",
|
||||
metadata: Arg.Any<object?>(),
|
||||
ct: Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
// ── Not found ───────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ConfigNotFound_ThrowsKeyNotFoundException()
|
||||
{
|
||||
_repo.GetByIdAsync(99L, Arg.Any<CancellationToken>())
|
||||
.Returns((ChargeableCharConfig?)null);
|
||||
|
||||
var act = async () => await _handler.Handle(new DeleteChargeableCharConfigCommand(Id: 99L));
|
||||
|
||||
await act.Should().ThrowAsync<KeyNotFoundException>();
|
||||
}
|
||||
|
||||
// ── Audit fail → rollback (fail-closed) ─────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_AuditThrows_ExceptionPropagates()
|
||||
{
|
||||
_audit.LogAsync(
|
||||
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
|
||||
Arg.Any<object?>(), Arg.Any<CancellationToken>())
|
||||
.ThrowsAsync(new InvalidOperationException("Audit down"));
|
||||
|
||||
var act = async () => await _handler.Handle(ValidCmd());
|
||||
|
||||
await act.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("Audit down");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_AuditThrows_DeleteWasCalled_TransactionNotCompleted()
|
||||
{
|
||||
_audit.LogAsync(
|
||||
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
|
||||
Arg.Any<object?>(), Arg.Any<CancellationToken>())
|
||||
.ThrowsAsync(new InvalidOperationException("Audit down"));
|
||||
|
||||
var act = async () => await _handler.Handle(ValidCmd());
|
||||
await act.Should().ThrowAsync<InvalidOperationException>();
|
||||
|
||||
await _repo.Received(1).DeleteAsync(1L, Arg.Any<CancellationToken>());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user