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',
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(' · '),
}),

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

View File

@@ -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);

View File

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