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.
120 lines
4.5 KiB
C#
120 lines
4.5 KiB
C#
using FluentAssertions;
|
|
using NSubstitute;
|
|
using SIGCM2.Application.Abstractions.Persistence;
|
|
using SIGCM2.Application.Common;
|
|
using SIGCM2.Application.Pricing.ChargeableChars;
|
|
using SIGCM2.Application.Pricing.ChargeableChars.List;
|
|
using SIGCM2.Domain.Pricing.ChargeableChars;
|
|
|
|
namespace SIGCM2.Application.Tests.Pricing.ChargeableChars;
|
|
|
|
/// <summary>
|
|
/// PRC-001 — ListChargeableCharConfigQueryHandler tests.
|
|
/// Covers: happy path with items, empty page, projection to DTO, pagination metadata.
|
|
/// </summary>
|
|
public class ListChargeableCharConfigHandlerTests
|
|
{
|
|
private readonly IChargeableCharConfigRepository _repo = Substitute.For<IChargeableCharConfigRepository>();
|
|
private readonly ListChargeableCharConfigQueryHandler _handler;
|
|
|
|
private static readonly DateOnly Today = new(2026, 4, 20);
|
|
|
|
private static ChargeableCharConfig MakeConfig(long id, string symbol, decimal price) =>
|
|
ChargeableCharConfig.Rehydrate(id, null, symbol, ChargeableCharCategories.Currency, price, Today, null, true);
|
|
|
|
public ListChargeableCharConfigHandlerTests()
|
|
{
|
|
_handler = new ListChargeableCharConfigQueryHandler(_repo);
|
|
}
|
|
|
|
// ── Happy path ──────────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task Handle_WithItems_ReturnsPagedDtos()
|
|
{
|
|
var items = new List<ChargeableCharConfig>
|
|
{
|
|
MakeConfig(1, "$", 1.0m),
|
|
MakeConfig(2, "%", 0.5m),
|
|
};
|
|
|
|
_repo.ListAsync(null, true, 0, 20, Arg.Any<CancellationToken>())
|
|
.Returns(items);
|
|
_repo.CountAsync(null, true, Arg.Any<CancellationToken>())
|
|
.Returns(2);
|
|
|
|
var query = new ListChargeableCharConfigQuery(ProductTypeId: null, ActiveOnly: true, Page: 1, PageSize: 20);
|
|
var result = await _handler.Handle(query);
|
|
|
|
result.Items.Should().HaveCount(2);
|
|
result.Total.Should().Be(2);
|
|
result.Page.Should().Be(1);
|
|
result.PageSize.Should().Be(20);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Handle_WithItems_ProjectsToDto()
|
|
{
|
|
var items = new List<ChargeableCharConfig> { MakeConfig(5, "$", 1.5m) };
|
|
|
|
_repo.ListAsync(Arg.Any<long?>(), Arg.Any<bool>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
|
.Returns(items);
|
|
_repo.CountAsync(Arg.Any<long?>(), Arg.Any<bool>(), Arg.Any<CancellationToken>())
|
|
.Returns(1);
|
|
|
|
var query = new ListChargeableCharConfigQuery(null, true, 1, 20);
|
|
var result = await _handler.Handle(query);
|
|
|
|
var dto = result.Items[0];
|
|
dto.Id.Should().Be(5);
|
|
dto.Symbol.Should().Be("$");
|
|
dto.PricePerUnit.Should().Be(1.5m);
|
|
dto.ValidFrom.Should().Be(Today);
|
|
dto.IsActive.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Handle_EmptyPage_ReturnsEmptyList()
|
|
{
|
|
_repo.ListAsync(Arg.Any<long?>(), Arg.Any<bool>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
|
.Returns(new List<ChargeableCharConfig>());
|
|
_repo.CountAsync(Arg.Any<long?>(), Arg.Any<bool>(), Arg.Any<CancellationToken>())
|
|
.Returns(0);
|
|
|
|
var query = new ListChargeableCharConfigQuery(null, true, 1, 20);
|
|
var result = await _handler.Handle(query);
|
|
|
|
result.Items.Should().BeEmpty();
|
|
result.Total.Should().Be(0);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Handle_SkipIsComputed_FromPageAndPageSize()
|
|
{
|
|
// Page 3, PageSize 10 → skip = 20
|
|
_repo.ListAsync(null, false, 20, 10, Arg.Any<CancellationToken>())
|
|
.Returns(new List<ChargeableCharConfig>());
|
|
_repo.CountAsync(null, false, Arg.Any<CancellationToken>())
|
|
.Returns(0);
|
|
|
|
var query = new ListChargeableCharConfigQuery(null, false, 3, 10);
|
|
await _handler.Handle(query);
|
|
|
|
await _repo.Received(1).ListAsync(null, false, 20, 10, Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Handle_FiltersByProductTypeId_WhenProvided()
|
|
{
|
|
_repo.ListAsync(7L, true, 0, 20, Arg.Any<CancellationToken>())
|
|
.Returns(new List<ChargeableCharConfig>());
|
|
_repo.CountAsync(7L, true, Arg.Any<CancellationToken>())
|
|
.Returns(0);
|
|
|
|
var query = new ListChargeableCharConfigQuery(7L, true, 1, 20);
|
|
await _handler.Handle(query);
|
|
|
|
await _repo.Received(1).ListAsync(7L, true, 0, 20, Arg.Any<CancellationToken>());
|
|
}
|
|
}
|