asdasd
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-01 03:37:41 +02:00
parent 5a154f896b
commit c444ece852
10 changed files with 436 additions and 152 deletions

View File

@@ -293,6 +293,7 @@ final class Router
$measurements = $this->measurements($projectKey);
$targets = $this->targets($projectKey);
$dashboards = $this->dashboards($projectKey);
$fxSnapshots = $this->measurementFxSnapshots($measurements);
return [
'project' => $this->repository()->getProject($projectKey),
@@ -300,6 +301,7 @@ final class Router
'measurements' => $measurements,
'targets' => $targets,
'dashboards' => $dashboards,
'fx_snapshots' => $fxSnapshots,
'summary' => $this->analytics()->buildSummary($measurements, $settings, $targets),
];
}
@@ -691,7 +693,42 @@ final class Router
private function fxHistory(): array
{
return $this->repository()->listFxRates(30);
if (!modules()->isEnabled('fx-rates') || !modules()->hasFunction('fx-rates', 'recent_fetches')) {
return [];
}
$fetches = module_fn('fx-rates', 'recent_fetches', 30);
if (!is_array($fetches)) {
return [];
}
$rows = [];
foreach ($fetches as $fetch) {
$fetchId = is_numeric($fetch['id'] ?? null) ? (int) $fetch['id'] : 0;
if ($fetchId <= 0) {
continue;
}
$snapshot = $this->fx()->snapshotByFetchId($fetchId, (string) ($fetch['base_currency'] ?? ''), null);
$rates = is_array($snapshot['rates'] ?? null) ? $snapshot['rates'] : [];
foreach ($rates as $currencyCode => $rate) {
if (!is_numeric($rate)) {
continue;
}
$rows[] = [
'fetch_id' => $fetchId,
'base_currency' => strtoupper((string) ($snapshot['base_currency'] ?? $fetch['base_currency'] ?? '')),
'target_currency' => strtoupper((string) $currencyCode),
'rate' => (float) $rate,
'rate_date' => (string) ($snapshot['rate_date'] ?? $fetch['rate_date'] ?? ''),
'provider' => (string) ($snapshot['provider'] ?? $fetch['provider'] ?? ''),
'fetched_at' => (string) ($snapshot['fetched_at'] ?? $fetch['fetched_at'] ?? ''),
];
}
}
return $rows;
}
private function settings(string $projectKey): array
@@ -729,7 +766,7 @@ final class Router
$base['payouts'] = $this->payouts($projectKey);
$base['miner_offers'] = $this->minerOffers($projectKey);
$base['purchased_miners'] = $this->purchasedMiners($projectKey);
$base['measurement_rates'] = $this->measurementRates($projectKey);
$base['measurement_rates'] = [];
return $base;
}
@@ -765,6 +802,7 @@ final class Router
{
$settings = $this->settings($projectKey);
$rows = $this->repository()->listMeasurements($projectKey, 500);
$rows = $this->ensureMeasurementFxReferences($projectKey, $rows);
return $this->analytics()->enrichMeasurements($rows, $settings);
}
@@ -785,6 +823,7 @@ final class Router
'ocr_raw_text' => $this->optionalString($input['ocr_raw_text'] ?? null, 65535),
'ocr_confidence' => $this->optionalDecimal($input['ocr_confidence'] ?? null),
'ocr_flags' => $this->optionalArray($input['ocr_flags'] ?? null),
'fx_fetch_id' => null,
];
if (($payload['price_per_coin'] === null) xor ($payload['price_currency'] === null)) {
@@ -792,8 +831,8 @@ final class Router
}
$this->syncCurrencyCatalogForMeasurement($payload);
$payload['fx_fetch_id'] = $this->resolveMeasurementFxFetchId($projectKey, $payload, true);
$created = $this->repository()->createMeasurement($projectKey, $payload);
$this->captureMeasurementRates($projectKey, $created);
$measurements = $this->measurements($projectKey);
return $measurements[array_key_last($measurements)];
}
@@ -823,11 +862,11 @@ final class Router
try {
$payload = $this->parseImportLine($trimmed, $defaultCurrency, $defaultSource, $this->projectTimezone($projectKey));
$this->syncCurrencyCatalogForMeasurement($payload);
$payload['fx_fetch_id'] = $this->resolveMeasurementFxFetchId($projectKey, $payload, false);
$result = $this->repository()->createMeasurementIfNotExists($projectKey, $payload);
if ($result === null) {
$duplicates++;
} else {
$this->captureMeasurementRates($projectKey, $result);
$imported++;
}
} catch (\Throwable $exception) {
@@ -954,7 +993,7 @@ final class Router
private function measurementRates(string $projectKey): array
{
return $this->repository()->listMeasurementRates($projectKey);
return [];
}
private function currencies(): array
@@ -1544,80 +1583,98 @@ final class Router
return $result;
}
private function captureMeasurementRates(string $projectKey, array $measurement): void
private function resolveMeasurementFxFetchId(string $projectKey, array $payload, bool $allowRefresh): ?int
{
$measurementId = (int) ($measurement['id'] ?? 0);
$price = is_numeric($measurement['price_per_coin'] ?? null) ? (float) $measurement['price_per_coin'] : null;
$priceCurrency = strtoupper(trim((string) ($measurement['price_currency'] ?? '')));
if ($measurementId <= 0) {
return;
}
$this->ensureFreshFxForMeasurement($projectKey, $priceCurrency !== '' ? $priceCurrency : 'USD');
$rates = [];
if ($price !== null && $price > 0 && $priceCurrency !== '') {
$rates[] = ['base_currency' => 'DOGE', 'quote_currency' => $priceCurrency, 'rate' => $price, 'provider' => 'measurement'];
$rates[] = ['base_currency' => $priceCurrency, 'quote_currency' => 'DOGE', 'rate' => 1 / $price, 'provider' => 'measurement'];
foreach (['USD', 'EUR'] as $fiatCurrency) {
if ($fiatCurrency === $priceCurrency) {
continue;
}
$convertedPrice = $this->priceForCurrency($price, $priceCurrency, $fiatCurrency);
if ($convertedPrice === null || $convertedPrice <= 0) {
continue;
}
$rates[] = ['base_currency' => 'DOGE', 'quote_currency' => $fiatCurrency, 'rate' => $convertedPrice, 'provider' => 'derived'];
$rates[] = ['base_currency' => $fiatCurrency, 'quote_currency' => 'DOGE', 'rate' => 1 / $convertedPrice, 'provider' => 'derived'];
}
} else {
foreach (['USD', 'EUR'] as $fiatCurrency) {
$fxPrice = $this->fx()->rate('DOGE', $fiatCurrency);
if ($fxPrice === null || $fxPrice <= 0) {
continue;
}
$rates[] = ['base_currency' => 'DOGE', 'quote_currency' => $fiatCurrency, 'rate' => $fxPrice, 'provider' => 'fx'];
$rates[] = ['base_currency' => $fiatCurrency, 'quote_currency' => 'DOGE', 'rate' => 1 / $fxPrice, 'provider' => 'fx'];
}
}
$eurUsd = $this->fx()->rate('EUR', 'USD');
if ($eurUsd !== null) {
$rates[] = ['base_currency' => 'EUR', 'quote_currency' => 'USD', 'rate' => $eurUsd, 'provider' => 'fx'];
$rates[] = ['base_currency' => 'USD', 'quote_currency' => 'EUR', 'rate' => 1 / $eurUsd, 'provider' => 'fx'];
}
if ($rates !== []) {
$this->repository()->replaceMeasurementRates($measurementId, $projectKey, $rates);
}
}
private function priceForCurrency(float $price, string $fromCurrency, string $toCurrency): ?float
{
$from = strtoupper(trim($fromCurrency));
$to = strtoupper(trim($toCurrency));
if ($from === $to) {
return $price;
}
$converted = $this->fx()->convert($price, $from, $to);
return is_numeric($converted) ? (float) $converted : null;
}
private function ensureFreshFxForMeasurement(string $projectKey, string $priceCurrency): void
{
$normalizedCurrency = strtoupper(trim($priceCurrency));
if ($normalizedCurrency === '') {
return;
}
$measuredAt = trim((string) ($payload['measured_at'] ?? ''));
$settings = $this->settings($projectKey);
$maxAgeHours = is_numeric($settings['fx_max_age_hours'] ?? null) ? (float) $settings['fx_max_age_hours'] : 3.0;
$this->fx()->ensureFreshLatestRates($maxAgeHours, 'USD');
if ($allowRefresh && $measuredAt !== '' && $this->isRecentTimestamp($measuredAt, $maxAgeHours)) {
$fresh = $this->fx()->ensureFreshLatestRates($maxAgeHours, 'USD');
if (is_array($fresh) && is_numeric($fresh['fetch_id'] ?? null)) {
return (int) $fresh['fetch_id'];
}
}
if ($measuredAt !== '') {
$nearest = $this->fx()->nearestSnapshot('USD', $measuredAt, null, null);
if (is_array($nearest) && is_numeric($nearest['id'] ?? null)) {
return (int) $nearest['id'];
}
}
$latest = $this->fx()->latestSnapshot('USD', null);
if (is_array($latest) && is_numeric($latest['id'] ?? null)) {
return (int) $latest['id'];
}
if ($allowRefresh) {
$fresh = $this->fx()->refreshLatestRates(null, 'USD');
if (is_array($fresh) && is_numeric($fresh['fetch_id'] ?? null)) {
return (int) $fresh['fetch_id'];
}
}
return null;
}
private function ensureMeasurementFxReferences(string $projectKey, array $rows): array
{
$resolved = [];
foreach ($rows as $row) {
if (!is_array($row)) {
continue;
}
$fetchId = is_numeric($row['fx_fetch_id'] ?? null) ? (int) $row['fx_fetch_id'] : 0;
if ($fetchId <= 0) {
$resolvedFetchId = $this->resolveMeasurementFxFetchId($projectKey, $row, false);
if ($resolvedFetchId !== null) {
$row = $this->repository()->setMeasurementFxFetchId($projectKey, (int) ($row['id'] ?? 0), $resolvedFetchId) ?? array_merge($row, ['fx_fetch_id' => $resolvedFetchId]);
}
}
$resolved[] = $row;
}
return $resolved;
}
private function measurementFxSnapshots(array $measurements): array
{
$snapshots = [];
foreach ($measurements as $measurement) {
$fetchId = is_numeric($measurement['fx_fetch_id'] ?? null) ? (int) $measurement['fx_fetch_id'] : 0;
if ($fetchId <= 0 || isset($snapshots[$fetchId])) {
continue;
}
$snapshot = $this->fx()->snapshotByFetchId($fetchId, null, null);
if (!is_array($snapshot)) {
continue;
}
$snapshots[(string) $fetchId] = [
'id' => is_numeric($snapshot['id'] ?? null) ? (int) $snapshot['id'] : $fetchId,
'base_currency' => (string) ($snapshot['base_currency'] ?? ''),
'rate_date' => (string) ($snapshot['rate_date'] ?? ''),
'provider' => (string) ($snapshot['provider'] ?? ''),
'fetched_at' => (string) ($snapshot['fetched_at'] ?? ''),
'rates' => is_array($snapshot['rates'] ?? null) ? $snapshot['rates'] : [],
];
}
return $snapshots;
}
private function isRecentTimestamp(string $timestamp, float $maxAgeHours): bool
{
$parsed = strtotime($timestamp);
if ($parsed === false) {
return false;
}
return abs(time() - $parsed) <= (int) round(max(0.25, $maxAgeHours) * 3600);
}
private function resolveOfferPurchaseCost(array $offer): float