fx update
This commit is contained in:
@@ -385,6 +385,30 @@ final class FxRatesRepository
|
||||
}
|
||||
}
|
||||
|
||||
public function findFetchByBaseAndFetchedAt(string $baseCurrency, string $fetchedAt): ?array
|
||||
{
|
||||
$baseCurrency = strtoupper(trim($baseCurrency));
|
||||
$fetchedAt = trim($fetchedAt);
|
||||
if ($baseCurrency === '' || $fetchedAt === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->prepare(
|
||||
'SELECT id, provider, trigger_source, base_currency, rate_date, fetched_at, created_at
|
||||
FROM ' . $this->table('fetches') . '
|
||||
WHERE base_currency = :base_currency
|
||||
AND fetched_at = :fetched_at
|
||||
ORDER BY id ASC
|
||||
LIMIT 1'
|
||||
);
|
||||
$stmt->execute([
|
||||
'base_currency' => $baseCurrency,
|
||||
'fetched_at' => $fetchedAt,
|
||||
]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
return is_array($row) ? $this->normalizeFetch($row) : null;
|
||||
}
|
||||
|
||||
private function getFetchById(int $fetchId): ?array
|
||||
{
|
||||
$stmt = $this->pdo->prepare(
|
||||
@@ -496,7 +520,7 @@ final class FxRatesRepository
|
||||
{
|
||||
$source = strtolower(trim($source));
|
||||
return match ($source) {
|
||||
'cron', 'manual', 'api' => $source,
|
||||
'cron', 'manual', 'api', 'migration' => $source,
|
||||
default => 'manual',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ final class Router
|
||||
public function handle(string $relativePath): never
|
||||
{
|
||||
try {
|
||||
$this->releaseSessionLock();
|
||||
$method = strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET'));
|
||||
$path = trim($relativePath, '/');
|
||||
|
||||
@@ -293,11 +294,38 @@ final class Router
|
||||
|
||||
private function bootstrap(string $projectKey): array
|
||||
{
|
||||
$settings = $this->settings($projectKey);
|
||||
$measurements = $this->measurements($projectKey, $settings);
|
||||
$targets = $this->targets($projectKey);
|
||||
$dashboards = $this->dashboards($projectKey);
|
||||
$fxSnapshots = $this->measurementFxSnapshots($measurements);
|
||||
$startedAt = microtime(true);
|
||||
$settings = $this->safeRead(fn () => $this->settings($projectKey), [
|
||||
'project_key' => $projectKey,
|
||||
'baseline_measured_at' => null,
|
||||
'baseline_coins_total' => null,
|
||||
'daily_cost_amount' => null,
|
||||
'daily_cost_currency' => 'EUR',
|
||||
'report_currency' => 'EUR',
|
||||
'crypto_currency' => 'DOGE',
|
||||
'display_timezone' => 'Europe/Berlin',
|
||||
'fx_max_age_hours' => 3,
|
||||
'module_theme_mode' => 'inherit',
|
||||
'module_theme_accent' => 'teal',
|
||||
'preferred_currencies' => ['DOGE', 'USD', 'EUR'],
|
||||
'cost_plans' => [],
|
||||
'currencies' => [],
|
||||
'payouts' => [],
|
||||
'miner_offers' => [],
|
||||
'purchased_miners' => [],
|
||||
'measurement_rates' => [],
|
||||
]);
|
||||
$measurements = $this->safeRead(fn () => $this->measurements($projectKey, $settings, false), []);
|
||||
$targets = $this->safeRead(fn () => $this->targets($projectKey), []);
|
||||
$dashboards = $this->safeRead(fn () => $this->dashboards($projectKey), []);
|
||||
$fxSnapshots = $this->safeRead(fn () => $this->measurementFxSnapshots($measurements), []);
|
||||
$summary = $this->safeRead(fn () => $this->analytics()->buildSummary($measurements, $settings, $targets), [
|
||||
'latest_measurement' => $measurements !== [] ? $measurements[array_key_last($measurements)] : null,
|
||||
'baseline' => $settings,
|
||||
'targets' => [],
|
||||
'payouts' => [],
|
||||
'miner_offers' => [],
|
||||
]);
|
||||
|
||||
return [
|
||||
'project' => $this->repository()->getProject($projectKey),
|
||||
@@ -306,7 +334,11 @@ final class Router
|
||||
'targets' => $targets,
|
||||
'dashboards' => $dashboards,
|
||||
'fx_snapshots' => $fxSnapshots,
|
||||
'summary' => $this->analytics()->buildSummary($measurements, $settings, $targets),
|
||||
'summary' => $summary,
|
||||
'bootstrap_meta' => [
|
||||
'degraded' => false,
|
||||
'duration_ms' => round((microtime(true) - $startedAt) * 1000, 2),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -752,7 +784,7 @@ final class Router
|
||||
$migratedRates = 0;
|
||||
|
||||
foreach ($legacyFetches as $legacyFetchId => $legacyFetch) {
|
||||
$existing = $this->findMatchingFxRatesFetch($legacyFetch);
|
||||
$existing = $this->findExistingFxFetchForLegacy($fxRepository, $legacyFetch);
|
||||
if (is_array($existing) && !empty($existing['id'])) {
|
||||
$fetchIdMap[$legacyFetchId] = (int) $existing['id'];
|
||||
$reusedFetches++;
|
||||
@@ -803,11 +835,6 @@ final class Router
|
||||
);
|
||||
$resolvedFetchId = $legacyFetchId !== null ? ($fetchIdMap[$legacyFetchId] ?? null) : null;
|
||||
|
||||
if (!is_numeric($resolvedFetchId) || (int) $resolvedFetchId <= 0) {
|
||||
$nearestSnapshot = $this->fx()->nearestSnapshot(null, (string) ($measurement['measured_at'] ?? ''), null, null);
|
||||
$resolvedFetchId = is_numeric($nearestSnapshot['id'] ?? null) ? (int) $nearestSnapshot['id'] : null;
|
||||
}
|
||||
|
||||
if (!is_numeric($resolvedFetchId) || (int) $resolvedFetchId <= 0) {
|
||||
$unresolvedMeasurements++;
|
||||
continue;
|
||||
@@ -873,7 +900,7 @@ final class Router
|
||||
return $grouped;
|
||||
}
|
||||
|
||||
private function findMatchingFxRatesFetch(array $legacyFetch): ?array
|
||||
private function findExistingFxFetchForLegacy(object $fxRepository, array $legacyFetch): ?array
|
||||
{
|
||||
$baseCurrency = strtoupper(trim((string) ($legacyFetch['base_currency'] ?? '')));
|
||||
$fetchedAt = $this->normalizeTimestamp((string) ($legacyFetch['fetched_at'] ?? ''));
|
||||
@@ -881,45 +908,12 @@ final class Router
|
||||
return null;
|
||||
}
|
||||
|
||||
$snapshot = $this->fx()->nearestSnapshot($baseCurrency, $fetchedAt, null, 1);
|
||||
if (!is_array($snapshot)) {
|
||||
if (!method_exists($fxRepository, 'findFetchByBaseAndFetchedAt')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$snapshotFetchedAt = $this->normalizeTimestamp((string) ($snapshot['fetched_at'] ?? ''));
|
||||
if ($snapshotFetchedAt !== $fetchedAt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->snapshotMatchesLegacyFetch($snapshot, $legacyFetch) ? $snapshot : null;
|
||||
}
|
||||
|
||||
private function snapshotMatchesLegacyFetch(array $snapshot, array $legacyFetch): bool
|
||||
{
|
||||
$snapshotBase = strtoupper(trim((string) ($snapshot['base_currency'] ?? '')));
|
||||
$legacyBase = strtoupper(trim((string) ($legacyFetch['base_currency'] ?? '')));
|
||||
if ($snapshotBase === '' || $snapshotBase !== $legacyBase) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$snapshotRates = is_array($snapshot['rates'] ?? null) ? $snapshot['rates'] : [];
|
||||
$legacyRates = is_array($legacyFetch['rates'] ?? null) ? $legacyFetch['rates'] : [];
|
||||
if ($legacyRates === []) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($legacyRates as $currencyCode => $rate) {
|
||||
$currencyCode = strtoupper(trim((string) $currencyCode));
|
||||
if ($currencyCode === '' || !is_numeric($rate) || !array_key_exists($currencyCode, $snapshotRates) || !is_numeric($snapshotRates[$currencyCode])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->ratesAreEquivalent((float) $snapshotRates[$currencyCode], (float) $rate)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
$existing = $fxRepository->findFetchByBaseAndFetchedAt($baseCurrency, $fetchedAt);
|
||||
return is_array($existing) ? $existing : null;
|
||||
}
|
||||
|
||||
private function resolveLegacyMeasurementFetchId(array $measurement, array $measurementRates, array $legacyFetches): ?int
|
||||
@@ -1062,13 +1056,6 @@ final class Router
|
||||
return abs($leftTs - $rightTs);
|
||||
}
|
||||
|
||||
private function ratesAreEquivalent(float $left, float $right): bool
|
||||
{
|
||||
$diff = abs($left - $right);
|
||||
$tolerance = max(0.00000001, max(abs($left), abs($right)) * 0.000001);
|
||||
return $diff <= $tolerance;
|
||||
}
|
||||
|
||||
private function settings(string $projectKey): array
|
||||
{
|
||||
$settings = $this->repository()->getSettings($projectKey);
|
||||
@@ -1136,11 +1123,13 @@ final class Router
|
||||
return $this->settings($projectKey);
|
||||
}
|
||||
|
||||
private function measurements(string $projectKey, ?array $settingsOverride = null): array
|
||||
private function measurements(string $projectKey, ?array $settingsOverride = null, bool $resolveFxReferences = true): array
|
||||
{
|
||||
$settings = $settingsOverride ?? $this->settings($projectKey);
|
||||
$rows = $this->repository()->listMeasurements($projectKey, 500);
|
||||
$rows = $this->ensureMeasurementFxReferences($projectKey, $rows, $settings);
|
||||
if ($resolveFxReferences) {
|
||||
$rows = $this->ensureMeasurementFxReferences($projectKey, $rows, $settings);
|
||||
}
|
||||
return $this->analytics()->enrichMeasurements($rows, $settings);
|
||||
}
|
||||
|
||||
@@ -2125,6 +2114,17 @@ final class Router
|
||||
Http::json($payload, $statusCode);
|
||||
}
|
||||
|
||||
private function releaseSessionLock(): void
|
||||
{
|
||||
if (PHP_SAPI === 'cli') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (session_status() === PHP_SESSION_ACTIVE) {
|
||||
session_write_close();
|
||||
}
|
||||
}
|
||||
|
||||
private function debugLatest(): array
|
||||
{
|
||||
$filePath = DebugState::latestFilePath();
|
||||
|
||||
Reference in New Issue
Block a user