diff --git a/custom/apps/mining-checker/assets/js/app.js b/custom/apps/mining-checker/assets/js/app.js index 36ace412..eb3aa874 100644 --- a/custom/apps/mining-checker/assets/js/app.js +++ b/custom/apps/mining-checker/assets/js/app.js @@ -3357,6 +3357,7 @@ value: earnedValue !== null ? fmtMoney(earnedValue, reportCurrency) : 'n/a', sub: [ totalHoldingsValue !== null ? `Variabel ${fmtMoney(totalHoldingsValue, reportCurrency)}` : null, + settledCryptoSpendValue !== null ? `Fix ${fmtMoney(settledCryptoSpendValue, reportCurrency)}` : null, currentSettings?.baseline_measured_at ? `Seit Baseline ${fmtDate(currentSettings.baseline_measured_at)}` : 'Benötigt Baseline', ].filter(Boolean).join(' · '), }), diff --git a/custom/apps/mining-checker/docs/README.md b/custom/apps/mining-checker/docs/README.md index 906dded6..6fcc2188 100644 --- a/custom/apps/mining-checker/docs/README.md +++ b/custom/apps/mining-checker/docs/README.md @@ -142,7 +142,7 @@ Diese Regeln sind bei jeder Erweiterung der Wallet-, Miner- oder Uebersichtslogi 4. Wird ein Miner mit der Kryptowaehrung bezahlt, reduziert der Kauf den Walletbestand um den gezahlten Coin-Betrag. Gleichzeitig wird dessen Gegenwert zum Kaufzeitpunkt als feste Buchung gespeichert. Dieser Betrag darf durch spaetere Kursaenderungen nicht mehr beeinflusst werden. 5. Ein Wallet-Screenshot ist fuer seinen Zeitstempel autoritativ. Sein erkannter Hauptbestand ueberschreibt die rechnerische Wallet-Historie bis zu diesem Zeitpunkt; erst danach werden weitere Transfers, Krypto-Minerkaeufe und externe Wallet-Auszahlungen verrechnet. 6. Jede erfasste Auszahlung aus dem Mining-Tool muss den Walletbestand automatisch fortschreiben. Externe Auszahlungen aus dem hier verfolgten Wallet reduzieren ihn. -7. `Bisher verdient gesamt` zeigt ausschliesslich variable Werte: Coins im Mining-Tool und Wallet zum aktuellen Kurs. Bereits mit Krypto bezahlte Miner sind Ausgaben und duerfen dort niemals als fixer Ertrag erscheinen. +7. `Bisher verdient gesamt` trennt variable Werte (Coins im Mining-Tool und Wallet zum aktuellen Kurs) von fixen Werten (historischer Gegenwert bereits ausgezahlter und fuer Reinvest verwendeter Coins). Eine Krypto-Miete bleibt zugleich eine Ausgabe; ihr fixer Gegenwert darf in den Ausgaben und im fixen Verdienstteil jeweils nur einmal erscheinen. 8. Jede neue feste Krypto-Buchung referenziert die `fetch_id` des Moduls `fx-rates`. Der Mining-Checker fuehrt keine eigene Kurstabelle. Umrechnungen in andere Berichtswährungen verwenden fuer fixe Werte denselben historischen API-Snapshot. 9. Wird ein gemieteter Miner geloescht, wird sein kompletter Datensatz entfernt: Mietkosten, feste Krypto-Bewertung, FX-Referenz und der daraus abgeleitete Wallet-Abzug entfallen. Der Miner beeinflusst danach weder Walletbestand noch Ausgaben, Reinvest, Hashrate oder Break-even. Basis-Angebote, Mining-Uploads und andere Wallet-Buchungen bleiben unveraendert. 10. `Kosten/kH/s/Tag` ist ein laufzeitbereinigter Vergleichswert: feste historische Gesamtkosten geteilt durch die gesamte Hashrate einschliesslich Bonus und die exakten Kalendertage zwischen Mietzeitpunkt und Laufzeitende. Das Laufzeitende entsteht durch das Addieren der gebuchten Kalendermonate; bei kuerzeren Zielmonaten wird auf deren letzten Kalendertag begrenzt. Fuer Krypto-Mieten sind das `settled_value_amount / (Basis-Hashrate + Bonus-Hashrate) / Laufzeittage` in `settled_value_currency`; der aktuelle Coin-Kurs und ein Angebots-Referenzpreis duerfen diese Kennzahl nicht beeinflussen. Fuer FIAT-Mieten gilt derselbe Quotient mit dem tatsaechlich gezahlten FIAT-Betrag und dessen Zahlungswaehrung. diff --git a/custom/apps/mining-checker/src/Api/Router.php b/custom/apps/mining-checker/src/Api/Router.php index 303815bc..43976660 100644 --- a/custom/apps/mining-checker/src/Api/Router.php +++ b/custom/apps/mining-checker/src/Api/Router.php @@ -374,6 +374,9 @@ final class Router 'measurement_limit' => self::BOOTSTRAP_MEASUREMENT_LIMIT, 'snapshot_limit' => self::BOOTSTRAP_SNAPSHOT_LIMIT, ]); + $this->safeTimed('bootstrap.historical_fx_repair', fn () => $this->repairMalformedHistoricalRecoverySnapshots($projectKey), null, [ + 'project_key' => $projectKey, + ]); $settings = $this->safeTimed('bootstrap.settings', fn () => $this->settings($projectKey, $this->bootstrapSettingsOptions($view)), [ 'project_key' => $projectKey, @@ -906,6 +909,49 @@ final class Router } } + private function repairMalformedHistoricalRecoverySnapshots(string $projectKey): void + { + $fxRepository = $this->sharedFxRatesRepository(); + if ($fxRepository === null) { + return; + } + + foreach ($this->repository()->listPurchasedMiners($projectKey) as $miner) { + $fetchId = is_numeric($miner['settled_fx_fetch_id'] ?? null) ? (int) $miner['settled_fx_fetch_id'] : 0; + if ($fetchId <= 0 || strtoupper((string) ($miner['currency'] ?? '')) !== 'DOGE') { + continue; + } + $snapshot = $fxRepository->getSnapshotByFetchId($fetchId); + $rates = is_array($snapshot['rates'] ?? null) ? $snapshot['rates'] : []; + $dogePerUsd = is_numeric($rates['DOGE'] ?? null) ? (float) $rates['DOGE'] : null; + $usdEur = is_numeric($rates['EUR'] ?? null) ? (float) $rates['EUR'] : null; + if ( + !is_array($snapshot) + || (string) ($snapshot['provider'] ?? '') !== 'coingecko+valutafx' + || $dogePerUsd === null + || $dogePerUsd <= 0 + || $dogePerUsd >= 1 + || $usdEur === null + || $usdEur <= 0 + ) { + continue; + } + + $spent = is_numeric($miner['total_cost_amount'] ?? null) ? (float) $miner['total_cost_amount'] : null; + $minerId = is_numeric($miner['id'] ?? null) ? (int) $miner['id'] : 0; + if ($spent === null || $spent <= 0 || $minerId <= 0) { + continue; + } + + $dogeUsd = $dogePerUsd; + $fxRepository->replaceFetchRates($fetchId, ['DOGE' => 1 / $dogeUsd, 'EUR' => $usdEur]); + $this->repository()->updatePurchasedMinerSettlement($projectKey, $minerId, $spent * $dogeUsd * $usdEur, 'EUR', $fetchId); + $this->fx = null; + $this->sharedFxRatesService = null; + $this->sharedFxRatesResolved = false; + } + } + private function offerBasisHistory(string $projectKey): array { return $this->repository()->listOfferBasisHistory($projectKey, 50); diff --git a/custom/apps/mining-checker/src/Domain/AnalyticsService.php b/custom/apps/mining-checker/src/Domain/AnalyticsService.php index fb8397ee..f349c7b3 100644 --- a/custom/apps/mining-checker/src/Domain/AnalyticsService.php +++ b/custom/apps/mining-checker/src/Domain/AnalyticsService.php @@ -461,11 +461,17 @@ final class AnalyticsService // Reinvestments are spent coins, not a current crypto holding. Their historical // settlement value is therefore the only valid break-even display basis. $reinvestedCapital = $settledCryptoSpendValue; - // Income is only the current value of coins still held in the miner or wallet. - // A crypto-paid miner is an expense and must never be added as fixed income. - $settledCryptoSpendCurrentAsset = null; - $earnedCurrentAsset = $holdingsCurrentAsset; - $earnedValue = $totalHoldingsValue; + // Fixed income is the historical value of payouts that were already reinvested. + // Variable income remains the current value of coins still held in miner or wallet. + $settledCryptoSpendCurrentAsset = $latestAsset !== '' + ? $this->settledCryptoSpendValue($purchasedMiners, $latestAsset, $latest) + : null; + $earnedCurrentAsset = $settledCryptoSpendCurrentAsset !== null + ? $holdingsCurrentAsset + $settledCryptoSpendCurrentAsset + : $holdingsCurrentAsset; + $earnedValue = ($totalHoldingsValue !== null || $settledCryptoSpendValue !== null) + ? (float) ($totalHoldingsValue ?? 0.0) + (float) ($settledCryptoSpendValue ?? 0.0) + : null; $currentDailyRevenue = is_numeric($latest['theoretical_daily_revenue'] ?? null) ? (float) $latest['theoretical_daily_revenue'] : null; $totalInvestedCapital = ($cashInvestedCapital !== null || $reinvestedCapital !== null) ? (float) ($cashInvestedCapital ?? 0.0) + (float) ($reinvestedCapital ?? 0.0) diff --git a/docs/CONTENT.md b/docs/CONTENT.md index 6ad71214..2b9c2e9a 100644 --- a/docs/CONTENT.md +++ b/docs/CONTENT.md @@ -137,7 +137,7 @@ Aus [custom/apps/README.md](/home/lars/Schreibtisch/Projekte/desktop.kusche.berl - `Mining-Checker` ist eine Desktop-App aus `custom/apps/mining-checker/` - Historische Mining-Nachtraege verwenden ihren belegten Zeitpunkt; fuer feste Krypto-Buchungen ist ein zeitnaher, referenzierter `fx-rates`-Snapshot Pflicht und ein aktueller Kurs darf keinen historischen Snapshot ersetzen. - Der Bereich `Nachtragen` im Mining-Checker speichert zusammenhaengende historische Miner-, Transfer-, Wallet- und Mining-Daten in chronologischer Reihenfolge und legt den belegten FX-Snapshot davor an. -- `Bisher verdient` besteht ausschliesslich aus variablen Coin-Bestaenden; fixe Krypto-Mieten sind historische Ausgaben und gehoeren nicht in den Ertrag. +- `Bisher verdient` trennt variable Coin-Bestaende von fixen, bereits ausgezahlten und reinvestierten Coin-Werten; Krypto-Mieten bleiben dabei zugleich historische Ausgaben. - `Waehrungs-Checker` ist eine Desktop-App aus `custom/apps/fx-rates/` - die Desktop-Shell kann Moduldefinitionen zentral erkennen und als `App` bereitstellen - wiederverwendbare Modul-Helfer liegen im globalen Kern, die Fachlogik bleibt im Modul