asdasd
This commit is contained in:
@@ -59,17 +59,11 @@
|
|||||||
cursor: wait;
|
cursor: wait;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fx-meta-grid,
|
|
||||||
.fx-form-grid {
|
.fx-form-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fx-meta-grid {
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
||||||
margin-top: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fx-form-grid {
|
.fx-form-grid {
|
||||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
}
|
}
|
||||||
@@ -96,7 +90,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.fx-message {
|
.fx-message {
|
||||||
min-height: 1.5rem;
|
margin-bottom: 0.9rem;
|
||||||
color: #1c2734;
|
color: #1c2734;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,11 +131,10 @@
|
|||||||
color: #1c2734;
|
color: #1c2734;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fx-history-block {
|
.fx-card-meta {
|
||||||
margin-top: 1.25rem;
|
display: grid;
|
||||||
}
|
gap: 0.35rem;
|
||||||
|
color: #5b6573;
|
||||||
.fx-history-block h3 {
|
font-size: 0.95rem;
|
||||||
margin: 0 0 0.75rem;
|
text-align: right;
|
||||||
font-size: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,40 +7,19 @@
|
|||||||
const page = JSON.parse(root.dataset.page || '{}');
|
const page = JSON.parse(root.dataset.page || '{}');
|
||||||
const settings = page.settings || {};
|
const settings = page.settings || {};
|
||||||
const nodes = {
|
const nodes = {
|
||||||
message: root.querySelector('[data-bind="message"]'),
|
|
||||||
lastFetch: root.querySelector('[data-bind="last-fetch"]'),
|
|
||||||
defaultBase: root.querySelector('[data-bind="default-base"]'),
|
|
||||||
displayBase: root.querySelector('[data-bind="display-base"]'),
|
|
||||||
ratesBody: root.querySelector('[data-bind="rates-body"]'),
|
ratesBody: root.querySelector('[data-bind="rates-body"]'),
|
||||||
fetchesBody: root.querySelector('[data-bind="fetches-body"]'),
|
fetchesBody: root.querySelector('[data-bind="fetches-body"]'),
|
||||||
convertResult: root.querySelector('[data-bind="convert-result"]'),
|
convertResult: root.querySelector('[data-bind="convert-result"]'),
|
||||||
defaultBaseInput: root.querySelector('input[name="default_base_currency"]'),
|
|
||||||
displayBaseInput: root.querySelector('input[name="display_base_currency"]'),
|
|
||||||
preferredCurrenciesInput: root.querySelector('input[name="preferred_currencies"]'),
|
|
||||||
convertFrom: root.querySelector('select[name="convert_from"]'),
|
convertFrom: root.querySelector('select[name="convert_from"]'),
|
||||||
convertTo: root.querySelector('select[name="convert_to"]'),
|
convertTo: root.querySelector('select[name="convert_to"]'),
|
||||||
convertAmount: root.querySelector('input[name="convert_amount"]'),
|
convertAmount: root.querySelector('input[name="convert_amount"]'),
|
||||||
};
|
};
|
||||||
const apiBase = '/api/fx-rates/v1';
|
const apiBase = '/api/fx-rates/v1';
|
||||||
|
const preferredCurrencies = Array.isArray(page.preferred_currencies)
|
||||||
const setMessage = (text, type = '') => {
|
? page.preferred_currencies
|
||||||
if (!nodes.message) {
|
.map((item) => String(item || '').trim().toUpperCase())
|
||||||
return;
|
.filter(Boolean)
|
||||||
}
|
: [];
|
||||||
nodes.message.textContent = text || '';
|
|
||||||
nodes.message.className = `fx-message${type ? ` is-${type}` : ''}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const setLoading = (state) => {
|
|
||||||
root.querySelectorAll('button[data-action]').forEach((button) => {
|
|
||||||
button.disabled = state;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const parsePreferredCurrencies = () => String(nodes.preferredCurrenciesInput?.value || '')
|
|
||||||
.split(/[\s,;]+/)
|
|
||||||
.map((item) => item.trim().toUpperCase())
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
const renderSnapshot = (snapshot) => {
|
const renderSnapshot = (snapshot) => {
|
||||||
const rates = snapshot && snapshot.rates ? snapshot.rates : null;
|
const rates = snapshot && snapshot.rates ? snapshot.rates : null;
|
||||||
@@ -73,7 +52,7 @@
|
|||||||
}
|
}
|
||||||
nodes.fetchesBody.innerHTML = entries.map((entry) => `
|
nodes.fetchesBody.innerHTML = entries.map((entry) => `
|
||||||
<tr>
|
<tr>
|
||||||
<td>${entry?.fetched_at || ''}</td>
|
<td>${entry?.fetched_at_display || entry?.fetched_at || ''}</td>
|
||||||
<td>${entry?.base_currency || ''}</td>
|
<td>${entry?.base_currency || ''}</td>
|
||||||
<td>${entry?.provider || ''}</td>
|
<td>${entry?.provider || ''}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -99,23 +78,16 @@
|
|||||||
|
|
||||||
const loadLatest = async () => {
|
const loadLatest = async () => {
|
||||||
const base = String(
|
const base = String(
|
||||||
nodes.displayBaseInput?.value || settings.display_base_currency || settings.default_base_currency || 'EUR'
|
settings.display_base_currency || settings.default_base_currency || 'EUR'
|
||||||
).trim().toUpperCase();
|
).trim().toUpperCase();
|
||||||
const preferred = parsePreferredCurrencies();
|
|
||||||
const query = new URLSearchParams();
|
const query = new URLSearchParams();
|
||||||
query.set('base', base);
|
query.set('base', base);
|
||||||
if (preferred.length) {
|
if (preferredCurrencies.length) {
|
||||||
query.set('symbols', preferred.join(','));
|
query.set('symbols', preferredCurrencies.join(','));
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await request(`/latest?${query.toString()}`);
|
const data = await request(`/latest?${query.toString()}`);
|
||||||
renderSnapshot(data);
|
renderSnapshot(data);
|
||||||
if (nodes.lastFetch) {
|
|
||||||
nodes.lastFetch.textContent = data?.fetched_at || 'noch keiner';
|
|
||||||
}
|
|
||||||
if (nodes.displayBase) {
|
|
||||||
nodes.displayBase.textContent = base;
|
|
||||||
}
|
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -152,54 +124,6 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
root.querySelector('[data-action="refresh-rates"]')?.addEventListener('click', async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
setMessage('Ich rufe jetzt die aktuellen Kurse ab.');
|
|
||||||
const payload = {
|
|
||||||
force: true,
|
|
||||||
base: String(nodes.defaultBaseInput?.value || settings.default_base_currency || 'EUR').trim().toUpperCase(),
|
|
||||||
currencies: parsePreferredCurrencies(),
|
|
||||||
};
|
|
||||||
const data = await request('/refresh', { method: 'POST', body: JSON.stringify(payload) });
|
|
||||||
setMessage(`Aktuelle Kurse gespeichert. ${data?.updated_count || 0} Werte aktualisiert.`, 'success');
|
|
||||||
await loadLatest();
|
|
||||||
const recentFetches = await request('/recent-fetches?limit=12');
|
|
||||||
renderFetches(recentFetches);
|
|
||||||
await calculateConversion();
|
|
||||||
} catch (error) {
|
|
||||||
setMessage(error.message || 'Kurse konnten nicht aktualisiert werden.', 'error');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
root.querySelector('[data-action="save-settings"]')?.addEventListener('click', async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
setMessage('Ich speichere jetzt die Waehrungs-Auswahl.');
|
|
||||||
const payload = {
|
|
||||||
default_base_currency: String(nodes.defaultBaseInput?.value || '').trim().toUpperCase(),
|
|
||||||
display_base_currency: String(nodes.displayBaseInput?.value || '').trim().toUpperCase(),
|
|
||||||
preferred_currencies: parsePreferredCurrencies(),
|
|
||||||
};
|
|
||||||
const data = await request('/settings', { method: 'PUT', body: JSON.stringify(payload) });
|
|
||||||
if (nodes.defaultBase) {
|
|
||||||
nodes.defaultBase.textContent = data?.default_base_currency || '';
|
|
||||||
}
|
|
||||||
if (nodes.displayBase) {
|
|
||||||
nodes.displayBase.textContent = data?.display_base_currency || '';
|
|
||||||
}
|
|
||||||
setMessage('Waehrungs-Auswahl gespeichert.', 'success');
|
|
||||||
await loadLatest();
|
|
||||||
await calculateConversion();
|
|
||||||
} catch (error) {
|
|
||||||
setMessage(error.message || 'Waehrungs-Auswahl konnte nicht gespeichert werden.', 'error');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
[nodes.convertFrom, nodes.convertTo, nodes.convertAmount].forEach((node) => {
|
[nodes.convertFrom, nodes.convertTo, nodes.convertAmount].forEach((node) => {
|
||||||
node?.addEventListener('change', () => {
|
node?.addEventListener('change', () => {
|
||||||
calculateConversion().catch(() => {});
|
calculateConversion().catch(() => {});
|
||||||
@@ -211,8 +135,6 @@
|
|||||||
|
|
||||||
renderFetches(page.recent_fetches || []);
|
renderFetches(page.recent_fetches || []);
|
||||||
|
|
||||||
loadLatest().catch((error) => {
|
loadLatest().catch(() => {});
|
||||||
setMessage(error.message || 'Letzter Snapshot konnte nicht geladen werden.', 'error');
|
|
||||||
});
|
|
||||||
calculateConversion().catch(() => {});
|
calculateConversion().catch(() => {});
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -11,9 +11,28 @@ if ($assets) {
|
|||||||
|
|
||||||
$settings = module_fn('fx-rates', 'settings');
|
$settings = module_fn('fx-rates', 'settings');
|
||||||
$service = module_fn('fx-rates', 'service');
|
$service = module_fn('fx-rates', 'service');
|
||||||
|
$preferredCurrencies = is_array($settings['preferred_currencies'] ?? null) ? $settings['preferred_currencies'] : [];
|
||||||
|
$notice = trim((string) ($_GET['notice'] ?? ''));
|
||||||
|
$error = trim((string) ($_GET['error'] ?? ''));
|
||||||
|
|
||||||
|
if ((string) ($_GET['refresh'] ?? '') === '1') {
|
||||||
|
try {
|
||||||
|
$result = $service->refreshLatestRates(null, (string) ($settings['default_base_currency'] ?? ''));
|
||||||
|
$params = [
|
||||||
|
'notice' => sprintf(
|
||||||
|
'Aktuelle Kurse gespeichert. %d Werte aktualisiert.',
|
||||||
|
(int) ($result['updated_count'] ?? 0)
|
||||||
|
),
|
||||||
|
];
|
||||||
|
} catch (\Throwable $exception) {
|
||||||
|
$params = ['error' => $exception->getMessage() !== '' ? $exception->getMessage() : 'Kurse konnten nicht aktualisiert werden.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
redirect('/module/fx-rates?' . http_build_query($params));
|
||||||
|
}
|
||||||
|
|
||||||
$latest = $service->latestStatus();
|
$latest = $service->latestStatus();
|
||||||
$recentFetches = $service->recentFetches(12);
|
$recentFetches = $service->recentFetches(12);
|
||||||
$preferredCurrencies = is_array($settings['preferred_currencies'] ?? null) ? $settings['preferred_currencies'] : [];
|
|
||||||
$pageData = json_encode([
|
$pageData = json_encode([
|
||||||
'settings' => $settings,
|
'settings' => $settings,
|
||||||
'latest' => $latest,
|
'latest' => $latest,
|
||||||
@@ -21,49 +40,22 @@ $pageData = json_encode([
|
|||||||
'recent_fetches' => $recentFetches,
|
'recent_fetches' => $recentFetches,
|
||||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
?>
|
?>
|
||||||
<?= module_shell_header('fx-rates', ['title' => 'Waehrungskurse']) ?>
|
<?= module_shell_header('fx-rates', [
|
||||||
|
'title' => 'Waehrungskurse',
|
||||||
|
'actions' => [
|
||||||
|
['label' => 'Setup', 'href' => '/modules/setup/fx-rates', 'variant' => 'secondary', 'size' => 'sm'],
|
||||||
|
['label' => 'Aktuelle Kurse abrufen', 'href' => '/module/fx-rates?refresh=1', 'variant' => 'secondary', 'size' => 'sm'],
|
||||||
|
],
|
||||||
|
]) ?>
|
||||||
<div id="fx-rates-app" data-page='<?= e(is_string($pageData) ? $pageData : '{}') ?>'>
|
<div id="fx-rates-app" data-page='<?= e(is_string($pageData) ? $pageData : '{}') ?>'>
|
||||||
<div class="fx-stack">
|
<div class="fx-stack">
|
||||||
<div class="fx-card">
|
<div class="fx-card">
|
||||||
<div class="fx-card-head">
|
<?php if ($notice !== ''): ?>
|
||||||
<div>
|
<div class="fx-message is-success"><?= e($notice) ?></div>
|
||||||
<h1>Waehrungskurse</h1>
|
<?php elseif ($error !== ''): ?>
|
||||||
<p>Zentrale Quelle fuer FX-Snapshots, Zeitabfragen und manuelle Aktualisierung.</p>
|
<div class="fx-message is-error"><?= e($error) ?></div>
|
||||||
</div>
|
<?php endif; ?>
|
||||||
</div>
|
|
||||||
<div class="fx-meta-grid">
|
|
||||||
<div><strong>Standard-Basis:</strong> <span data-bind="default-base"><?= e((string) ($settings['default_base_currency'] ?? 'EUR')) ?></span></div>
|
|
||||||
<div><strong>Anzeige-Basis:</strong> <span data-bind="display-base"><?= e((string) ($settings['display_base_currency'] ?? 'EUR')) ?></span></div>
|
|
||||||
<div><strong>Scheduler:</strong> taeglich um <?= e(str_pad((string) ($settings['daily_refresh_hour'] ?? 18), 2, '0', STR_PAD_LEFT)) ?>:00 (<?= e((string) ($settings['schedule_timezone'] ?? 'Europe/Berlin')) ?>)</div>
|
|
||||||
<div><strong>Letzter Abruf:</strong> <span data-bind="last-fetch"><?= e((string) ($latest['fetched_at'] ?? 'noch keiner')) ?></span></div>
|
|
||||||
</div>
|
|
||||||
<div class="fx-message" data-bind="message"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="fx-card">
|
|
||||||
<h2>Setup</h2>
|
|
||||||
<p>Die Auswahl wird in den Modul-Settings gespeichert und steuert den Standardaufruf der letzten Kurse.</p>
|
|
||||||
<div class="fx-form-grid">
|
|
||||||
<label>
|
|
||||||
<span>Standard-Basiswaehrung</span>
|
|
||||||
<input type="text" name="default_base_currency" value="<?= e((string) ($settings['default_base_currency'] ?? 'EUR')) ?>">
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span>Anzeige-Basiswaehrung</span>
|
|
||||||
<input type="text" name="display_base_currency" value="<?= e((string) ($settings['display_base_currency'] ?? 'EUR')) ?>">
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<label class="fx-block">
|
|
||||||
<span>Bevorzugte Waehrungen</span>
|
|
||||||
<input type="text" name="preferred_currencies" value="<?= e(implode(', ', $preferredCurrencies)) ?>" placeholder="EUR, USD, DOGE, BTC">
|
|
||||||
</label>
|
|
||||||
<div class="fx-actions">
|
|
||||||
<button type="button" class="fx-button" data-action="save-settings">Auswahl speichern</button>
|
|
||||||
<button type="button" class="fx-button fx-button--primary" data-action="refresh-rates">Aktuelle Kurse abrufen</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="fx-card">
|
|
||||||
<h2>Umrechnung</h2>
|
<h2>Umrechnung</h2>
|
||||||
<p>Umrechnung auf Basis des letzten verfuegbaren Kurses zwischen den bevorzugten Waehrungen.</p>
|
<p>Umrechnung auf Basis des letzten verfuegbaren Kurses zwischen den bevorzugten Waehrungen.</p>
|
||||||
<div class="fx-form-grid">
|
<div class="fx-form-grid">
|
||||||
@@ -92,7 +84,16 @@ $pageData = json_encode([
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="fx-card">
|
<div class="fx-card">
|
||||||
<h2>Letzter Snapshot</h2>
|
<div class="fx-card-head">
|
||||||
|
<div>
|
||||||
|
<h2>Letzte Kurse</h2>
|
||||||
|
<p>Letzter gespeicherter Snapshot fuer <?= e((string) ($settings['display_base_currency'] ?? $settings['default_base_currency'] ?? 'EUR')) ?>.</p>
|
||||||
|
</div>
|
||||||
|
<div class="fx-card-meta">
|
||||||
|
<div><strong>Letzter Abruf:</strong> <?= e((string) ($latest['fetched_at_display'] ?? $latest['fetched_at'] ?? 'noch keiner')) ?></div>
|
||||||
|
<div><strong>Basis:</strong> <?= e((string) ($latest['base_currency'] ?? $settings['default_base_currency'] ?? '')) ?></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="fx-table-wrap">
|
<div class="fx-table-wrap">
|
||||||
<table class="fx-table">
|
<table class="fx-table">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -106,8 +107,10 @@ $pageData = json_encode([
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="fx-history-block">
|
</div>
|
||||||
<h3>Letzte Abrufe</h3>
|
|
||||||
|
<div class="fx-card">
|
||||||
|
<h2>Letzte Abrufe</h2>
|
||||||
<div class="fx-table-wrap">
|
<div class="fx-table-wrap">
|
||||||
<table class="fx-table">
|
<table class="fx-table">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -123,7 +126,7 @@ $pageData = json_encode([
|
|||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?php foreach ($recentFetches as $fetch): ?>
|
<?php foreach ($recentFetches as $fetch): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td><?= e((string) ($fetch['fetched_at'] ?? '')) ?></td>
|
<td><?= e((string) ($fetch['fetched_at_display'] ?? $fetch['fetched_at'] ?? '')) ?></td>
|
||||||
<td><?= e((string) ($fetch['base_currency'] ?? '')) ?></td>
|
<td><?= e((string) ($fetch['base_currency'] ?? '')) ?></td>
|
||||||
<td><?= e((string) ($fetch['provider'] ?? '')) ?></td>
|
<td><?= e((string) ($fetch['provider'] ?? '')) ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -134,6 +137,5 @@ $pageData = json_encode([
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<?= module_shell_footer() ?>
|
<?= module_shell_footer() ?>
|
||||||
|
|||||||
@@ -73,17 +73,12 @@ final class Router
|
|||||||
if ($path === 'v1/refresh' && $method === 'POST') {
|
if ($path === 'v1/refresh' && $method === 'POST') {
|
||||||
$input = $this->input();
|
$input = $this->input();
|
||||||
$base = $this->stringOrNull($input['base'] ?? null);
|
$base = $this->stringOrNull($input['base'] ?? null);
|
||||||
$currencies = $this->parseCsv($input['currencies'] ?? null);
|
|
||||||
if ($currencies === null) {
|
|
||||||
$settings = module_fn('fx-rates', 'settings');
|
|
||||||
$currencies = is_array($settings['preferred_currencies'] ?? null) ? $settings['preferred_currencies'] : null;
|
|
||||||
}
|
|
||||||
$force = !empty($input['force']);
|
$force = !empty($input['force']);
|
||||||
$maxAgeHours = is_numeric($input['max_age_hours'] ?? null) ? (float) $input['max_age_hours'] : 24.0;
|
$maxAgeHours = is_numeric($input['max_age_hours'] ?? null) ? (float) $input['max_age_hours'] : 24.0;
|
||||||
|
|
||||||
$result = $force
|
$result = $force
|
||||||
? $this->service->refreshLatestRates($currencies, $base)
|
? $this->service->refreshLatestRates(null, $base)
|
||||||
: $this->service->ensureFreshLatestRates($maxAgeHours, $base, $currencies);
|
: $this->service->ensureFreshLatestRates($maxAgeHours, $base, null);
|
||||||
|
|
||||||
$this->respond(['data' => $result], 201);
|
$this->respond(['data' => $result], 201);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,17 +19,17 @@ final class FxRatesService
|
|||||||
|
|
||||||
public function latestStatus(): ?array
|
public function latestStatus(): ?array
|
||||||
{
|
{
|
||||||
return $this->repository->getLatestFetch($this->defaultBaseCurrency());
|
return $this->localizeFetch($this->repository->getLatestFetch($this->defaultBaseCurrency()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function latestStatuses(): array
|
public function latestStatuses(): array
|
||||||
{
|
{
|
||||||
return $this->repository->listLatestFetches();
|
return array_map(fn (array $fetch): array => $this->localizeFetch($fetch), $this->repository->listLatestFetches());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function recentFetches(int $limit = 20): array
|
public function recentFetches(int $limit = 20): array
|
||||||
{
|
{
|
||||||
return $this->repository->listRecentFetches($limit);
|
return array_map(fn (array $fetch): array => $this->localizeFetch($fetch), $this->repository->listRecentFetches($limit));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function snapshot(?string $baseCurrency = null, ?string $at = null, ?array $symbols = null, ?int $windowMinutes = null): ?array
|
public function snapshot(?string $baseCurrency = null, ?string $at = null, ?array $symbols = null, ?int $windowMinutes = null): ?array
|
||||||
@@ -53,7 +53,7 @@ final class FxRatesService
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->rebaseSnapshot($snapshot, $base, $symbols);
|
return $this->localizeSnapshot($this->rebaseSnapshot($snapshot, $base, $symbols));
|
||||||
}
|
}
|
||||||
|
|
||||||
$atUtc = $this->normalizeTimestamp($at);
|
$atUtc = $this->normalizeTimestamp($at);
|
||||||
@@ -76,10 +76,10 @@ final class FxRatesService
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $rebased + [
|
return $this->localizeSnapshot($rebased + [
|
||||||
'requested_at' => $atUtc,
|
'requested_at' => $atUtc,
|
||||||
'distance_seconds' => $nearest['distance_seconds'] ?? null,
|
'distance_seconds' => $nearest['distance_seconds'] ?? null,
|
||||||
];
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function findRate(?string $fromCurrency, ?string $toCurrency, ?string $at = null, ?int $windowMinutes = null): ?array
|
public function findRate(?string $fromCurrency, ?string $toCurrency, ?string $at = null, ?int $windowMinutes = null): ?array
|
||||||
@@ -91,7 +91,7 @@ final class FxRatesService
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($from === $to) {
|
if ($from === $to) {
|
||||||
return [
|
return $this->localizeRateResult([
|
||||||
'base_currency' => $from,
|
'base_currency' => $from,
|
||||||
'target_currency' => $to,
|
'target_currency' => $to,
|
||||||
'rate' => 1.0,
|
'rate' => 1.0,
|
||||||
@@ -99,7 +99,7 @@ final class FxRatesService
|
|||||||
'fetched_at' => $at ? $this->normalizeTimestamp($at) : null,
|
'fetched_at' => $at ? $this->normalizeTimestamp($at) : null,
|
||||||
'rate_date' => $at ? substr((string) $this->normalizeTimestamp($at), 0, 10) : gmdate('Y-m-d'),
|
'rate_date' => $at ? substr((string) $this->normalizeTimestamp($at), 0, 10) : gmdate('Y-m-d'),
|
||||||
'is_exact_pair' => true,
|
'is_exact_pair' => true,
|
||||||
];
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$cacheKey = implode(':', [$from, $to, $at ?? '', (string) ($windowMinutes ?? 0)]);
|
$cacheKey = implode(':', [$from, $to, $at ?? '', (string) ($windowMinutes ?? 0)]);
|
||||||
@@ -124,7 +124,7 @@ final class FxRatesService
|
|||||||
$rates = is_array($snapshot['rates'] ?? null) ? $snapshot['rates'] : [];
|
$rates = is_array($snapshot['rates'] ?? null) ? $snapshot['rates'] : [];
|
||||||
$resolved = $this->resolveRateFromSnapshot($snapshot, $rates, $from, $to);
|
$resolved = $this->resolveRateFromSnapshot($snapshot, $rates, $from, $to);
|
||||||
if ($resolved !== null) {
|
if ($resolved !== null) {
|
||||||
return $this->memoryCache[$cacheKey] = $resolved;
|
return $this->memoryCache[$cacheKey] = $this->localizeRateResult($resolved);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +148,7 @@ final class FxRatesService
|
|||||||
public function refreshLatestRates(?array $currencies = null, ?string $baseCurrency = null): array
|
public function refreshLatestRates(?array $currencies = null, ?string $baseCurrency = null): array
|
||||||
{
|
{
|
||||||
$requestedBase = $this->normalizeCurrency($baseCurrency ?: $this->defaultBaseCurrency());
|
$requestedBase = $this->normalizeCurrency($baseCurrency ?: $this->defaultBaseCurrency());
|
||||||
$payload = $this->fetchLatestPayload($requestedBase, $currencies);
|
$payload = $this->fetchLatestPayload($requestedBase, null);
|
||||||
$base = $this->normalizeCurrency((string) ($payload['base'] ?? $requestedBase));
|
$base = $this->normalizeCurrency((string) ($payload['base'] ?? $requestedBase));
|
||||||
if ($base === '') {
|
if ($base === '') {
|
||||||
$base = $requestedBase !== '' ? $requestedBase : 'USD';
|
$base = $requestedBase !== '' ? $requestedBase : 'USD';
|
||||||
@@ -169,7 +169,7 @@ final class FxRatesService
|
|||||||
'rate_date' => $rateDate,
|
'rate_date' => $rateDate,
|
||||||
'updated_count' => count($saved['rates'] ?? []),
|
'updated_count' => count($saved['rates'] ?? []),
|
||||||
'rates' => $saved['rates'] ?? [],
|
'rates' => $saved['rates'] ?? [],
|
||||||
'fetch' => $saved['fetch'] ?? null,
|
'fetch' => $this->localizeFetch(is_array($saved['fetch'] ?? null) ? $saved['fetch'] : null),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +186,7 @@ final class FxRatesService
|
|||||||
'rate_date' => $latest['rate_date'] ?? null,
|
'rate_date' => $latest['rate_date'] ?? null,
|
||||||
'updated_count' => 0,
|
'updated_count' => 0,
|
||||||
'rates' => [],
|
'rates' => [],
|
||||||
'fetch' => $latest,
|
'fetch' => $this->localizeFetch($latest),
|
||||||
'reused' => true,
|
'reused' => true,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -210,7 +210,7 @@ final class FxRatesService
|
|||||||
|
|
||||||
$direct = $this->repository->listDirectHistory($fromCurrency, $toCurrency, $this->normalizeTimestamp($from), $this->normalizeTimestamp($to), $limit);
|
$direct = $this->repository->listDirectHistory($fromCurrency, $toCurrency, $this->normalizeTimestamp($from), $this->normalizeTimestamp($to), $limit);
|
||||||
if ($direct !== []) {
|
if ($direct !== []) {
|
||||||
return $direct;
|
return array_map(fn (array $row): array => $this->localizeRateResult($row), $direct);
|
||||||
}
|
}
|
||||||
|
|
||||||
$inverse = $this->repository->listDirectHistory($toCurrency, $fromCurrency, $this->normalizeTimestamp($from), $this->normalizeTimestamp($to), $limit);
|
$inverse = $this->repository->listDirectHistory($toCurrency, $fromCurrency, $this->normalizeTimestamp($from), $this->normalizeTimestamp($to), $limit);
|
||||||
@@ -237,7 +237,7 @@ final class FxRatesService
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
return array_map(fn (array $row): array => $this->localizeRateResult($row), $result);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function runScheduledRefresh(array $context = []): array
|
public function runScheduledRefresh(array $context = []): array
|
||||||
@@ -268,7 +268,7 @@ final class FxRatesService
|
|||||||
'ok' => true,
|
'ok' => true,
|
||||||
'message' => 'Kein FX-Abruf: fuer heute existiert bereits ein Snapshot nach ' . str_pad((string) $targetHour, 2, '0', STR_PAD_LEFT) . ':00.',
|
'message' => 'Kein FX-Abruf: fuer heute existiert bereits ein Snapshot nach ' . str_pad((string) $targetHour, 2, '0', STR_PAD_LEFT) . ':00.',
|
||||||
'skipped' => true,
|
'skipped' => true,
|
||||||
'fetch' => $latest,
|
'fetch' => $this->localizeFetch($latest),
|
||||||
'context' => $context,
|
'context' => $context,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -647,6 +647,57 @@ final class FxRatesService
|
|||||||
return $filtered;
|
return $filtered;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function localizeFetch(?array $fetch): ?array
|
||||||
|
{
|
||||||
|
if (!is_array($fetch)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fetch['fetched_at_display'] = $this->formatDisplayTimestamp($fetch['fetched_at'] ?? null);
|
||||||
|
$fetch['created_at_display'] = $this->formatDisplayTimestamp($fetch['created_at'] ?? null);
|
||||||
|
return $fetch;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function localizeSnapshot(?array $snapshot): ?array
|
||||||
|
{
|
||||||
|
if (!is_array($snapshot)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$snapshot['fetched_at_display'] = $this->formatDisplayTimestamp($snapshot['fetched_at'] ?? null);
|
||||||
|
if (array_key_exists('requested_at', $snapshot)) {
|
||||||
|
$snapshot['requested_at_display'] = $this->formatDisplayTimestamp($snapshot['requested_at']);
|
||||||
|
}
|
||||||
|
return $snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function localizeRateResult(array $rate): array
|
||||||
|
{
|
||||||
|
$rate['fetched_at_display'] = $this->formatDisplayTimestamp($rate['fetched_at'] ?? null);
|
||||||
|
if (array_key_exists('requested_at', $rate)) {
|
||||||
|
$rate['requested_at_display'] = $this->formatDisplayTimestamp($rate['requested_at']);
|
||||||
|
}
|
||||||
|
return $rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatDisplayTimestamp(mixed $value): string
|
||||||
|
{
|
||||||
|
$raw = trim((string) $value);
|
||||||
|
if ($raw === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$date = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $raw, new DateTimeZone('UTC'));
|
||||||
|
if (!$date instanceof DateTimeImmutable) {
|
||||||
|
$date = new DateTimeImmutable($raw, new DateTimeZone('UTC'));
|
||||||
|
}
|
||||||
|
return $date->setTimezone($this->displayTimezone())->format('d.m.Y H:i:s');
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return $raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function normalizeCurrencyApiComCurrenciesPayload(array $payload): array
|
private function normalizeCurrencyApiComCurrenciesPayload(array $payload): array
|
||||||
{
|
{
|
||||||
$rawCurrencies = is_array($payload['data'] ?? null) ? $payload['data'] : null;
|
$rawCurrencies = is_array($payload['data'] ?? null) ? $payload['data'] : null;
|
||||||
@@ -815,4 +866,9 @@ final class FxRatesService
|
|||||||
return new DateTimeZone('Europe/Berlin');
|
return new DateTimeZone('Europe/Berlin');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function displayTimezone(): DateTimeZone
|
||||||
|
{
|
||||||
|
return $this->scheduleTimezone();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user