Accounting A-2: neutrale Periodenabrechnung + BWA-Uebersicht + FX + CSV-Export
Alles laenderneutral, ohne steuerliche Einordnung (A-3 US-Steuerschicht bleibt bewusst offen, haengt an den CPA-Fragebogen-Antworten). Konzept: docs/konzepte/KONZEPT-Modul-Accounting.md, A-2. Pure Logik (unit-getestet): - AccountingEngine.BuildStatement: aggregiert Ledger-Saetze eines Zeitraums (x Account/alle) zu Anfangs-/Endsaldo, Ein-/Auszahlungen, Handelsvolumen, Redeems, Rewards, Fees und Netto-Handelsergebnis (Cash-Basis, EXKL. Ein-/Auszahlungen); Invariante Endsaldo-Anfang = Ergebnis + Einz. - Ausz. BuildMonthlyBreakdown: Monatsvergleich mit verketteten Anfangssalden. - FxConverter: USDC->USD (dokumentierte 1:1-Annahme) + USD->EUR ueber amtliche Tageskurse (acc_fx_rates, Nearest-on-or-before fuer Wochenend-/Feiertage). - CsvExporter: Ledger + Statement als RFC-4180-CSV, kulturinvariant (Punkt-Dezimal, ISO-Datum). Infrastruktur: - acc_fx_rates (FxRate, PK Datum) + EfFxRateRepository (Upsert je Datum). Migration AddFxRates angewendet. - ILedgerRepository.GetUpTo (alle Saetze <= to fuer die Abrechnung inkl. Anfangssaldo). - AccountingReportService: baut Abrechnungen + Waehrungs-View (USDC/USD immer, EUR wenn Kurse geladen). UI (designerfaehig, partial + .Designer.cs): neuer erster Tab 'Uebersicht / BWA' mit KPI-Kacheln (Netto-Handelsergebnis gruen/rot, Endsaldo, Ein-/Auszahlungen, Fees, Rewards, Volumen, #Trades), Monatsvergleich-Grid, Zeitraum-Picker (Standard laufender Monat), Konto- und Waehrungswahl, CSV-Export via SaveFileDialog. Tests: +6 (Periodenaggregation+Invariante, Monatsverkettung, FX-Umrechnung/Nearest-Kurs, CSV-Quoting). Build 0 Fehler, 385 Tests gruen, --smoke-ui alle 6 Views gruen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
a3c145c0ed
commit
42a599a3a3
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using PolyTrader.Modules.Accounting.Logic;
|
||||
using PolyTrader.Modules.Accounting.Models;
|
||||
using PolyTrader.Modules.Accounting.Persistence;
|
||||
|
||||
namespace PolyTrader.Modules.Accounting.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Anzeige-Währung einer Abrechnung. USDC/USD sind immer verfügbar (USD über die dokumentierte
|
||||
/// 1:1-Annahme), EUR nur, wenn ein amtlicher Tageskurs geladen ist.
|
||||
/// </summary>
|
||||
public sealed record CurrencyContext(string Code, decimal Factor, bool Available, string Note);
|
||||
|
||||
/// <summary>
|
||||
/// Report-Service (A-2): baut neutrale Periodenabrechnungen aus dem Ledger und stellt die
|
||||
/// Währungs-Umrechnung (USDC/USD/EUR) bereit. Reine Aggregation liegt im <see cref="AccountingEngine"/>,
|
||||
/// FX im <see cref="FxConverter"/> – der Service verdrahtet nur Persistenz + pure Logik.
|
||||
/// </summary>
|
||||
public class AccountingReportService
|
||||
{
|
||||
private readonly ILedgerRepository _ledger;
|
||||
private readonly IFxRateRepository _fx;
|
||||
|
||||
/// <summary>USDC→USD-Faktor (dokumentierte 1:1-Annahme; Zielland ggf. über Settings umstellbar).</summary>
|
||||
public decimal UsdcToUsd { get; set; } = FxConverter.DefaultUsdcToUsd;
|
||||
|
||||
public AccountingReportService(ILedgerRepository ledger, IFxRateRepository fx)
|
||||
{
|
||||
_ledger = ledger;
|
||||
_fx = fx;
|
||||
}
|
||||
|
||||
public PeriodStatement BuildStatement(int? accountId, DateTime from, DateTime to) =>
|
||||
AccountingEngine.BuildStatement(_ledger.GetUpTo(accountId, to), from, to, accountId);
|
||||
|
||||
public List<PeriodStatement> BuildMonthly(int? accountId, DateTime from, DateTime to) =>
|
||||
AccountingEngine.BuildMonthlyBreakdown(_ledger.GetUpTo(accountId, to), from, to, accountId);
|
||||
|
||||
/// <summary>Löst die Anzeige-Währung zum Stichtag auf (für EUR wird der EZB-Kurs am/vor dem Datum genutzt).</summary>
|
||||
public CurrencyContext ResolveCurrency(string code, DateTime asOf)
|
||||
{
|
||||
switch ((code ?? "USDC").Trim().ToUpperInvariant())
|
||||
{
|
||||
case "USD":
|
||||
return new CurrencyContext("USD", UsdcToUsd, true, $"USDC≈USD (1:1-Annahme, Faktor {UsdcToUsd}).");
|
||||
case "EUR":
|
||||
var rate = FxConverter.NearestOnOrBefore(_fx.GetAll(), asOf);
|
||||
if (rate == null)
|
||||
return new CurrencyContext("EUR", 0m, false, "Keine EZB-Tageskurse geladen – EUR nicht verfügbar.");
|
||||
return new CurrencyContext("EUR", UsdcToUsd * rate.UsdToEur, true,
|
||||
$"USD→EUR {rate.UsdToEur} ({rate.Date:yyyy-MM-dd}, {rate.Source}); USDC≈USD 1:1.");
|
||||
default:
|
||||
return new CurrencyContext("USDC", 1.0m, true, "Native Buchungswährung.");
|
||||
}
|
||||
}
|
||||
|
||||
public static decimal Convert(decimal usdc, CurrencyContext ctx) =>
|
||||
Math.Round(usdc * ctx.Factor, 2, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user