Feature: lesende API für den Buchungsabgleich (Issue #13)
Neue, rein lesende HTTP-Schnittstelle für den Finanzmanager: GET /api/v1/buchungen sucht Buchungen anhand Zeitraum, Betrag (mit Toleranz) und Währung; GET /api/v1/status dient als Verbindungstest. Beide sind per HTTP Basic Auth mit den bestehenden FSCash-Zugangsdaten geschützt und rechnen die intern positiv geführten Bargeldausgaben in die vom Finanzmanager erwartete Vorzeichenkonvention (Ausgaben negativ) um. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
efae709868
commit
03a6cf774d
7 changed files with 596 additions and 0 deletions
|
|
@ -8,6 +8,10 @@ Dokumentiere Ausgaben on-the-go als Webapp
|
||||||
- [ ] Zusammenfassen von mehreren Einträgen zu einer Aktie
|
- [ ] Zusammenfassen von mehreren Einträgen zu einer Aktie
|
||||||
- [ ] Prüfen ob APIKEYS leer sind und ggfs die Aktualisierung ausblenden
|
- [ ] Prüfen ob APIKEYS leer sind und ggfs die Aktualisierung ausblenden
|
||||||
|
|
||||||
|
## V.2.5
|
||||||
|
|
||||||
|
- [X] Lesende HTTP-Schnittstelle für den Buchungsabgleich externer Tools (Issue #13): `GET /api/v1/buchungen` sucht Buchungen anhand von Zeitraum, Betrag (mit Toleranz) und Währung; `GET /api/v1/status` dient als Verbindungstest. Beide Endpunkte sind rein lesend, ohne Seiteneffekte, und per HTTP Basic Auth mit den bestehenden FSCash-Zugangsdaten (Tabelle `users`) geschützt. Die Endpunkte benötigen `mod_rewrite` (siehe `html/.htaccess`), da die Pfade ohne `.php`-Endung erreichbar sein müssen. FSCash führt Bargeldausgaben intern als positiven Betrag; die Schnittstelle rechnet das für den Abgleich in die vom Finanzmanager erwartete Konvention um (Ausgaben negativ).
|
||||||
|
|
||||||
## V.2.4
|
## V.2.4
|
||||||
|
|
||||||
- [X] Buchungszeilen im Dashboard haben ein zusätzliches Feld "abgeglichen" (Standard: nicht abgeglichen). Ein Klick auf die Zeile schaltet den Wert um und speichert ihn sofort in der DB. Abgeglichene Zeilen werden mit grünem Hintergrund dargestellt.
|
- [X] Buchungszeilen im Dashboard haben ein zusätzliches Feld "abgeglichen" (Standard: nicht abgeglichen). Ein Klick auf die Zeile schaltet den Wert um und speichert ihn sofort in der DB. Abgeglichene Zeilen werden mit grünem Hintergrund dargestellt.
|
||||||
|
|
@ -114,6 +118,7 @@ Weiteres
|
||||||
4. config_sample.php in config.php umbenennen und DB credentials setzen
|
4. config_sample.php in config.php umbenennen und DB credentials setzen
|
||||||
5. webroot (z.B. http://localhost/) aufrufen und mit user test / test123 einloggen.
|
5. webroot (z.B. http://localhost/) aufrufen und mit user test / test123 einloggen.
|
||||||
6. obige Schritte sind nur das Nötigste, um die Funktionalität herzustellen. Insbesondere bei Zugriffsmöglichkeiten aus dem Internet ist es wichtig, https Protokolle, vpns und/oder ggfs zusätzliche Sicherungsmassnahmen (hardening) zu ergreifen.
|
6. obige Schritte sind nur das Nötigste, um die Funktionalität herzustellen. Insbesondere bei Zugriffsmöglichkeiten aus dem Internet ist es wichtig, https Protokolle, vpns und/oder ggfs zusätzliche Sicherungsmassnahmen (hardening) zu ergreifen.
|
||||||
|
7. für die lesende API unter `/api/v1/` (siehe Abschnitt V.2.5) muss auf dem Apache-Server `mod_rewrite` aktiviert sein (`a2enmod rewrite`) und `AllowOverride All` für den Webroot gesetzt sein, damit die Regeln aus `html/.htaccess` greifen.
|
||||||
|
|
||||||
Ausserdem solltet Ihr User registrieren und ggfs. die Kategorien anpassen.
|
Ausserdem solltet Ihr User registrieren und ggfs. die Kategorien anpassen.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,18 @@
|
||||||
<FilesMatch "\.(css|js)$">
|
<FilesMatch "\.(css|js)$">
|
||||||
Header set Cache-Control "no-store, no-cache, must-revalidate, max-age=0"
|
Header set Cache-Control "no-store, no-cache, must-revalidate, max-age=0"
|
||||||
</FilesMatch>
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Lesende API für den Buchungsabgleich (Issue #13): erlaubt den vom
|
||||||
|
# Finanzmanager erwarteten erweiterungslosen Pfaden /api/v1/buchungen und
|
||||||
|
# /api/v1/status, auf die tatsächlichen PHP-Dateien zu zeigen. Manche
|
||||||
|
# SAPIs (z. B. php-fpm) geben den Authorization-Header nicht automatisch an
|
||||||
|
# PHP weiter, daher wird er hier explizit durchgereicht (für Basic Auth).
|
||||||
|
<IfModule mod_rewrite.c>
|
||||||
|
RewriteEngine On
|
||||||
|
|
||||||
|
RewriteCond %{HTTP:Authorization} .
|
||||||
|
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
||||||
|
|
||||||
|
RewriteRule ^api/v1/buchungen$ api/v1/buchungen.php [L,QSA]
|
||||||
|
RewriteRule ^api/v1/status$ api/v1/status.php [L,QSA]
|
||||||
|
</IfModule>
|
||||||
|
|
|
||||||
38
html/api/v1/_auth.php
Normal file
38
html/api/v1/_auth.php
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft die HTTP-Basic-Auth-Zugangsdaten der lesenden API (Issue #13) gegen
|
||||||
|
* die bestehende FSCash-Benutzertabelle (dieselben Zugangsdaten wie beim
|
||||||
|
* Login). Bei fehlender oder falscher Authentifizierung wird sofort eine
|
||||||
|
* 401-Antwort gesendet und die Ausführung beendet.
|
||||||
|
*/
|
||||||
|
function fscash_api_require_auth(PDO $pdo): void
|
||||||
|
{
|
||||||
|
$authHeader = $_SERVER['HTTP_AUTHORIZATION']
|
||||||
|
?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION']
|
||||||
|
?? null;
|
||||||
|
|
||||||
|
$credentials = fscash_parse_basic_auth_header($authHeader);
|
||||||
|
|
||||||
|
$authenticated = false;
|
||||||
|
if ($credentials !== null) {
|
||||||
|
$stmt = $pdo->prepare('SELECT password FROM users WHERE username = :username');
|
||||||
|
$stmt->execute(['username' => $credentials['username']]);
|
||||||
|
$hash = $stmt->fetchColumn();
|
||||||
|
if ($hash !== false && password_verify($credentials['password'], $hash)) {
|
||||||
|
$authenticated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$authenticated) {
|
||||||
|
header('WWW-Authenticate: Basic realm="FSCash API"');
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(
|
||||||
|
fscash_api_error('Zugangsdaten fehlen oder stimmen nicht.'),
|
||||||
|
JSON_UNESCAPED_UNICODE
|
||||||
|
);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
81
html/api/v1/buchungen.php
Normal file
81
html/api/v1/buchungen.php
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* GET /api/v1/buchungen — Buchungssuche für den Abgleich im Finanzmanager
|
||||||
|
* (Issue #13). Rein lesend, keine Seiteneffekte.
|
||||||
|
*
|
||||||
|
* FSCash führt Bargeldausgaben intern als positiven Betrag; der
|
||||||
|
* Finanzmanager sendet und erwartet Ausgaben negativ. Die Umrechnung
|
||||||
|
* übernimmt fscash_signed_betrag() in html/lib/fscash.php.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require __DIR__ . '/../../config.php';
|
||||||
|
require_once __DIR__ . '/../../lib/fscash.php';
|
||||||
|
require_once __DIR__ . '/_auth.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
fscash_api_require_auth($pdo);
|
||||||
|
|
||||||
|
$validated = fscash_validate_buchungen_query($_GET);
|
||||||
|
if ($validated['error'] !== null) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(fscash_api_error($validated['error']), JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FSCash speichert ausschließlich EUR-Beträge; andere Währungen liefern
|
||||||
|
// definitionsgemäß keine Treffer (kein Fehler, siehe Issue #13 Abschnitt 4).
|
||||||
|
if ($validated['waehrung'] !== 'EUR') {
|
||||||
|
echo json_encode(['anzahl' => 0, 'buchungen' => []], JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$matchParams = [
|
||||||
|
'von' => $validated['von'],
|
||||||
|
'bis' => $validated['bis'],
|
||||||
|
'betrag' => $validated['betrag'],
|
||||||
|
'toleranz' => $validated['toleranz'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$countStmt = $pdo->prepare('
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM entries e
|
||||||
|
WHERE e.date BETWEEN :von AND :bis
|
||||||
|
AND ABS((-1 * e.amount) - :betrag) <= :toleranz
|
||||||
|
');
|
||||||
|
$countStmt->execute($matchParams);
|
||||||
|
$anzahl = (int)$countStmt->fetchColumn();
|
||||||
|
|
||||||
|
$selectStmt = $pdo->prepare('
|
||||||
|
SELECT e.id, e.date AS datum, e.amount AS betrag, e.subject AS zweck,
|
||||||
|
c.name AS kategorie, pm.name AS konto
|
||||||
|
FROM entries e
|
||||||
|
LEFT JOIN categories c ON e.category = c.category_id
|
||||||
|
LEFT JOIN payment_methods pm ON e.payment_method = pm.payment_method_id
|
||||||
|
WHERE e.date BETWEEN :von AND :bis
|
||||||
|
AND ABS((-1 * e.amount) - :betrag) <= :toleranz
|
||||||
|
ORDER BY e.date DESC, e.id DESC
|
||||||
|
LIMIT :limit
|
||||||
|
');
|
||||||
|
$selectStmt->bindValue(':von', $validated['von']);
|
||||||
|
$selectStmt->bindValue(':bis', $validated['bis']);
|
||||||
|
$selectStmt->bindValue(':betrag', $validated['betrag']);
|
||||||
|
$selectStmt->bindValue(':toleranz', $validated['toleranz']);
|
||||||
|
$selectStmt->bindValue(':limit', $validated['limit'], PDO::PARAM_INT);
|
||||||
|
$selectStmt->execute();
|
||||||
|
$rows = $selectStmt->fetchAll();
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'anzahl' => $anzahl,
|
||||||
|
'buchungen' => array_map('fscash_format_buchung', $rows),
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(
|
||||||
|
fscash_api_error('Interner Fehler bei der Buchungssuche.'),
|
||||||
|
JSON_UNESCAPED_UNICODE
|
||||||
|
);
|
||||||
|
}
|
||||||
22
html/api/v1/status.php
Normal file
22
html/api/v1/status.php
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* GET /api/v1/status — Verbindungstest für den Finanzmanager (Issue #13).
|
||||||
|
*
|
||||||
|
* Rein lesend, keine Seiteneffekte. Meldet nur, ob Adresse und Zugangsdaten
|
||||||
|
* stimmen; setzt keine Suchabfrage voraus.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require __DIR__ . '/../../config.php';
|
||||||
|
require_once __DIR__ . '/../../lib/fscash.php';
|
||||||
|
require_once __DIR__ . '/_auth.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
fscash_api_require_auth($pdo);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'name' => 'FSCash',
|
||||||
|
'version' => FSCASH_API_VERSION,
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
|
@ -11,6 +11,11 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Von /api/v1/status gemeldete Versionsnummer (Issue #13).
|
||||||
|
*/
|
||||||
|
const FSCASH_API_VERSION = '2.5';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalisiert Monat/Jahr, wenn beim Monats-Blättern (< 1 bzw. > 12) die
|
* Normalisiert Monat/Jahr, wenn beim Monats-Blättern (< 1 bzw. > 12) die
|
||||||
* Jahresgrenze überschritten wird.
|
* Jahresgrenze überschritten wird.
|
||||||
|
|
@ -231,3 +236,183 @@ function fscash_pending_migrations(array $availableFiles, array $appliedFilename
|
||||||
fn(string $f): bool => !in_array(basename($f), $appliedFilenames, true)
|
fn(string $f): bool => !in_array(basename($f), $appliedFilenames, true)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut eine einheitliche Fehler-Antwort für die lesende API (Issue #13).
|
||||||
|
*
|
||||||
|
* @return array{fehler:string}
|
||||||
|
*/
|
||||||
|
function fscash_api_error(string $message): array
|
||||||
|
{
|
||||||
|
return ['fehler' => $message];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parst den Wert eines Authorization-Headers für HTTP Basic Auth.
|
||||||
|
*
|
||||||
|
* @return array{username:string,password:string}|null Null, wenn der Header fehlt oder nicht dem Basic-Schema entspricht.
|
||||||
|
*/
|
||||||
|
function fscash_parse_basic_auth_header(?string $headerValue): ?array
|
||||||
|
{
|
||||||
|
if ($headerValue === null || stripos($headerValue, 'Basic ') !== 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = base64_decode(trim(substr($headerValue, 6)), true);
|
||||||
|
if ($decoded === false || !str_contains($decoded, ':')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
[$username, $password] = explode(':', $decoded, 2);
|
||||||
|
|
||||||
|
return ['username' => $username, 'password' => $password];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft, ob ein String ein gültiges Kalenderdatum im Format JJJJ-MM-TT ist.
|
||||||
|
*/
|
||||||
|
function fscash_is_valid_date(string $value): bool
|
||||||
|
{
|
||||||
|
if (!preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $value, $m)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return checkdate((int)$m[2], (int)$m[3], (int)$m[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validiert die Query-Parameter der Buchungssuche (/api/v1/buchungen, Issue #13).
|
||||||
|
*
|
||||||
|
* Reine Validierung ohne DB-Zugriff: prüft Pflichtfelder, Formate und die
|
||||||
|
* Reihenfolge von 'von'/'bis'. Bei Erfolg ist 'error' null und die übrigen
|
||||||
|
* Felder sind typisiert und einsatzbereit; sonst ist 'error' die für den
|
||||||
|
* Finanzmanager lesbare Fehlermeldung (siehe Abschnitt "Fehlerverhalten").
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $query Roher Query-String, z. B. $_GET.
|
||||||
|
* @return array{error:?string,von:?string,bis:?string,betrag:?float,toleranz:float,waehrung:string,limit:int}
|
||||||
|
*/
|
||||||
|
function fscash_validate_buchungen_query(array $query): array
|
||||||
|
{
|
||||||
|
$result = [
|
||||||
|
'error' => null,
|
||||||
|
'von' => null,
|
||||||
|
'bis' => null,
|
||||||
|
'betrag' => null,
|
||||||
|
'toleranz' => 0.0,
|
||||||
|
'waehrung' => 'EUR',
|
||||||
|
'limit' => 20,
|
||||||
|
];
|
||||||
|
|
||||||
|
$von = isset($query['von']) ? trim((string)$query['von']) : '';
|
||||||
|
if ($von === '') {
|
||||||
|
$result['error'] = "Parameter 'von' fehlt.";
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
if (!fscash_is_valid_date($von)) {
|
||||||
|
$result['error'] = "Parameter 'von' ist unbrauchbar.";
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bis = isset($query['bis']) ? trim((string)$query['bis']) : '';
|
||||||
|
if ($bis === '') {
|
||||||
|
$result['error'] = "Parameter 'bis' fehlt.";
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
if (!fscash_is_valid_date($bis)) {
|
||||||
|
$result['error'] = "Parameter 'bis' ist unbrauchbar.";
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($von > $bis) {
|
||||||
|
$result['error'] = "Parameter 'von' darf nicht nach 'bis' liegen.";
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
$betragRaw = isset($query['betrag']) ? trim((string)$query['betrag']) : '';
|
||||||
|
if ($betragRaw === '') {
|
||||||
|
$result['error'] = "Parameter 'betrag' fehlt.";
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
if (!preg_match('/^-?\d+(\.\d+)?$/', $betragRaw)) {
|
||||||
|
$result['error'] = "Parameter 'betrag' ist unbrauchbar.";
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($query['toleranz']) && trim((string)$query['toleranz']) !== '') {
|
||||||
|
$toleranzRaw = trim((string)$query['toleranz']);
|
||||||
|
if (!preg_match('/^\d+(\.\d+)?$/', $toleranzRaw)) {
|
||||||
|
$result['error'] = "Parameter 'toleranz' ist unbrauchbar.";
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
$result['toleranz'] = (float)$toleranzRaw;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($query['waehrung']) && trim((string)$query['waehrung']) !== '') {
|
||||||
|
$waehrungRaw = trim((string)$query['waehrung']);
|
||||||
|
if (!preg_match('/^[A-Za-z]{3}$/', $waehrungRaw)) {
|
||||||
|
$result['error'] = "Parameter 'waehrung' ist unbrauchbar.";
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
$result['waehrung'] = strtoupper($waehrungRaw);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($query['limit']) && trim((string)$query['limit']) !== '') {
|
||||||
|
$limitRaw = trim((string)$query['limit']);
|
||||||
|
if (!preg_match('/^\d+$/', $limitRaw) || (int)$limitRaw < 1) {
|
||||||
|
$result['error'] = "Parameter 'limit' ist unbrauchbar.";
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
$result['limit'] = (int)$limitRaw;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result['von'] = $von;
|
||||||
|
$result['bis'] = $bis;
|
||||||
|
$result['betrag'] = (float)$betragRaw;
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rechnet einen in FSCash als positive Bargeldausgabe gespeicherten Betrag in
|
||||||
|
* die vom Finanzmanager erwartete Vorzeichenkonvention um (Issue #13).
|
||||||
|
*
|
||||||
|
* FSCash kennt intern keine Einnahmen und führt Bargeldausgaben stets als
|
||||||
|
* positiven Betrag. Der Finanzmanager führt Ausgaben negativ, daher wird das
|
||||||
|
* Vorzeichen für den Abgleich gedreht: eine gesuchte Ausgabe von -12.50 muss
|
||||||
|
* auf einen in FSCash gespeicherten Betrag von 12.50 treffen.
|
||||||
|
*/
|
||||||
|
function fscash_signed_betrag(float $storedAmount): float
|
||||||
|
{
|
||||||
|
return -$storedAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut aus einer DB-Zeile der Buchungssuche das vom Finanzmanager erwartete
|
||||||
|
* JSON-Objekt für eine einzelne Buchung (Feldnamen laut Issue #13).
|
||||||
|
*
|
||||||
|
* Erwartete Zeilen-Schlüssel: id, datum, betrag (intern positiv gespeichert),
|
||||||
|
* zweck sowie optional kategorie/konto (null, falls die verknüpfte
|
||||||
|
* Kategorie/Zahlweise gelöscht wurde).
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $row
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
function fscash_format_buchung(array $row): array
|
||||||
|
{
|
||||||
|
$buchung = [
|
||||||
|
'id' => (string)$row['id'],
|
||||||
|
'datum' => (string)$row['datum'],
|
||||||
|
'betrag' => round(fscash_signed_betrag((float)$row['betrag']), 2),
|
||||||
|
'waehrung' => 'EUR',
|
||||||
|
'zweck' => (string)$row['zweck'],
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!empty($row['kategorie'])) {
|
||||||
|
$buchung['kategorie'] = (string)$row['kategorie'];
|
||||||
|
}
|
||||||
|
if (!empty($row['konto'])) {
|
||||||
|
$buchung['konto'] = (string)$row['konto'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $buchung;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -330,4 +330,254 @@ final class FscashTest extends TestCase
|
||||||
|
|
||||||
$this->assertSame([], fscash_pending_migrations($available, $applied));
|
$this->assertSame([], fscash_pending_migrations($available, $applied));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- fscash_parse_basic_auth_header --------------------------------
|
||||||
|
|
||||||
|
public function testParseBasicAuthHeaderMitGueltigenZugangsdaten(): void
|
||||||
|
{
|
||||||
|
$header = 'Basic ' . base64_encode('finanzmanager:geheim123');
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
['username' => 'finanzmanager', 'password' => 'geheim123'],
|
||||||
|
fscash_parse_basic_auth_header($header)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testParseBasicAuthHeaderErlaubtDoppelpunktImPasswort(): void
|
||||||
|
{
|
||||||
|
$header = 'Basic ' . base64_encode('user:pass:mit:doppelpunkt');
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
['username' => 'user', 'password' => 'pass:mit:doppelpunkt'],
|
||||||
|
fscash_parse_basic_auth_header($header)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testParseBasicAuthHeaderOhneHeaderLiefertNull(): void
|
||||||
|
{
|
||||||
|
$this->assertNull(fscash_parse_basic_auth_header(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testParseBasicAuthHeaderOhneBasicSchemaLiefertNull(): void
|
||||||
|
{
|
||||||
|
$this->assertNull(fscash_parse_basic_auth_header('Bearer irgendwas'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testParseBasicAuthHeaderMitUngueltigemBase64LiefertNull(): void
|
||||||
|
{
|
||||||
|
$this->assertNull(fscash_parse_basic_auth_header('Basic ###nicht-base64###'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testParseBasicAuthHeaderOhneDoppelpunktLiefertNull(): void
|
||||||
|
{
|
||||||
|
$this->assertNull(fscash_parse_basic_auth_header('Basic ' . base64_encode('ohnedoppelpunkt')));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- fscash_is_valid_date -------------------------------------------
|
||||||
|
|
||||||
|
public function testIsValidDateMitGueltigemDatum(): void
|
||||||
|
{
|
||||||
|
$this->assertTrue(fscash_is_valid_date('2026-08-19'));
|
||||||
|
$this->assertTrue(fscash_is_valid_date('2024-02-29'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsValidDateMitUngueltigemDatumOderFormat(): void
|
||||||
|
{
|
||||||
|
$this->assertFalse(fscash_is_valid_date('2026-02-30'));
|
||||||
|
$this->assertFalse(fscash_is_valid_date('2023-02-29'));
|
||||||
|
$this->assertFalse(fscash_is_valid_date('19.08.2026'));
|
||||||
|
$this->assertFalse(fscash_is_valid_date('2026-8-19'));
|
||||||
|
$this->assertFalse(fscash_is_valid_date(''));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- fscash_validate_buchungen_query ---------------------------------
|
||||||
|
|
||||||
|
public function testValidateBuchungenQueryMitPflichtfeldern(): void
|
||||||
|
{
|
||||||
|
$result = fscash_validate_buchungen_query([
|
||||||
|
'von' => '2026-08-09',
|
||||||
|
'bis' => '2026-08-19',
|
||||||
|
'betrag' => '-12.50',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertNull($result['error']);
|
||||||
|
$this->assertSame('2026-08-09', $result['von']);
|
||||||
|
$this->assertSame('2026-08-19', $result['bis']);
|
||||||
|
$this->assertSame(-12.50, $result['betrag']);
|
||||||
|
$this->assertSame(0.0, $result['toleranz']);
|
||||||
|
$this->assertSame('EUR', $result['waehrung']);
|
||||||
|
$this->assertSame(20, $result['limit']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateBuchungenQueryMitAllenOptionalenParametern(): void
|
||||||
|
{
|
||||||
|
$result = fscash_validate_buchungen_query([
|
||||||
|
'von' => '2026-08-09',
|
||||||
|
'bis' => '2026-08-19',
|
||||||
|
'betrag' => '-12.50',
|
||||||
|
'toleranz' => '0.50',
|
||||||
|
'waehrung' => 'eur',
|
||||||
|
'limit' => '5',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertNull($result['error']);
|
||||||
|
$this->assertSame(0.50, $result['toleranz']);
|
||||||
|
$this->assertSame('EUR', $result['waehrung']);
|
||||||
|
$this->assertSame(5, $result['limit']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateBuchungenQueryOhneVonLiefertFehler(): void
|
||||||
|
{
|
||||||
|
$result = fscash_validate_buchungen_query(['bis' => '2026-08-19', 'betrag' => '-12.50']);
|
||||||
|
|
||||||
|
$this->assertSame("Parameter 'von' fehlt.", $result['error']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateBuchungenQueryOhneBisLiefertFehler(): void
|
||||||
|
{
|
||||||
|
$result = fscash_validate_buchungen_query(['von' => '2026-08-09', 'betrag' => '-12.50']);
|
||||||
|
|
||||||
|
$this->assertSame("Parameter 'bis' fehlt.", $result['error']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateBuchungenQueryMitUngueltigemDatumLiefertFehler(): void
|
||||||
|
{
|
||||||
|
$result = fscash_validate_buchungen_query([
|
||||||
|
'von' => '2026-08-32',
|
||||||
|
'bis' => '2026-08-19',
|
||||||
|
'betrag' => '-12.50',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame("Parameter 'von' ist unbrauchbar.", $result['error']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateBuchungenQueryMitVonNachBisLiefertFehler(): void
|
||||||
|
{
|
||||||
|
$result = fscash_validate_buchungen_query([
|
||||||
|
'von' => '2026-08-19',
|
||||||
|
'bis' => '2026-08-09',
|
||||||
|
'betrag' => '-12.50',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame("Parameter 'von' darf nicht nach 'bis' liegen.", $result['error']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateBuchungenQueryOhneBetragLiefertFehler(): void
|
||||||
|
{
|
||||||
|
$result = fscash_validate_buchungen_query(['von' => '2026-08-09', 'bis' => '2026-08-19']);
|
||||||
|
|
||||||
|
$this->assertSame("Parameter 'betrag' fehlt.", $result['error']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateBuchungenQueryMitUnbrauchbaremBetragLiefertFehler(): void
|
||||||
|
{
|
||||||
|
$result = fscash_validate_buchungen_query([
|
||||||
|
'von' => '2026-08-09',
|
||||||
|
'bis' => '2026-08-19',
|
||||||
|
'betrag' => 'zwölffünfzig',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame("Parameter 'betrag' ist unbrauchbar.", $result['error']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateBuchungenQueryMitNegativerToleranzLiefertFehler(): void
|
||||||
|
{
|
||||||
|
$result = fscash_validate_buchungen_query([
|
||||||
|
'von' => '2026-08-09',
|
||||||
|
'bis' => '2026-08-19',
|
||||||
|
'betrag' => '-12.50',
|
||||||
|
'toleranz' => '-1',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame("Parameter 'toleranz' ist unbrauchbar.", $result['error']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateBuchungenQueryMitUngueltigerWaehrungLiefertFehler(): void
|
||||||
|
{
|
||||||
|
$result = fscash_validate_buchungen_query([
|
||||||
|
'von' => '2026-08-09',
|
||||||
|
'bis' => '2026-08-19',
|
||||||
|
'betrag' => '-12.50',
|
||||||
|
'waehrung' => 'EURO',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame("Parameter 'waehrung' ist unbrauchbar.", $result['error']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateBuchungenQueryMitUngueltigemLimitLiefertFehler(): void
|
||||||
|
{
|
||||||
|
$result = fscash_validate_buchungen_query([
|
||||||
|
'von' => '2026-08-09',
|
||||||
|
'bis' => '2026-08-19',
|
||||||
|
'betrag' => '-12.50',
|
||||||
|
'limit' => '0',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame("Parameter 'limit' ist unbrauchbar.", $result['error']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- fscash_signed_betrag ---------------------------------------------
|
||||||
|
|
||||||
|
public function testSignedBetragDrehtVorzeichenVonGespeicherterAusgabe(): void
|
||||||
|
{
|
||||||
|
$this->assertSame(-12.50, fscash_signed_betrag(12.50));
|
||||||
|
$this->assertSame(0.0, fscash_signed_betrag(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- fscash_format_buchung ---------------------------------------------
|
||||||
|
|
||||||
|
public function testFormatBuchungMitAllenFeldern(): void
|
||||||
|
{
|
||||||
|
$row = [
|
||||||
|
'id' => 42,
|
||||||
|
'datum' => '2026-08-14',
|
||||||
|
'betrag' => 12.50,
|
||||||
|
'zweck' => 'Wocheneinkauf',
|
||||||
|
'kategorie' => 'Lebensmittel',
|
||||||
|
'konto' => 'Portemonnaie',
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
[
|
||||||
|
'id' => '42',
|
||||||
|
'datum' => '2026-08-14',
|
||||||
|
'betrag' => -12.50,
|
||||||
|
'waehrung' => 'EUR',
|
||||||
|
'zweck' => 'Wocheneinkauf',
|
||||||
|
'kategorie' => 'Lebensmittel',
|
||||||
|
'konto' => 'Portemonnaie',
|
||||||
|
],
|
||||||
|
fscash_format_buchung($row)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFormatBuchungOhneKategorieUndKontoLaesstFelderWeg(): void
|
||||||
|
{
|
||||||
|
$row = [
|
||||||
|
'id' => 42,
|
||||||
|
'datum' => '2026-08-14',
|
||||||
|
'betrag' => 12.50,
|
||||||
|
'zweck' => 'Wocheneinkauf',
|
||||||
|
'kategorie' => null,
|
||||||
|
'konto' => null,
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
[
|
||||||
|
'id' => '42',
|
||||||
|
'datum' => '2026-08-14',
|
||||||
|
'betrag' => -12.50,
|
||||||
|
'waehrung' => 'EUR',
|
||||||
|
'zweck' => 'Wocheneinkauf',
|
||||||
|
],
|
||||||
|
fscash_format_buchung($row)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- fscash_api_error ---------------------------------------------------
|
||||||
|
|
||||||
|
public function testApiErrorBautFehlerobjekt(): void
|
||||||
|
{
|
||||||
|
$this->assertSame(['fehler' => 'Parameter fehlt.'], fscash_api_error('Parameter fehlt.'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue