diff --git a/custom/apps/mining-checker/assets/js/app.js b/custom/apps/mining-checker/assets/js/app.js index 7123e0f4..3e0c3483 100644 --- a/custom/apps/mining-checker/assets/js/app.js +++ b/custom/apps/mining-checker/assets/js/app.js @@ -904,6 +904,7 @@ const [targetModalOpen, setTargetModalOpen] = useState(false); const [selectedMinerScenarioId, setSelectedMinerScenarioId] = useState(null); const [minerOfferFilters, setMinerOfferFilters] = useState({ + offer_type: 'crypto', speed_min: '', speed_unit: 'auto', price_max: '', @@ -944,11 +945,9 @@ const [walletWithdrawalRows, setWalletWithdrawalRows] = useState([]); const [minerOfferForm, setMinerOfferForm] = useState({ label: '', - runtime_months: '', - bonus_percent: '', base_price_amount: '', base_price_currency: 'USD', - payment_type: 'fiat', + payment_type: 'crypto', auto_renew: false, note: '', is_active: true, @@ -1036,8 +1035,9 @@ : currencies; const selectableCurrencies = preferredSelectableCurrencies.length ? preferredSelectableCurrencies : currencies; const evaluatedMinerOffers = Array.isArray(payload?.summary?.miner_offers) ? payload.summary.miner_offers : []; - const availableMinerOffers = evaluatedMinerOffers.filter((offer) => isOfferAvailableForWallet(offer, currentMiningCurrency, currentWalletMiningBalance)); + const selectedOfferType = String(minerOfferFilters.offer_type || 'crypto').toLowerCase() === 'fiat' ? 'fiat' : 'crypto'; const filteredMinerOffers = evaluatedMinerOffers.filter((offer) => { + const paymentType = String(offer.payment_type || '').toLowerCase(); const speedMin = minerOfferFilters.speed_min === '' ? null : Number(minerOfferFilters.speed_min); const speedUnit = String(minerOfferFilters.speed_unit || 'auto'); const priceMax = minerOfferFilters.price_max === '' ? null : Number(minerOfferFilters.price_max); @@ -1053,6 +1053,18 @@ ? offerHashrate * 1000 : offerHashrate; + if (paymentType !== selectedOfferType) { + return false; + } + + if (selectedOfferType === 'crypto' && !isOfferAvailableForWallet(offer, currentMiningCurrency, currentWalletMiningBalance)) { + return false; + } + + if (selectedOfferType === 'fiat' && !Number.isFinite(speedMin)) { + return false; + } + if (Number.isFinite(speedMin) && (!Number.isFinite(comparableHashrate) || comparableHashrate < speedMin)) { return false; } @@ -1067,6 +1079,7 @@ return true; }); + const scenarioMinerOffers = filteredMinerOffers.filter((offer) => isOfferAvailableForWallet(offer, currentMiningCurrency, currentWalletMiningBalance)); const speedUnits = ['kH/s', 'MH/s']; const fiatCurrencies = currencies.filter((currency) => !currency.is_crypto); const cryptoCurrencies = currencies.filter((currency) => !!currency.is_crypto); @@ -1078,7 +1091,7 @@ : cryptoCurrencies; const selectableFiatCurrencies = preferredSelectableFiatCurrencies.length ? preferredSelectableFiatCurrencies : fiatCurrencies; const selectableCryptoCurrencies = preferredSelectableCryptoCurrencies.length ? preferredSelectableCryptoCurrencies : cryptoCurrencies; - const selectedMinerScenario = availableMinerOffers.find((offer) => String(offer.id) === String(selectedMinerScenarioId)) || null; + const selectedMinerScenario = scenarioMinerOffers.find((offer) => String(offer.id) === String(selectedMinerScenarioId)) || null; const activeMinerRows = currentPurchasedMiners.map((miner) => ({ id: `purchase-${miner.id}`, source: 'miete', @@ -1130,19 +1143,19 @@ return; } - const exists = availableMinerOffers.some((offer) => String(offer.id) === String(selectedMinerScenarioId)); + const exists = scenarioMinerOffers.some((offer) => String(offer.id) === String(selectedMinerScenarioId)); if (!exists) { setSelectedMinerScenarioId(null); } - }, [availableMinerOffers, selectedMinerScenarioId]); + }, [scenarioMinerOffers, selectedMinerScenarioId]); useEffect(() => { if (!purchaseMinerModalOpen) { return; } - const selectedOffer = availableMinerOffers.find((offer) => String(offer.id) === String(purchaseMinerForm.offer_id)) - || availableMinerOffers[0] + const selectedOffer = scenarioMinerOffers.find((offer) => String(purchaseMinerForm.offer_id) === String(offer.id)) + || scenarioMinerOffers[0] || null; if (!selectedOffer) { return; @@ -1162,7 +1175,7 @@ reference_price_currency: current.reference_price_currency || selectedOffer.reference_price_currency || '', auto_renew: current.auto_renew || !!selectedOffer.auto_renew, })); - }, [purchaseMinerModalOpen, availableMinerOffers, purchaseMinerForm.offer_id, currentSettings.crypto_currency]); + }, [purchaseMinerModalOpen, scenarioMinerOffers, purchaseMinerForm.offer_id, currentSettings.crypto_currency]); function measurementFxRate(measurementId, fromCurrency, toCurrency) { const from = String(fromCurrency || '').toUpperCase(); @@ -2104,7 +2117,9 @@ try { const offerPayload = { ...minerOfferForm, - base_price_currency: minerOfferForm.payment_type === 'crypto' ? 'USD' : minerOfferForm.base_price_currency, + base_price_currency: minerOfferForm.payment_type === 'crypto' + ? (minerOfferForm.base_price_currency || 'USD') + : 'EUR', }; await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/miner-offers`, { method: 'POST', @@ -2114,11 +2129,9 @@ setMessage('Miner-Angebot gespeichert.'); setMinerOfferForm({ label: '', - runtime_months: '', - bonus_percent: '', base_price_amount: '', base_price_currency: 'USD', - payment_type: 'fiat', + payment_type: 'crypto', auto_renew: false, note: '', is_active: true, @@ -2173,7 +2186,7 @@ return; } - const selectedOffer = availableMinerOffers.find((offer) => String(offer.id) === String(purchaseMinerForm.offer_id)); + const selectedOffer = scenarioMinerOffers.find((offer) => String(offer.id) === String(purchaseMinerForm.offer_id)); if (!selectedOffer) { setError('Bitte ein gueltiges Miner-Angebot auswaehlen.'); return; @@ -2990,7 +3003,7 @@ }); setPurchaseMinerModalOpen(true); }, - disabled: !availableMinerOffers.length, + disabled: !scenarioMinerOffers.length, }, 'Neuen Miner mieten'), ]), h('div', { key: 'list', className: 'mc-table-shell' }, [ @@ -3036,9 +3049,11 @@ ]), ]), ]), - panel('Miner-Angebote', currentWalletMiningBalance !== null - ? `Hier werden nur Basis-Miner gepflegt. Die Verfuegbarkeit von Krypto-Angeboten wird gegen deinen aktuellen NC-Wallet-Bestand (${fmtNumber(currentWalletMiningBalance, 8)} ${currentMiningCurrency}) bewertet.` - : 'Hier werden nur Basis-Miner gepflegt. Alle vorgegebenen Geschwindigkeiten und Preise werden daraus automatisch abgeleitet.', + panel('Miner-Angebote', selectedOfferType === 'crypto' + ? (currentWalletMiningBalance !== null + ? `Crypto-Angebote sind aktiv. Angezeigt werden nur Angebote, die dein NC-Wallet aktuell tragen kann (${fmtNumber(currentWalletMiningBalance, 8)} ${currentMiningCurrency}).` + : 'Crypto-Angebote sind aktiv. Solange kein Wallet-Bestand erkannt wird, bleiben die Krypto-Angebote sichtbar.') + : 'Fiat-Angebote sind aktiv. Die Berechnung startet erst, wenn du eine Geschwindigkeit eingibst.', [ h('div', { key: 'actions', className: 'mc-inline-row' }, [ h('button', { @@ -3049,7 +3064,11 @@ }, 'Basis-Angebot anlegen'), ]), h('div', { key: 'filters', className: 'mc-filter-grid' }, [ - inputField(`Min. Geschwindigkeit (${minerOfferFilters.speed_unit === 'kh' ? 'kH/s' : 'MH/s'})`, 'number', minerOfferFilters.speed_min, (value) => setMinerOfferFilters({ ...minerOfferFilters, speed_min: value }), '0.0001'), + selectField('Angebotsart', minerOfferFilters.offer_type, [ + { value: 'crypto', label: 'Crypto' }, + { value: 'fiat', label: 'Fiat' }, + ], (value) => setMinerOfferFilters({ ...minerOfferFilters, offer_type: value || 'crypto' })), + inputField(`${selectedOfferType === 'fiat' ? 'Geschwindigkeit' : 'Min. Geschwindigkeit'} (${minerOfferFilters.speed_unit === 'kh' ? 'kH/s' : 'MH/s'})`, 'number', minerOfferFilters.speed_min, (value) => setMinerOfferFilters({ ...minerOfferFilters, speed_min: value }), '0.0001'), selectField('Geschwindigkeitseinheit', minerOfferFilters.speed_unit, [ { value: 'auto', label: 'MH/s' }, { value: 'kh', label: 'kH/s' }, @@ -3140,7 +3159,7 @@ }, 'Mieten'), ]), ])) - : [h('tr', { key: 'empty' }, h('td', { colSpan: 7 }, 'Keine Angebote passen auf die gesetzten Filter.'))] + : [h('tr', { key: 'empty' }, h('td', { colSpan: 7 }, selectedOfferType === 'fiat' && minerOfferFilters.speed_min === '' ? 'Bitte zuerst eine Geschwindigkeit eingeben, damit Fiat-Angebote berechnet werden.' : 'Keine Angebote passen auf die gesetzten Filter.'))] ), ]), ]), @@ -3301,20 +3320,20 @@ minerOfferModalOpen ? renderModal('Basis-Miner-Angebot anlegen', [ h('form', { key: 'form', className: 'mc-form', onSubmit: submitMinerOffer }, [ inputField('Label', 'text', minerOfferForm.label, (value) => setMinerOfferForm({ ...minerOfferForm, label: value })), - 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(minerOfferForm.payment_type === 'crypto' ? 'Basispreis in USD' : 'Basispreis', 'number', minerOfferForm.base_price_amount, (value) => setMinerOfferForm({ ...minerOfferForm, base_price_amount: value }), '0.000001'), + displayField('Basis-Laufzeit', '3 Monate'), + displayField('Ableitung', baseOfferDerivationLabel(minerOfferForm.payment_type)), + inputField(baseOfferPriceLabel(minerOfferForm.payment_type, currentSettings.crypto_currency), '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' ? 'USD' - : (selectableFiatCurrencies[0]?.code || 'USD'), + : 'EUR', })), 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 })), + ? selectField('Basiswaehrung', minerOfferForm.base_price_currency, Array.from(new Set(['USD', String(currentSettings.crypto_currency || 'DOGE').toUpperCase()])), (value) => setMinerOfferForm({ ...minerOfferForm, base_price_currency: value || 'USD' })) + : displayField('Basiswaehrung', 'EUR'), 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', @@ -3332,11 +3351,11 @@ ], () => setMinerOfferModalOpen(false)) : null, purchaseMinerModalOpen ? renderModal('Neuen Miner mieten', [ h('form', { key: 'form', className: 'mc-form', onSubmit: submitPurchaseMiner }, [ - selectField('Angebot', purchaseMinerForm.offer_id, [{ value: '', label: 'Bitte waehlen' }].concat(availableMinerOffers.map((offer) => ({ + selectField('Angebot', purchaseMinerForm.offer_id, [{ value: '', label: 'Bitte waehlen' }].concat(scenarioMinerOffers.map((offer) => ({ value: String(offer.id), label: `${offer.label} · ${fmtNumber(offer.effective_price_amount, 6)} ${offer.effective_price_currency}`, }))), (value) => { - const offer = availableMinerOffers.find((item) => String(item.id) === String(value)); + const offer = scenarioMinerOffers.find((item) => String(item.id) === String(value)); setPurchaseMinerForm({ offer_id: value, base_offer_id: offer ? String(offer.base_offer_id || offer.id || '') : '', @@ -3666,7 +3685,19 @@ } function baseOfferSpeedLabel(paymentType) { - return String(paymentType || 'fiat') === 'crypto' ? '75 kH/s' : '50 kH/s'; + return String(paymentType || 'fiat') === 'crypto' ? '50 kH/s' : '100 kH/s'; + } + + function baseOfferPriceLabel(paymentType, cryptoCurrency) { + return String(paymentType || 'fiat') === 'crypto' + ? `Basispreis fuer 50 kH/s auf 3 Monate in USD oder ${String(cryptoCurrency || 'DOGE').toUpperCase()}` + : 'Basispreis fuer 100 kH/s auf 3 Monate in EUR'; + } + + function baseOfferDerivationLabel(paymentType) { + return String(paymentType || 'fiat') === 'crypto' + ? 'Varianten fuer 3, 6, 12, 18, 24 und 36 Monate mit festen Rabatten und Bonuswerten' + : 'Varianten fuer 3, 6 und 12 Monate mit festen Rabatten und Bonuswerten'; } function formatHashrateWithBonus(speedValue, speedUnit, bonusValue, bonusUnit) { diff --git a/custom/apps/mining-checker/src/Api/Router.php b/custom/apps/mining-checker/src/Api/Router.php index 9306af30..3ed90ca8 100644 --- a/custom/apps/mining-checker/src/Api/Router.php +++ b/custom/apps/mining-checker/src/Api/Router.php @@ -30,8 +30,8 @@ final class Router private const OVERVIEW_WINDOW_DAYS = 15; private const FX_FETCH_MAX_AGE_HOURS = 3.0; private const BASE_OFFER_SPEEDS = [ - 'fiat' => ['value' => 50.0, 'unit' => 'kH/s'], - 'crypto' => ['value' => 75.0, 'unit' => 'kH/s'], + 'fiat' => ['value' => 100.0, 'unit' => 'kH/s'], + 'crypto' => ['value' => 50.0, 'unit' => 'kH/s'], ]; private string $moduleBasePath; @@ -1907,18 +1907,23 @@ 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 = $paymentType === 'crypto' - ? 'USD' - : $this->requiredCurrency($input['base_price_currency'] ?? ($input['reference_price_currency'] ?? $input['price_currency'] ?? null), 'base_price_currency'); + $projectCryptoCurrency = $this->requiredCurrency($this->settings($projectKey, [ + 'cost_plans' => false, + 'currencies' => false, + 'payouts' => false, + 'wallet_withdrawals' => false, + 'miner_offers' => false, + 'purchased_miners' => false, + ])['crypto_currency'] ?? 'DOGE', 'crypto_currency'); + $basePriceCurrency = $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), + 'runtime_months' => 3, 'mining_speed_value' => $baseSpeed['value'], 'mining_speed_unit' => $baseSpeed['unit'], - 'bonus_speed_value' => $this->bonusSpeedFromPercent((float) $baseSpeed['value'], (string) $baseSpeed['unit'], $bonusPercent), - 'bonus_speed_unit' => $bonusPercent !== null ? (string) $baseSpeed['unit'] : null, - 'bonus_percent' => $bonusPercent, + 'bonus_speed_value' => null, + 'bonus_speed_unit' => null, + 'bonus_percent' => null, 'base_price_amount' => $this->requiredDecimal($input['base_price_amount'] ?? ($input['reference_price_amount'] ?? $input['price_amount'] ?? null), 'base_price_amount'), 'base_price_currency' => $basePriceCurrency, 'payment_type' => $paymentType, @@ -1927,12 +1932,23 @@ final class Router 'is_active' => !empty($input['is_active']) ? 1 : 0, ]; - if ($bonusPercent !== null && $bonusPercent < 0) { - throw new ApiException('Bonus-Prozent darf nicht negativ sein.', 422); + if ($paymentType === 'crypto') { + $allowedCryptoCurrencies = array_values(array_unique(['USD', $projectCryptoCurrency])); + if (!in_array($payload['base_price_currency'], $allowedCryptoCurrencies, true)) { + throw new ApiException('Bei Krypto-Angeboten sind nur USD oder die Projekt-Kryptowaehrung erlaubt.', 422, [ + 'field' => 'base_price_currency', + 'allowed' => $allowedCryptoCurrencies, + ]); + } + } else { + if ($payload['base_price_currency'] !== 'EUR') { + throw new ApiException('Bei FIAT-Angeboten ist aktuell nur EUR als Basiswaehrung erlaubt.', 422, [ + 'field' => 'base_price_currency', + 'allowed' => ['EUR'], + ]); + } } - $this->assertCurrencyType($payload['base_price_currency'], false, 'base_price_currency'); - return $this->repository()->saveMinerOffer($projectKey, $payload); } diff --git a/custom/apps/mining-checker/src/Domain/AnalyticsService.php b/custom/apps/mining-checker/src/Domain/AnalyticsService.php index c103821f..2ae0181e 100644 --- a/custom/apps/mining-checker/src/Domain/AnalyticsService.php +++ b/custom/apps/mining-checker/src/Domain/AnalyticsService.php @@ -8,13 +8,51 @@ use Modules\MiningChecker\Support\ApiException; final class AnalyticsService { private const BASE_OFFER_SPEEDS = [ - 'fiat' => ['value' => 50.0, 'unit' => 'kH/s'], - 'crypto' => ['value' => 75.0, 'unit' => 'kH/s'], + 'fiat' => ['value' => 100.0, 'unit' => 'kH/s'], + 'crypto' => ['value' => 50.0, 'unit' => 'kH/s'], ]; - private const OFFER_SPEED_MULTIPLIERS = [ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, - 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, + private const OFFER_SPEED_OPTIONS = [ + ['value' => 50.0, 'unit' => 'kH/s'], + ['value' => 75.0, 'unit' => 'kH/s'], + ['value' => 100.0, 'unit' => 'kH/s'], + ['value' => 150.0, 'unit' => 'kH/s'], + ['value' => 200.0, 'unit' => 'kH/s'], + ['value' => 250.0, 'unit' => 'kH/s'], + ['value' => 300.0, 'unit' => 'kH/s'], + ['value' => 350.0, 'unit' => 'kH/s'], + ['value' => 400.0, 'unit' => 'kH/s'], + ['value' => 450.0, 'unit' => 'kH/s'], + ['value' => 500.0, 'unit' => 'kH/s'], + ['value' => 1.0, 'unit' => 'MH/s'], + ['value' => 5.0, 'unit' => 'MH/s'], + ['value' => 10.0, 'unit' => 'MH/s'], + ['value' => 15.0, 'unit' => 'MH/s'], + ['value' => 20.0, 'unit' => 'MH/s'], + ['value' => 30.0, 'unit' => 'MH/s'], + ['value' => 50.0, 'unit' => 'MH/s'], + ['value' => 75.0, 'unit' => 'MH/s'], + ['value' => 100.0, 'unit' => 'MH/s'], + ['value' => 150.0, 'unit' => 'MH/s'], + ['value' => 200.0, 'unit' => 'MH/s'], + ['value' => 250.0, 'unit' => 'MH/s'], + ['value' => 300.0, 'unit' => 'MH/s'], + ]; + + private const OFFER_RUNTIME_CONFIGS = [ + 'crypto' => [ + ['months' => 3, 'discount_percent' => 0.0, 'bonus_percent' => 5.0], + ['months' => 6, 'discount_percent' => 10.0, 'bonus_percent' => 10.0], + ['months' => 12, 'discount_percent' => 35.0, 'bonus_percent' => 10.0], + ['months' => 18, 'discount_percent' => 45.0, 'bonus_percent' => 15.0], + ['months' => 24, 'discount_percent' => 55.0, 'bonus_percent' => 20.0], + ['months' => 36, 'discount_percent' => 65.0, 'bonus_percent' => 20.0], + ], + 'fiat' => [ + ['months' => 3, 'discount_percent' => 0.0, 'bonus_percent' => 5.0], + ['months' => 6, 'discount_percent' => 10.0, 'bonus_percent' => 10.0], + ['months' => 12, 'discount_percent' => 35.0, 'bonus_percent' => 10.0], + ], ]; private ?FxService $fx; @@ -970,7 +1008,6 @@ final class AnalyticsService $baseSpeed = self::BASE_OFFER_SPEEDS[$paymentType] ?? self::BASE_OFFER_SPEEDS['fiat']; $baseSpeedValue = (float) $baseSpeed['value']; $baseSpeedUnit = (string) $baseSpeed['unit']; - $baseBonusPercent = $this->deriveBonusPercent($offer, $baseSpeedValue, $baseSpeedUnit); $basePriceAmount = is_numeric($offer['base_price_amount'] ?? null) ? (float) $offer['base_price_amount'] : (is_numeric($offer['reference_price_amount'] ?? null) @@ -978,33 +1015,60 @@ final class AnalyticsService : (is_numeric($offer['usd_reference_amount'] ?? null) ? (float) $offer['usd_reference_amount'] : (is_numeric($offer['price_amount'] ?? null) ? (float) $offer['price_amount'] : null))); + $baseSpeedMh = $this->normalizeHashrateMh($baseSpeedValue, $baseSpeedUnit); + $runtimeConfigs = self::OFFER_RUNTIME_CONFIGS[$paymentType] ?? self::OFFER_RUNTIME_CONFIGS['fiat']; - foreach (self::OFFER_SPEED_MULTIPLIERS as $multiplier) { - $derivedSpeedValue = $baseSpeedValue * $multiplier; - $derivedBonusValue = $baseBonusPercent !== null - ? round($derivedSpeedValue * ($baseBonusPercent / 100), 4) - : null; - $derivedPriceAmount = $basePriceAmount !== null - ? round($basePriceAmount * $multiplier, 8) - : null; + foreach ($runtimeConfigs as $runtimeConfig) { + $runtimeMonths = (int) ($runtimeConfig['months'] ?? 0); + $discountPercent = (float) ($runtimeConfig['discount_percent'] ?? 0.0); + $bonusPercent = (float) ($runtimeConfig['bonus_percent'] ?? 0.0); + if ($runtimeMonths <= 0 || $baseSpeedMh <= 0) { + continue; + } - $expanded[] = array_merge($offer, [ - 'id' => sprintf('%s:%s:%s', (string) ($offer['id'] ?? 'offer'), $paymentType, rtrim(rtrim(number_format($derivedSpeedValue, 4, '.', ''), '0'), '.')), - 'base_offer_id' => $offer['id'] ?? null, - 'base_offer_label' => $offer['label'] ?? null, - 'payment_type' => $paymentType, - 'label' => trim(((string) ($offer['label'] ?? 'Miner-Angebot')) . ' · ' . $this->formatSpeedLabel($derivedSpeedValue, $baseSpeedUnit)), - 'mining_speed_value' => $derivedSpeedValue, - 'mining_speed_unit' => $baseSpeedUnit, - 'bonus_speed_value' => $derivedBonusValue, - 'bonus_speed_unit' => $derivedBonusValue !== null ? $baseSpeedUnit : null, - 'bonus_percent' => $baseBonusPercent, - 'speed_factor' => $multiplier, - 'base_speed_value' => $baseSpeedValue, - 'base_speed_unit' => $baseSpeedUnit, - 'base_price_amount' => $derivedPriceAmount, - 'reference_price_amount' => $derivedPriceAmount, - ]); + foreach (self::OFFER_SPEED_OPTIONS as $speedOption) { + $derivedSpeedValue = (float) ($speedOption['value'] ?? 0.0); + $derivedSpeedUnit = (string) ($speedOption['unit'] ?? ''); + $derivedSpeedMh = $this->normalizeHashrateMh($derivedSpeedValue, $derivedSpeedUnit); + if ($derivedSpeedMh <= 0) { + continue; + } + + $speedFactor = $derivedSpeedMh / $baseSpeedMh; + $runtimeFactor = $runtimeMonths / 3.0; + $discountFactor = max(0.0, 1.0 - ($discountPercent / 100.0)); + $derivedBonusValue = round($derivedSpeedValue * ($bonusPercent / 100.0), 4); + $derivedPriceAmount = $basePriceAmount !== null + ? round($basePriceAmount * $speedFactor * $runtimeFactor * $discountFactor, 8) + : null; + + $expanded[] = array_merge($offer, [ + 'id' => sprintf( + '%s:%s:%s:%s', + (string) ($offer['id'] ?? 'offer'), + $paymentType, + $runtimeMonths, + rtrim(rtrim(number_format($derivedSpeedMh, 4, '.', ''), '0'), '.') + ), + 'base_offer_id' => $offer['id'] ?? null, + 'base_offer_label' => $offer['label'] ?? null, + 'payment_type' => $paymentType, + 'label' => trim(((string) ($offer['label'] ?? 'Miner-Angebot')) . ' · ' . $this->formatSpeedLabel($derivedSpeedValue, $derivedSpeedUnit) . ' · ' . $runtimeMonths . ' Monate'), + 'runtime_months' => $runtimeMonths, + 'mining_speed_value' => $derivedSpeedValue, + 'mining_speed_unit' => $derivedSpeedUnit, + 'bonus_speed_value' => $derivedBonusValue > 0 ? $derivedBonusValue : null, + 'bonus_speed_unit' => $derivedBonusValue > 0 ? $derivedSpeedUnit : null, + 'bonus_percent' => $bonusPercent, + 'discount_percent' => $discountPercent, + 'speed_factor' => $speedFactor, + 'base_speed_value' => $baseSpeedValue, + 'base_speed_unit' => $baseSpeedUnit, + 'base_runtime_months' => 3, + 'base_price_amount' => $derivedPriceAmount, + 'reference_price_amount' => $derivedPriceAmount, + ]); + } } } @@ -1019,7 +1083,7 @@ final class AnalyticsService return $runtimeCompare; } - return ((int) ($left['speed_factor'] ?? 0)) <=> ((int) ($right['speed_factor'] ?? 0)); + return ((float) ($left['speed_factor'] ?? 0.0)) <=> ((float) ($right['speed_factor'] ?? 0.0)); }); return $expanded;