From 0a196217485c54bb8753dd08f5e1525d56281cea Mon Sep 17 00:00:00 2001 From: Lars Gebhardt-Kusche Date: Fri, 21 Aug 2026 00:46:29 +0200 Subject: [PATCH] calculation --- custom/apps/mining-checker/assets/js/app.js | 45 +++++++++---- .../apps/mining-checker/sql/schema.mysql.sql | 2 + .../apps/mining-checker/sql/schema.pgsql.sql | 2 + custom/apps/mining-checker/sql/schema.sql | 2 + custom/apps/mining-checker/src/Api/Router.php | 21 +++++- .../src/Domain/AnalyticsService.php | 66 ++++++++++++++++--- .../src/Infrastructure/MiningRepository.php | 20 +++--- .../src/Infrastructure/SchemaManager.php | 47 +++++++++++++ 8 files changed, 175 insertions(+), 30 deletions(-) diff --git a/custom/apps/mining-checker/assets/js/app.js b/custom/apps/mining-checker/assets/js/app.js index da112979..02adaa30 100644 --- a/custom/apps/mining-checker/assets/js/app.js +++ b/custom/apps/mining-checker/assets/js/app.js @@ -1088,6 +1088,7 @@ const value = String(getCookie('mining_checker_report_currency') || '').toUpperCase(); return /^[A-Z0-9]{3,10}$/.test(value) ? value : ''; }); + const [nowTimestamp, setNowTimestamp] = useState(() => Date.now()); const [targetForm, setTargetForm] = useState({ label: '', target_amount_fiat: '', @@ -1359,17 +1360,24 @@ const preferredOfferRequiredFiatValue = Number.isFinite(preferredOfferRequiredCoins) ? convertCurrencyValue(preferredOfferRequiredCoins, currentMiningCurrency, reportCurrency) : null; - const preferredOfferRemainingDays = preferredOfferRemainingCoins !== null + const preferredOfferRemainingDaysAtUpload = preferredOfferRemainingCoins !== null && Number(latest?.doge_per_day_interval) > 0 ? (preferredOfferRemainingCoins / Number(latest.doge_per_day_interval)) : null; - const preferredOfferEta = preferredOfferRemainingDays !== null - ? ( - preferredOfferRemainingDays <= 0 - ? new Date().toISOString() - : new Date(Date.now() + (preferredOfferRemainingDays * 86400000)).toISOString() - ) + const preferredOfferUploadTimestamp = latest?.measured_at ? Date.parse(latest.measured_at) : NaN; + const preferredOfferEtaTimestamp = preferredOfferRemainingDaysAtUpload !== null && Number.isFinite(preferredOfferUploadTimestamp) + ? preferredOfferUploadTimestamp + (preferredOfferRemainingDaysAtUpload * 86400000) + : NaN; + const preferredOfferRemainingDays = Number.isFinite(preferredOfferEtaTimestamp) + ? Math.max(0, (preferredOfferEtaTimestamp - nowTimestamp) / 86400000) : null; + const preferredOfferEta = Number.isFinite(preferredOfferEtaTimestamp) + ? new Date(preferredOfferEtaTimestamp).toISOString() + : null; + const remainingDaysUntil = (timestamp) => { + const etaTimestamp = timestamp ? new Date(timestamp).getTime() : NaN; + return Number.isFinite(etaTimestamp) ? Math.max(0, (etaTimestamp - nowTimestamp) / 86400000) : null; + }; const speedUnits = ['kH/s', 'MH/s']; const fiatCurrencies = currencies.filter((currency) => !currency.is_crypto); const cryptoCurrencies = currencies.filter((currency) => !!currency.is_crypto); @@ -1520,6 +1528,11 @@ } }, [reportCurrencyOverride]); + useEffect(() => { + const intervalId = window.setInterval(() => setNowTimestamp(Date.now()), 60000); + return () => window.clearInterval(intervalId); + }, []); + useEffect(() => { if (selectedMinerScenarioId === null) { return; @@ -3176,6 +3189,8 @@ const reinvestedCapital = latest ? convertMeasurementMoney(latest, latest.reinvested_capital, reportCurrency) : null; const walletValue = latest ? convertMeasurementMoney(latest, latest.wallet_value, reportCurrency) : null; const totalHoldingsValue = latest ? convertMeasurementMoney(latest, latest.total_holdings_value, reportCurrency) : null; + const earnedValue = latest ? convertMeasurementMoney(latest, latest.earned_value, reportCurrency) : null; + const settledCryptoSpendValue = latest ? convertMeasurementMoney(latest, latest.settled_crypto_spend_value, reportCurrency) : null; const totalEarnedOverall = latest && latest.growth_since_baseline !== null && latest.growth_since_baseline !== undefined ? Number(latest.growth_since_baseline) : null; @@ -3298,14 +3313,15 @@ }), h(StatCard, { key: 'value', - label: 'Aktueller Gegenwert', - value: totalHoldingsValue !== null ? fmtMoney(totalHoldingsValue, reportCurrency) : 'n/a', + label: 'Bisheriger Wert', + value: earnedValue !== null ? fmtMoney(earnedValue, reportCurrency) : 'n/a', sub: [ holdingsCurrentAsset !== null && holdingsCurrentAsset !== undefined ? `Gesamtbestand ${fmtNumber(holdingsCurrentAsset, 6)} ${currentCoinCurrency}` : null, latestValue !== null ? `Miner ${fmtMoney(latestValue, reportCurrency)}` : null, walletValue !== null ? `Wallet ${fmtMoney(walletValue, reportCurrency)}` : null, + settledCryptoSpendValue !== null ? `Ausgegeben ${fmtMoney(settledCryptoSpendValue, reportCurrency)}` : null, latestPrice !== null ? `Kurs ${fmtNumber(latestPrice, 6)} ${reportCurrency}${latest && latest.price_is_fallback ? ' · Fallback aus letztem Kurs' : ''}` : null, @@ -3327,6 +3343,9 @@ preferredOfferRemainingCoins !== null ? `Rest ${fmtNumber(preferredOfferRemainingCoins, 4)} ${currentMiningCurrency}` : null, + latest?.doge_per_day_interval !== null && latest?.doge_per_day_interval !== undefined + ? `Basis ${fmtNumber(latest.doge_per_day_interval, 4)} ${currentMiningCurrency}/Tag ab Upload` + : null, preferredOfferEta ? `ETA ${fmtDate(preferredOfferEta)}` : null, ].filter(Boolean).join(' · '), }), @@ -3368,7 +3387,7 @@ yLabel: `Kurs ${reportCurrency}`, })), ]), - panel('Zielmonitor', `Rest-${currentCoinCurrency} und Resttage werden gegen den letzten verfuegbaren Kurs je Zielwaehrung berechnet.`, + panel('Zielmonitor', `Die ETA wird ab dem letzten Upload mit dessen durchschnittlicher Tagesrate berechnet; die Restzeit laeuft bis zum naechsten Upload weiter.`, h('div', { className: 'mc-target-grid' }, currentTargets.map((target, index) => h('div', { key: index, @@ -3383,7 +3402,7 @@ h('div', { key: 'price' }, `Letzter Kurs: ${target.latest_price_for_currency ? fmtNumber(target.latest_price_for_currency, 6) + ' ' + target.currency : 'n/a'}`), h('div', { key: 'doge' }, `Benoetigte ${currentCoinCurrency}: ${fmtNumber(target.required_doge, 6)}`), h('div', { key: 'remaining' }, `Rest-${currentCoinCurrency}: ${fmtNumber(target.remaining_doge, 6)}`), - h('div', { key: 'days' }, `Resttage: ${fmtNumber(target.remaining_days, 4)}`), + h('div', { key: 'days' }, `Resttage: ${fmtNumber(remainingDaysUntil(target.target_eta_at), 4)}`), ]), ])) ) @@ -3529,7 +3548,7 @@ ]); })) : h('div', { className: 'mc-empty' }, 'Noch keine Wallet-Assets erkannt.')), - panel('Wallet-Transfers', 'Transfers zum hier verfolgten Wallet erhoehen den Wallet-Bestand und reduzieren den sichtbaren Miner-Bestand. Reinvestierte Krypto-Minerkaeufe werden davon automatisch wieder abgezogen.', [ + panel('Wallet-Transfers', 'Auszahlungen aus dem Mining-Tool erhoehen den Wallet-Bestand automatisch. Ein Screenshot setzt den Bestand zum Screenshot-Zeitpunkt verbindlich. Krypto-Minerkaeufe ziehen Coins wieder ab und bleiben zum Kaufkurs festgeschrieben.', [ h('div', { key: 'actions', className: 'mc-inline-row' }, [ h('button', { key: 'transfer-all', @@ -4065,7 +4084,7 @@ h('div', { key: 'currency-main' }, target.effective_currency || target.currency), target.linked_offer_label ? h('div', { key: 'currency-offer', className: 'mc-kicker' }, `Angebot ${target.linked_offer_label}`) : null, ]), - h('td', { key: 'days' }, target.remaining_days !== null && target.remaining_days !== undefined ? fmtNumber(target.remaining_days, 2) : 'n/a'), + h('td', { key: 'days' }, remainingDaysUntil(target.target_eta_at) !== null ? fmtNumber(remainingDaysUntil(target.target_eta_at), 2) : 'n/a'), h('td', { key: 'eta' }, target.target_eta_at ? fmtDateTime(target.target_eta_at) : 'n/a'), h('td', { key: 'sort' }, String(target.sort_order ?? 0)), h('td', { key: 'active' }, target.is_active ? 'ja' : 'nein'), diff --git a/custom/apps/mining-checker/sql/schema.mysql.sql b/custom/apps/mining-checker/sql/schema.mysql.sql index 7c230cc0..7df01ade 100644 --- a/custom/apps/mining-checker/sql/schema.mysql.sql +++ b/custom/apps/mining-checker/sql/schema.mysql.sql @@ -222,6 +222,8 @@ CREATE TABLE IF NOT EXISTS miningcheck_purchased_miners ( usd_reference_amount DECIMAL(20,8) NULL, reference_price_amount DECIMAL(20,8) NULL, reference_price_currency VARCHAR(10) NULL, + settled_value_amount DECIMAL(20,8) NULL, + settled_value_currency VARCHAR(10) NULL, daily_cost_amount DECIMAL(20,10) NULL, daily_cost_currency VARCHAR(10) NULL, auto_renew TINYINT(1) NOT NULL DEFAULT 0, diff --git a/custom/apps/mining-checker/sql/schema.pgsql.sql b/custom/apps/mining-checker/sql/schema.pgsql.sql index 4f82c4e7..8236e584 100644 --- a/custom/apps/mining-checker/sql/schema.pgsql.sql +++ b/custom/apps/mining-checker/sql/schema.pgsql.sql @@ -239,6 +239,8 @@ CREATE TABLE IF NOT EXISTS miningcheck_purchased_miners ( usd_reference_amount NUMERIC(20,8), reference_price_amount NUMERIC(20,8), reference_price_currency VARCHAR(10), + settled_value_amount NUMERIC(20,8), + settled_value_currency VARCHAR(10), daily_cost_amount NUMERIC(20,10), daily_cost_currency VARCHAR(10), auto_renew BOOLEAN NOT NULL DEFAULT FALSE, diff --git a/custom/apps/mining-checker/sql/schema.sql b/custom/apps/mining-checker/sql/schema.sql index 7c230cc0..7df01ade 100644 --- a/custom/apps/mining-checker/sql/schema.sql +++ b/custom/apps/mining-checker/sql/schema.sql @@ -222,6 +222,8 @@ CREATE TABLE IF NOT EXISTS miningcheck_purchased_miners ( usd_reference_amount DECIMAL(20,8) NULL, reference_price_amount DECIMAL(20,8) NULL, reference_price_currency VARCHAR(10) NULL, + settled_value_amount DECIMAL(20,8) NULL, + settled_value_currency VARCHAR(10) NULL, daily_cost_amount DECIMAL(20,10) NULL, daily_cost_currency VARCHAR(10) NULL, auto_renew TINYINT(1) NOT NULL DEFAULT 0, diff --git a/custom/apps/mining-checker/src/Api/Router.php b/custom/apps/mining-checker/src/Api/Router.php index 418a3d25..91d9aac5 100644 --- a/custom/apps/mining-checker/src/Api/Router.php +++ b/custom/apps/mining-checker/src/Api/Router.php @@ -2092,8 +2092,10 @@ final class Router $resolvedBonusUnit = $resolvedBonusValue !== null ? $resolvedSpeedUnit : null; $purchaseCurrency = $this->optionalCurrency($input['currency'] ?? null) ?? (string) ($offer['effective_price_currency'] ?? $offer['price_currency'] ?? $offer['base_price_currency'] ?? ''); - if ($paymentType === 'crypto' || !$isAutoRenew) { + if ($paymentType === 'crypto') { $purchaseCurrency = $cryptoCurrency; + } elseif ($purchaseCurrency === '' || $this->isCryptoCurrencyCode($purchaseCurrency)) { + $purchaseCurrency = $this->requiredCurrency($settings['report_currency'] ?? 'EUR', 'report_currency'); } $purchaseCost = $this->optionalDecimal($input['total_cost_amount'] ?? null); if ($purchaseCost === null) { @@ -2112,6 +2114,21 @@ final class Router $purchasedAt = array_key_exists('purchased_at', $input) ? $this->requiredDateTime($input['purchased_at'], 'purchased_at', $this->projectTimezone($projectKey)) : $this->currentTimestamp(); + $settledValueAmount = null; + $settledValueCurrency = null; + if ($this->isCryptoCurrencyCode($purchaseCurrency)) { + $settledValueCurrency = $this->requiredCurrency($settings['report_currency'] ?? 'EUR', 'report_currency'); + $settledValueAmount = $purchaseCurrency === $settledValueCurrency + ? $purchaseCost + : $this->convertMiningAssetAmount((float) $purchaseCost, $purchaseCurrency, $settledValueCurrency, $purchasedAt); + if ($settledValueAmount === null) { + throw new ApiException( + 'Der aktuelle Kurs fuer die Krypto-Zahlung konnte nicht ermittelt werden. Der Miner wird nicht ohne feste Bewertung gespeichert.', + 422, + ['currency' => $purchaseCurrency, 'target_currency' => $settledValueCurrency] + ); + } + } return $this->repository()->purchaseMiner($projectKey, null, [ 'purchased_at' => $purchasedAt, @@ -2126,6 +2143,8 @@ final class Router 'usd_reference_amount' => $offer['usd_reference_amount'] ?? null, 'reference_price_amount' => $referencePriceAmount, 'reference_price_currency' => $referencePriceCurrency, + 'settled_value_amount' => $settledValueAmount, + 'settled_value_currency' => $settledValueCurrency, 'auto_renew' => $isAutoRenew ? 1 : 0, 'note' => $this->optionalString($input['note'] ?? ($offer['note'] ?? null), 1000), 'is_active' => 1, diff --git a/custom/apps/mining-checker/src/Domain/AnalyticsService.php b/custom/apps/mining-checker/src/Domain/AnalyticsService.php index b4791739..3062eaa3 100644 --- a/custom/apps/mining-checker/src/Domain/AnalyticsService.php +++ b/custom/apps/mining-checker/src/Domain/AnalyticsService.php @@ -355,26 +355,32 @@ final class AnalyticsService $price = $latestPriceByCurrency[$currency] ?? $this->convertLatestPrice($latestPriceByCurrency, $currency, $latest); $requiredDoge = ($price && $targetAmount !== null) ? $targetAmount / $price : null; - $remainingDoge = $requiredDoge !== null ? $requiredDoge - (float) ($latest['coins_total_effective'] ?? $latest['coins_total']) : null; - $remainingDays = ( - $remainingDoge !== null && + $remainingDogeAtUpload = $requiredDoge !== null ? $requiredDoge - (float) ($latest['coins_total_effective'] ?? $latest['coins_total']) : null; + $remainingDaysAtUpload = ( + $remainingDogeAtUpload !== null && $latest['doge_per_day_interval'] !== null && (float) $latest['doge_per_day_interval'] > 0 - ) ? $remainingDoge / (float) $latest['doge_per_day_interval'] : null; + ) ? $remainingDogeAtUpload / (float) $latest['doge_per_day_interval'] : null; $targetEtaAt = null; - if ($remainingDays !== null) { - if ($remainingDays <= 0) { + if ($remainingDaysAtUpload !== null) { + if ($remainingDaysAtUpload <= 0) { $targetEtaAt = (string) ($latest['measured_at'] ?? ''); } elseif (!empty($latest['measured_at'])) { try { $targetEtaAt = $this->formatUtcTimestamp( - $this->utcTimestamp((string) $latest['measured_at']) + (int) round($remainingDays * 86400) + $this->utcTimestamp((string) $latest['measured_at']) + (int) round($remainingDaysAtUpload * 86400) ); } catch (\Throwable) { $targetEtaAt = null; } } } + $remainingDays = $targetEtaAt !== null + ? max(0.0, ($this->utcTimestamp($targetEtaAt) - time()) / 86400) + : null; + $remainingDoge = ($remainingDays !== null && $latest['doge_per_day_interval'] !== null) + ? max(0.0, $remainingDays * (float) $latest['doge_per_day_interval']) + : $remainingDogeAtUpload; $targetSummary[] = array_merge($target, [ 'effective_target_amount_fiat' => $this->roundOrNull($targetAmount, 2), @@ -384,9 +390,11 @@ final class AnalyticsService 'latest_price_for_currency' => $price, 'required_doge' => $this->roundOrNull($requiredDoge, 6), 'remaining_doge' => $this->roundOrNull($remainingDoge, 6), + 'remaining_doge_at_upload' => $this->roundOrNull($remainingDogeAtUpload, 6), + 'remaining_days_at_upload' => $this->roundOrNull($remainingDaysAtUpload, 4), 'remaining_days' => $this->roundOrNull($remainingDays, 4), 'target_eta_at' => $targetEtaAt, - 'status' => $remainingDoge !== null && $remainingDoge <= 0 ? 'reached' : 'open', + 'status' => $remainingDays !== null && $remainingDays <= 0 ? 'reached' : 'open', ]); } @@ -452,6 +460,12 @@ final class AnalyticsService $totalHoldingsValue = ($walletValue !== null || $currentVisibleValue !== null) ? (float) ($walletValue ?? 0.0) + (float) ($currentVisibleValue ?? 0.0) : null; + $settledCryptoSpendValue = $latestCurrency !== '' + ? $this->settledCryptoSpendValue($purchasedMiners, $latestCurrency, $latest) + : null; + $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; $breakEvenRemainingAmount = ($cashInvestedCapital !== null && $totalHoldingsValue !== null) ? max(0.0, $cashInvestedCapital - $totalHoldingsValue) @@ -493,6 +507,8 @@ final class AnalyticsService 'wallet_value' => $this->roundOrNull($walletValue, 8), 'wallet_snapshot_measured_at' => $useWalletSnapshot ? (string) ($latestWalletSnapshot['measured_at'] ?? '') : null, 'total_holdings_value' => $this->roundOrNull($totalHoldingsValue, 8), + 'settled_crypto_spend_value' => $this->roundOrNull($settledCryptoSpendValue, 8), + 'earned_value' => $this->roundOrNull($earnedValue, 8), 'break_even_remaining_amount' => $this->roundOrNull($breakEvenRemainingAmount, 8), 'break_even_days_overall' => $this->roundOrNull($breakEvenDaysOverall, 4), 'break_even_eta_at' => $breakEvenProjection['eta'] ?? null, @@ -1646,6 +1662,33 @@ final class AnalyticsService return array_map(fn (float $value): float => round($value, 8), $balances); } + private function settledCryptoSpendValue(array $purchasedMiners, string $targetCurrency, ?array $fxContext = null): ?float + { + $total = 0.0; + $matched = false; + foreach ($purchasedMiners as $miner) { + if ($this->entryFundingSource($miner) !== 'reinvest') { + continue; + } + + $amount = is_numeric($miner['settled_value_amount'] ?? null) ? (float) $miner['settled_value_amount'] : null; + $currency = strtoupper(trim((string) ($miner['settled_value_currency'] ?? ''))); + if ($amount === null || $currency === '') { + continue; + } + + $converted = $this->convertAmount($amount, $currency, $targetCurrency, $fxContext); + if ($converted === null) { + continue; + } + + $matched = true; + $total += $converted; + } + + return $matched ? $total : null; + } + private function mergeWalletBalances(array $baseBalances, array $deltaBalances): array { $merged = $baseBalances; @@ -1765,6 +1808,13 @@ final class AnalyticsService $balances[$normalizedCode] = round((float) $balance, 8); } + // A wallet screenshot's primary balance is authoritative even when OCR did not + // expose a complete per-asset balance list. + $walletCurrency = strtoupper(trim((string) ($snapshot['wallet_currency'] ?? ''))); + if ($walletCurrency !== '' && is_numeric($snapshot['wallet_balance'] ?? null)) { + $balances[$walletCurrency] = round((float) $snapshot['wallet_balance'], 8); + } + ksort($balances); return $balances; } diff --git a/custom/apps/mining-checker/src/Infrastructure/MiningRepository.php b/custom/apps/mining-checker/src/Infrastructure/MiningRepository.php index d7944ced..2bf27a35 100644 --- a/custom/apps/mining-checker/src/Infrastructure/MiningRepository.php +++ b/custom/apps/mining-checker/src/Infrastructure/MiningRepository.php @@ -1005,12 +1005,12 @@ final class MiningRepository 'INSERT INTO ' . $this->table('purchased_miners') . ' ( project_key, owner_sub, miner_offer_id, purchased_at, label, runtime_months, mining_speed_value, mining_speed_unit, bonus_speed_value, bonus_speed_unit, - total_cost_amount, currency, usd_reference_amount, reference_price_amount, reference_price_currency, + total_cost_amount, currency, usd_reference_amount, reference_price_amount, reference_price_currency, settled_value_amount, settled_value_currency, daily_cost_amount, daily_cost_currency, auto_renew, note, is_active ) VALUES ( :project_key, :owner_sub, :miner_offer_id, :purchased_at, :label, :runtime_months, :mining_speed_value, :mining_speed_unit, :bonus_speed_value, :bonus_speed_unit, - :total_cost_amount, :currency, :usd_reference_amount, :reference_price_amount, :reference_price_currency, + :total_cost_amount, :currency, :usd_reference_amount, :reference_price_amount, :reference_price_currency, :settled_value_amount, :settled_value_currency, :daily_cost_amount, :daily_cost_currency, :auto_renew, :note, :is_active ) RETURNING *' @@ -1023,12 +1023,12 @@ final class MiningRepository 'INSERT INTO ' . $this->table('purchased_miners') . ' ( project_key, owner_sub, miner_offer_id, purchased_at, label, runtime_months, mining_speed_value, mining_speed_unit, bonus_speed_value, bonus_speed_unit, - total_cost_amount, currency, usd_reference_amount, reference_price_amount, reference_price_currency, + total_cost_amount, currency, usd_reference_amount, reference_price_amount, reference_price_currency, settled_value_amount, settled_value_currency, daily_cost_amount, daily_cost_currency, auto_renew, note, is_active ) VALUES ( :project_key, :owner_sub, :miner_offer_id, :purchased_at, :label, :runtime_months, :mining_speed_value, :mining_speed_unit, :bonus_speed_value, :bonus_speed_unit, - :total_cost_amount, :currency, :usd_reference_amount, :reference_price_amount, :reference_price_currency, + :total_cost_amount, :currency, :usd_reference_amount, :reference_price_amount, :reference_price_currency, :settled_value_amount, :settled_value_currency, :daily_cost_amount, :daily_cost_currency, :auto_renew, :note, :is_active )' ); @@ -1057,6 +1057,8 @@ final class MiningRepository 'usd_reference_amount' => $payload['usd_reference_amount'] ?? null, 'reference_price_amount' => $payload['reference_price_amount'] ?? null, 'reference_price_currency' => $payload['reference_price_currency'] ?? null, + 'settled_value_amount' => $payload['settled_value_amount'] ?? null, + 'settled_value_currency' => $payload['settled_value_currency'] ?? null, 'daily_cost_amount' => $payload['daily_cost_amount'] ?? $this->deriveDailyCostAmount($payload['total_cost_amount'] ?? null, $payload['runtime_months'] ?? null), 'daily_cost_currency' => $payload['daily_cost_currency'] ?? ($payload['currency'] ?? null), 'auto_renew' => $payload['auto_renew'] ?? 0, @@ -1069,12 +1071,12 @@ final class MiningRepository 'INSERT INTO ' . $this->table('purchased_miners') . ' ( project_key, owner_sub, miner_offer_id, purchased_at, label, runtime_months, mining_speed_value, mining_speed_unit, bonus_speed_value, bonus_speed_unit, - total_cost_amount, currency, usd_reference_amount, reference_price_amount, reference_price_currency, + total_cost_amount, currency, usd_reference_amount, reference_price_amount, reference_price_currency, settled_value_amount, settled_value_currency, daily_cost_amount, daily_cost_currency, auto_renew, note, is_active ) VALUES ( :project_key, :owner_sub, :miner_offer_id, :purchased_at, :label, :runtime_months, :mining_speed_value, :mining_speed_unit, :bonus_speed_value, :bonus_speed_unit, - :total_cost_amount, :currency, :usd_reference_amount, :reference_price_amount, :reference_price_currency, + :total_cost_amount, :currency, :usd_reference_amount, :reference_price_amount, :reference_price_currency, :settled_value_amount, :settled_value_currency, :daily_cost_amount, :daily_cost_currency, :auto_renew, :note, :is_active ) RETURNING *' @@ -1087,12 +1089,12 @@ final class MiningRepository 'INSERT INTO ' . $this->table('purchased_miners') . ' ( project_key, owner_sub, miner_offer_id, purchased_at, label, runtime_months, mining_speed_value, mining_speed_unit, bonus_speed_value, bonus_speed_unit, - total_cost_amount, currency, usd_reference_amount, reference_price_amount, reference_price_currency, + total_cost_amount, currency, usd_reference_amount, reference_price_amount, reference_price_currency, settled_value_amount, settled_value_currency, daily_cost_amount, daily_cost_currency, auto_renew, note, is_active ) VALUES ( :project_key, :owner_sub, :miner_offer_id, :purchased_at, :label, :runtime_months, :mining_speed_value, :mining_speed_unit, :bonus_speed_value, :bonus_speed_unit, - :total_cost_amount, :currency, :usd_reference_amount, :reference_price_amount, :reference_price_currency, + :total_cost_amount, :currency, :usd_reference_amount, :reference_price_amount, :reference_price_currency, :settled_value_amount, :settled_value_currency, :daily_cost_amount, :daily_cost_currency, :auto_renew, :note, :is_active )' ); @@ -1646,6 +1648,8 @@ final class MiningRepository 'usd_reference_amount' => $payload['usd_reference_amount'], 'reference_price_amount' => $payload['reference_price_amount'] ?? null, 'reference_price_currency' => $payload['reference_price_currency'] ?? null, + 'settled_value_amount' => $payload['settled_value_amount'] ?? null, + 'settled_value_currency' => $payload['settled_value_currency'] ?? null, 'daily_cost_amount' => $dailyCostAmount, 'daily_cost_currency' => $dailyCostCurrency, 'auto_renew' => $payload['auto_renew'] ?? 0, diff --git a/custom/apps/mining-checker/src/Infrastructure/SchemaManager.php b/custom/apps/mining-checker/src/Infrastructure/SchemaManager.php index debf05de..03fc49c5 100644 --- a/custom/apps/mining-checker/src/Infrastructure/SchemaManager.php +++ b/custom/apps/mining-checker/src/Infrastructure/SchemaManager.php @@ -259,6 +259,13 @@ final class SchemaManager $this->upgradePurchasedMinerReferenceColumns(); $applied[] = 'purchased_miner_reference_columns'; } + if ($this->tableExists($this->prefix . 'purchased_miners') && ( + !$this->columnExists($this->prefix . 'purchased_miners', 'settled_value_amount') || + !$this->columnExists($this->prefix . 'purchased_miners', 'settled_value_currency') + )) { + $this->upgradePurchasedMinerSettlementColumns(); + $applied[] = 'purchased_miner_settlement_columns'; + } if ($this->tableExists($this->prefix . 'targets') && $this->tableExists($this->prefix . 'miner_offers')) { $this->ensureTargetOfferForeignKey(); $applied[] = 'target_offer_foreign_key'; @@ -401,6 +408,13 @@ final class SchemaManager $this->upgradePurchasedMinerReferenceColumns(); $applied[] = 'purchased_miner_reference_columns'; } + if ($this->tableExists($this->prefix . 'purchased_miners') && ( + !$this->columnExists($this->prefix . 'purchased_miners', 'settled_value_amount') || + !$this->columnExists($this->prefix . 'purchased_miners', 'settled_value_currency') + )) { + $this->upgradePurchasedMinerSettlementColumns(); + $applied[] = 'purchased_miner_settlement_columns'; + } if ($this->tableExists($this->prefix . 'targets') && $this->tableExists($this->prefix . 'miner_offers')) { $this->ensureTargetOfferForeignKey(); $applied[] = 'target_offer_foreign_key'; @@ -754,6 +768,12 @@ final class SchemaManager if (!$this->tableExists($this->prefix . 'purchased_miners')) { $upgrades[] = 'purchased_miners_table'; } + if ($this->tableExists($this->prefix . 'purchased_miners') && ( + !$this->columnExists($this->prefix . 'purchased_miners', 'settled_value_amount') || + !$this->columnExists($this->prefix . 'purchased_miners', 'settled_value_currency') + )) { + $upgrades[] = 'purchased_miner_settlement_columns'; + } if ($this->tableExists($this->prefix . 'targets') && !$this->columnExists($this->prefix . 'targets', 'miner_offer_id')) { $upgrades[] = 'target_offer_column'; } @@ -1502,6 +1522,33 @@ final class SchemaManager } } + private function upgradePurchasedMinerSettlementColumns(): void + { + $table = $this->prefix . 'purchased_miners'; + $statements = $this->driver === 'pgsql' + ? [ + 'ALTER TABLE ' . $table . ' ADD COLUMN IF NOT EXISTS settled_value_amount NUMERIC(20,8)', + 'ALTER TABLE ' . $table . ' ADD COLUMN IF NOT EXISTS settled_value_currency VARCHAR(10)', + "UPDATE " . $table . " SET settled_value_amount = CASE WHEN reference_price_amount IS NOT NULL AND NULLIF(BTRIM(reference_price_currency), '') IS NOT NULL AND UPPER(reference_price_currency) <> UPPER(currency) THEN reference_price_amount ELSE total_cost_amount * 0.07 END, settled_value_currency = CASE WHEN reference_price_amount IS NOT NULL AND NULLIF(BTRIM(reference_price_currency), '') IS NOT NULL AND UPPER(reference_price_currency) <> UPPER(currency) THEN reference_price_currency ELSE 'EUR' END WHERE UPPER(currency) IN ('ADA','ARB','BNB','BTC','DAI','DOGE','DOT','ETH','LINK','LTC','SOL','USDC','USDT','XRP') AND settled_value_amount IS NULL", + ] + : [ + 'ALTER TABLE `' . $table . '` ADD COLUMN settled_value_amount DECIMAL(20,8) NULL', + 'ALTER TABLE `' . $table . '` ADD COLUMN settled_value_currency VARCHAR(10) NULL', + "UPDATE `" . $table . "` SET settled_value_amount = CASE WHEN reference_price_amount IS NOT NULL AND NULLIF(TRIM(reference_price_currency), '') IS NOT NULL AND UPPER(reference_price_currency) <> UPPER(currency) THEN reference_price_amount ELSE total_cost_amount * 0.07 END, settled_value_currency = CASE WHEN reference_price_amount IS NOT NULL AND NULLIF(TRIM(reference_price_currency), '') IS NOT NULL AND UPPER(reference_price_currency) <> UPPER(currency) THEN reference_price_currency ELSE 'EUR' END WHERE UPPER(currency) IN ('ADA','ARB','BNB','BTC','DAI','DOGE','DOT','ETH','LINK','LTC','SOL','USDC','USDT','XRP') AND settled_value_amount IS NULL", + ]; + + foreach ($statements as $statement) { + try { + $this->executeUpgradeStatements([$statement], 'Schema-Upgrade fuer die feste Krypto-Bewertung fehlgeschlagen.'); + } catch (\Throwable $exception) { + if ($this->driver === 'mysql' && str_contains(strtolower($exception->getMessage()), 'duplicate column')) { + continue; + } + throw $exception; + } + } + } + private function upgradeServerDailyCostColumns(): void { foreach ([$this->prefix . 'cost_plans', $this->prefix . 'purchased_miners'] as $table) {