ysdsd
All checks were successful
Deploy / deploy-staging (push) Successful in 32s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-08-21 23:58:03 +02:00
parent d8d1ad96ee
commit 49ea6bde76
3 changed files with 97 additions and 33 deletions

View File

@@ -1447,10 +1447,16 @@
? (dailyCurrency === reportCurrency ? dailyAmount : convertCurrencyValue(dailyAmount, dailyCurrency, reportCurrency)) ? (dailyCurrency === reportCurrency ? dailyAmount : convertCurrencyValue(dailyAmount, dailyCurrency, reportCurrency))
: null; : null;
const totalHashrateKh = toKhPerSecond(miner.mining_speed_value, miner.mining_speed_unit) + toKhPerSecond(miner.bonus_speed_value, miner.bonus_speed_unit); 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 const settledAmount = Number(miner.settled_value_amount);
? baseAmount const settledCurrency = String(miner.settled_value_currency || '').toUpperCase();
: convertCurrencyValue(miner.total_cost_amount, effectiveCurrency, reportCurrency); const hasHistoricalSettlement = Number.isFinite(settledAmount) && settledAmount > 0 && settledCurrency;
const costPerKh = totalHashrateKh > 0 && Number.isFinite(effectiveAmountReport) ? (effectiveAmountReport / totalHashrateKh) : null; 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 { return {
id: `purchase-${miner.id}`, id: `purchase-${miner.id}`,
source: 'miete', source: 'miete',
@@ -1464,10 +1470,12 @@
daily_cost_currency: dailyCurrency, daily_cost_currency: dailyCurrency,
daily_cost_report_amount: Number.isFinite(dailyReportAmount) ? dailyReportAmount : null, daily_cost_report_amount: Number.isFinite(dailyReportAmount) ? dailyReportAmount : null,
total_hashrate_kh: totalHashrateKh > 0 ? totalHashrateKh : null, total_hashrate_kh: totalHashrateKh > 0 ? totalHashrateKh : null,
cost_per_kh_amount: Number.isFinite(costPerKh) && costPerKh > 0 ? costPerKh : null, runtime_days: Number.isFinite(runtimeDays) && runtimeDays > 0 ? runtimeDays : null,
cost_per_kh_currency: reportCurrency, cost_per_kh_amount: Number.isFinite(costPerKhAmount) && costPerKhAmount > 0 ? costPerKhAmount : null,
base_amount: baseAmount, cost_per_kh_currency: costPerKhCurrency,
base_currency: baseCurrency, 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_id: miner.id,
miner_offer_id: miner.miner_offer_id, miner_offer_id: miner.miner_offer_id,
payment_type: paymentType, payment_type: paymentType,
@@ -1490,8 +1498,10 @@
? (dailyCurrency === reportCurrency ? dailyAmount : convertCurrencyValue(dailyAmount, dailyCurrency, reportCurrency)) ? (dailyCurrency === reportCurrency ? dailyAmount : convertCurrencyValue(dailyAmount, dailyCurrency, reportCurrency))
: null; : null;
const totalHashrateKh = toKhPerSecond(plan.mining_speed_value, plan.mining_speed_unit) + toKhPerSecond(plan.bonus_speed_value, plan.bonus_speed_unit); 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 runtimeDays = Number(plan.runtime_months) * 30.4375;
const costPerKh = totalHashrateKh > 0 && Number.isFinite(effectiveAmountReport) ? (effectiveAmountReport / totalHashrateKh) : null; const costPerKhAmount = totalHashrateKh > 0 && Number.isFinite(runtimeDays) && runtimeDays > 0
? Number(plan.total_cost_amount) / totalHashrateKh / runtimeDays
: null;
return { return {
id: `plan-${plan.id}`, id: `plan-${plan.id}`,
source: 'manual', source: 'manual',
@@ -1505,8 +1515,9 @@
daily_cost_currency: dailyCurrency, daily_cost_currency: dailyCurrency,
daily_cost_report_amount: Number.isFinite(dailyReportAmount) ? dailyReportAmount : null, daily_cost_report_amount: Number.isFinite(dailyReportAmount) ? dailyReportAmount : null,
total_hashrate_kh: totalHashrateKh > 0 ? totalHashrateKh : null, total_hashrate_kh: totalHashrateKh > 0 ? totalHashrateKh : null,
cost_per_kh_amount: Number.isFinite(costPerKh) && costPerKh > 0 ? costPerKh : null, runtime_days: Number.isFinite(runtimeDays) && runtimeDays > 0 ? runtimeDays : null,
cost_per_kh_currency: reportCurrency, cost_per_kh_amount: Number.isFinite(costPerKhAmount) && costPerKhAmount > 0 ? costPerKhAmount : null,
cost_per_kh_currency: plan.currency,
base_amount: plan.base_price_amount, base_amount: plan.base_price_amount,
base_currency: currentSettings.report_currency || 'EUR', base_currency: currentSettings.report_currency || 'EUR',
payment_type: plan.payment_type, payment_type: plan.payment_type,
@@ -3706,7 +3717,7 @@
const renderMinerTable = (rows, emptyText) => ( const renderMinerTable = (rows, emptyText) => (
h('div', { className: 'mc-table-shell' }, [ h('div', { className: 'mc-table-shell' }, [
h('table', { key: 'table', className: 'mc-table' }, [ 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' }, h('tbody', { key: 'body' },
rows.length rows.length
? rows.map((row) => h('tr', { key: row.id }, [ ? rows.map((row) => h('tr', { key: row.id }, [
@@ -3732,14 +3743,14 @@
].join('')) ].join(''))
: null, : null,
row.base_amount !== null && row.base_amount !== undefined && row.base_currency 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, : null,
]), ]),
h('td', { key: 'cost-kh' }, row.cost_per_kh_amount !== null && row.cost_per_kh_amount !== undefined 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)), 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 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, : null,
] ]
: 'n/a'), : 'n/a'),

View File

@@ -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. 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. 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 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 ### Historische Krypto-Miner

View File

@@ -700,24 +700,7 @@ final class AnalyticsService
continue; continue;
} }
$storedDailyCost = is_numeric($entry['daily_cost_amount'] ?? null) ? (float) $entry['daily_cost_amount'] : null; $convertedDailyCost = $this->entryDailyCostInCurrency($entry, $runtimeMonths, $currency, $fxContext);
$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);
if ($convertedDailyCost === null) { if ($convertedDailyCost === null) {
continue; continue;
} }
@@ -729,6 +712,74 @@ final class AnalyticsService
return $matched ? $dailyTotal : null; 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 private function convertLatestPrice(array $latestPriceByCurrency, string $targetCurrency, ?array $fxContext = null): ?float
{ {
foreach ($latestPriceByCurrency as $sourceCurrency => $price) { foreach ($latestPriceByCurrency as $sourceCurrency => $price) {