Vervollstaendigt den Export (CSV war A-2) um PDF - laenderneutral, KEINE steuerliche Einordnung. PDFsharp/MigraDoc-GDI 6.2.4 (echte MIT ohne Umsatzschwelle; GDI-Variante nutzt System-Fonts auf Windows/WinForms; keine NU1701-Transitiven - bewusst NICHT QuestPDF). - PdfExporter (Logic): Kopf (Konto/Zeitraum/Waehrung/Datum) + Aggregat-Tabelle + Monatsvergleich + Transaktionsliste + Methodik-/Nachweis-Seite (append-only-Quelle, Cash-Basis, 'keine Steuerberatung', SHA-256-Daten-Hash fuer Reproduzierbarkeit). Betraege in Anzeige-Waehrung (Faktor uebergeben -> Services-unabhaengig). Landscape. - UI: Button 'PDF-Export' (via Designer) neben CSV; Export via SaveFileDialog in gewaehlter Waehrung. - Test: PdfExporter erzeugt valide PDF-Bytes (%PDF-Signatur) headless -> MigraDoc-Rendering verifiziert. Build 0 Fehler, 386 Tests gruen, --smoke-ui alle 6 Views gruen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
152 lines
6.7 KiB
C#
152 lines
6.7 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using PolyTrader.Modules.Accounting.Logic;
|
||
using PolyTrader.Modules.Accounting.Models;
|
||
using Xunit;
|
||
|
||
namespace PolyTrader.Tests
|
||
{
|
||
/// <summary>Sicherheitsnetz für die neutrale Abrechnung (A-2): Periodenaggregation, FX, CSV.</summary>
|
||
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<LedgerEntry>
|
||
{
|
||
// 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<LedgerEntry>
|
||
{
|
||
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<FxRate>
|
||
{
|
||
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<LedgerEntry>
|
||
{
|
||
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);
|
||
}
|
||
|
||
// ---------------- PdfExporter (MigraDoc-Rendering headless) ----------------
|
||
|
||
[Fact]
|
||
public void Pdf_render_produces_valid_pdf_bytes()
|
||
{
|
||
var s = new PeriodStatement(1, Jul, Jul, 100m, 135m, 200m, 50m, 230m, 0m, 5m, 2m, 35m, 2, 1);
|
||
var entries = new List<LedgerEntry>
|
||
{
|
||
new() { AccountId = 1, EventType = LedgerEventType.TradeSell, Timestamp = Jul,
|
||
MarketSlug = "market-x", Side = "SELL", Size = 10m, PriceUsdc = 0.5m,
|
||
GrossUsdc = 130m, FeeUsdc = 1m, NetUsdc = 129m, TxHash = "0xabc" }
|
||
};
|
||
|
||
byte[] pdf = PdfExporter.Render(s, new[] { s }, entries, "USDC", 1.0m, "Native Buchungswährung.");
|
||
|
||
Assert.True(pdf.Length > 500);
|
||
// PDF-Signatur %PDF-
|
||
Assert.Equal(new byte[] { 0x25, 0x50, 0x44, 0x46 }, pdf.Take(4).ToArray());
|
||
}
|
||
}
|
||
}
|