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

This commit is contained in:
2026-05-20 00:54:00 +02:00
parent 7323673158
commit 127a0e71e1
2 changed files with 195 additions and 19 deletions

View File

@@ -318,7 +318,7 @@ final class Router
'snapshot_limit' => self::BOOTSTRAP_SNAPSHOT_LIMIT, '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, 'project_key' => $projectKey,
'baseline_measured_at' => null, 'baseline_measured_at' => null,
'baseline_coins_total' => null, 'baseline_coins_total' => null,
@@ -358,7 +358,7 @@ final class Router
'view' => $view, 'view' => $view,
'measurement_count' => is_array($measurements) ? count($measurements) : 0, '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, 'latest_measurement' => $measurements !== [] ? $measurements[array_key_last($measurements)] : null,
'baseline' => $settings, 'baseline' => $settings,
'targets' => [], 'targets' => [],
@@ -1147,8 +1147,14 @@ final class Router
return abs($leftTs - $rightTs); 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); $settings = $this->repository()->getSettings($projectKey);
$base = is_array($settings) ? $settings : [ $base = is_array($settings) ? $settings : [
'project_key' => $projectKey, 'project_key' => $projectKey,
@@ -1173,12 +1179,12 @@ final class Router
$base['module_theme_accent'] = 'teal'; $base['module_theme_accent'] = 'teal';
} }
$base['cost_plans'] = $this->costPlans($projectKey); $base['cost_plans'] = $includeCostPlans ? $this->costPlans($projectKey) : [];
$base['currencies'] = $this->currencies(); $base['currencies'] = $includeCurrencies ? $this->currencies() : [];
$base['preferred_currencies'] = $this->preferredCurrencies($base['preferred_currencies'] ?? null); $base['preferred_currencies'] = $this->preferredCurrencies($base['preferred_currencies'] ?? null);
$base['payouts'] = $this->payouts($projectKey); $base['payouts'] = $includePayouts ? $this->payouts($projectKey) : [];
$base['miner_offers'] = $this->minerOffers($projectKey); $base['miner_offers'] = $includeMinerOffers ? $this->minerOffers($projectKey) : [];
$base['purchased_miners'] = $this->purchasedMiners($projectKey); $base['purchased_miners'] = $includePurchasedMiners ? $this->purchasedMiners($projectKey) : [];
$base['measurement_rates'] = []; $base['measurement_rates'] = [];
return $base; return $base;
} }
@@ -1242,16 +1248,22 @@ final class Router
'row_count' => count($rows), 'row_count' => count($rows),
'limit' => self::BOOTSTRAP_MEASUREMENT_LIMIT, '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 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 [];
} }
return $this->repository()->listWalletSnapshots($projectKey, 50); return $this->repository()->listWalletSnapshots($projectKey, 1);
} }
private function bootstrapTargets(string $projectKey, string $view): array private function bootstrapTargets(string $projectKey, string $view): array
@@ -1279,7 +1291,7 @@ final class Router
return $this->measurementFxSnapshots($measurements, $limit); 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)) { if (!in_array($view, ['overview', 'mining'], true)) {
return [ return [
@@ -1294,6 +1306,7 @@ final class Router
return $this->analytics()->buildSummary($measurements, $settings, $targets, [ return $this->analytics()->buildSummary($measurements, $settings, $targets, [
'include_offer_scenarios' => $view === 'mining', 'include_offer_scenarios' => $view === 'mining',
'include_long_term_projection' => $view === 'mining', 'include_long_term_projection' => $view === 'mining',
'wallet_snapshots' => $walletSnapshots,
]); ]);
} }
@@ -1327,6 +1340,54 @@ final class Router
: 'overview'; : '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 private function createMeasurement(string $projectKey, array $input): array
{ {
$projectTimezone = $this->projectTimezone($projectKey); $projectTimezone = $this->projectTimezone($projectKey);

View File

@@ -14,8 +14,9 @@ final class AnalyticsService
$this->fx = $fx; $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); $baselineCoins = (float) ($settings['baseline_coins_total'] ?? 0.0);
$baselineAt = (string) ($settings['baseline_measured_at'] ?? ''); $baselineAt = (string) ($settings['baseline_measured_at'] ?? '');
$costPlans = is_array($settings['cost_plans'] ?? null) ? $settings['cost_plans'] : []; $costPlans = is_array($settings['cost_plans'] ?? null) ? $settings['cost_plans'] : [];
@@ -38,9 +39,11 @@ final class AnalyticsService
$payoutsByAsset = []; $payoutsByAsset = [];
$latestPriceByCurrency = []; $latestPriceByCurrency = [];
foreach ($measurements as $row) { $lastIndex = count($measurements) - 1;
foreach ($measurements as $index => $row) {
$measuredTs = $this->utcTimestamp((string) $row['measured_at']); $measuredTs = $this->utcTimestamp((string) $row['measured_at']);
$coinCurrency = strtoupper(trim((string) ($row['coin_currency'] ?? $settings['crypto_currency'] ?? 'DOGE'))); $coinCurrency = strtoupper(trim((string) ($row['coin_currency'] ?? $settings['crypto_currency'] ?? 'DOGE')));
$includeFullDetail = !$fullLatestOnly || $index === $lastIndex;
while (isset($payouts[$payoutIndex])) { while (isset($payouts[$payoutIndex])) {
$payoutTs = $this->utcTimestamp((string) ($payouts[$payoutIndex]['payout_at'] ?? '')); $payoutTs = $this->utcTimestamp((string) ($payouts[$payoutIndex]['payout_at'] ?? ''));
if ($payoutTs <= 0 || $payoutTs > $measuredTs) { if ($payoutTs <= 0 || $payoutTs > $measuredTs) {
@@ -134,20 +137,23 @@ final class AnalyticsService
} }
} }
$effectiveDailyCost = $this->effectiveDailyCost($costPlans, $measuredTs, $priceCurrency, $row); $effectiveDailyCost = $includeFullDetail ? $this->effectiveDailyCost($costPlans, $measuredTs, $priceCurrency, $row) : null;
$currentValue = $price !== null ? $visibleCoinsTotal * $price : null; $currentValue = ($includeFullDetail && $price !== null) ? $visibleCoinsTotal * $price : null;
$currentValueEffective = $price !== null ? $effectiveCoinsTotal * $price : null; $currentValueEffective = ($includeFullDetail && $price !== null) ? $effectiveCoinsTotal * $price : null;
$theoreticalDailyRevenue = ($price !== null && $perDayInterval !== null) ? $perDayInterval * $price : null; $theoreticalDailyRevenue = ($includeFullDetail && $price !== null && $perDayInterval !== null) ? $perDayInterval * $price : null;
$theoreticalDailyProfit = ( $theoreticalDailyProfit = (
$includeFullDetail &&
$theoreticalDailyRevenue !== null && $theoreticalDailyRevenue !== null &&
$effectiveDailyCost !== null $effectiveDailyCost !== null
) ? $theoreticalDailyRevenue - $effectiveDailyCost : null; ) ? $theoreticalDailyRevenue - $effectiveDailyCost : null;
$breakEvenPricePerCoin = ( $breakEvenPricePerCoin = (
$includeFullDetail &&
$effectiveDailyCost !== null && $effectiveDailyCost !== null &&
$perDayInterval !== null && $perDayInterval !== null &&
$perDayInterval > 0 $perDayInterval > 0
) ? $effectiveDailyCost / $perDayInterval : null; ) ? $effectiveDailyCost / $perDayInterval : null;
$profitMarginPercent = ( $profitMarginPercent = (
$includeFullDetail &&
$theoreticalDailyRevenue !== null && $theoreticalDailyRevenue !== null &&
$theoreticalDailyRevenue > 0 && $theoreticalDailyRevenue > 0 &&
$theoreticalDailyProfit !== null $theoreticalDailyProfit !== null
@@ -325,9 +331,22 @@ final class AnalyticsService
) )
: null; : null;
$walletBalances = $this->walletBalances($payouts, $purchasedMiners, $latestMeasuredTs); $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); $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); $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; $currentVisibleValue = is_numeric($latest['current_value'] ?? null) ? (float) $latest['current_value'] : null;
$totalHoldingsValue = ($walletValue !== null || $currentVisibleValue !== null) $totalHoldingsValue = ($walletValue !== null || $currentVisibleValue !== null)
? (float) ($walletValue ?? 0.0) + (float) ($currentVisibleValue ?? 0.0) ? (float) ($walletValue ?? 0.0) + (float) ($currentVisibleValue ?? 0.0)
@@ -353,6 +372,7 @@ final class AnalyticsService
'wallet_balance_current_asset' => $this->roundOrNull($walletBalanceCurrentAsset, 6), 'wallet_balance_current_asset' => $this->roundOrNull($walletBalanceCurrentAsset, 6),
'holdings_current_asset' => $this->roundOrNull($holdingsCurrentAsset, 6), 'holdings_current_asset' => $this->roundOrNull($holdingsCurrentAsset, 6),
'wallet_value' => $this->roundOrNull($walletValue, 8), '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), 'total_holdings_value' => $this->roundOrNull($totalHoldingsValue, 8),
'break_even_remaining_amount' => $this->roundOrNull($breakEvenRemainingAmount, 8), 'break_even_remaining_amount' => $this->roundOrNull($breakEvenRemainingAmount, 8),
'break_even_days_overall' => $this->roundOrNull($breakEvenDaysOverall, 4), '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); 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 private function walletBalanceValue(array $balances, string $targetCurrency, ?array $fxContext = null): ?float
{ {
$target = strtoupper(trim($targetCurrency)); $target = strtoupper(trim($targetCurrency));