diff --git a/modules/mining-checker/src/Api/Router.php b/modules/mining-checker/src/Api/Router.php index 7ed2102..35f9fde 100644 --- a/modules/mining-checker/src/Api/Router.php +++ b/modules/mining-checker/src/Api/Router.php @@ -318,7 +318,7 @@ final class Router 'snapshot_limit' => self::BOOTSTRAP_SNAPSHOT_LIMIT, ]); - $settings = $this->safeTimed('bootstrap.settings', fn () => $this->settings($projectKey), [ + $settings = $this->safeTimed('bootstrap.settings', fn () => $this->settings($projectKey, $this->bootstrapSettingsOptions($view)), [ 'project_key' => $projectKey, 'baseline_measured_at' => null, 'baseline_coins_total' => null, @@ -358,7 +358,7 @@ final class Router 'view' => $view, 'measurement_count' => is_array($measurements) ? count($measurements) : 0, ]); - $summary = $this->safeTimed('bootstrap.summary', fn () => $this->bootstrapSummary($measurements, $settings, $targets, $view), [ + $summary = $this->safeTimed('bootstrap.summary', fn () => $this->bootstrapSummary($measurements, $settings, $targets, $walletSnapshots, $view), [ 'latest_measurement' => $measurements !== [] ? $measurements[array_key_last($measurements)] : null, 'baseline' => $settings, 'targets' => [], @@ -1147,8 +1147,14 @@ final class Router return abs($leftTs - $rightTs); } - private function settings(string $projectKey): array + private function settings(string $projectKey, array $options = []): array { + $includeCostPlans = !array_key_exists('cost_plans', $options) || (bool) $options['cost_plans']; + $includeCurrencies = !array_key_exists('currencies', $options) || (bool) $options['currencies']; + $includePayouts = !array_key_exists('payouts', $options) || (bool) $options['payouts']; + $includeMinerOffers = !array_key_exists('miner_offers', $options) || (bool) $options['miner_offers']; + $includePurchasedMiners = !array_key_exists('purchased_miners', $options) || (bool) $options['purchased_miners']; + $settings = $this->repository()->getSettings($projectKey); $base = is_array($settings) ? $settings : [ 'project_key' => $projectKey, @@ -1173,12 +1179,12 @@ final class Router $base['module_theme_accent'] = 'teal'; } - $base['cost_plans'] = $this->costPlans($projectKey); - $base['currencies'] = $this->currencies(); + $base['cost_plans'] = $includeCostPlans ? $this->costPlans($projectKey) : []; + $base['currencies'] = $includeCurrencies ? $this->currencies() : []; $base['preferred_currencies'] = $this->preferredCurrencies($base['preferred_currencies'] ?? null); - $base['payouts'] = $this->payouts($projectKey); - $base['miner_offers'] = $this->minerOffers($projectKey); - $base['purchased_miners'] = $this->purchasedMiners($projectKey); + $base['payouts'] = $includePayouts ? $this->payouts($projectKey) : []; + $base['miner_offers'] = $includeMinerOffers ? $this->minerOffers($projectKey) : []; + $base['purchased_miners'] = $includePurchasedMiners ? $this->purchasedMiners($projectKey) : []; $base['measurement_rates'] = []; return $base; } @@ -1242,16 +1248,22 @@ final class Router 'row_count' => count($rows), 'limit' => self::BOOTSTRAP_MEASUREMENT_LIMIT, ]); - return $this->analytics()->enrichMeasurements($rows, $settings); + return $this->analytics()->enrichMeasurements($rows, $settings, [ + 'full_latest_only' => in_array($view, ['overview', 'mining'], true), + ]); } private function bootstrapWalletSnapshots(string $projectKey, string $view): array { - if (!in_array($view, ['wallet'], true)) { + if ($view === 'wallet') { + return $this->repository()->listWalletSnapshots($projectKey, 50); + } + + if (!in_array($view, ['overview', 'mining'], true)) { return []; } - return $this->repository()->listWalletSnapshots($projectKey, 50); + return $this->repository()->listWalletSnapshots($projectKey, 1); } private function bootstrapTargets(string $projectKey, string $view): array @@ -1279,7 +1291,7 @@ final class Router return $this->measurementFxSnapshots($measurements, $limit); } - private function bootstrapSummary(array $measurements, array $settings, array $targets, string $view): array + private function bootstrapSummary(array $measurements, array $settings, array $targets, array $walletSnapshots, string $view): array { if (!in_array($view, ['overview', 'mining'], true)) { return [ @@ -1294,6 +1306,7 @@ final class Router return $this->analytics()->buildSummary($measurements, $settings, $targets, [ 'include_offer_scenarios' => $view === 'mining', 'include_long_term_projection' => $view === 'mining', + 'wallet_snapshots' => $walletSnapshots, ]); } @@ -1327,6 +1340,54 @@ final class Router : 'overview'; } + private function bootstrapSettingsOptions(string $view): array + { + return match ($view) { + 'overview' => [ + 'cost_plans' => true, + 'currencies' => true, + 'payouts' => true, + 'miner_offers' => false, + 'purchased_miners' => true, + ], + 'upload' => [ + 'cost_plans' => false, + 'currencies' => true, + 'payouts' => false, + 'miner_offers' => false, + 'purchased_miners' => false, + ], + 'measurements' => [ + 'cost_plans' => false, + 'currencies' => false, + 'payouts' => false, + 'miner_offers' => false, + 'purchased_miners' => false, + ], + 'wallet' => [ + 'cost_plans' => false, + 'currencies' => false, + 'payouts' => false, + 'miner_offers' => false, + 'purchased_miners' => false, + ], + 'dashboards' => [ + 'cost_plans' => false, + 'currencies' => false, + 'payouts' => false, + 'miner_offers' => false, + 'purchased_miners' => false, + ], + default => [ + 'cost_plans' => true, + 'currencies' => true, + 'payouts' => true, + 'miner_offers' => true, + 'purchased_miners' => true, + ], + }; + } + private function createMeasurement(string $projectKey, array $input): array { $projectTimezone = $this->projectTimezone($projectKey); diff --git a/modules/mining-checker/src/Domain/AnalyticsService.php b/modules/mining-checker/src/Domain/AnalyticsService.php index ecd49b2..571c673 100644 --- a/modules/mining-checker/src/Domain/AnalyticsService.php +++ b/modules/mining-checker/src/Domain/AnalyticsService.php @@ -14,8 +14,9 @@ final class AnalyticsService $this->fx = $fx; } - public function enrichMeasurements(array $measurements, array $settings): array + public function enrichMeasurements(array $measurements, array $settings, array $options = []): array { + $fullLatestOnly = !empty($options['full_latest_only']); $baselineCoins = (float) ($settings['baseline_coins_total'] ?? 0.0); $baselineAt = (string) ($settings['baseline_measured_at'] ?? ''); $costPlans = is_array($settings['cost_plans'] ?? null) ? $settings['cost_plans'] : []; @@ -38,9 +39,11 @@ final class AnalyticsService $payoutsByAsset = []; $latestPriceByCurrency = []; - foreach ($measurements as $row) { + $lastIndex = count($measurements) - 1; + foreach ($measurements as $index => $row) { $measuredTs = $this->utcTimestamp((string) $row['measured_at']); $coinCurrency = strtoupper(trim((string) ($row['coin_currency'] ?? $settings['crypto_currency'] ?? 'DOGE'))); + $includeFullDetail = !$fullLatestOnly || $index === $lastIndex; while (isset($payouts[$payoutIndex])) { $payoutTs = $this->utcTimestamp((string) ($payouts[$payoutIndex]['payout_at'] ?? '')); if ($payoutTs <= 0 || $payoutTs > $measuredTs) { @@ -134,20 +137,23 @@ final class AnalyticsService } } - $effectiveDailyCost = $this->effectiveDailyCost($costPlans, $measuredTs, $priceCurrency, $row); - $currentValue = $price !== null ? $visibleCoinsTotal * $price : null; - $currentValueEffective = $price !== null ? $effectiveCoinsTotal * $price : null; - $theoreticalDailyRevenue = ($price !== null && $perDayInterval !== null) ? $perDayInterval * $price : null; + $effectiveDailyCost = $includeFullDetail ? $this->effectiveDailyCost($costPlans, $measuredTs, $priceCurrency, $row) : null; + $currentValue = ($includeFullDetail && $price !== null) ? $visibleCoinsTotal * $price : null; + $currentValueEffective = ($includeFullDetail && $price !== null) ? $effectiveCoinsTotal * $price : null; + $theoreticalDailyRevenue = ($includeFullDetail && $price !== null && $perDayInterval !== null) ? $perDayInterval * $price : null; $theoreticalDailyProfit = ( + $includeFullDetail && $theoreticalDailyRevenue !== null && $effectiveDailyCost !== null ) ? $theoreticalDailyRevenue - $effectiveDailyCost : null; $breakEvenPricePerCoin = ( + $includeFullDetail && $effectiveDailyCost !== null && $perDayInterval !== null && $perDayInterval > 0 ) ? $effectiveDailyCost / $perDayInterval : null; $profitMarginPercent = ( + $includeFullDetail && $theoreticalDailyRevenue !== null && $theoreticalDailyRevenue > 0 && $theoreticalDailyProfit !== null @@ -325,9 +331,22 @@ final class AnalyticsService ) : null; $walletBalances = $this->walletBalances($payouts, $purchasedMiners, $latestMeasuredTs); + $latestWalletSnapshot = $this->latestWalletSnapshot(is_array($options['wallet_snapshots'] ?? null) ? $options['wallet_snapshots'] : []); + if (is_array($latestWalletSnapshot)) { + $snapshotBalances = $this->walletSnapshotBalances($latestWalletSnapshot); + if ($snapshotBalances !== []) { + $walletBalances = $snapshotBalances; + } + } $walletBalanceCurrentAsset = (float) ($walletBalances[strtoupper(trim((string) ($latest['coin_currency'] ?? $settings['crypto_currency'] ?? 'DOGE')))] ?? 0.0); $holdingsCurrentAsset = $walletBalanceCurrentAsset + (float) ($latest['coins_total_visible'] ?? $latest['coins_total'] ?? 0); - $walletValue = $latestCurrency !== '' ? $this->walletBalanceValue($walletBalances, $latestCurrency, $latest) : null; + $walletValue = $latestCurrency !== '' + ? ( + is_array($latestWalletSnapshot) + ? $this->walletSnapshotValue($latestWalletSnapshot, $latestCurrency, $latest) + : $this->walletBalanceValue($walletBalances, $latestCurrency, $latest) + ) + : null; $currentVisibleValue = is_numeric($latest['current_value'] ?? null) ? (float) $latest['current_value'] : null; $totalHoldingsValue = ($walletValue !== null || $currentVisibleValue !== null) ? (float) ($walletValue ?? 0.0) + (float) ($currentVisibleValue ?? 0.0) @@ -353,6 +372,7 @@ final class AnalyticsService 'wallet_balance_current_asset' => $this->roundOrNull($walletBalanceCurrentAsset, 6), 'holdings_current_asset' => $this->roundOrNull($holdingsCurrentAsset, 6), 'wallet_value' => $this->roundOrNull($walletValue, 8), + 'wallet_snapshot_measured_at' => is_array($latestWalletSnapshot) ? (string) ($latestWalletSnapshot['measured_at'] ?? '') : null, 'total_holdings_value' => $this->roundOrNull($totalHoldingsValue, 8), 'break_even_remaining_amount' => $this->roundOrNull($breakEvenRemainingAmount, 8), 'break_even_days_overall' => $this->roundOrNull($breakEvenDaysOverall, 4), @@ -1121,6 +1141,101 @@ final class AnalyticsService return array_map(fn (float $value): float => round($value, 8), $balances); } + private function latestWalletSnapshot(array $walletSnapshots): ?array + { + foreach ($walletSnapshots as $snapshot) { + if (is_array($snapshot)) { + return $snapshot; + } + } + + return null; + } + + private function walletSnapshotBalances(array $snapshot): array + { + $balances = []; + $rawBalances = is_array($snapshot['balances_json'] ?? null) ? $snapshot['balances_json'] : []; + foreach ($rawBalances as $code => $asset) { + $normalizedCode = strtoupper(trim((string) $code)); + if ($normalizedCode === '') { + continue; + } + + $balance = is_array($asset) + ? ($asset['balance'] ?? null) + : $asset; + if (!is_numeric($balance)) { + continue; + } + + $balances[$normalizedCode] = round((float) $balance, 8); + } + + ksort($balances); + return $balances; + } + + private function walletSnapshotValue(array $snapshot, string $targetCurrency, ?array $fxContext = null): ?float + { + $target = strtoupper(trim($targetCurrency)); + if ($target === '') { + return null; + } + + $balances = is_array($snapshot['balances_json'] ?? null) ? $snapshot['balances_json'] : []; + if ($balances === []) { + return null; + } + + $total = 0.0; + $matched = false; + foreach ($balances as $code => $asset) { + $currency = strtoupper(trim((string) $code)); + if ($currency === '') { + continue; + } + + $balance = is_array($asset) ? ($asset['balance'] ?? null) : $asset; + if (!is_numeric($balance)) { + continue; + } + + $numericBalance = (float) $balance; + if ($currency === $target) { + $total += $numericBalance; + $matched = true; + continue; + } + + $snapshotPrice = is_array($asset) && is_numeric($asset['price_amount'] ?? null) + ? (float) $asset['price_amount'] + : null; + $snapshotPriceCurrency = is_array($asset) + ? strtoupper(trim((string) ($asset['price_currency'] ?? ''))) + : ''; + + if ($snapshotPrice !== null && $snapshotPrice > 0 && $snapshotPriceCurrency !== '') { + $convertedPrice = $snapshotPriceCurrency === $target + ? $snapshotPrice + : $this->convertAmount($snapshotPrice, $snapshotPriceCurrency, $target, $fxContext); + if ($convertedPrice !== null) { + $total += $numericBalance * $convertedPrice; + $matched = true; + continue; + } + } + + $convertedBalance = $this->convertAmount($numericBalance, $currency, $target, $fxContext); + if ($convertedBalance !== null) { + $total += $convertedBalance; + $matched = true; + } + } + + return $matched ? $total : null; + } + private function walletBalanceValue(array $balances, string $targetCurrency, ?array $fxContext = null): ?float { $target = strtoupper(trim($targetCurrency));