Restore fixed reinvest income and repair FX snapshots
All checks were successful
Deploy / deploy-staging (push) Successful in 41s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-09-13 00:26:39 +02:00
parent 5cecda5394
commit c709b8e3de
5 changed files with 60 additions and 7 deletions

View File

@@ -3357,6 +3357,7 @@
value: earnedValue !== null ? fmtMoney(earnedValue, reportCurrency) : 'n/a', value: earnedValue !== null ? fmtMoney(earnedValue, reportCurrency) : 'n/a',
sub: [ sub: [
totalHoldingsValue !== null ? `Variabel ${fmtMoney(totalHoldingsValue, reportCurrency)}` : null, 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', currentSettings?.baseline_measured_at ? `Seit Baseline ${fmtDate(currentSettings.baseline_measured_at)}` : 'Benötigt Baseline',
].filter(Boolean).join(' · '), ].filter(Boolean).join(' · '),
}), }),

View File

@@ -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. 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. 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. 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. 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. 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. 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.

View File

@@ -374,6 +374,9 @@ final class Router
'measurement_limit' => self::BOOTSTRAP_MEASUREMENT_LIMIT, 'measurement_limit' => self::BOOTSTRAP_MEASUREMENT_LIMIT,
'snapshot_limit' => self::BOOTSTRAP_SNAPSHOT_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)), [ $settings = $this->safeTimed('bootstrap.settings', fn () => $this->settings($projectKey, $this->bootstrapSettingsOptions($view)), [
'project_key' => $projectKey, '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 private function offerBasisHistory(string $projectKey): array
{ {
return $this->repository()->listOfferBasisHistory($projectKey, 50); return $this->repository()->listOfferBasisHistory($projectKey, 50);

View File

@@ -461,11 +461,17 @@ final class AnalyticsService
// Reinvestments are spent coins, not a current crypto holding. Their historical // Reinvestments are spent coins, not a current crypto holding. Their historical
// settlement value is therefore the only valid break-even display basis. // settlement value is therefore the only valid break-even display basis.
$reinvestedCapital = $settledCryptoSpendValue; $reinvestedCapital = $settledCryptoSpendValue;
// Income is only the current value of coins still held in the miner or wallet. // Fixed income is the historical value of payouts that were already reinvested.
// A crypto-paid miner is an expense and must never be added as fixed income. // Variable income remains the current value of coins still held in miner or wallet.
$settledCryptoSpendCurrentAsset = null; $settledCryptoSpendCurrentAsset = $latestAsset !== ''
$earnedCurrentAsset = $holdingsCurrentAsset; ? $this->settledCryptoSpendValue($purchasedMiners, $latestAsset, $latest)
$earnedValue = $totalHoldingsValue; : 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; $currentDailyRevenue = is_numeric($latest['theoretical_daily_revenue'] ?? null) ? (float) $latest['theoretical_daily_revenue'] : null;
$totalInvestedCapital = ($cashInvestedCapital !== null || $reinvestedCapital !== null) $totalInvestedCapital = ($cashInvestedCapital !== null || $reinvestedCapital !== null)
? (float) ($cashInvestedCapital ?? 0.0) + (float) ($reinvestedCapital ?? 0.0) ? (float) ($cashInvestedCapital ?? 0.0) + (float) ($reinvestedCapital ?? 0.0)

View File

@@ -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/` - `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. - 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. - 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/` - `Waehrungs-Checker` ist eine Desktop-App aus `custom/apps/fx-rates/`
- die Desktop-Shell kann Moduldefinitionen zentral erkennen und als `App` bereitstellen - die Desktop-Shell kann Moduldefinitionen zentral erkennen und als `App` bereitstellen
- wiederverwendbare Modul-Helfer liegen im globalen Kern, die Fachlogik bleibt im Modul - wiederverwendbare Modul-Helfer liegen im globalen Kern, die Fachlogik bleibt im Modul