feat(application): commands/queries + IChargeableCharConfigService (PRC-001)
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
using FluentAssertions;
|
||||
using NSubstitute;
|
||||
using SIGCM2.Application.Abstractions.Persistence;
|
||||
using SIGCM2.Application.Pricing.ChargeableChars;
|
||||
using SIGCM2.Domain.Pricing.ChargeableChars;
|
||||
|
||||
namespace SIGCM2.Application.Tests.Pricing.ChargeableChars;
|
||||
|
||||
/// <summary>
|
||||
/// PRC-001 — ChargeableCharConfigService tests.
|
||||
/// Covers: per-medio wins over global, global fallback when no per-medio,
|
||||
/// empty result when no config at all.
|
||||
/// </summary>
|
||||
public class ChargeableCharConfigServiceTests
|
||||
{
|
||||
private readonly IChargeableCharConfigRepository _repo = Substitute.For<IChargeableCharConfigRepository>();
|
||||
private readonly ChargeableCharConfigService _service;
|
||||
|
||||
private static readonly DateOnly AsOf = new(2026, 4, 20);
|
||||
|
||||
public ChargeableCharConfigServiceTests()
|
||||
{
|
||||
_service = new ChargeableCharConfigService(_repo);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// ── Global fallback ─────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task GetActiveConfig_NoPerMedio_ReturnsGlobalConfigs()
|
||||
{
|
||||
_repo.GetActiveForMedioAsync(1, AsOf, Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ChargeableCharConfig>
|
||||
{
|
||||
GlobalConfig("$", 1.0m),
|
||||
GlobalConfig("%", 0.5m),
|
||||
});
|
||||
|
||||
var result = await _service.GetActiveConfigForMedioAsync(1, AsOf, CancellationToken.None);
|
||||
|
||||
result.Should().ContainKey("$");
|
||||
result["$"].PricePerUnit.Should().Be(1.0m);
|
||||
result.Should().ContainKey("%");
|
||||
}
|
||||
|
||||
// ── Per-medio wins over global ───────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task GetActiveConfig_PerMedioExists_OverridesGlobalForSameSymbol()
|
||||
{
|
||||
_repo.GetActiveForMedioAsync(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
|
||||
});
|
||||
|
||||
var result = await _service.GetActiveConfigForMedioAsync(5, AsOf, CancellationToken.None);
|
||||
|
||||
result["$"].PricePerUnit.Should().Be(3.0m); // per-medio wins
|
||||
result["%"].PricePerUnit.Should().Be(0.5m); // global only
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetActiveConfig_PerMedioExists_IncludesCorrectCategory()
|
||||
{
|
||||
_repo.GetActiveForMedioAsync(5, AsOf, Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ChargeableCharConfig>
|
||||
{
|
||||
MedioConfig(20L, 5, "$", 3.0m),
|
||||
});
|
||||
|
||||
var result = await _service.GetActiveConfigForMedioAsync(5, AsOf, CancellationToken.None);
|
||||
|
||||
result["$"].Category.Should().Be(ChargeableCharCategories.Currency);
|
||||
}
|
||||
|
||||
// ── Empty result ─────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task GetActiveConfig_NoConfigAtAll_ReturnsEmptyDictionary()
|
||||
{
|
||||
_repo.GetActiveForMedioAsync(1, AsOf, Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ChargeableCharConfig>());
|
||||
|
||||
var result = await _service.GetActiveConfigForMedioAsync(1, AsOf, CancellationToken.None);
|
||||
|
||||
result.Should().BeEmpty();
|
||||
}
|
||||
|
||||
// ── Key: Symbol, Value: snapshot ─────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task GetActiveConfig_KeyIsSymbol()
|
||||
{
|
||||
_repo.GetActiveForMedioAsync(1, AsOf, Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ChargeableCharConfig>
|
||||
{
|
||||
GlobalConfig("!", 2.0m),
|
||||
});
|
||||
|
||||
var result = await _service.GetActiveConfigForMedioAsync(1, AsOf, CancellationToken.None);
|
||||
|
||||
result.Should().ContainKey("!");
|
||||
result["!"].PricePerUnit.Should().Be(2.0m);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user