12) die * Jahresgrenze überschritten wird. * * @return array{month:int,year:int} */ function fscash_normalize_month_year(int $month, int $year): array { if ($month < 1) { $month = 12; $year--; } elseif ($month > 12) { $month = 1; $year++; } return ['month' => $month, 'year' => $year]; } /** * Ermittelt anhand der konfigurierten ID-Listen, ob ein Benutzer Admin- bzw. * Aktien-Rechte hat. * * @param int[] $adminIds * @param int[] $aktienIds * @return array{is_admin:bool,is_aktien:bool} */ function fscash_user_roles(int $userId, array $adminIds, array $aktienIds): array { return [ 'is_admin' => in_array($userId, $adminIds, true), 'is_aktien' => in_array($userId, $aktienIds, true), ]; } /** * Berechnet Kaufsumme, aktuellen Wert, Gewinn/Verlust (GuV) und die * zugehörige CSS-Klasse für einen Aktien-Eintrag. * * @return array{kaufSumme:float,aktuellerWert:float,guv:float,guvClass:string} */ function fscash_aktie_guv(float $kaufpreis, float $anzahl, float $kurs): array { $kaufSumme = $kaufpreis * $anzahl; $aktuellerWert = $anzahl * $kurs; $guv = $aktuellerWert - $kaufSumme; $guvClass = ''; if ($guv > 0) { $guvClass = 'guv-positive'; } elseif ($guv < 0) { $guvClass = 'guv-negative'; } return [ 'kaufSumme' => $kaufSumme, 'aktuellerWert' => $aktuellerWert, 'guv' => $guv, 'guvClass' => $guvClass, ]; } /** * Wählt aus den von der OpenFIGI-API gelieferten Mapping-Einträgen das * passende Symbol aus. Bevorzugt wird ein Eintrag an der Deutschen Börse * Xetra (XETR), ansonsten wird der erste Eintrag verwendet. * * @param array $figiEntries Der "data"-Teil der OpenFIGI-Antwort. */ function fscash_select_figi_symbol(array $figiEntries): ?string { if (empty($figiEntries)) { return null; } $symbolEntry = null; foreach ($figiEntries as $entry) { if (isset($entry['exchCode']) && $entry['exchCode'] === 'XETR') { $symbolEntry = $entry; break; } } if ($symbolEntry === null) { $symbolEntry = $figiEntries[0]; } if (isset($symbolEntry['ticker']) && !empty($symbolEntry['ticker'])) { $exchange = $symbolEntry['exchCode'] ?? ''; return $exchange ? "{$symbolEntry['ticker']}.{$exchange}" : $symbolEntry['ticker']; } return null; } /** * Ermittelt die im Personenreport ausgewählten Kategorie-IDs: die per * Request übergebenen, sofern vorhanden, sonst alle verfügbaren. * * @param int[] $allCategoryIds Alle in der DB vorhandenen Kategorie-IDs. * @param mixed $requestedCategoryIds Roher Wert aus $_GET['categories'] (oder null, falls nicht gesetzt). * @return array */ function fscash_resolve_selected_categories(array $allCategoryIds, $requestedCategoryIds): array { if (is_array($requestedCategoryIds) && $requestedCategoryIds !== []) { return $requestedCategoryIds; } return $allCategoryIds; } /** * Ermittelt die im Personenreport ausgewählten Zahlweise-IDs: die per * Request übergebenen, sofern vorhanden, sonst alle verfügbaren. * * @param int[] $allPaymentMethodIds Alle in der DB vorhandenen Zahlweise-IDs. * @param mixed $requestedPaymentMethodIds Roher Wert aus $_GET['payment_methods'] (oder null, falls nicht gesetzt). * @return array */ function fscash_resolve_selected_payment_methods(array $allPaymentMethodIds, $requestedPaymentMethodIds): array { if (is_array($requestedPaymentMethodIds) && $requestedPaymentMethodIds !== []) { return $requestedPaymentMethodIds; } return $allPaymentMethodIds; } /** * Extrahiert den aktuellen Kurs aus einer Alpha-Vantage GLOBAL_QUOTE-Antwort. * * @param array $alphaVantageResponse */ function fscash_extract_quote_price(array $alphaVantageResponse): ?float { $price = $alphaVantageResponse['Global Quote']['05. price'] ?? null; if ($price === null || $price === '') { return null; } return (float)$price; } /** * Wandelt die Alpha-Vantage SYMBOL_SEARCH-Antwort in die vom Frontend * erwartete, vereinfachte Liste (symbol/name/region) um. * * @param array $alphaVantageResponse * @return array */ function fscash_map_symbol_search_results(array $alphaVantageResponse): array { $matches = $alphaVantageResponse['bestMatches'] ?? []; $result = []; foreach ($matches as $match) { $result[] = [ 'symbol' => $match['1. symbol'] ?? '', 'name' => $match['2. name'] ?? '', 'region' => $match['4. region'] ?? '', ]; } return $result; } /** * Prüft, ob die per JSON-Body übermittelten Daten zum Speichern eines * Aktien-Symbols gültig sind (id und symbol müssen "truthy" sein). * * @param mixed $id * @param mixed $symbol */ function fscash_is_valid_symbol_payload($id, $symbol): bool { return (bool)$id && (bool)$symbol; } /** * Baut aus einem DB-Eintrag des Monatsreports die Zeile für den CSV-Export. * * @param array $entry * @return array */ function fscash_format_report_month_csv_row(array $entry): array { return [ (string)($entry['id'] ?? ''), (string)($entry['datum'] ?? ''), (string)($entry['benutzer_name'] ?? ''), (string)($entry['kategorie_id'] ?? ''), (string)($entry['kategorie_name'] ?? ''), (string)($entry['betreff'] ?? ''), number_format((float)($entry['betrag'] ?? 0), 2, ',', '.'), (string)($entry['zahlweise'] ?? $entry['zahlungsmethode'] ?? ''), ]; } /** * Ermittelt aus allen verfügbaren Migrations-Dateien diejenigen, die laut * schema_migrations-Tabelle noch nicht angewendet wurden. Die Rückgabe ist * alphabetisch/numerisch sortiert, damit Migrationen in der richtigen * Reihenfolge angewendet werden. * * @param string[] $availableFiles Absolute oder relative Pfade der *.sql-Dateien. * @param string[] $appliedFilenames Basenamen der bereits angewendeten Migrationen. * @return string[] Pfade der noch ausstehenden Migrationen, sortiert. */ function fscash_pending_migrations(array $availableFiles, array $appliedFilenames): array { $files = $availableFiles; sort($files, SORT_STRING); return array_values(array_filter( $files, fn(string $f): bool => !in_array(basename($f), $appliedFilenames, true) )); }