From aa7ec1d3217fbc805035bd06a735e283661bfb41 Mon Sep 17 00:00:00 2001 From: Lars Gebhardt-Kusche Date: Wed, 3 Jun 2026 01:54:51 +0200 Subject: [PATCH] ycdfdsf --- modules/mining-checker/assets/js/app.js | 71 ++++++++++++++--- modules/mining-checker/src/Api/Router.php | 78 +++++++++++++++++-- .../src/Domain/AnalyticsService.php | 63 +++++++++++++++ 3 files changed, 194 insertions(+), 18 deletions(-) diff --git a/modules/mining-checker/assets/js/app.js b/modules/mining-checker/assets/js/app.js index 72ec550..1478240 100644 --- a/modules/mining-checker/assets/js/app.js +++ b/modules/mining-checker/assets/js/app.js @@ -885,6 +885,9 @@ const currentWalletSnapshots = Array.isArray(payload?.wallet_snapshots) ? payload.wallet_snapshots : []; const currentMinerOffers = Array.isArray(currentSettings.miner_offers) ? currentSettings.miner_offers : []; const currentPurchasedMiners = Array.isArray(currentSettings.purchased_miners) ? currentSettings.purchased_miners : []; + const currentMiningCurrency = String((latest && latest.coin_currency) || currentSettings.crypto_currency || 'DOGE').toUpperCase(); + const latestWalletSnapshot = currentWalletSnapshots.length ? currentWalletSnapshots[0] : null; + const currentWalletMiningBalance = walletAssetBalance(latestWalletSnapshot, currentMiningCurrency); const renewableOfferIds = new Set( currentMinerOffers .filter((offer) => !!offer.auto_renew) @@ -900,7 +903,7 @@ : currencies; const selectableCurrencies = preferredSelectableCurrencies.length ? preferredSelectableCurrencies : currencies; const evaluatedMinerOffers = Array.isArray(payload?.summary?.miner_offers) ? payload.summary.miner_offers : []; - const availableMinerOffers = evaluatedMinerOffers.filter((offer) => !!offer.is_active); + const availableMinerOffers = evaluatedMinerOffers.filter((offer) => isOfferAvailableForWallet(offer, currentMiningCurrency, currentWalletMiningBalance)); const filteredMinerOffers = availableMinerOffers.filter((offer) => { const speedMin = minerOfferFilters.speed_min === '' ? null : Number(minerOfferFilters.speed_min); const speedUnit = String(minerOfferFilters.speed_unit || 'auto'); @@ -1124,6 +1127,43 @@ return rate === null ? null : numericValue * rate; } + function walletAssetBalance(snapshot, currency) { + const code = String(currency || '').toUpperCase(); + if (!snapshot || !code) { + return null; + } + + const assets = snapshot.balances_json && typeof snapshot.balances_json === 'object' ? snapshot.balances_json : {}; + const asset = assets[code] ?? assets[code.toLowerCase()] ?? assets[code.toUpperCase()]; + const snapshotCurrency = String(snapshot.wallet_currency || '').toUpperCase(); + const rawBalance = asset && typeof asset === 'object' + ? asset.balance + : (asset ?? (snapshotCurrency === code ? snapshot.wallet_balance : null)); + const balance = Number(rawBalance); + return Number.isFinite(balance) ? balance : null; + } + + function isOfferAvailableForWallet(offer, miningCurrency, walletBalance) { + if (!offer || !offer.is_active) { + return false; + } + + if (String(offer.payment_type || '').toLowerCase() !== 'crypto') { + return true; + } + + const currency = String(offer.effective_price_currency || offer.price_currency || '').toUpperCase(); + const expectedCurrency = String(miningCurrency || '').toUpperCase(); + const price = Number(offer.effective_price_amount); + const balance = Number(walletBalance); + + if (!currency || !expectedCurrency || currency !== expectedCurrency || !Number.isFinite(price) || !Number.isFinite(balance)) { + return true; + } + + return price <= balance + 0.00000001; + } + function latestFxHistoryRate(fromCurrency, toCurrency) { const from = String(fromCurrency || '').toUpperCase(); const to = String(toCurrency || '').toUpperCase(); @@ -1719,10 +1759,14 @@ setSaving(true); setError(''); try { + const offerPayload = { + ...minerOfferForm, + base_price_currency: minerOfferForm.payment_type === 'crypto' ? 'USD' : minerOfferForm.base_price_currency, + }; await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/miner-offers`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(minerOfferForm), + body: JSON.stringify(offerPayload), }); setMessage('Miner-Angebot gespeichert.'); setMinerOfferForm({ @@ -2090,7 +2134,7 @@ ]); function renderTab() { - const currentCoinCurrency = String((latest && latest.coin_currency) || currentSettings.crypto_currency || 'DOGE').toUpperCase(); + const currentCoinCurrency = currentMiningCurrency; const perDayLabel = `${currentCoinCurrency} pro Tag`; if (activeTab === 'overview') { @@ -2534,7 +2578,10 @@ ]), ]), ]), - panel('Miner-Angebote', 'Hier werden nur Basis-Miner gepflegt. Alle vorgegebenen Geschwindigkeiten und Preise werden daraus automatisch abgeleitet.', [ + panel('Miner-Angebote', currentWalletMiningBalance !== null + ? `Hier werden nur Basis-Miner gepflegt. Krypto-Angebote werden gegen deinen aktuellen Wallet-Bestand (${fmtNumber(currentWalletMiningBalance, 8)} ${currentMiningCurrency}) gefiltert.` + : 'Hier werden nur Basis-Miner gepflegt. Alle vorgegebenen Geschwindigkeiten und Preise werden daraus automatisch abgeleitet.', + [ h('div', { key: 'actions', className: 'mc-inline-row' }, [ h('button', { key: 'add-offer', @@ -2579,7 +2626,7 @@ h('td', { key: 'break' }, offer.break_even_days !== null ? `${fmtNumber(offer.break_even_days, 2)} Tage` : 'n/a'), h('td', { key: 'rec' }, [ h('div', { key: 'rec-main' }, offer.recommendation), - offer.base_price_amount !== null && offer.base_price_currency && offer.payment_type !== 'crypto' + offer.base_price_amount !== null && offer.base_price_currency ? h('div', { key: 'rec-ref', className: 'mc-kicker' }, `Basis ${fmtNumber(offer.base_price_amount, 6)} ${offer.base_price_currency}`) : null, offer.payment_type ? h('div', { key: 'paytype', className: 'mc-kicker' }, offer.payment_type === 'crypto' ? `Zahlung in Krypto (${currentSettings.crypto_currency || 'DOGE'})` : `Zahlung in FIAT (${offer.base_price_currency || 'EUR'})`) : null, @@ -2784,15 +2831,17 @@ inputField('Laufzeit in Monaten', 'number', minerOfferForm.runtime_months, (value) => setMinerOfferForm({ ...minerOfferForm, runtime_months: value })), displayField('Basis-Geschwindigkeit', baseOfferSpeedLabel(minerOfferForm.payment_type)), inputField('Bonus-Hashrate in %', 'number', minerOfferForm.bonus_percent, (value) => setMinerOfferForm({ ...minerOfferForm, bonus_percent: value }), '0.01'), - inputField('Basispreis', 'number', minerOfferForm.base_price_amount, (value) => setMinerOfferForm({ ...minerOfferForm, base_price_amount: value }), '0.000001'), + inputField(minerOfferForm.payment_type === 'crypto' ? 'Basispreis in USD' : 'Basispreis', 'number', minerOfferForm.base_price_amount, (value) => setMinerOfferForm({ ...minerOfferForm, base_price_amount: value }), '0.000001'), selectField('Zahlungsart', minerOfferForm.payment_type, [{ value: 'fiat', label: 'FIAT' }, { value: 'crypto', label: 'Krypto' }], (value) => setMinerOfferForm({ ...minerOfferForm, payment_type: value, base_price_currency: value === 'crypto' - ? (currentSettings.crypto_currency || selectableCryptoCurrencies[0]?.code || 'DOGE') + ? 'USD' : (selectableFiatCurrencies[0]?.code || 'USD'), })), - selectField('Basiswährung', minerOfferForm.base_price_currency, (minerOfferForm.payment_type === 'crypto' ? selectableCryptoCurrencies : selectableFiatCurrencies).map((currency) => currency.code), (value) => setMinerOfferForm({ ...minerOfferForm, base_price_currency: value })), + minerOfferForm.payment_type === 'crypto' + ? displayField('Kostenwaehrung', 'USD') + : selectField('Basiswährung', minerOfferForm.base_price_currency, selectableFiatCurrencies.map((currency) => currency.code), (value) => setMinerOfferForm({ ...minerOfferForm, base_price_currency: value })), h('label', { className: 'mc-checkbox' }, [ h('input', { type: 'checkbox', checked: !!minerOfferForm.auto_renew, onChange: (event) => setMinerOfferForm({ ...minerOfferForm, auto_renew: event.target.checked }) }), 'Automatische Verlängerung', @@ -2832,8 +2881,10 @@ }); }), inputField('Mietdatum/-zeit', 'datetime-local', purchaseMinerForm.purchased_at, (value) => setPurchaseMinerForm({ ...purchaseMinerForm, purchased_at: value })), - displayField('Gewaehlte Geschwindigkeit', purchaseMinerForm.mining_speed_value && purchaseMinerForm.mining_speed_unit ? `${purchaseMinerForm.mining_speed_value} ${purchaseMinerForm.mining_speed_unit}` : 'n/a'), - displayField('Bonus-Hashrate', purchaseMinerForm.bonus_percent ? `${purchaseMinerForm.bonus_percent}%` : 'kein Bonus'), + inputField('Label', 'text', purchaseMinerForm.label, (value) => setPurchaseMinerForm({ ...purchaseMinerForm, label: value })), + inputField('Mining-Geschwindigkeit', 'number', purchaseMinerForm.mining_speed_value, (value) => setPurchaseMinerForm({ ...purchaseMinerForm, mining_speed_value: value }), '0.0001'), + selectField('Mining-Einheit', purchaseMinerForm.mining_speed_unit, speedUnits, (value) => setPurchaseMinerForm({ ...purchaseMinerForm, mining_speed_unit: value })), + inputField('Bonus-Hashrate in %', 'number', purchaseMinerForm.bonus_percent, (value) => setPurchaseMinerForm({ ...purchaseMinerForm, bonus_percent: value }), '0.01'), inputField('Exakter Mietpreis', 'number', purchaseMinerForm.total_cost_amount, (value) => setPurchaseMinerForm({ ...purchaseMinerForm, total_cost_amount: value }), '0.000001'), selectField('Mietwährung', purchaseMinerForm.currency, selectableCurrencies.map((currency) => currency.code), (value) => setPurchaseMinerForm({ ...purchaseMinerForm, currency: value })), inputField('Referenzpreis', 'number', purchaseMinerForm.reference_price_amount, (value) => setPurchaseMinerForm({ ...purchaseMinerForm, reference_price_amount: value }), '0.000001'), diff --git a/modules/mining-checker/src/Api/Router.php b/modules/mining-checker/src/Api/Router.php index 60ecb38..635b29f 100644 --- a/modules/mining-checker/src/Api/Router.php +++ b/modules/mining-checker/src/Api/Router.php @@ -1801,7 +1801,9 @@ final class Router $paymentType = $this->enumValue($input['payment_type'] ?? 'fiat', ['fiat', 'crypto'], 'payment_type'); $baseSpeed = self::BASE_OFFER_SPEEDS[$paymentType] ?? self::BASE_OFFER_SPEEDS['fiat']; $bonusPercent = $this->optionalDecimal($input['bonus_percent'] ?? null); - $basePriceCurrency = $this->requiredCurrency($input['base_price_currency'] ?? ($input['reference_price_currency'] ?? $input['price_currency'] ?? null), 'base_price_currency'); + $basePriceCurrency = $paymentType === 'crypto' + ? 'USD' + : $this->requiredCurrency($input['base_price_currency'] ?? ($input['reference_price_currency'] ?? $input['price_currency'] ?? null), 'base_price_currency'); $payload = [ 'label' => $this->requiredString($input['label'] ?? null, 'label', 120), 'runtime_months' => $this->optionalPositiveInt($input['runtime_months'] ?? null), @@ -1822,7 +1824,7 @@ final class Router throw new ApiException('Bonus-Prozent darf nicht negativ sein.', 422); } - $this->assertCurrencyType($payload['base_price_currency'], $paymentType === 'crypto', 'base_price_currency'); + $this->assertCurrencyType($payload['base_price_currency'], false, 'base_price_currency'); return $this->repository()->saveMinerOffer($projectKey, $payload); } @@ -1837,6 +1839,7 @@ final class Router $settings = $this->settings($projectKey); $cryptoCurrency = $this->requiredCurrency($settings['crypto_currency'] ?? 'DOGE', 'crypto_currency'); $isAutoRenew = array_key_exists('auto_renew', $input) ? !empty($input['auto_renew']) : !empty($offer['auto_renew']); + $paymentType = $this->enumValue($offer['payment_type'] ?? 'fiat', ['fiat', 'crypto'], 'payment_type'); $selectedSpeedValue = $this->optionalDecimal($input['mining_speed_value'] ?? null); $selectedSpeedUnit = $this->optionalSpeedUnit($input['mining_speed_unit'] ?? null); if (($selectedSpeedValue === null) xor ($selectedSpeedUnit === null)) { @@ -1845,17 +1848,21 @@ final class Router $resolvedSpeedValue = $selectedSpeedValue ?? $this->optionalDecimal($offer['mining_speed_value'] ?? null); $resolvedSpeedUnit = $selectedSpeedUnit ?? $this->optionalSpeedUnit($offer['mining_speed_unit'] ?? null); - $resolvedBonusPercent = $this->offerBonusPercent($offer); + $selectedBonusPercent = $this->optionalDecimal($input['bonus_percent'] ?? null); + if ($selectedBonusPercent !== null && $selectedBonusPercent < 0) { + throw new ApiException('Bonus-Prozent darf nicht negativ sein.', 422); + } + $resolvedBonusPercent = $selectedBonusPercent ?? $this->offerBonusPercent($offer); $resolvedBonusValue = $this->bonusSpeedFromPercent($resolvedSpeedValue, $resolvedSpeedUnit, $resolvedBonusPercent); $resolvedBonusUnit = $resolvedBonusValue !== null ? $resolvedSpeedUnit : null; $purchaseCurrency = $this->optionalCurrency($input['currency'] ?? null) ?? (string) ($offer['effective_price_currency'] ?? $offer['price_currency'] ?? $offer['base_price_currency'] ?? ''); - if (!$isAutoRenew) { + if ($paymentType === 'crypto' || !$isAutoRenew) { $purchaseCurrency = $cryptoCurrency; } $purchaseCost = $this->optionalDecimal($input['total_cost_amount'] ?? null); if ($purchaseCost === null) { - $purchaseCost = $this->resolveOfferPurchaseCost(array_merge($offer, [ + $purchaseCost = $this->resolveOfferPurchaseCost($projectKey, array_merge($offer, [ 'mining_speed_value' => $resolvedSpeedValue, 'mining_speed_unit' => $resolvedSpeedUnit, 'bonus_speed_value' => $resolvedBonusValue, @@ -1865,8 +1872,8 @@ final class Router 'price_currency' => $purchaseCurrency !== '' ? $purchaseCurrency : ($offer['price_currency'] ?? ''), ])); } - $referencePriceAmount = $this->optionalDecimal($input['reference_price_amount'] ?? ($offer['reference_price_amount'] ?? $offer['usd_reference_amount'] ?? null)); - $referencePriceCurrency = $this->optionalCurrency($input['reference_price_currency'] ?? ($offer['reference_price_currency'] ?? (is_numeric($offer['usd_reference_amount'] ?? null) ? 'USD' : null))); + $referencePriceAmount = $this->optionalDecimal($input['reference_price_amount'] ?? ($offer['base_price_amount'] ?? $offer['reference_price_amount'] ?? $offer['usd_reference_amount'] ?? null)); + $referencePriceCurrency = $this->optionalCurrency($input['reference_price_currency'] ?? ($offer['base_price_currency'] ?? $offer['reference_price_currency'] ?? (is_numeric($offer['usd_reference_amount'] ?? null) ? 'USD' : null))); $purchasedAt = array_key_exists('purchased_at', $input) ? $this->requiredDateTime($input['purchased_at'], 'purchased_at', $this->projectTimezone($projectKey)) : $this->currentTimestamp(); @@ -2457,7 +2464,7 @@ final class Router return abs(time() - $parsed) <= (int) round(max(0.25, $maxAgeHours) * 3600); } - private function resolveOfferPurchaseCost(array $offer): float + private function resolveOfferPurchaseCost(string $projectKey, array $offer): float { $purchaseCurrency = (string) ($offer['price_currency'] ?? ''); $baseAmount = is_numeric($offer['base_price_amount'] ?? null) @@ -2468,6 +2475,11 @@ final class Router $baseCurrency = (string) ($offer['base_price_currency'] ?? $offer['reference_price_currency'] ?? (is_numeric($offer['usd_reference_amount'] ?? null) ? 'USD' : '')); if ($purchaseCurrency !== '' && $baseAmount !== null && $baseAmount > 0 && $baseCurrency !== '') { + $latestConverted = $this->convertWithLatestMeasurement($projectKey, $baseAmount, $baseCurrency, $purchaseCurrency); + if ($latestConverted !== null && $latestConverted > 0) { + return $latestConverted; + } + $converted = $this->fx()->convert($baseAmount, $baseCurrency, $purchaseCurrency); if (is_numeric($converted) && (float) $converted > 0) { return (float) $converted; @@ -2477,6 +2489,56 @@ final class Router return (float) ($baseAmount ?? $offer['price_amount'] ?? 0); } + private function convertWithLatestMeasurement(string $projectKey, float $amount, string $fromCurrency, string $toCurrency): ?float + { + $from = strtoupper(trim($fromCurrency)); + $to = strtoupper(trim($toCurrency)); + if ($from === '' || $to === '' || $amount <= 0) { + return null; + } + if ($from === $to) { + return $amount; + } + + $latestRows = $this->repository()->listRecentMeasurements($projectKey, 1); + $latest = $latestRows[0] ?? null; + if (!is_array($latest)) { + return null; + } + + $coinCurrency = strtoupper(trim((string) ($latest['coin_currency'] ?? $this->settings($projectKey)['crypto_currency'] ?? ''))); + $priceCurrency = strtoupper(trim((string) ($latest['price_currency'] ?? ''))); + $pricePerCoin = is_numeric($latest['price_per_coin'] ?? null) ? (float) $latest['price_per_coin'] : null; + if ($coinCurrency === '' || $priceCurrency === '' || $pricePerCoin === null || $pricePerCoin <= 0) { + return null; + } + + if ($from === $priceCurrency && $to === $coinCurrency) { + return $amount / $pricePerCoin; + } + + if ($from === $coinCurrency && $to === $priceCurrency) { + return $amount * $pricePerCoin; + } + + if ($to === $coinCurrency) { + $convertedFiat = $this->fx()->convert($amount, $from, $priceCurrency); + if (is_numeric($convertedFiat) && (float) $convertedFiat > 0) { + return (float) $convertedFiat / $pricePerCoin; + } + } + + if ($from === $coinCurrency) { + $convertedFiat = $amount * $pricePerCoin; + $convertedTarget = $this->fx()->convert($convertedFiat, $priceCurrency, $to); + if (is_numeric($convertedTarget)) { + return (float) $convertedTarget; + } + } + + return null; + } + private function bonusSpeedFromPercent(?float $speedValue, ?string $speedUnit, ?float $bonusPercent): ?float { if ($speedValue === null || $speedUnit === null || $bonusPercent === null) { diff --git a/modules/mining-checker/src/Domain/AnalyticsService.php b/modules/mining-checker/src/Domain/AnalyticsService.php index 653a2b0..8fa2f0b 100644 --- a/modules/mining-checker/src/Domain/AnalyticsService.php +++ b/modules/mining-checker/src/Domain/AnalyticsService.php @@ -634,6 +634,11 @@ final class AnalyticsService return $amount; } + $measurementConverted = $this->convertAmountFromMeasurementPrice($amount, $from, $to, $fxContext); + if ($measurementConverted !== null) { + return $measurementConverted; + } + if ($this->fx === null) { return null; } @@ -646,6 +651,62 @@ final class AnalyticsService return $this->fx->convertAt($amount, $from, $to, $at, null, $fetchId); } + private function convertAmountFromMeasurementPrice(float $amount, string $from, string $to, ?array $fxContext = null): ?float + { + if (!is_array($fxContext)) { + return null; + } + + $coinCurrency = strtoupper(trim((string) ($fxContext['coin_currency'] ?? ''))); + $priceCurrency = strtoupper(trim((string) ($fxContext['effective_price_currency'] ?? $fxContext['price_currency'] ?? ''))); + $pricePerCoin = is_numeric($fxContext['effective_price_per_coin'] ?? null) + ? (float) $fxContext['effective_price_per_coin'] + : (is_numeric($fxContext['price_per_coin'] ?? null) ? (float) $fxContext['price_per_coin'] : null); + + if ($coinCurrency === '' || $priceCurrency === '' || $pricePerCoin === null || $pricePerCoin <= 0) { + return null; + } + + if ($from === $coinCurrency && $to === $priceCurrency) { + return $amount * $pricePerCoin; + } + + if ($from === $priceCurrency && $to === $coinCurrency) { + return $amount / $pricePerCoin; + } + + if ($to === $coinCurrency && $this->fx !== null) { + $convertedFiat = $this->fx->convertAt( + $amount, + $from, + $priceCurrency, + is_string($fxContext['measured_at'] ?? null) ? (string) $fxContext['measured_at'] : null, + null, + isset($fxContext['fx_fetch_id']) && is_numeric($fxContext['fx_fetch_id']) ? (int) $fxContext['fx_fetch_id'] : null + ); + if (is_numeric($convertedFiat) && (float) $convertedFiat > 0) { + return (float) $convertedFiat / $pricePerCoin; + } + } + + if ($from === $coinCurrency && $this->fx !== null) { + $convertedFiat = $amount * $pricePerCoin; + $convertedTarget = $this->fx->convertAt( + $convertedFiat, + $priceCurrency, + $to, + is_string($fxContext['measured_at'] ?? null) ? (string) $fxContext['measured_at'] : null, + null, + isset($fxContext['fx_fetch_id']) && is_numeric($fxContext['fx_fetch_id']) ? (int) $fxContext['fx_fetch_id'] : null + ); + if (is_numeric($convertedTarget)) { + return (float) $convertedTarget; + } + } + + return null; + } + private function totalHashrateMh(array $entries): float { $total = 0.0; @@ -956,6 +1017,8 @@ final class AnalyticsService $convertedReference = $this->convertAmount($basePriceAmount, $basePriceCurrency, $effectivePriceCurrency, $latest); if ($convertedReference !== null && $convertedReference > 0) { $effectivePriceAmount = $convertedReference; + } else { + $effectivePriceAmount = null; } } $referencePriceAmount = $basePriceAmount;