registerFunction($moduleName, 'table', static function (string $name): string { $prefix = 'fxrate_'; $sanitized = preg_replace('/[^a-zA-Z0-9_]/', '', $name); return $prefix . $sanitized; }); $mm->registerFunction($moduleName, 'settings', static function (): array { $saved = modules()->settings('fx-rates'); $provider = trim((string) ($saved['provider'] ?? (getenv('FX_RATES_PROVIDER') ?: getenv('MINING_CHECKER_FX_PROVIDER') ?: 'currencyapi'))); $apiVersion = strtolower(trim((string) ($saved['api_version'] ?? 'v2'))); $apiUrl = rtrim((string) ($saved['api_url'] ?? (getenv('FX_RATES_API_URL') ?: getenv('MINING_CHECKER_FX_URL') ?: 'https://currencyapi.net')), '/'); $apiKey = trim((string) ($saved['api_key'] ?? (getenv('FX_RATES_API_KEY') ?: getenv('MINING_CHECKER_FX_API_KEY') ?: ''))); $timeout = max(2, (int) ($saved['timeout_sec'] ?? (getenv('FX_RATES_TIMEOUT') ?: getenv('MINING_CHECKER_FX_TIMEOUT') ?: 10))); $preferredCurrencies = $saved['preferred_currencies'] ?? ['EUR', 'USD', 'DOGE']; if (is_string($preferredCurrencies)) { $preferredCurrencies = preg_split('/[\s,;]+/', $preferredCurrencies) ?: []; } $preferredCurrencies = array_values(array_unique(array_filter(array_map( static fn (mixed $code): string => strtoupper(trim((string) $code)), is_array($preferredCurrencies) ? $preferredCurrencies : [] ), static fn (string $code): bool => $code !== ''))); $currencyCatalog = $saved['currency_catalog'] ?? []; $currencyCatalog = array_values(array_filter(array_map(static function (mixed $item): ?array { if (!is_array($item)) { return null; } $code = strtoupper(trim((string) ($item['code'] ?? ''))); $name = trim((string) ($item['name'] ?? '')); if ($code === '' || $name === '') { return null; } return ['code' => $code, 'name' => $name]; }, is_array($currencyCatalog) ? $currencyCatalog : []))); return [ 'provider' => $provider !== '' ? $provider : 'currencyapi', 'api_version' => in_array($apiVersion, ['v2', 'v3'], true) ? $apiVersion : 'v2', '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, 'currency_catalog' => $currencyCatalog, 'currency_catalog_synced_at' => trim((string) ($saved['currency_catalog_synced_at'] ?? '')), 'schedule_timezone' => trim((string) ($saved['schedule_timezone'] ?? 'Europe/Berlin')) ?: 'Europe/Berlin', ]; }); $mm->registerFunction($moduleName, 'save_runtime_settings', static function (array $payload): array { $current = modules()->settings('fx-rates'); $normalized = module_fn('fx-rates', 'settings'); if (array_key_exists('default_base_currency', $payload)) { $normalized['default_base_currency'] = strtoupper(trim((string) $payload['default_base_currency'])) ?: $normalized['default_base_currency']; } if (array_key_exists('display_base_currency', $payload)) { $normalized['display_base_currency'] = strtoupper(trim((string) $payload['display_base_currency'])) ?: $normalized['display_base_currency']; } if (array_key_exists('preferred_currencies', $payload)) { $preferredCurrencies = $payload['preferred_currencies']; if (is_string($preferredCurrencies)) { $preferredCurrencies = preg_split('/[\s,;]+/', $preferredCurrencies) ?: []; } $normalized['preferred_currencies'] = array_values(array_unique(array_filter(array_map( static fn (mixed $code): string => strtoupper(trim((string) $code)), is_array($preferredCurrencies) ? $preferredCurrencies : [] ), static fn (string $code): bool => $code !== ''))); } $catalogCodes = []; foreach (($normalized['currency_catalog'] ?? []) as $currency) { if (is_array($currency)) { $code = strtoupper(trim((string) ($currency['code'] ?? ''))); if ($code !== '') { $catalogCodes[$code] = true; } } } if ($catalogCodes !== [] && !isset($catalogCodes[$normalized['display_base_currency']])) { $normalized['display_base_currency'] = $normalized['default_base_currency']; } if ($catalogCodes !== []) { $normalized['preferred_currencies'] = array_values(array_filter( $normalized['preferred_currencies'], static fn (string $code): bool => isset($catalogCodes[$code]) )); } $toSave = array_merge($current, [ 'default_base_currency' => $normalized['default_base_currency'], 'display_base_currency' => $normalized['display_base_currency'], 'preferred_currencies' => $normalized['preferred_currencies'], ]); modules()->saveSettings('fx-rates', $toSave); return module_fn('fx-rates', 'settings'); }); $mm->registerFunction($moduleName, 'pdo', function () use ($moduleName): PDO { $settings = modules()->settings($moduleName); $useSeparate = !empty($settings['use_separate_db']); if ($useSeparate) { $module = modules()->get($moduleName); $fallback = $module['db_defaults'] ?? []; return modules()->modulePdo($moduleName, $fallback); } $base = app()->basePdo(); if ($base instanceof PDO) { return $base; } throw new ModuleConfigException( $moduleName, 'Base-DB ist deaktiviert. Bitte Base-DB aktivieren oder eine eigene Modul-DB konfigurieren.' ); }); $mm->registerFunction($moduleName, 'repository', static function (): FxRatesRepository { return new FxRatesRepository(module_fn('fx-rates', 'pdo'), 'fxrate_'); }); $mm->registerFunction($moduleName, 'ensure_schema', static function (): void { module_fn('fx-rates', 'repository')->ensureSchema(); }); $mm->registerFunction($moduleName, 'service', static function (): FxRatesService { module_fn('fx-rates', 'ensure_schema'); return new FxRatesService( module_fn('fx-rates', 'repository'), module_fn('fx-rates', 'settings') ); }); $mm->registerFunction($moduleName, 'setup_actions', static function (): array { return [ [ 'name' => 'sync_currency_catalog', 'label' => 'Waehrungskatalog synchronisieren', 'help' => 'Laedt die verfuegbaren Waehrungen einmalig aus dem konfigurierten FX-Provider.', ], ]; }); $mm->registerFunction($moduleName, 'run_setup_action', static function (string $action): array { return match ($action) { 'sync_currency_catalog' => (static function (): array { $result = module_fn('fx-rates', 'service')->refreshCurrencyCatalog(); $current = modules()->settings('fx-rates'); $catalog = []; foreach (is_array($result['currencies'] ?? null) ? $result['currencies'] : [] as $item) { if (!is_array($item)) { continue; } $code = strtoupper(trim((string) ($item['code'] ?? ''))); $name = trim((string) ($item['name'] ?? '')); if ($code === '' || $name === '') { continue; } $catalog[$code] = ['code' => $code, 'name' => $name]; } $latest = module_fn('fx-rates', 'service')->latestStatus(); if (is_array($latest) && !empty($latest['id'])) { $snapshot = module_fn('fx-rates', 'snapshot', null, null, null, null); $rates = is_array($snapshot['rates'] ?? null) ? $snapshot['rates'] : []; foreach (array_keys($rates) as $code) { $code = strtoupper(trim((string) $code)); if ($code !== '' && !isset($catalog[$code])) { $catalog[$code] = ['code' => $code, 'name' => $code]; } } $baseCode = strtoupper(trim((string) ($snapshot['base_currency'] ?? ''))); if ($baseCode !== '' && !isset($catalog[$baseCode])) { $catalog[$baseCode] = ['code' => $baseCode, 'name' => $baseCode]; } } foreach ([ (string) ($current['default_base_currency'] ?? ''), (string) ($current['display_base_currency'] ?? ''), ...((is_array($current['preferred_currencies'] ?? null) ? $current['preferred_currencies'] : [])), ] as $code) { $code = strtoupper(trim((string) $code)); if ($code !== '' && !isset($catalog[$code])) { $catalog[$code] = ['code' => $code, 'name' => $code]; } } ksort($catalog); $current['currency_catalog'] = array_values($catalog); $current['currency_catalog_synced_at'] = gmdate('Y-m-d H:i:s'); modules()->saveSettings('fx-rates', $current); return $result + [ 'currencies' => array_values($catalog), 'synced_count' => count($catalog), 'message' => 'Waehrungskatalog synchronisiert. ' . count($catalog) . ' Waehrungen verarbeitet.', ]; })(), default => throw new \RuntimeException('Unbekannte Setup-Aktion.'), }; }); $mm->registerFunction($moduleName, 'refresh_latest', static function (?array $currencies = null, ?string $baseCurrency = null): array { return module_fn('fx-rates', 'service')->refreshLatestRates($currencies, $baseCurrency); }); $mm->registerFunction($moduleName, 'ensure_fresh_latest_rates', static function (float $maxAgeHours = 24.0, ?string $baseCurrency = null, ?array $currencies = null): array { return module_fn('fx-rates', 'service')->ensureFreshLatestRates($maxAgeHours, $baseCurrency, $currencies); }); $mm->registerFunction($moduleName, 'rate', static function (string $fromCurrency, string $toCurrency, ?string $at = null, ?int $windowMinutes = null): ?array { return module_fn('fx-rates', 'service')->findRate($fromCurrency, $toCurrency, $at, $windowMinutes); }); $mm->registerFunction($moduleName, 'convert', static function (?float $amount, ?string $fromCurrency, ?string $toCurrency, ?string $at = null, ?int $windowMinutes = null): ?float { return module_fn('fx-rates', 'service')->convert($amount, $fromCurrency, $toCurrency, $at, $windowMinutes); }); $mm->registerFunction($moduleName, 'snapshot', static function (?string $baseCurrency = null, ?string $at = null, ?array $symbols = null, ?int $windowMinutes = null): ?array { return module_fn('fx-rates', 'service')->snapshot($baseCurrency, $at, $symbols, $windowMinutes); }); $mm->registerFunction($moduleName, 'recent_fetches', static function (int $limit = 20): array { return module_fn('fx-rates', 'service')->recentFetches($limit); }); $mm->registerFunction($moduleName, 'scheduled_refresh', static function (array $context = []): array { $result = module_fn('fx-rates', 'service')->runScheduledRefresh($context); if (function_exists('module_debug_push')) { module_debug_push('fx-rates', [ 'label' => 'Intervall-Aufgabe', 'type' => 'scheduler:run', 'task' => 'daily_refresh', 'context' => $context, 'message' => (string) ($result['message'] ?? ''), ]); } return $result; });