asdasd
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user