FSCash/html/lib/fscash.php
fds2610-bot 6f377ea251 V.2.2: Unit-Tests für wichtige Funktionen, automatisch geprüft bei Update
Extrahiert die wichtigste DB-/netzwerkfreie Geschäftslogik (Monatsnavigation,
Admin-/Aktien-Rollenprüfung, Aktien-GuV-Berechnung, OpenFIGI-/Alpha-Vantage-
Antwortauswertung, Eingabevalidierung, CSV-Export, Migrations-Logik) nach
html/lib/fscash.php und deckt sie mit 31 PHPUnit-Tests ab. update.sh
installiert Composer-Abhängigkeiten bei Bedarf und führt die Tests jetzt vor
jedem Deploy aus - bei Fehlschlag wird nichts kopiert oder migriert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 17:54:30 +02:00

216 lines
6.3 KiB
PHP

<?php
/**
* Reine, DB- und netzwerkfreie Hilfsfunktionen für FSCash.
*
* Diese Datei enthält bewusst nur Funktionen ohne Seiteneffekte (keine
* Session-, DB- oder HTTP-Zugriffe), damit sie ohne Testumgebung
* (Datenbank, Mock-Server) per Unit-Test geprüft werden können.
* Die aufrufenden Seiten unter html/ und migrations/ binden diese Datei ein
* und übernehmen weiterhin die Seiteneffekte (DB-Query, HTTP-Request, ...).
*/
declare(strict_types=1);
/**
* Normalisiert Monat/Jahr, wenn beim Monats-Blättern (< 1 bzw. > 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<int,array{ticker?:string,exchCode?:string}> $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<int,mixed>
*/
function fscash_resolve_selected_categories(array $allCategoryIds, $requestedCategoryIds): array
{
if (is_array($requestedCategoryIds) && $requestedCategoryIds !== []) {
return $requestedCategoryIds;
}
return $allCategoryIds;
}
/**
* Extrahiert den aktuellen Kurs aus einer Alpha-Vantage GLOBAL_QUOTE-Antwort.
*
* @param array<string,mixed> $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<string,mixed> $alphaVantageResponse
* @return array<int,array{symbol:string,name:string,region:string}>
*/
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<string,mixed> $entry
* @return array<int,string>
*/
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)
));
}