ydasd
This commit is contained in:
@@ -62,6 +62,7 @@ $mm->registerFunction($moduleName, 'settings', static function (): array {
|
||||
'api_url' => $apiUrl,
|
||||
'api_key' => $apiKey,
|
||||
'timeout_sec' => $timeout,
|
||||
'refresh_max_age_minutes' => max(1, (int) ($saved['refresh_max_age_minutes'] ?? 60)),
|
||||
'default_base_currency' => strtoupper(trim((string) ($saved['default_base_currency'] ?? 'EUR'))) ?: 'EUR',
|
||||
'display_base_currency' => strtoupper(trim((string) ($saved['display_base_currency'] ?? ($saved['default_base_currency'] ?? 'EUR')))) ?: 'EUR',
|
||||
'preferred_currencies' => $preferredCurrencies,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"title": "Waehrungskurse",
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.5",
|
||||
"description": "Zentrales Modul fuer Waehrungskurse, Historie und API-Abrufe.",
|
||||
"enabled_by_default": true,
|
||||
"setup": {
|
||||
@@ -18,6 +18,7 @@
|
||||
{ "name": "api_url", "label": "FX API URL", "type": "text", "required": false, "help": "Nur die Basis-URL eintragen, z.B. https://api.currencyapi.com oder https://currencyapi.net." },
|
||||
{ "name": "api_key", "label": "FX API Key", "type": "password", "required": false },
|
||||
{ "name": "timeout_sec", "label": "Timeout (Sek.)", "type": "number", "required": false },
|
||||
{ "name": "refresh_max_age_minutes", "label": "Max. Alter fuer API-Refresh (Min.)", "type": "number", "required": false, "help": "Refresh aktualisiert nur, wenn der letzte gespeicherte Abruf aelter ist." },
|
||||
{ "name": "default_base_currency", "label": "Standard-Basiswaehrung", "type": "text", "required": false, "help": "Wird fuer taegliche Abrufe und Snapshot-Abfragen verwendet." },
|
||||
{ "name": "display_base_currency", "label": "Anzeige-Basiswaehrung", "type": "select", "required": false, "help": "Basis fuer die Anzeige der zuletzt gespeicherten Kurse im Modul." },
|
||||
{ "name": "preferred_currencies", "label": "Bevorzugte Waehrungen", "type": "multiselect", "required": false, "help": "Auswahl aus dem synchronisierten Waehrungskatalog." },
|
||||
|
||||
@@ -42,6 +42,23 @@ final class Router
|
||||
$this->respond(['data' => $snapshot]);
|
||||
}
|
||||
|
||||
if ($path === 'v1/fetch' && $method === 'GET') {
|
||||
$fetchId = max(0, (int) ($_GET['fetch_id'] ?? 0));
|
||||
$base = $this->stringOrNull($_GET['base'] ?? null);
|
||||
$symbols = $this->parseCsv($_GET['symbols'] ?? null);
|
||||
$snapshot = $this->service->snapshotByFetchId($fetchId, $base, $symbols);
|
||||
$this->respond(['data' => $snapshot]);
|
||||
}
|
||||
|
||||
if ($path === 'v1/nearest' && $method === 'GET') {
|
||||
$base = $this->stringOrNull($_GET['base'] ?? null);
|
||||
$symbols = $this->parseCsv($_GET['symbols'] ?? null);
|
||||
$at = $this->stringOrNull($_GET['at'] ?? null);
|
||||
$windowMinutes = $this->intOrNull($_GET['window_minutes'] ?? null);
|
||||
$snapshot = $this->service->nearestSnapshot($base, (string) $at, $symbols, $windowMinutes);
|
||||
$this->respond(['data' => $snapshot]);
|
||||
}
|
||||
|
||||
if ($path === 'v1/snapshot' && $method === 'GET') {
|
||||
$symbols = $this->parseCsv($_GET['symbols'] ?? null);
|
||||
$base = $this->stringOrNull($_GET['base'] ?? null);
|
||||
@@ -74,11 +91,11 @@ final class Router
|
||||
$input = $this->input();
|
||||
$base = $this->stringOrNull($input['base'] ?? null);
|
||||
$force = !empty($input['force']);
|
||||
$maxAgeHours = is_numeric($input['max_age_hours'] ?? null) ? (float) $input['max_age_hours'] : 24.0;
|
||||
$maxAgeMinutes = is_numeric($input['max_age_minutes'] ?? null) ? (int) $input['max_age_minutes'] : null;
|
||||
|
||||
$result = $force
|
||||
? $this->service->refreshLatestRates(null, $base)
|
||||
: $this->service->ensureFreshLatestRates($maxAgeHours, $base, null);
|
||||
: $this->service->autoRefreshLatestRates($base, null, $maxAgeMinutes);
|
||||
|
||||
$this->respond(['data' => $result], 201);
|
||||
}
|
||||
|
||||
@@ -79,6 +79,58 @@ final class FxRatesService
|
||||
]);
|
||||
}
|
||||
|
||||
public function snapshotByFetchId(int $fetchId, ?string $baseCurrency = null, ?array $symbols = null): ?array
|
||||
{
|
||||
if ($fetchId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$requestedBase = $this->normalizeCurrency($baseCurrency ?: $this->defaultBaseCurrency());
|
||||
if ($requestedBase === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$snapshot = $this->repository->getSnapshotByFetchId($fetchId, null);
|
||||
if ($snapshot === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->localizeSnapshot($this->rebaseSnapshot($snapshot, $requestedBase, $symbols));
|
||||
}
|
||||
|
||||
public function nearestSnapshot(?string $baseCurrency = null, string $at = '', ?array $symbols = null, ?int $windowMinutes = null): ?array
|
||||
{
|
||||
$timestamp = $this->normalizeTimestamp($at);
|
||||
if ($timestamp === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$requestedBase = $this->normalizeCurrency($baseCurrency ?: $this->defaultBaseCurrency());
|
||||
if ($requestedBase === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$nearest = $this->repository->findNearestFetch(null, $timestamp, $windowMinutes);
|
||||
if ($nearest === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$snapshot = $this->repository->getSnapshotByFetchId((int) ($nearest['id'] ?? 0), null);
|
||||
if ($snapshot === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$rebased = $this->rebaseSnapshot($snapshot, $requestedBase, $symbols);
|
||||
if ($rebased === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->localizeSnapshot($rebased + [
|
||||
'requested_at' => $timestamp,
|
||||
'distance_seconds' => $nearest['distance_seconds'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function findRate(?string $fromCurrency, ?string $toCurrency, ?string $at = null, ?int $windowMinutes = null): ?array
|
||||
{
|
||||
$from = $this->normalizeCurrency($fromCurrency);
|
||||
@@ -193,6 +245,13 @@ final class FxRatesService
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function autoRefreshLatestRates(?string $baseCurrency = null, ?array $currencies = null, ?int $maxAgeMinutes = null): array
|
||||
{
|
||||
$minutes = $maxAgeMinutes ?? $this->refreshMaxAgeMinutes();
|
||||
$hours = max(1, $minutes) / 60;
|
||||
return $this->ensureFreshLatestRates($hours, $baseCurrency, $currencies);
|
||||
}
|
||||
|
||||
public function history(string $fromCurrency, string $toCurrency, ?string $from = null, ?string $to = null, int $limit = 200): array
|
||||
{
|
||||
$fromCurrency = $this->normalizeCurrency($fromCurrency);
|
||||
@@ -600,9 +659,14 @@ final class FxRatesService
|
||||
}
|
||||
|
||||
if ($requestedBase === '' || $requestedBase === $snapshotBase) {
|
||||
$filteredRates = $this->filterRates($rates, $symbols);
|
||||
if ($this->symbolsContain($symbols, $requestedBase)) {
|
||||
$filteredRates = [$requestedBase => 1.0] + $filteredRates;
|
||||
}
|
||||
|
||||
return $snapshot + [
|
||||
'base_currency' => $snapshotBase,
|
||||
'rates' => $this->filterRates($rates, $symbols),
|
||||
'rates' => $filteredRates,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -620,9 +684,14 @@ final class FxRatesService
|
||||
$rebasedRates[$code] = (float) $rate / (float) $baseRate;
|
||||
}
|
||||
|
||||
$filteredRates = $this->filterRates($rebasedRates, $symbols);
|
||||
if ($this->symbolsContain($symbols, $requestedBase)) {
|
||||
$filteredRates = [$requestedBase => 1.0] + $filteredRates;
|
||||
}
|
||||
|
||||
return $snapshot + [
|
||||
'base_currency' => $requestedBase,
|
||||
'rates' => $this->filterRates($rebasedRates, $symbols),
|
||||
'rates' => $filteredRates,
|
||||
'snapshot_base_currency' => $snapshotBase,
|
||||
];
|
||||
}
|
||||
@@ -646,6 +715,21 @@ final class FxRatesService
|
||||
return $filtered;
|
||||
}
|
||||
|
||||
private function symbolsContain(?array $symbols, string $currency): bool
|
||||
{
|
||||
if (!is_array($symbols) || $symbols === []) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($symbols as $symbol) {
|
||||
if ($this->normalizeCurrency((string) $symbol) === $currency) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function crossHistory(string $fromCurrency, string $toCurrency, ?string $from = null, ?string $to = null, int $limit = 200): array
|
||||
{
|
||||
$fromAt = $this->normalizeTimestamp($from);
|
||||
@@ -884,6 +968,11 @@ final class FxRatesService
|
||||
return max(2, (int) ($this->settings['timeout_sec'] ?? 10));
|
||||
}
|
||||
|
||||
private function refreshMaxAgeMinutes(): int
|
||||
{
|
||||
return max(1, (int) ($this->settings['refresh_max_age_minutes'] ?? 60));
|
||||
}
|
||||
|
||||
private function defaultBaseCurrency(): string
|
||||
{
|
||||
return $this->normalizeCurrency((string) ($this->settings['default_base_currency'] ?? 'EUR')) ?: 'EUR';
|
||||
|
||||
@@ -146,6 +146,55 @@ final class FxRatesRepository
|
||||
];
|
||||
}
|
||||
|
||||
public function findNearestFetch(?string $baseCurrency, string $timestamp, ?int $windowMinutes = null): ?array
|
||||
{
|
||||
$targetTs = strtotime($timestamp);
|
||||
if ($targetTs === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($baseCurrency !== null && trim($baseCurrency) !== '') {
|
||||
return $this->getNearestFetch(strtoupper(trim($baseCurrency)), $timestamp, $windowMinutes);
|
||||
}
|
||||
|
||||
$candidates = [];
|
||||
foreach (['<=', '>='] as $operator) {
|
||||
$order = $operator === '<=' ? 'DESC' : 'ASC';
|
||||
$stmt = $this->pdo->prepare(
|
||||
'SELECT id, provider, base_currency, rate_date, fetched_at, created_at
|
||||
FROM ' . $this->table('fetches') . '
|
||||
WHERE fetched_at ' . $operator . ' :target_at
|
||||
ORDER BY fetched_at ' . $order . ', id ' . $order . '
|
||||
LIMIT 1'
|
||||
);
|
||||
$stmt->execute(['target_at' => $timestamp]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (is_array($row)) {
|
||||
$candidate = $this->normalizeFetch($row);
|
||||
$candidateTs = strtotime((string) ($candidate['fetched_at'] ?? ''));
|
||||
if ($candidateTs !== false) {
|
||||
$candidate['distance_seconds'] = abs($candidateTs - $targetTs);
|
||||
$candidates[] = $candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($candidates === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
usort($candidates, static function (array $left, array $right): int {
|
||||
return ((int) ($left['distance_seconds'] ?? PHP_INT_MAX)) <=> ((int) ($right['distance_seconds'] ?? PHP_INT_MAX));
|
||||
});
|
||||
|
||||
$selected = $candidates[0];
|
||||
if ($windowMinutes !== null && $windowMinutes > 0 && (int) ($selected['distance_seconds'] ?? 0) > ($windowMinutes * 60)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $selected;
|
||||
}
|
||||
|
||||
public function getNearestFetch(string $baseCurrency, string $timestamp, ?int $windowMinutes = null): ?array
|
||||
{
|
||||
$baseCurrency = strtoupper(trim($baseCurrency));
|
||||
|
||||
Reference in New Issue
Block a user