Files
RichardandClaude Opus 4.8 ca0d4ceed0 Accounting A-4 (PDF): neutrale Abrechnung als PDF via PDFsharp/MigraDoc (MIT)
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>
2026-07-20 15:10:52 +02:00

152 lines
6.7 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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());
}
}
}