using System; using System.Collections.Generic; using System.Linq; using PolyTrader.Modules.Accounting.Models; using PolyTrader.Modules.Accounting.Persistence; using PolyTrader.Modules.Accounting.Services; using Xunit; namespace PolyTrader.Tests.Accounting { /// /// Die Warnung über veraltete EZB-Kurse (#533). Die Schwelle liegt bei drei Tagen, weil genau so /// groß die planmäßige Wochenendlücke ist: Der Freitagskurs gilt bis Montag und ist dabei kein /// Mangel. Erst darüber hinaus fehlt wirklich etwas. /// public class CurrencyStalenessTests { private sealed class StubFx : IFxRateRepository { private readonly List _rates; public StubFx(params FxRate[] rates) => _rates = rates.ToList(); public void Upsert(FxRate rate) => _rates.Add(rate); public List GetAll() => _rates.OrderBy(r => r.Date).ToList(); } private sealed class ThrowingFx : IFxRateRepository { public void Upsert(FxRate rate) => throw new InvalidOperationException("DB weg"); public List GetAll() => throw new InvalidOperationException("DB weg"); } /// Leeres Ledger: Hier zählt allein die Währungsauflösung, nicht die Aggregation. private sealed class EmptyLedger : ILedgerRepository { public bool Upsert(LedgerEntry entry) => false; public DateTime? LatestTimestamp(int accountId) => null; public decimal SumNet(int accountId) => 0m; public int Count(int accountId) => 0; public List Query(int? accountId, DateTime? from, DateTime? to, int limit) => new(); public List GetUpTo(int? accountId, DateTime to) => new(); } private static AccountingReportService Build(IFxRateRepository fx) => new(new EmptyLedger(), fx); private static FxRate Rate(int year, int month, int day, decimal usdToEur = 0.86m) => new() { Date = new DateTime(year, month, day), UsdToEur = usdToEur, Source = "ECB" }; [Fact] public void Frisch_gerechneter_Kurs_warnt_nicht() { var ctx = Build(new StubFx(Rate(2026, 9, 9))).ResolveCurrency("EUR", new DateTime(2026, 9, 10)); Assert.True(ctx.Available); Assert.False(ctx.IsStale); Assert.Null(ctx.Warning); Assert.Equal(1, ctx.RateAgeDays); Assert.Equal(new DateTime(2026, 9, 9), ctx.RateDate); } [Fact] public void Wochenende_gilt_noch_nicht_als_veraltet() { // Freitag 2026-09-04 → Montag 2026-09-07: drei Tage, der Normalfall. var ctx = Build(new StubFx(Rate(2026, 9, 4))).ResolveCurrency("EUR", new DateTime(2026, 9, 7)); Assert.Equal(3, ctx.RateAgeDays); Assert.False(ctx.IsStale); } [Fact] public void Aelter_als_drei_Tage_warnt() { var ctx = Build(new StubFx(Rate(2026, 9, 4))).ResolveCurrency("EUR", new DateTime(2026, 9, 8)); Assert.True(ctx.Available); // benutzbar bleibt er Assert.True(ctx.IsStale); Assert.Equal(4, ctx.RateAgeDays); Assert.Contains("04.09.2026", ctx.Warning); Assert.Contains("4 Tage", ctx.Warning); } [Fact] public void Ohne_Kurse_ist_EUR_nicht_verfuegbar() { var ctx = Build(new StubFx()).ResolveCurrency("EUR", new DateTime(2026, 9, 10)); Assert.False(ctx.Available); Assert.Equal(0m, ctx.Factor); Assert.Contains("Keine EZB-Tageskurse", ctx.Note); } [Fact] public void Rueckwirkende_Abrechnung_nimmt_den_Kurs_von_damals() { var fx = new StubFx(Rate(2026, 3, 31, 0.92m), Rate(2026, 9, 9, 0.86m)); var maerz = Build(fx).ResolveCurrency("EUR", new DateTime(2026, 3, 31)); Assert.Equal(0.92m, maerz.Factor); // nicht der heutige Kurs Assert.False(maerz.IsStale); // am Stichtag war er taggenau } [Fact] public void Datenbankausfall_sperrt_EUR_statt_zu_werfen() { var ctx = Build(new ThrowingFx()).ResolveCurrency("EUR", new DateTime(2026, 9, 10)); Assert.False(ctx.Available); Assert.Equal(0m, ctx.Factor); } [Fact] public void USDC_und_USD_bleiben_ohne_Kurse_verfuegbar() { var svc = Build(new StubFx()); var usdc = svc.ResolveCurrency("USDC", new DateTime(2026, 9, 10)); var usd = svc.ResolveCurrency("USD", new DateTime(2026, 9, 10)); Assert.True(usdc.Available); Assert.Equal(1.0m, usdc.Factor); Assert.True(usd.Available); Assert.False(usd.IsStale); } } }