dsfd
All checks were successful
Deploy / deploy-staging (push) Successful in 5s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-05-02 03:36:09 +02:00
parent 6d59d94273
commit 7f038f03e8
6 changed files with 189 additions and 64 deletions

View File

@@ -3,18 +3,6 @@ declare(strict_types=1);
use App\ModuleConfigException; use App\ModuleConfigException;
spl_autoload_register(static function (string $class): void {
$prefix = 'Modules\\MiningChecker\\';
if (!str_starts_with($class, $prefix)) {
return;
}
$file = dirname(__DIR__) . '/mining-checker/src/' . str_replace('\\', '/', substr($class, strlen($prefix))) . '.php';
if (is_file($file)) {
require_once $file;
}
});
spl_autoload_register(static function (string $class): void { spl_autoload_register(static function (string $class): void {
$prefix = 'Modules\\Boersenchecker\\'; $prefix = 'Modules\\Boersenchecker\\';
if (!str_starts_with($class, $prefix)) { if (!str_starts_with($class, $prefix)) {
@@ -228,34 +216,11 @@ $mm->registerFunction($moduleName, 'fx_service', static function (): ?object {
try { try {
return module_fn('fx-rates', 'service'); return module_fn('fx-rates', 'service');
} catch (\Throwable) { } catch (\Throwable) {
return null;
} }
} }
if (!is_dir(dirname(__DIR__) . '/mining-checker')) { return null;
return null;
}
try {
$config = \Modules\MiningChecker\Infrastructure\ModuleConfig::load(dirname(__DIR__) . '/mining-checker');
$repo = new \Modules\MiningChecker\Infrastructure\MiningRepository(
\Modules\MiningChecker\Infrastructure\ConnectionFactory::make($config),
$config->tablePrefix()
);
$fx = $config->fx();
return new \Modules\MiningChecker\Domain\FxService(
$repo,
(string) ($fx['url'] ?? 'https://currencyapi.net'),
(string) ($fx['currencies_url'] ?? ($fx['url'] ?? 'https://currencyapi.net')),
(int) ($fx['timeout'] ?? 10),
(int) ($fx['cache_ttl'] ?? 21600),
false,
(string) ($fx['provider'] ?? 'currencyapi'),
(string) ($fx['api_key'] ?? '')
);
} catch (\Throwable) {
return null;
}
}); });
$mm->registerFunction($moduleName, 'fx_refresh', static function (string $baseCurrency = 'EUR', float $maxAgeHours = 6.0): array { $mm->registerFunction($moduleName, 'fx_refresh', static function (string $baseCurrency = 'EUR', float $maxAgeHours = 6.0): array {
@@ -263,7 +228,7 @@ $mm->registerFunction($moduleName, 'fx_refresh', static function (string $baseCu
if (!$service || !method_exists($service, 'ensureFreshLatestRates')) { if (!$service || !method_exists($service, 'ensureFreshLatestRates')) {
return [ return [
'ok' => false, 'ok' => false,
'message' => 'FX-Service ist aktuell nicht verfuegbar.', 'message' => 'Das Modul fx-rates ist aktuell nicht verfuegbar oder nicht aktiviert.',
]; ];
} }
@@ -284,6 +249,119 @@ $mm->registerFunction($moduleName, 'fx_refresh', static function (string $baseCu
} }
}); });
$mm->registerFunction($moduleName, 'fx_prepare_fetch', static function (
string $baseCurrency = 'EUR',
array $currencies = [],
float $maxAgeHours = 6.0
): array {
$service = module_fn('boersenchecker', 'fx_service');
if (!$service || !method_exists($service, 'ensureFreshLatestRates')) {
return [
'ok' => false,
'message' => 'Das Modul fx-rates ist aktuell nicht verfuegbar oder nicht aktiviert.',
];
}
$baseCurrency = strtoupper(trim($baseCurrency)) ?: 'EUR';
$currencies = array_values(array_unique(array_filter(array_map(
static fn (mixed $code): string => strtoupper(trim((string) $code)),
$currencies
), static fn (string $code): bool => $code !== '')));
try {
$result = $service->ensureFreshLatestRates($maxAgeHours, $baseCurrency, $currencies);
return [
'ok' => true,
'message' => !empty($result['reused']) ? 'Vorhandene FX-Daten weiterverwendet.' : 'FX-Daten aktualisiert.',
'result' => $result,
'fetch_id' => is_numeric($result['fetch_id'] ?? null) ? (int) $result['fetch_id'] : null,
'reused' => !empty($result['reused']),
];
} catch (\Throwable $e) {
return [
'ok' => false,
'message' => 'FX-Aktualisierung fehlgeschlagen: ' . $e->getMessage(),
];
}
});
$mm->registerFunction($moduleName, 'fx_source_with_fetch_id', static function (string $source, ?int $fetchId = null): string {
$source = trim($source) !== '' ? trim($source) : 'manual';
if ($fetchId === null || $fetchId <= 0) {
return preg_replace('/\|fx_fetch:\d+$/', '', $source) ?: $source;
}
$source = preg_replace('/\|fx_fetch:\d+$/', '', $source) ?: $source;
return $source . '|fx_fetch:' . $fetchId;
});
$mm->registerFunction($moduleName, 'fx_extract_fetch_id', static function (?string $source): ?int {
$source = trim((string) $source);
if ($source === '') {
return null;
}
if (preg_match('/\|fx_fetch:(\d+)$/', $source, $matches) === 1) {
$fetchId = (int) ($matches[1] ?? 0);
return $fetchId > 0 ? $fetchId : null;
}
return null;
});
$mm->registerFunction($moduleName, 'fx_convert_with_fetch', static function (
?float $amount,
?string $fromCurrency,
?string $toCurrency,
?int $fetchId = null
): ?float {
if ($amount === null) {
return null;
}
$from = strtoupper(trim((string) $fromCurrency));
$to = strtoupper(trim((string) $toCurrency));
if ($from === '' || $to === '') {
return null;
}
if ($from === $to) {
return $amount;
}
$service = module_fn('boersenchecker', 'fx_service');
if (!$service) {
return null;
}
$normalizedFetchId = $fetchId !== null && $fetchId > 0 ? $fetchId : null;
if ($normalizedFetchId !== null && method_exists($service, 'snapshotByFetchId')) {
try {
$snapshot = $service->snapshotByFetchId($normalizedFetchId, null, [$from, $to]);
if (is_array($snapshot)) {
$base = strtoupper(trim((string) ($snapshot['base_currency'] ?? '')));
$rates = is_array($snapshot['rates'] ?? null) ? $snapshot['rates'] : [];
$fromRate = $from === $base ? 1.0 : (is_numeric($rates[$from] ?? null) ? (float) $rates[$from] : null);
$toRate = $to === $base ? 1.0 : (is_numeric($rates[$to] ?? null) ? (float) $rates[$to] : null);
if ($fromRate !== null && $fromRate > 0 && $toRate !== null && $toRate > 0) {
return $amount * ($toRate / $fromRate);
}
}
} catch (\Throwable) {
}
}
if (!method_exists($service, 'convert')) {
return null;
}
try {
$value = $service->convert($amount, $from, $to);
return is_numeric($value) ? (float) $value : null;
} catch (\Throwable) {
return null;
}
});
$mm->registerFunction($moduleName, 'alpha_vantage_request', static function ( $mm->registerFunction($moduleName, 'alpha_vantage_request', static function (
string $functionName, string $functionName,
array $params = [] array $params = []
@@ -648,6 +726,16 @@ $mm->registerFunction($moduleName, 'scheduled_refresh_quotes', static function (
]; ];
} }
$quoteCurrencies = array_values(array_unique(array_filter(array_map(
static fn (array $row): string => strtoupper(trim((string) ($row['quote_currency'] ?? ''))),
$candidates
), static fn (string $code): bool => $code !== '')));
$fxResult = module_fn('boersenchecker', 'fx_prepare_fetch', $defaultReportCurrency, $quoteCurrencies, (float) (($settings['fx_max_age_hours'] ?? null) ?: 6));
if (empty($fxResult['ok'])) {
return $fxResult;
}
$fxFetchId = is_numeric($fxResult['fetch_id'] ?? null) ? (int) $fxResult['fetch_id'] : null;
$bulkResult = module_fn('boersenchecker', 'alpha_vantage_fetch_quotes', $candidates); $bulkResult = module_fn('boersenchecker', 'alpha_vantage_fetch_quotes', $candidates);
if (empty($bulkResult['ok'])) { if (empty($bulkResult['ok'])) {
return [ return [
@@ -673,7 +761,7 @@ $mm->registerFunction($moduleName, 'scheduled_refresh_quotes', static function (
(float) $quote['price'], (float) $quote['price'],
strtoupper(trim((string) ($quote['currency'] ?? $row['quote_currency'] ?? $defaultReportCurrency))) ?: $defaultReportCurrency, strtoupper(trim((string) ($quote['currency'] ?? $row['quote_currency'] ?? $defaultReportCurrency))) ?: $defaultReportCurrency,
(string) ($quote['fetched_at'] ?? gmdate('Y-m-d H:i:s')), (string) ($quote['fetched_at'] ?? gmdate('Y-m-d H:i:s')),
(string) ($quote['source'] ?? 'alphavantage:global_quote') (string) module_fn('boersenchecker', 'fx_source_with_fetch_id', (string) ($quote['source'] ?? 'alphavantage:global_quote'), $fxFetchId)
); );
if (!empty($storeResult['inserted'])) { if (!empty($storeResult['inserted'])) {
$updated++; $updated++;

View File

@@ -14,7 +14,7 @@
{ "name": "db.user", "label": "DB User", "type": "text", "required": false }, { "name": "db.user", "label": "DB User", "type": "text", "required": false },
{ "name": "db.password", "label": "DB Passwort", "type": "password", "required": false }, { "name": "db.password", "label": "DB Passwort", "type": "password", "required": false },
{ "name": "report_currency", "label": "Standard-Berichtswahrung", "type": "text", "required": false, "help": "Zielwaehrung fuer Portfolio-Summen, z.B. EUR." }, { "name": "report_currency", "label": "Standard-Berichtswahrung", "type": "text", "required": false, "help": "Zielwaehrung fuer Portfolio-Summen, z.B. EUR." },
{ "name": "fx_max_age_hours", "label": "Maximales FX-Alter (Stunden)", "type": "number", "required": false, "help": "Wird bei manueller Aktualisierung ueber den Mining-Checker genutzt." }, { "name": "fx_max_age_hours", "label": "Maximales FX-Alter (Stunden)", "type": "number", "required": false, "help": "Wird bei manueller Aktualisierung ueber das Modul fx-rates genutzt." },
{ "name": "alpha_vantage_api_key", "label": "Alpha Vantage API Key", "type": "password", "required": false, "help": "API Key fuer Aktienkursabrufe und Suche ueber Alpha Vantage." }, { "name": "alpha_vantage_api_key", "label": "Alpha Vantage API Key", "type": "password", "required": false, "help": "API Key fuer Aktienkursabrufe und Suche ueber Alpha Vantage." },
{ "name": "alpha_vantage_timeout_sec", "label": "Alpha Vantage Timeout (Sek.)", "type": "number", "required": false, "help": "HTTP-Timeout fuer API-Abrufe." }, { "name": "alpha_vantage_timeout_sec", "label": "Alpha Vantage Timeout (Sek.)", "type": "number", "required": false, "help": "HTTP-Timeout fuer API-Abrufe." },
{ "name": "alpha_vantage_min_interval_minutes", "label": "Alpha Vantage Mindestabstand (Min.)", "type": "number", "required": false, "help": "Wenn bereits ein frischer Alpha-Vantage-Kurs existiert, wird dieser wiederverwendet statt erneut abzurufen." }, { "name": "alpha_vantage_min_interval_minutes", "label": "Alpha Vantage Mindestabstand (Min.)", "type": "number", "required": false, "help": "Wenn bereits ein frischer Alpha-Vantage-Kurs existiert, wird dieser wiederverwendet statt erneut abzurufen." },

View File

@@ -77,8 +77,8 @@
</div> </div>
</div> </div>
<p class="muted" style="margin-top:16px;"> <p class="muted" style="margin-top:16px;">
Die Umrechnung liest gespeicherte FX-Daten aus dem Mining-Checker. Eine Aktualisierung wird nur manuell Die Umrechnung liest gespeicherte FX-Daten zentral aus dem Modul fx-rates. Eine Aktualisierung wird nur manuell
angestossen und respektiert die dortige Max-Age-Logik. angestossen und respektiert die dortige Max-Age- und Reuse-Logik.
</p> </p>
<p class="muted" style="margin-top:12px;"> <p class="muted" style="margin-top:12px;">
Aktienkurse werden ueber Alpha Vantage anhand des hinterlegten Symbols abgerufen. Die ISIN bleibt als Stammdatum erhalten. Aktienkurse werden ueber Alpha Vantage anhand des hinterlegten Symbols abgerufen. Die ISIN bleibt als Stammdatum erhalten.
@@ -99,7 +99,10 @@
Alpha Vantage Mindestabstand: <?= e((string) $marketDataMinIntervalMinutes) ?> Min. Alpha Vantage Mindestabstand: <?= e((string) $marketDataMinIntervalMinutes) ?> Min.
</div> </div>
<div class="muted" style="margin-top:6px;"> <div class="muted" style="margin-top:6px;">
API-Key und Timeout werden ueber <a href="/modules/setup/boersenchecker">Modul-Setup</a> gepflegt. API-Key und Timeout fuer Aktienkurse werden ueber <a href="/modules/setup/boersenchecker">dieses Modul-Setup</a> gepflegt.
</div>
<div class="muted" style="margin-top:6px;">
FX-Provider, API-Key und Waehrungskatalog werden im Modul <a href="/module/fx-rates">fx-rates</a> gepflegt.
</div> </div>
<div class="muted" style="margin-top:12px;"> <div class="muted" style="margin-top:12px;">
Standard-Berichtswahrung: <?= e($defaultReportCurrency) ?> · Max. Alter: <?= e((string) $fxMaxAgeHours) ?>h Standard-Berichtswahrung: <?= e($defaultReportCurrency) ?> · Max. Alter: <?= e((string) $fxMaxAgeHours) ?>h

View File

@@ -426,6 +426,11 @@ final class DashboardPage
if (empty($apiResult['ok'])) { if (empty($apiResult['ok'])) {
throw new RuntimeException((string) ($apiResult['message'] ?? 'API-Abruf fehlgeschlagen.')); throw new RuntimeException((string) ($apiResult['message'] ?? 'API-Abruf fehlgeschlagen.'));
} }
$fxResult = \module_fn('boersenchecker', 'fx_prepare_fetch', $this->defaultReportCurrency, [$quoteCurrency], $this->fxMaxAgeHours);
if (empty($fxResult['ok'])) {
throw new RuntimeException((string) ($fxResult['message'] ?? 'FX-Aktualisierung fehlgeschlagen.'));
}
$fxFetchId = is_numeric($fxResult['fetch_id'] ?? null) ? (int) $fxResult['fetch_id'] : null;
if ($this->isApiSnapshotStale($latestApiQuote, (string) ($apiResult['fetched_at'] ?? ''))) { if ($this->isApiSnapshotStale($latestApiQuote, (string) ($apiResult['fetched_at'] ?? ''))) {
$displayTime = (string) \module_fn( $displayTime = (string) \module_fn(
'boersenchecker', 'boersenchecker',
@@ -443,7 +448,7 @@ final class DashboardPage
(float) $apiResult['price'], (float) $apiResult['price'],
$quoteCurrency, $quoteCurrency,
(string) $apiResult['fetched_at'], (string) $apiResult['fetched_at'],
(string) $apiResult['source'] (string) \module_fn('boersenchecker', 'fx_source_with_fetch_id', (string) $apiResult['source'], $fxFetchId)
); );
if (!empty($storeResult['inserted'])) { if (!empty($storeResult['inserted'])) {
return 'Alpha-Vantage-Kurs fuer ' . (string) $row['instrument_name'] . ' gespeichert.'; return 'Alpha-Vantage-Kurs fuer ' . (string) $row['instrument_name'] . ' gespeichert.';
@@ -498,6 +503,16 @@ final class DashboardPage
} }
if ($bulkRows !== []) { if ($bulkRows !== []) {
$quoteCurrencies = array_values(array_unique(array_filter(array_map(
fn (array $row): string => $this->normalizeCurrency((string) ($row['quote_currency'] ?? '')),
$bulkRows
))));
$fxResult = \module_fn('boersenchecker', 'fx_prepare_fetch', $this->defaultReportCurrency, $quoteCurrencies, $this->fxMaxAgeHours);
if (empty($fxResult['ok'])) {
throw new RuntimeException((string) ($fxResult['message'] ?? 'FX-Aktualisierung fehlgeschlagen.'));
}
$fxFetchId = is_numeric($fxResult['fetch_id'] ?? null) ? (int) $fxResult['fetch_id'] : null;
$bulkResult = \module_fn('boersenchecker', 'alpha_vantage_fetch_quotes', $bulkRows); $bulkResult = \module_fn('boersenchecker', 'alpha_vantage_fetch_quotes', $bulkRows);
if (empty($bulkResult['ok'])) { if (empty($bulkResult['ok'])) {
throw new RuntimeException((string) ($bulkResult['message'] ?? 'Alpha-Vantage-Abruf fehlgeschlagen.')); throw new RuntimeException((string) ($bulkResult['message'] ?? 'Alpha-Vantage-Abruf fehlgeschlagen.'));
@@ -526,7 +541,7 @@ final class DashboardPage
(float) $apiResult['price'], (float) $apiResult['price'],
$quoteCurrency, $quoteCurrency,
(string) $apiResult['fetched_at'], (string) $apiResult['fetched_at'],
(string) $apiResult['source'] (string) \module_fn('boersenchecker', 'fx_source_with_fetch_id', (string) $apiResult['source'], $fxFetchId)
); );
if (!empty($storeResult['inserted'])) { if (!empty($storeResult['inserted'])) {
$fetched++; $fetched++;
@@ -698,7 +713,7 @@ final class DashboardPage
if (is_array($latestQuote) && is_numeric($latestQuote['price'] ?? null)) { if (is_array($latestQuote) && is_numeric($latestQuote['price'] ?? null)) {
$currentOriginal = $quantity * (float) $latestQuote['price']; $currentOriginal = $quantity * (float) $latestQuote['price'];
$currentTotalBase = $this->convertAmount($currentOriginal, (string) $latestQuote['currency'], $baseCurrency); $currentTotalBase = $this->convertAmount($currentOriginal, (string) $latestQuote['currency'], $baseCurrency, (string) ($latestQuote['source'] ?? ''));
$position['latest_price'] = (float) $latestQuote['price']; $position['latest_price'] = (float) $latestQuote['price'];
$position['latest_currency'] = (string) $latestQuote['currency']; $position['latest_currency'] = (string) $latestQuote['currency'];
$position['latest_quoted_at'] = (string) $latestQuote['quoted_at']; $position['latest_quoted_at'] = (string) $latestQuote['quoted_at'];
@@ -796,7 +811,7 @@ final class DashboardPage
]); ]);
} }
private function convertAmount(?float $amount, string $from, string $to): ?float private function convertAmount(?float $amount, string $from, string $to, ?string $quoteSource = null): ?float
{ {
if ($amount === null) { if ($amount === null) {
return null; return null;
@@ -808,13 +823,9 @@ final class DashboardPage
return $amount; return $amount;
} }
$fxService = \module_fn('boersenchecker', 'fx_service');
if (!$fxService || !method_exists($fxService, 'convert')) {
return null;
}
try { try {
$value = $fxService->convert($amount, $from, $to); $fetchId = (int) (\module_fn('boersenchecker', 'fx_extract_fetch_id', (string) $quoteSource) ?? 0);
$value = \module_fn('boersenchecker', 'fx_convert_with_fetch', $amount, $from, $to, $fetchId > 0 ? $fetchId : null);
return is_numeric($value) ? (float) $value : null; return is_numeric($value) ? (float) $value : null;
} catch (\Throwable) { } catch (\Throwable) {
return null; return null;

View File

@@ -15,6 +15,7 @@ final class HomePage
private string $positionTable; private string $positionTable;
private string $quoteTable; private string $quoteTable;
private string $defaultReportCurrency; private string $defaultReportCurrency;
private float $fxMaxAgeHours;
private int $marketDataMinIntervalMinutes; private int $marketDataMinIntervalMinutes;
public function __construct() public function __construct()
@@ -26,6 +27,10 @@ final class HomePage
$settings = \modules()->settings('boersenchecker'); $settings = \modules()->settings('boersenchecker');
$this->defaultReportCurrency = strtoupper(trim((string) ($settings['report_currency'] ?? 'EUR'))) ?: 'EUR'; $this->defaultReportCurrency = strtoupper(trim((string) ($settings['report_currency'] ?? 'EUR'))) ?: 'EUR';
$this->fxMaxAgeHours = (float) ($settings['fx_max_age_hours'] ?? 6);
if ($this->fxMaxAgeHours <= 0) {
$this->fxMaxAgeHours = 6.0;
}
$this->marketDataMinIntervalMinutes = (int) (($settings['alpha_vantage_min_interval_minutes'] ?? null) ?: 60); $this->marketDataMinIntervalMinutes = (int) (($settings['alpha_vantage_min_interval_minutes'] ?? null) ?: 60);
if ($this->marketDataMinIntervalMinutes <= 0) { if ($this->marketDataMinIntervalMinutes <= 0) {
$this->marketDataMinIntervalMinutes = 60; $this->marketDataMinIntervalMinutes = 60;
@@ -77,7 +82,8 @@ final class HomePage
$currentReport = $this->convertAmount( $currentReport = $this->convertAmount(
$currentNative, $currentNative,
(string) ($position['latest_currency'] ?: ($position['quote_currency'] ?? $this->defaultReportCurrency)), (string) ($position['latest_currency'] ?: ($position['quote_currency'] ?? $this->defaultReportCurrency)),
$this->defaultReportCurrency $this->defaultReportCurrency,
(string) ($position['latest_source'] ?? '')
); );
$position['current_total_report'] = $currentReport; $position['current_total_report'] = $currentReport;
if ($position['purchase_total_report'] !== null && $currentReport !== null) { if ($position['purchase_total_report'] !== null && $currentReport !== null) {
@@ -156,6 +162,16 @@ final class HomePage
} }
if ($bulkCandidates !== []) { if ($bulkCandidates !== []) {
$quoteCurrencies = array_values(array_unique(array_filter(array_map(
fn (array $row): string => $this->normalizeCurrency((string) ($row['quote_currency'] ?? '')),
$bulkCandidates
))));
$fxResult = \module_fn('boersenchecker', 'fx_prepare_fetch', $this->defaultReportCurrency, $quoteCurrencies, $this->fxMaxAgeHours);
if (empty($fxResult['ok'])) {
throw new RuntimeException((string) ($fxResult['message'] ?? 'FX-Aktualisierung fehlgeschlagen.'));
}
$fxFetchId = is_numeric($fxResult['fetch_id'] ?? null) ? (int) $fxResult['fetch_id'] : null;
$bulkResult = \module_fn('boersenchecker', 'alpha_vantage_fetch_quotes', $bulkCandidates); $bulkResult = \module_fn('boersenchecker', 'alpha_vantage_fetch_quotes', $bulkCandidates);
$quotes = is_array($bulkResult['quotes'] ?? null) ? $bulkResult['quotes'] : []; $quotes = is_array($bulkResult['quotes'] ?? null) ? $bulkResult['quotes'] : [];
foreach ($bulkCandidates as $row) { foreach ($bulkCandidates as $row) {
@@ -176,7 +192,7 @@ final class HomePage
(float) $quote['price'], (float) $quote['price'],
strtoupper(trim((string) ($quote['currency'] ?? $row['quote_currency'] ?? $this->defaultReportCurrency))) ?: $this->defaultReportCurrency, strtoupper(trim((string) ($quote['currency'] ?? $row['quote_currency'] ?? $this->defaultReportCurrency))) ?: $this->defaultReportCurrency,
(string) ($quote['fetched_at'] ?? gmdate('Y-m-d H:i:s')), (string) ($quote['fetched_at'] ?? gmdate('Y-m-d H:i:s')),
(string) ($quote['source'] ?? 'alphavantage:global_quote') (string) \module_fn('boersenchecker', 'fx_source_with_fetch_id', (string) ($quote['source'] ?? 'alphavantage:global_quote'), $fxFetchId)
); );
if (!empty($storeResult['inserted'])) { if (!empty($storeResult['inserted'])) {
$updated++; $updated++;
@@ -319,7 +335,7 @@ final class HomePage
]; ];
} }
private function convertAmount(?float $amount, string $from, string $to): ?float private function convertAmount(?float $amount, string $from, string $to, ?string $quoteSource = null): ?float
{ {
if ($amount === null) { if ($amount === null) {
return null; return null;
@@ -331,13 +347,9 @@ final class HomePage
return $amount; return $amount;
} }
$fxService = \module_fn('boersenchecker', 'fx_service');
if (!$fxService || !method_exists($fxService, 'convert')) {
return null;
}
try { try {
$value = $fxService->convert($amount, $from, $to); $fetchId = (int) (\module_fn('boersenchecker', 'fx_extract_fetch_id', (string) $quoteSource) ?? 0);
$value = \module_fn('boersenchecker', 'fx_convert_with_fetch', $amount, $from, $to, $fetchId > 0 ? $fetchId : null);
return is_numeric($value) ? (float) $value : null; return is_numeric($value) ? (float) $value : null;
} catch (\Throwable) { } catch (\Throwable) {
return null; return null;

View File

@@ -14,6 +14,7 @@ final class InstrumentPage
private string $positionTable; private string $positionTable;
private string $quoteTable; private string $quoteTable;
private string $defaultReportCurrency; private string $defaultReportCurrency;
private float $fxMaxAgeHours;
private string $searchKeywords = ''; private string $searchKeywords = '';
private array $searchResults = []; private array $searchResults = [];
private int $selectedInstrumentOverrideId = 0; private int $selectedInstrumentOverrideId = 0;
@@ -28,6 +29,10 @@ final class InstrumentPage
$settings = \modules()->settings('boersenchecker'); $settings = \modules()->settings('boersenchecker');
$this->defaultReportCurrency = strtoupper(trim((string) ($settings['report_currency'] ?? 'EUR'))) ?: 'EUR'; $this->defaultReportCurrency = strtoupper(trim((string) ($settings['report_currency'] ?? 'EUR'))) ?: 'EUR';
$this->fxMaxAgeHours = (float) ($settings['fx_max_age_hours'] ?? 6);
if ($this->fxMaxAgeHours <= 0) {
$this->fxMaxAgeHours = 6.0;
}
$table = static fn (string $name): string => \module_fn('boersenchecker', 'table', $name); $table = static fn (string $name): string => \module_fn('boersenchecker', 'table', $name);
$this->instrumentTable = $table('instruments'); $this->instrumentTable = $table('instruments');
$this->positionTable = $table('positions'); $this->positionTable = $table('positions');
@@ -223,6 +228,12 @@ final class InstrumentPage
if (empty($apiResult['ok'])) { if (empty($apiResult['ok'])) {
throw new RuntimeException((string) ($apiResult['message'] ?? 'API-Abruf fehlgeschlagen.')); throw new RuntimeException((string) ($apiResult['message'] ?? 'API-Abruf fehlgeschlagen.'));
} }
$quoteCurrency = strtoupper(trim((string) ($instrument['quote_currency'] ?? $this->defaultReportCurrency))) ?: $this->defaultReportCurrency;
$fxResult = \module_fn('boersenchecker', 'fx_prepare_fetch', $this->defaultReportCurrency, [$quoteCurrency], $this->fxMaxAgeHours);
if (empty($fxResult['ok'])) {
throw new RuntimeException((string) ($fxResult['message'] ?? 'FX-Aktualisierung fehlgeschlagen.'));
}
$fxFetchId = is_numeric($fxResult['fetch_id'] ?? null) ? (int) $fxResult['fetch_id'] : null;
$latestApiQuote = $this->latestApiQuoteForInstrument($instrumentId); $latestApiQuote = $this->latestApiQuoteForInstrument($instrumentId);
if ($this->isApiSnapshotStale($latestApiQuote, (string) ($apiResult['fetched_at'] ?? ''))) { if ($this->isApiSnapshotStale($latestApiQuote, (string) ($apiResult['fetched_at'] ?? ''))) {
$displayTime = (string) \module_fn( $displayTime = (string) \module_fn(
@@ -239,9 +250,9 @@ final class InstrumentPage
'store_market_quote', 'store_market_quote',
$instrumentId, $instrumentId,
(float) $apiResult['price'], (float) $apiResult['price'],
strtoupper(trim((string) ($instrument['quote_currency'] ?? $this->defaultReportCurrency))) ?: $this->defaultReportCurrency, $quoteCurrency,
(string) $apiResult['fetched_at'], (string) $apiResult['fetched_at'],
(string) $apiResult['source'] (string) \module_fn('boersenchecker', 'fx_source_with_fetch_id', (string) $apiResult['source'], $fxFetchId)
); );
return !empty($storeResult['inserted']) return !empty($storeResult['inserted'])