using System;
using System.Collections.Generic;
using System.Linq;
using PolyTrader.Modules.Accounting.Logic;
using PolyTrader.Modules.Accounting.Models;
using Xunit;
namespace PolyTrader.Tests
{
/// Sicherheitsnetz für die neutrale Abrechnung (A-2): Periodenaggregation, FX, CSV.
public class AccountingReportTests
{
private static readonly DateTime Jun = new(2026, 6, 15, 12, 0, 0, DateTimeKind.Utc);
private static readonly DateTime Jul = new(2026, 7, 10, 12, 0, 0, DateTimeKind.Utc);
private static LedgerEntry E(LedgerEventType type, decimal net, DateTime ts,
decimal gross = 0m, decimal fee = 0m) => new()
{
AccountId = 1, EventType = type, Timestamp = ts, NetUsdc = net,
GrossUsdc = gross == 0m ? Math.Abs(net) : gross, FeeUsdc = fee
};
// ---------------- AccountingEngine ----------------
[Fact]
public void Statement_aggregates_period_and_carries_opening_balance()
{
var entries = new List
{
// VOR dem Zeitraum → nur Anfangssaldo
E(LedgerEventType.Deposit, 1000m, new DateTime(2026, 5, 1, 0, 0, 0, DateTimeKind.Utc), gross: 1000m),
// im Zeitraum (Juli)
E(LedgerEventType.TradeBuy, -100m, Jul, gross: 100m, fee: 1m),
E(LedgerEventType.TradeSell, 130m, Jul, gross: 130m, fee: 1m),
E(LedgerEventType.Reward, 5m, Jul, gross: 5m),
E(LedgerEventType.Deposit, 200m, Jul, gross: 200m),
E(LedgerEventType.Withdrawal, -50m, Jul, gross: 50m),
};
var from = new DateTime(2026, 7, 1, 0, 0, 0, DateTimeKind.Utc);
var to = new DateTime(2026, 7, 31, 23, 59, 59, DateTimeKind.Utc);
var s = AccountingEngine.BuildStatement(entries, from, to, 1);
Assert.Equal(1000m, s.OpeningBalanceUsdc);
Assert.Equal(200m, s.Deposits);
Assert.Equal(50m, s.Withdrawals);
Assert.Equal(230m, s.TradeVolume); // 100 + 130
Assert.Equal(5m, s.Rewards);
Assert.Equal(2m, s.Fees);
Assert.Equal(35m, s.NetTradingResultUsdc); // -100 +130 +5 (exkl. Ein-/Auszahlungen)
Assert.Equal(2, s.TradeCount);
// Invariante: Endsaldo − Anfangssaldo = Handelsergebnis + Einzahlungen − Auszahlungen
Assert.Equal(1185m, s.ClosingBalanceUsdc); // 1000 + 35 + 200 - 50
Assert.Equal(s.NetTradingResultUsdc + s.Deposits - s.Withdrawals, s.BalanceChange);
}
[Fact]
public void Monthly_breakdown_chains_opening_balances()
{
var entries = new List
{
E(LedgerEventType.Deposit, 100m, Jun, gross: 100m), // Juni
E(LedgerEventType.TradeSell, 40m, Jul, gross: 40m), // Juli
};
var from = new DateTime(2026, 6, 1, 0, 0, 0, DateTimeKind.Utc);
var to = new DateTime(2026, 7, 31, 23, 59, 59, DateTimeKind.Utc);
var months = AccountingEngine.BuildMonthlyBreakdown(entries, from, to, 1);
Assert.Equal(2, months.Count);
Assert.Equal(0m, months[0].OpeningBalanceUsdc); // Juni startet bei 0
Assert.Equal(100m, months[0].ClosingBalanceUsdc);
Assert.Equal(100m, months[1].OpeningBalanceUsdc); // Juli erbt Juni-Endsaldo
Assert.Equal(140m, months[1].ClosingBalanceUsdc);
}
// ---------------- FxConverter ----------------
[Fact]
public void Fx_conversions_usdc_usd_eur()
{
Assert.Equal(100m, FxConverter.UsdcToUsd(100m, 1.0m));
Assert.Equal(92m, FxConverter.UsdToEur(100m, 0.92m));
Assert.Equal(92m, FxConverter.UsdcToEur(100m, 0.92m, 1.0m));
}
[Fact]
public void Fx_nearest_on_or_before_picks_latest_valid_rate()
{
var rates = new List
{
new() { Date = new DateTime(2026, 7, 3), UsdToEur = 0.90m },
new() { Date = new DateTime(2026, 7, 5), UsdToEur = 0.92m }, // Freitag
};
// Sonntag 2026-07-05 gibt es keinen Kurs → letzter gültiger (Freitag).
var pick = FxConverter.NearestOnOrBefore(rates, new DateTime(2026, 7, 6));
Assert.NotNull(pick);
Assert.Equal(0.92m, pick!.UsdToEur);
Assert.Null(FxConverter.NearestOnOrBefore(rates, new DateTime(2026, 7, 1))); // vor allen Kursen
}
// ---------------- CsvExporter ----------------
[Fact]
public void Csv_ledger_has_header_and_escapes_commas()
{
var entries = new List
{
new() { AccountId = 1, EventType = LedgerEventType.TradeBuy, Timestamp = Jul,
MarketSlug = "will-x-win, really?", Side = "BUY", Size = 10m, PriceUsdc = 0.5m,
GrossUsdc = 5m, FeeUsdc = 0.1m, NetUsdc = -5.1m, TxHash = "0xabc", Source = "polymarket-activity" }
};
string csv = CsvExporter.Ledger(entries);
var lines = csv.Split('\n', StringSplitOptions.RemoveEmptyEntries);
Assert.StartsWith("Timestamp,AccountId,EventType", lines[0]);
Assert.Contains("\"will-x-win, really?\"", lines[1]); // Komma-Feld gequotet
Assert.Contains("-5.1", lines[1]); // kulturinvariant (Punkt)
}
[Fact]
public void Csv_statement_lists_key_figures()
{
var s = new PeriodStatement(1, Jul, Jul, 100m, 135m, 0m, 0m, 230m, 0m, 5m, 2m, 35m, 2, 3);
string csv = CsvExporter.Statement(s);
Assert.Contains("Endsaldo,135", csv);
Assert.Contains("Netto-Handelsergebnis (Cash),35", csv);
}
}
}