From 49ea6bde76abc9cbd45352f053ff092481825833 Mon Sep 17 00:00:00 2001 From: Lars Gebhardt-Kusche Date: Fri, 21 Aug 2026 23:58:03 +0200 Subject: [PATCH] ysdsd --- custom/apps/mining-checker/assets/js/app.js | 41 +++++---- custom/apps/mining-checker/docs/README.md | 2 + .../src/Domain/AnalyticsService.php | 87 +++++++++++++++---- 3 files changed, 97 insertions(+), 33 deletions(-) diff --git a/custom/apps/mining-checker/assets/js/app.js b/custom/apps/mining-checker/assets/js/app.js index 59bf7a97..1791ee31 100644 --- a/custom/apps/mining-checker/assets/js/app.js +++ b/custom/apps/mining-checker/assets/js/app.js @@ -1447,10 +1447,16 @@ ? (dailyCurrency === reportCurrency ? dailyAmount : convertCurrencyValue(dailyAmount, dailyCurrency, reportCurrency)) : null; const totalHashrateKh = toKhPerSecond(miner.mining_speed_value, miner.mining_speed_unit) + toKhPerSecond(miner.bonus_speed_value, miner.bonus_speed_unit); - const effectiveAmountReport = Number.isFinite(baseAmount) && baseCurrency === reportCurrency - ? baseAmount - : convertCurrencyValue(miner.total_cost_amount, effectiveCurrency, reportCurrency); - const costPerKh = totalHashrateKh > 0 && Number.isFinite(effectiveAmountReport) ? (effectiveAmountReport / totalHashrateKh) : null; + const settledAmount = Number(miner.settled_value_amount); + const settledCurrency = String(miner.settled_value_currency || '').toUpperCase(); + const hasHistoricalSettlement = Number.isFinite(settledAmount) && settledAmount > 0 && settledCurrency; + const runtimeDays = Number(miner.runtime_months) * 30.4375; + const costPerKhAmount = totalHashrateKh > 0 && Number.isFinite(runtimeDays) && runtimeDays > 0 + ? ((paymentType === 'crypto' && hasHistoricalSettlement ? settledAmount : Number(miner.total_cost_amount)) / totalHashrateKh / runtimeDays) + : null; + const costPerKhCurrency = paymentType === 'crypto' && hasHistoricalSettlement + ? settledCurrency + : effectiveCurrency; return { id: `purchase-${miner.id}`, source: 'miete', @@ -1464,10 +1470,12 @@ daily_cost_currency: dailyCurrency, daily_cost_report_amount: Number.isFinite(dailyReportAmount) ? dailyReportAmount : null, total_hashrate_kh: totalHashrateKh > 0 ? totalHashrateKh : null, - cost_per_kh_amount: Number.isFinite(costPerKh) && costPerKh > 0 ? costPerKh : null, - cost_per_kh_currency: reportCurrency, - base_amount: baseAmount, - base_currency: baseCurrency, + runtime_days: Number.isFinite(runtimeDays) && runtimeDays > 0 ? runtimeDays : null, + cost_per_kh_amount: Number.isFinite(costPerKhAmount) && costPerKhAmount > 0 ? costPerKhAmount : null, + cost_per_kh_currency: costPerKhCurrency, + base_amount: paymentType === 'crypto' && hasHistoricalSettlement ? settledAmount : baseAmount, + base_currency: paymentType === 'crypto' && hasHistoricalSettlement ? settledCurrency : baseCurrency, + base_label: paymentType === 'crypto' && hasHistoricalSettlement ? 'Fix zum Mietzeitpunkt' : 'Basis', miner_id: miner.id, miner_offer_id: miner.miner_offer_id, payment_type: paymentType, @@ -1490,8 +1498,10 @@ ? (dailyCurrency === reportCurrency ? dailyAmount : convertCurrencyValue(dailyAmount, dailyCurrency, reportCurrency)) : null; const totalHashrateKh = toKhPerSecond(plan.mining_speed_value, plan.mining_speed_unit) + toKhPerSecond(plan.bonus_speed_value, plan.bonus_speed_unit); - const effectiveAmountReport = convertCurrencyValue(plan.total_cost_amount, plan.currency, reportCurrency); - const costPerKh = totalHashrateKh > 0 && Number.isFinite(effectiveAmountReport) ? (effectiveAmountReport / totalHashrateKh) : null; + const runtimeDays = Number(plan.runtime_months) * 30.4375; + const costPerKhAmount = totalHashrateKh > 0 && Number.isFinite(runtimeDays) && runtimeDays > 0 + ? Number(plan.total_cost_amount) / totalHashrateKh / runtimeDays + : null; return { id: `plan-${plan.id}`, source: 'manual', @@ -1505,8 +1515,9 @@ daily_cost_currency: dailyCurrency, daily_cost_report_amount: Number.isFinite(dailyReportAmount) ? dailyReportAmount : null, total_hashrate_kh: totalHashrateKh > 0 ? totalHashrateKh : null, - cost_per_kh_amount: Number.isFinite(costPerKh) && costPerKh > 0 ? costPerKh : null, - cost_per_kh_currency: reportCurrency, + runtime_days: Number.isFinite(runtimeDays) && runtimeDays > 0 ? runtimeDays : null, + cost_per_kh_amount: Number.isFinite(costPerKhAmount) && costPerKhAmount > 0 ? costPerKhAmount : null, + cost_per_kh_currency: plan.currency, base_amount: plan.base_price_amount, base_currency: currentSettings.report_currency || 'EUR', payment_type: plan.payment_type, @@ -3706,7 +3717,7 @@ const renderMinerTable = (rows, emptyText) => ( h('div', { className: 'mc-table-shell' }, [ h('table', { key: 'table', className: 'mc-table' }, [ - h('thead', { key: 'head' }, h('tr', null, ['Label', 'Start', 'Laufzeit', 'Auto', 'Kosten Server', 'Kosten/kH/s', 'Waehrung', 'Aktiv', 'Aktion'].map((label) => h('th', { key: label }, label)))), + h('thead', { key: 'head' }, h('tr', null, ['Label', 'Start', 'Laufzeit', 'Auto', 'Kosten Server', 'Kosten/kH/s/Tag', 'Waehrung', 'Aktiv', 'Aktion'].map((label) => h('th', { key: label }, label)))), h('tbody', { key: 'body' }, rows.length ? rows.map((row) => h('tr', { key: row.id }, [ @@ -3732,14 +3743,14 @@ ].join('')) : null, row.base_amount !== null && row.base_amount !== undefined && row.base_currency - ? h('div', { key: 'base', className: 'mc-kicker' }, `Basis ${fmtNumber(row.base_amount, 6)} ${row.base_currency}`) + ? h('div', { key: 'base', className: 'mc-kicker' }, `${row.base_label || 'Basis'} ${fmtNumber(row.base_amount, 6)} ${row.base_currency}`) : null, ]), h('td', { key: 'cost-kh' }, row.cost_per_kh_amount !== null && row.cost_per_kh_amount !== undefined ? [ h('div', { key: 'amount' }, fmtMoney(row.cost_per_kh_amount, row.cost_per_kh_currency || reportCurrency)), row.total_hashrate_kh !== null && row.total_hashrate_kh !== undefined - ? h('div', { key: 'hash', className: 'mc-kicker' }, `bei ${fmtNumber(row.total_hashrate_kh, 4)} kH/s`) + ? h('div', { key: 'hash', className: 'mc-kicker' }, `bei ${fmtNumber(row.total_hashrate_kh, 4)} kH/s${row.runtime_days !== null && row.runtime_days !== undefined ? ` · ${fmtNumber(row.runtime_days, 2)} Tage` : ''}`) : null, ] : 'n/a'), diff --git a/custom/apps/mining-checker/docs/README.md b/custom/apps/mining-checker/docs/README.md index a309418a..1f8109d5 100644 --- a/custom/apps/mining-checker/docs/README.md +++ b/custom/apps/mining-checker/docs/README.md @@ -139,6 +139,8 @@ Diese Regeln sind bei jeder Erweiterung der Wallet-, Miner- oder Uebersichtslogi 7. Der angezeigte bisherige Wert trennt variable Werte (Coins im Mining-Tool und Wallet zum aktuellen Kurs) von fixen Werten (bereits mit Krypto bezahlte Miner zum Ausgabezeitpunkt). Die Gesamtsumme besteht aus beiden Kategorien. 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 Laufzeit in Tagen (`Monate x 30,4375`). 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. +11. Die Startseitenkennzahl `Tageskosten` verteilt jede aktive Miete auf ihre Laufzeit (`Gesamtkosten / Laufzeittage`) und beruecksichtigt keine bereits abgelaufenen Miner. Bei Krypto-Mieten verwendet sie den festen Mietwert und dessen zugeordnete historische `fx-rates.fetch_id`, nicht den aktuellen Coin-Kurs. ### Historische Krypto-Miner diff --git a/custom/apps/mining-checker/src/Domain/AnalyticsService.php b/custom/apps/mining-checker/src/Domain/AnalyticsService.php index 199acb1d..0977128b 100644 --- a/custom/apps/mining-checker/src/Domain/AnalyticsService.php +++ b/custom/apps/mining-checker/src/Domain/AnalyticsService.php @@ -700,24 +700,7 @@ final class AnalyticsService continue; } - $storedDailyCost = is_numeric($entry['daily_cost_amount'] ?? null) ? (float) $entry['daily_cost_amount'] : null; - $entryCurrency = strtoupper(trim((string) ($entry['daily_cost_currency'] ?? $entry['currency'] ?? ''))); - if ($entryCurrency === '') { - continue; - } - - $planDailyCost = $storedDailyCost; - if ($planDailyCost === null || $planDailyCost <= 0) { - $amount = is_numeric($entry['total_cost_amount'] ?? null) ? (float) $entry['total_cost_amount'] : null; - if ($amount === null || $amount <= 0) { - continue; - } - - $runtimeDays = $runtimeMonths * 30.4375; - $planDailyCost = $amount / $runtimeDays; - } - - $convertedDailyCost = $this->convertAmount($planDailyCost, $entryCurrency, $currency, $fxContext); + $convertedDailyCost = $this->entryDailyCostInCurrency($entry, $runtimeMonths, $currency, $fxContext); if ($convertedDailyCost === null) { continue; } @@ -729,6 +712,74 @@ final class AnalyticsService return $matched ? $dailyTotal : null; } + private function entryDailyCostInCurrency(array $entry, int $runtimeMonths, string $targetCurrency, ?array $fxContext = null): ?float + { + $runtimeDays = $runtimeMonths * 30.4375; + if ($runtimeDays <= 0) { + return null; + } + + if ($this->entryIsCryptoPaid($entry)) { + $settledAmount = is_numeric($entry['settled_value_amount'] ?? null) ? (float) $entry['settled_value_amount'] : null; + $settledCurrency = strtoupper(trim((string) ($entry['settled_value_currency'] ?? ''))); + $spentAmount = is_numeric($entry['total_cost_amount'] ?? null) ? (float) $entry['total_cost_amount'] : null; + $usesLegacyFallback = false; + + if ($settledAmount === null || $settledAmount <= 0 || $settledCurrency === '') { + if ($spentAmount === null || $spentAmount <= 0) { + return null; + } + $settledAmount = $spentAmount * 0.07; + $settledCurrency = 'USD'; + $usesLegacyFallback = true; + } + + $dailyAmount = $settledAmount / $runtimeDays; + $target = strtoupper(trim($targetCurrency)); + if ($settledCurrency === $target) { + return $dailyAmount; + } + + // The legacy fallback has a separately agreed fixed EUR value and must not use current FX. + if ($usesLegacyFallback && $target === 'EUR' && $spentAmount !== null) { + return ($spentAmount * 0.068) / $runtimeDays; + } + + $historicalFxContext = [ + 'fx_fetch_id' => $entry['settled_fx_fetch_id'] ?? null, + 'measured_at' => $entry['purchased_at'] ?? null, + ]; + $converted = $this->convertAmount($dailyAmount, $settledCurrency, $target, $historicalFxContext); + if ($converted !== null) { + return $converted; + } + + // Older records without a snapshot retain the best available legacy conversion. + if (!is_numeric($entry['settled_fx_fetch_id'] ?? null)) { + return $this->convertAmount($dailyAmount, $settledCurrency, $target, $fxContext); + } + + return null; + } + + $storedDailyCost = is_numeric($entry['daily_cost_amount'] ?? null) ? (float) $entry['daily_cost_amount'] : null; + $entryCurrency = strtoupper(trim((string) ($entry['daily_cost_currency'] ?? $entry['currency'] ?? ''))); + if ($entryCurrency === '') { + return null; + } + + $dailyAmount = $storedDailyCost; + if ($dailyAmount === null || $dailyAmount <= 0) { + $amount = is_numeric($entry['total_cost_amount'] ?? null) ? (float) $entry['total_cost_amount'] : null; + if ($amount === null || $amount <= 0) { + return null; + } + $dailyAmount = $amount / $runtimeDays; + } + + return $this->convertAmount($dailyAmount, $entryCurrency, $targetCurrency, $fxContext); + } + private function convertLatestPrice(array $latestPriceByCurrency, string $targetCurrency, ?array $fxContext = null): ?float { foreach ($latestPriceByCurrency as $sourceCurrency => $price) {