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:
2026-04-21 10:54:47 -03:00
parent 5c1675e59a
commit f7fb76219a
35 changed files with 1273 additions and 273 deletions

View File

@@ -26,57 +26,57 @@ public class ChargeableCharConfigServiceTests
private static ChargeableCharConfig GlobalConfig(string symbol, decimal price) =>
ChargeableCharConfig.Rehydrate(10L, null, symbol, ChargeableCharCategories.Currency, price, AsOf, null, true);
private static ChargeableCharConfig MedioConfig(long id, int medioId, string symbol, decimal price) =>
ChargeableCharConfig.Rehydrate(id, medioId, symbol, ChargeableCharCategories.Currency, price, AsOf, null, true);
private static ChargeableCharConfig ProductTypeConfig(long id, int productTypeId, string symbol, decimal price) =>
ChargeableCharConfig.Rehydrate(id, productTypeId, symbol, ChargeableCharCategories.Currency, price, AsOf, null, true);
// ── Global fallback ─────────────────────────────────────────────────────────
[Fact]
public async Task GetActiveConfig_NoPerMedio_ReturnsGlobalConfigs()
public async Task GetActiveConfig_NoPerProductType_ReturnsGlobalConfigs()
{
_repo.GetActiveForMedioAsync(1, AsOf, Arg.Any<CancellationToken>())
_repo.GetActiveForProductTypeAsync(1, AsOf, Arg.Any<CancellationToken>())
.Returns(new List<ChargeableCharConfig>
{
GlobalConfig("$", 1.0m),
GlobalConfig("%", 0.5m),
});
var result = await _service.GetActiveConfigForMedioAsync(1, AsOf, CancellationToken.None);
var result = await _service.GetActiveConfigForProductTypeAsync(1, AsOf, CancellationToken.None);
result.Should().ContainKey("$");
result["$"].PricePerUnit.Should().Be(1.0m);
result.Should().ContainKey("%");
}
// ── Per-medio wins over global ───────────────────────────────────────────────
// ── Per-ProductType wins over global ─────────────────────────────────────────
[Fact]
public async Task GetActiveConfig_PerMedioExists_OverridesGlobalForSameSymbol()
public async Task GetActiveConfig_PerProductTypeExists_OverridesGlobalForSameSymbol()
{
_repo.GetActiveForMedioAsync(5, AsOf, Arg.Any<CancellationToken>())
_repo.GetActiveForProductTypeAsync(5, AsOf, Arg.Any<CancellationToken>())
.Returns(new List<ChargeableCharConfig>
{
GlobalConfig("$", 1.0m), // global price = 1.0
MedioConfig(20L, 5, "$", 3.0m), // per-medio price = 3.0 → wins
GlobalConfig("%", 0.5m), // global only
GlobalConfig("$", 1.0m), // global price = 1.0
ProductTypeConfig(20L, 5, "$", 3.0m), // per-PT price = 3.0 → wins
GlobalConfig("%", 0.5m), // global only
});
var result = await _service.GetActiveConfigForMedioAsync(5, AsOf, CancellationToken.None);
var result = await _service.GetActiveConfigForProductTypeAsync(5, AsOf, CancellationToken.None);
result["$"].PricePerUnit.Should().Be(3.0m); // per-medio wins
result["$"].PricePerUnit.Should().Be(3.0m); // per-PT wins
result["%"].PricePerUnit.Should().Be(0.5m); // global only
}
[Fact]
public async Task GetActiveConfig_PerMedioExists_IncludesCorrectCategory()
public async Task GetActiveConfig_PerProductTypeExists_IncludesCorrectCategory()
{
_repo.GetActiveForMedioAsync(5, AsOf, Arg.Any<CancellationToken>())
_repo.GetActiveForProductTypeAsync(5, AsOf, Arg.Any<CancellationToken>())
.Returns(new List<ChargeableCharConfig>
{
MedioConfig(20L, 5, "$", 3.0m),
ProductTypeConfig(20L, 5, "$", 3.0m),
});
var result = await _service.GetActiveConfigForMedioAsync(5, AsOf, CancellationToken.None);
var result = await _service.GetActiveConfigForProductTypeAsync(5, AsOf, CancellationToken.None);
result["$"].Category.Should().Be(ChargeableCharCategories.Currency);
}
@@ -86,10 +86,10 @@ public class ChargeableCharConfigServiceTests
[Fact]
public async Task GetActiveConfig_NoConfigAtAll_ReturnsEmptyDictionary()
{
_repo.GetActiveForMedioAsync(1, AsOf, Arg.Any<CancellationToken>())
_repo.GetActiveForProductTypeAsync(1, AsOf, Arg.Any<CancellationToken>())
.Returns(new List<ChargeableCharConfig>());
var result = await _service.GetActiveConfigForMedioAsync(1, AsOf, CancellationToken.None);
var result = await _service.GetActiveConfigForProductTypeAsync(1, AsOf, CancellationToken.None);
result.Should().BeEmpty();
}
@@ -99,13 +99,13 @@ public class ChargeableCharConfigServiceTests
[Fact]
public async Task GetActiveConfig_KeyIsSymbol()
{
_repo.GetActiveForMedioAsync(1, AsOf, Arg.Any<CancellationToken>())
_repo.GetActiveForProductTypeAsync(1, AsOf, Arg.Any<CancellationToken>())
.Returns(new List<ChargeableCharConfig>
{
GlobalConfig("!", 2.0m),
});
var result = await _service.GetActiveConfigForMedioAsync(1, AsOf, CancellationToken.None);
var result = await _service.GetActiveConfigForProductTypeAsync(1, AsOf, CancellationToken.None);
result.Should().ContainKey("!");
result["!"].PricePerUnit.Should().Be(2.0m);