Finanzmanager/tests/34_BargeldAbgleichTest.php
fds2610-bot aeb8254840 Fix #21: Bargeld-Tool auch bei Adresse mit /api/v1 erreichen
Der Verbindungstest meldete „Das Bargeld-Tool kennt die Schnittstelle nicht
(HTTP 404)“, sobald die hinterlegte Adresse den Schnittstellenpfad schon
enthielt: Der Finanzmanager hängt ihn selbst an und fragte damit unter
/api/v1/api/v1/status an.

Die Adresse aus der Browserzeile zu kopieren ist der naheliegende Weg, und
der Pfad ist dort mit dabei. Ein abschließendes /api oder /api/v1 kürzt
BargeldTool::basisadresse() deshalb weg – beim Speichern und noch einmal im
Konstruktor, damit auch schon gespeicherte Adressen greifen. Ein
Unterverzeichnis davor bleibt erhalten, ein Hostname wie apianbieter.example
bleibt unangetastet.

Bereits gespeicherte Adressen räumt Migration 017 auf.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 08:15:45 +02:00

667 lines
26 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
use Finanzmanager\Models\Buchung;
use Finanzmanager\Models\Einstellung;
use Finanzmanager\Models\Kategorie;
use Finanzmanager\Models\Konto;
use Finanzmanager\Services\BargeldAbgleich;
use Finanzmanager\Services\BargeldTool;
use Finanzmanager\Services\Crypto;
/**
* Der Abgleich mit dem Bargeld-Tool: Konfiguration, Kategorienauswahl, der
* Abruf selbst und der Status, den das Kontoblatt als Symbol zeigt.
*
* Der Abruf wird gegen einen echten HTTP-Server geprüft – den eingebauten
* Server von PHP mit einem Router, der sich wie das Bargeld-Tool verhält.
* Nachgebildete Antworten allein würden genau das übergehen, woran ein Abruf
* scheitert: Basic Auth, HTTP-Status, ein Rumpf, der kein JSON ist. Lässt sich
* der Server nicht starten, entfallen die Netzprüfungen; alles Übrige läuft
* ohne ihn.
*/
class BargeldAbgleichTest extends TestCase
{
protected string $testName = 'Test 34: Abgleich mit dem Bargeld-Tool';
private int $konto = 0;
private int $bargeldKategorie = 0;
private int $andereKategorie = 0;
/** @var resource|null */
private $server = null;
private string $adresse = '';
public function run(): void
{
$this->mandantAnlegen();
$this->stammdaten();
$this->antwortAuswerten();
$this->basisadresseKuerztDenSchnittstellenpfad();
$this->kategorienauswahl();
$this->ohneAdresseKeinAbgleich();
$this->passwortStehtVerschluesselt();
$this->suchfensterBleibtInGrenzen();
$this->mitServer();
$this->statusAufraeumenNimmtDenHakenWeg();
$this->kontoblattFuehrtDenStatusMit();
$this->kontoblattZeigtDieViervierSymbole();
$this->konfigurationsseiteZeigtDieAuswahl();
}
protected function tearDown(): void
{
$this->serverBeenden();
parent::tearDown();
}
private function stammdaten(): void
{
$this->konto = Konto::anlegen([
'name' => 'Bargeld Giro', 'kontoart' => 'bank', 'anfangssaldo' => 500, 'aktiv' => 1,
]);
$this->bargeldKategorie = Kategorie::anlegen(['name' => 'Bargeld Haushalt', 'art' => 'ausgabe']);
$this->andereKategorie = Kategorie::anlegen(['name' => 'Bargeld Versicherung', 'art' => 'ausgabe']);
Kategorie::bargeldAuswahlSetzen([$this->bargeldKategorie]);
}
private function buchung(float $betrag = -12.50, string $datum = '2026-03-10'): int
{
return Buchung::anlegen([
'konto_id' => $this->konto,
'datum' => $datum,
'betrag' => $betrag,
'empfaenger' => 'Bargeldabhebung',
]);
}
// ── Auswertung der Antwort, ohne Netz ────────────────────────────────────
private function antwortAuswerten(): void
{
$treffer = BargeldTool::trefferAus(
'{"anzahl":1,"buchungen":[{"id":"a1","datum":"2026-03-08","betrag":-12.50,'
. '"waehrung":"EUR","zweck":"Wocheneinkauf"}]}'
);
$this->assert($treffer['gefunden'], 'Eine Buchung in der Antwort gilt als gefunden');
$this->assert(!$treffer['fehler'], 'Ein Treffer ist kein Fehler');
$this->assertEquals(1, $treffer['anzahl'], 'Die Trefferzahl steht im Ergebnis');
$this->assert(
str_contains($treffer['meldung'], '08.03.2026') && str_contains($treffer['meldung'], 'Wocheneinkauf'),
'Die Meldung nennt Datum und Zweck des Treffers'
);
$leer = BargeldTool::trefferAus('{"anzahl":0,"buchungen":[]}');
$this->assert(!$leer['gefunden'], 'Eine leere Liste heißt: nicht gefunden');
$this->assert(!$leer['fehler'], 'Null Treffer sind kein Fehler');
$mehrere = BargeldTool::trefferAus(
'{"anzahl":3,"buchungen":[{"id":"a1","datum":"2026-03-08","betrag":-12.50}]}'
);
$this->assertEquals(3, $mehrere['anzahl'], 'Das Feld „anzahl“ zählt auch abgeschnittene Treffer');
$this->assert(str_contains($mehrere['meldung'], '3 Treffer'), 'Mehrere Treffer stehen in der Meldung');
$kaputt = BargeldTool::trefferAus('<html>Fehlerseite</html>');
$this->assert($kaputt['fehler'], 'Eine Antwort ohne JSON ist ein Fehler');
$this->assert(!$kaputt['gefunden'], 'Eine kaputte Antwort gilt nicht als Treffer');
$ohneFeld = BargeldTool::trefferAus('{"anzahl":1}');
$this->assert($ohneFeld['fehler'], 'Ohne Feld „buchungen“ ist die Antwort unbrauchbar');
}
// ── Basisadresse ─────────────────────────────────────────────────────────
/**
* Wer die Adresse aus dem Browser kopiert, hat `/api/v1` schon dabei.
* Bleibt es stehen, fragt der Finanzmanager unter `/api/v1/api/v1/status`
* an und der Verbindungstest scheitert mit einem 404.
*/
private function basisadresseKuerztDenSchnittstellenpfad(): void
{
$erwartet = 'https://bargeld.example.org';
foreach ([
'https://bargeld.example.org',
'https://bargeld.example.org/',
'https://bargeld.example.org/api',
'https://bargeld.example.org/api/v1',
'https://bargeld.example.org/api/v1/',
' https://bargeld.example.org/api/v2 ',
] as $eingabe) {
$this->assertEquals(
$erwartet,
BargeldTool::basisadresse($eingabe),
'Basisadresse aus „' . trim($eingabe) . '“'
);
}
$this->assertEquals(
'https://example.org/fscash',
BargeldTool::basisadresse('https://example.org/fscash/api/v1'),
'Ein Unterverzeichnis vor dem Schnittstellenpfad bleibt erhalten'
);
$this->assertEquals(
'https://apianbieter.example.org',
BargeldTool::basisadresse('https://apianbieter.example.org'),
'Nur ein eigener Pfadteil „api“ wird gekürzt, kein Namensbestandteil'
);
}
// ── Auswahl der Kategorien ───────────────────────────────────────────────
private function kategorienauswahl(): void
{
$this->assertEquals(
[$this->bargeldKategorie],
Kategorie::bargeldAuswahl(),
'Die vorgemerkte Kategorie steht in der Auswahl'
);
$kategorie = Kategorie::find($this->bargeldKategorie);
$this->assertEquals(1, (int)$kategorie['bargeld_abgleich'], 'Die Vormerkung steht an der Kategorie');
Kategorie::bargeldAuswahlSetzen([$this->bargeldKategorie, $this->andereKategorie]);
$this->assertEquals(2, count(Kategorie::bargeldAuswahl()), 'Die Auswahl lässt sich erweitern');
Kategorie::bargeldAuswahlSetzen([]);
$this->assertEquals([], Kategorie::bargeldAuswahl(), 'Eine leere Liste wählt alles ab');
Kategorie::bargeldAuswahlSetzen([$this->bargeldKategorie]);
$this->assertEquals(
[$this->bargeldKategorie],
Kategorie::bargeldAuswahl(),
'Die Auswahl wird ersetzt, nicht ergänzt'
);
}
// ── Ohne Konfiguration bleibt alles ruhig ────────────────────────────────
private function ohneAdresseKeinAbgleich(): void
{
Einstellung::set('bargeld_url', '', 'bargeld');
Einstellung::cacheLeeren();
$this->assert(!BargeldAbgleich::eingerichtet(), 'Ohne Adresse gilt der Abgleich als nicht eingerichtet');
$id = $this->buchung();
Buchung::zuordnen($id, $this->bargeldKategorie, null);
$this->assertNull(BargeldAbgleich::pruefe($id), 'Ohne Adresse wird nicht abgeglichen');
$this->assertNull(Buchung::find($id)['bargeld_status'], 'Ohne Adresse bleibt der Status leer');
}
private function passwortStehtVerschluesselt(): void
{
if (!Crypto::einsatzbereit()) {
return;
}
Einstellung::set('bargeld_passwort', Crypto::verschluessle('geheim123'), 'bargeld');
Einstellung::cacheLeeren();
$gespeichert = (string)Einstellung::get('bargeld_passwort');
$this->assert(
Crypto::istVerschluesselt($gespeichert),
'Das Passwort des Bargeld-Tools steht verschlüsselt in der Datenbank'
);
$this->assertEquals('geheim123', Crypto::entschluessle($gespeichert), 'Es lässt sich wieder lesen');
}
private function suchfensterBleibtInGrenzen(): void
{
Einstellung::set('bargeld_tage', '', 'bargeld');
Einstellung::cacheLeeren();
$this->assertEquals(10, BargeldAbgleich::fensterTage(), 'Ohne Angabe gilt das Fenster von zehn Tagen');
Einstellung::set('bargeld_tage', '0', 'bargeld');
Einstellung::cacheLeeren();
$this->assertEquals(10, BargeldAbgleich::fensterTage(), 'Null Tage ergäben kein Fenster – der Standard greift');
Einstellung::set('bargeld_tage', '9999', 'bargeld');
Einstellung::cacheLeeren();
$this->assertEquals(365, BargeldAbgleich::fensterTage(), 'Das Fenster wird bei einem Jahr gekappt');
Einstellung::set('bargeld_tage', '5', 'bargeld');
Einstellung::cacheLeeren();
$this->assertEquals(5, BargeldAbgleich::fensterTage(), 'Eine eigene Angabe gilt');
}
// ── Mit laufendem Bargeld-Tool ───────────────────────────────────────────
private function mitServer(): void
{
if (!$this->serverStarten()) {
return;
}
Einstellung::set('bargeld_url', $this->adresse, 'bargeld');
Einstellung::set('bargeld_benutzer', 'kasse', 'bargeld');
Einstellung::set(
'bargeld_passwort',
Crypto::einsatzbereit() ? Crypto::verschluessle('offen') : 'offen',
'bargeld'
);
Einstellung::cacheLeeren();
$this->assert(BargeldAbgleich::eingerichtet(), 'Mit Adresse gilt der Abgleich als eingerichtet');
$this->verbindungstest();
$this->gefunden();
$this->nichtGefunden();
$this->serverfehler();
$this->falscheZugangsdaten();
$this->umbuchungBleibtUnberuehrt();
$this->andereKategorieBleibtUnberuehrt();
$this->suchfensterWirdMitgeschickt();
}
private function verbindungstest(): void
{
$zugang = BargeldTool::ausEinstellungen();
$this->assertNotEmpty($zugang, 'Aus den Einstellungen entsteht ein Zugang');
$ergebnis = $zugang->test();
$this->assert($ergebnis['erfolg'], 'Der Verbindungstest erreicht das Bargeld-Tool');
$this->assert(
str_contains($ergebnis['meldung'], 'FSCash-Test'),
'Der Test nennt Namen und Fassung der Gegenstelle'
);
}
private function gefunden(): void
{
$id = $this->buchung(-12.50);
Buchung::zuordnen($id, $this->bargeldKategorie, null);
$this->assertEquals(BargeldAbgleich::GEFUNDEN, BargeldAbgleich::pruefe($id), 'Der Betrag wird gefunden');
$buchung = Buchung::find($id);
$this->assertEquals('gefunden', $buchung['bargeld_status'], 'Der Status steht an der Buchung');
$this->assert(
str_contains((string)$buchung['bargeld_hinweis'], 'Wocheneinkauf'),
'Der Treffer steht als Hinweis an der Buchung'
);
$this->assertNotEmpty($buchung['bargeld_geprueft_am'], 'Der Zeitpunkt der Prüfung wird festgehalten');
}
private function nichtGefunden(): void
{
$id = $this->buchung(-99.00);
Buchung::zuordnen($id, $this->bargeldKategorie, null);
$this->assertEquals(BargeldAbgleich::FEHLT, BargeldAbgleich::pruefe($id), 'Ein unbekannter Betrag fehlt');
$this->assertEquals('fehlt', Buchung::find($id)['bargeld_status'], 'Der Status „fehlt“ steht an der Buchung');
}
private function serverfehler(): void
{
$id = $this->buchung(-500.00);
Buchung::zuordnen($id, $this->bargeldKategorie, null);
$this->assertEquals(BargeldAbgleich::FEHLER, BargeldAbgleich::pruefe($id), 'HTTP 500 ergibt den Status Fehler');
$buchung = Buchung::find($id);
$this->assert(
str_contains((string)$buchung['bargeld_hinweis'], 'Kasse steht still'),
'Die Meldung der Gegenstelle steht im Hinweis'
);
}
private function falscheZugangsdaten(): void
{
Einstellung::set('bargeld_benutzer', 'niemand', 'bargeld');
Einstellung::cacheLeeren();
$id = $this->buchung(-12.50);
Buchung::zuordnen($id, $this->bargeldKategorie, null);
$this->assertEquals(
BargeldAbgleich::FEHLER,
BargeldAbgleich::pruefe($id),
'Abgewiesene Zugangsdaten ergeben einen Fehler, kein „nicht gefunden“'
);
Einstellung::set('bargeld_benutzer', 'kasse', 'bargeld');
Einstellung::cacheLeeren();
}
private function umbuchungBleibtUnberuehrt(): void
{
$spar = Konto::anlegen([
'name' => 'Bargeld Spar', 'kontoart' => 'sparkonto', 'anfangssaldo' => 0, 'aktiv' => 1,
]);
$id = $this->buchung(-12.50);
Buchung::zuordnen($id, null, $spar);
$this->assertNull(BargeldAbgleich::pruefe($id), 'Eine Umbuchung wird nicht abgeglichen');
$this->assertNull(Buchung::find($id)['bargeld_status'], 'Eine Umbuchung trägt keinen Status');
}
private function andereKategorieBleibtUnberuehrt(): void
{
$id = $this->buchung(-12.50);
Buchung::zuordnen($id, $this->andereKategorie, null);
$this->assertNull(BargeldAbgleich::pruefe($id), 'Eine nicht vorgemerkte Kategorie wird nicht abgeglichen');
$this->assertNull(Buchung::find($id)['bargeld_status'], 'Sie bleibt ohne Status – der graue Kreis');
}
/**
* Das Fenster reicht rückwärts ab dem Buchungsdatum. Der Testserver
* antwortet nur, wenn `von` und `bis` genau dazu passen.
*/
private function suchfensterWirdMitgeschickt(): void
{
Einstellung::set('bargeld_tage', '10', 'bargeld');
Einstellung::cacheLeeren();
$id = $this->buchung(-7.77, '2026-03-20');
Buchung::zuordnen($id, $this->bargeldKategorie, null);
$this->assertEquals(
BargeldAbgleich::GEFUNDEN,
BargeldAbgleich::pruefe($id),
'Gesucht wird von 2026-03-10 bis 2026-03-20 – zehn Tage rückwärts ab dem Buchungsdatum'
);
}
// ── Aufräumen und Kontoblatt ─────────────────────────────────────────────
private function statusAufraeumenNimmtDenHakenWeg(): void
{
$id = $this->buchung(-12.50);
Buchung::zuordnen($id, $this->bargeldKategorie, null);
Finanzmanager\Database::execute(
"UPDATE buchungen SET bargeld_status = 'gefunden', bargeld_hinweis = 'x' WHERE id = ?",
[$id]
);
BargeldAbgleich::statusAufraeumen([$this->bargeldKategorie]);
$this->assertEquals(
'gefunden',
Buchung::find($id)['bargeld_status'],
'Eine weiterhin vorgemerkte Kategorie behält ihren Status'
);
BargeldAbgleich::statusAufraeumen([$this->andereKategorie]);
$this->assertNull(
Buchung::find($id)['bargeld_status'],
'Wird die Kategorie abgewählt, verschwindet der Status'
);
$this->assertNull(Buchung::find($id)['bargeld_hinweis'], 'Der Hinweis verschwindet mit');
}
private function kontoblattFuehrtDenStatusMit(): void
{
$id = $this->buchung(-33.00, '2026-04-01');
Buchung::zuordnen($id, $this->bargeldKategorie, null);
Finanzmanager\Database::execute(
"UPDATE buchungen SET bargeld_status = 'fehlt', bargeld_hinweis = 'nicht da' WHERE id = ?",
[$id]
);
$zeilen = Buchung::kontoblatt($this->konto, ['von' => '2026-04-01', 'bis' => '2026-04-01']);
$this->assertEquals(1, count($zeilen), 'Die Buchung steht im Kontoblatt');
$this->assertEquals('fehlt', $zeilen[0]['bargeld_status'], 'Das Kontoblatt liefert den Abgleichstatus mit');
$this->assertEquals('nicht da', $zeilen[0]['bargeld_hinweis'], 'Der Hinweis kommt für den Tooltip mit');
}
// ── Die Anzeige ──────────────────────────────────────────────────────────
/**
* Die schmale Spalte zwischen Klasse und Betrag trägt genau vier Zustände.
* Geprüft wird am gerenderten Kontoblatt, nicht an der Absicht.
*/
private function kontoblattZeigtDieViervierSymbole(): void
{
$zeilen = [];
foreach ([null, 'gefunden', 'fehlt', 'fehler'] as $nr => $status) {
$zeilen[] = [
'id' => 900 + $nr, 'datum' => '2026-05-0' . ($nr + 1), 'valuta' => null,
'betrag' => -10.0, 'empfaenger' => 'Zeile ' . $nr, 'buchungstext' => '',
'nummer' => '', 'abgeglichen' => 0, 'kategorie_id' => $this->bargeldKategorie,
'klasse_id' => null, 'gesplittet' => 0, 'quelle' => 'manuell',
'bargeld_status' => $status, 'bargeld_hinweis' => $status === 'fehler' ? 'Kasse steht still.' : null,
'gegenkonto_id' => null, 'konto_id' => $this->konto, 'gegensicht' => false,
'split_id' => null, 'saldo' => 100.0,
];
}
$mitSpalte = $this->kontoblattHtml($zeilen, true);
$this->assert(
(bool)preg_match('#<th class="bargeldspalte"[^>]*>Bar</th>\s*<th class="rechts">Betrag</th>#', $mitSpalte),
'Die Abgleichspalte steht zwischen Klasse und Betrag'
);
$this->assert((bool)preg_match('#bargeld-neutral"[^>]*>●<#u', $mitSpalte), 'Ohne Status steht dort der graue Kreis');
$this->assert((bool)preg_match('#bargeld-gefunden"[^>]*>✓<#u', $mitSpalte), 'Gefunden ergibt den Haken');
$this->assert((bool)preg_match('#bargeld-fehlt"[^>]*>✗<#u', $mitSpalte), 'Nicht gefunden ergibt das X');
$this->assert((bool)preg_match('#bargeld-fehler"[^>]*>!<#u', $mitSpalte), 'Ein Abrufproblem ergibt das Ausrufezeichen');
$this->assert(
str_contains($mitSpalte, 'title="Kasse steht still."'),
'Die Meldung der Gegenstelle steht im Tooltip'
);
$stil = (string)file_get_contents(dirname(__DIR__) . '/public/static/app.css');
$this->assert(
(bool)preg_match('#\.bargeld-gefunden\s*\{[^}]*var\(--positiv\)#', $stil),
'Der Haken nimmt die Farbe des Farbschemas für Positives'
);
$this->assert(
(bool)preg_match('#\.bargeld-fehlt\s*\{[^}]*var\(--negativ\)#', $stil),
'Das X nimmt die Farbe für Negatives'
);
$this->assert(
(bool)preg_match('#\.bargeld-fehler\s*\{[^}]*color:\s*inherit#', $stil),
'Das Ausrufezeichen erbt die Textfarbe – je nach Farbschema'
);
$ohneSpalte = $this->kontoblattHtml($zeilen, false);
$this->assert(
!str_contains($ohneSpalte, 'bargeldspalte'),
'Ohne eingerichtetes Bargeld-Tool fehlt die Spalte ganz'
);
}
private function konfigurationsseiteZeigtDieAuswahl(): void
{
$html = $this->rendere('verwaltung/bargeld.php', [
'url' => 'https://kasse.example.net',
'benutzerkennung' => 'kasse',
'hatPasswort' => true,
'tage' => 10,
'kategorien' => Kategorie::baumFlach(),
'auswahl' => [$this->bargeldKategorie],
'schluessel' => true,
]);
foreach (['bargeld_url', 'bargeld_benutzer', 'bargeld_passwort', 'bargeld_tage'] as $feld) {
$this->assert(str_contains($html, 'name="' . $feld . '"'), 'Das Feld ' . $feld . ' steht im Formular');
}
$this->assert(
(bool)preg_match(
'#name="kategorien\[\]" value="' . $this->bargeldKategorie . '"\s+checked#',
$html
),
'Die vorgemerkte Kategorie ist angekreuzt'
);
$this->assert(
(bool)preg_match(
'#name="kategorien\[\]" value="' . $this->andereKategorie . '"\s*>#',
$html
),
'Die übrigen Kategorien stehen zur Wahl, aber ohne Kreuz'
);
$this->assert(
!str_contains($html, 'style="'),
'Kein style-Attribut – die Content-Security-Policy verwirft es'
);
$this->assert(
str_contains($html, 'action="/verwaltung/bargeld/test"'),
'Der Verbindungstest ist erreichbar'
);
}
/** @param list<array<string,mixed>> $zeilen */
private function kontoblattHtml(array $zeilen, bool $bargeld): string
{
return $this->rendere('buchungen/kontoblatt.php', [
'konto' => Konto::mitSalden(Konto::find($this->konto)),
'zeilen' => $zeilen,
'gesamt' => count($zeilen),
'seite' => 1,
'seiten' => 1,
'filter' => [],
'kategorien' => Kategorie::baumFlach(),
'klassen' => [],
'konten' => Konto::alle(),
'splits' => [],
'wertpapiere' => [],
'bargeld' => $bargeld,
]);
}
/**
* Eine View rendern wie der Controller es täte – ohne Webserver.
*
* @param array<string,mixed> $daten
*/
private function rendere(string $view, array $daten): string
{
$daten += ['titel' => 'Test', 'flash' => [], 'benutzer' => null, 'csrfToken' => 'x',
'aktuellerPfad' => '/verwaltung'];
extract($daten, EXTR_SKIP);
ob_start();
include dirname(__DIR__) . '/src/Views/' . $view;
return (string)ob_get_clean();
}
// ── Der Testserver ───────────────────────────────────────────────────────
/**
* Der eingebaute PHP-Server mit einem Router, der sich wie das Bargeld-Tool
* verhält: Basic Auth, Statusaufruf, Buchungssuche, ein Betrag, der einen
* Serverfehler auslöst.
*/
private function serverStarten(): bool
{
$verzeichnis = $this->tempPfad();
if (!mkdir($verzeichnis, 0o777, true) && !is_dir($verzeichnis)) {
return false;
}
file_put_contents($verzeichnis . '/router.php', $this->routerQuelltext());
$port = random_int(18000, 18999);
$prozess = @proc_open(
sprintf('exec php -S 127.0.0.1:%d -t %s %s/router.php', $port, escapeshellarg($verzeichnis), escapeshellarg($verzeichnis)),
[1 => ['file', '/dev/null', 'w'], 2 => ['file', '/dev/null', 'w']],
$rohre
);
if (!is_resource($prozess)) {
return false;
}
$this->server = $prozess;
$this->adresse = 'http://127.0.0.1:' . $port;
// Der Server braucht einen Augenblick, bis er den Port annimmt.
for ($versuch = 0; $versuch < 50; $versuch++) {
$verbindung = @fsockopen('127.0.0.1', $port, $fehler, $meldung, 0.2);
if (is_resource($verbindung)) {
fclose($verbindung);
return true;
}
usleep(100_000);
}
$this->serverBeenden();
return false;
}
private function serverBeenden(): void
{
if (is_resource($this->server)) {
proc_terminate($this->server);
proc_close($this->server);
}
$this->server = null;
}
private function routerQuelltext(): string
{
return <<<'PHP'
<?php
// Ein Bargeld-Tool im Kleinformat – nur so viel, wie der Abgleich sieht.
$pfad = (string)parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
header('Content-Type: application/json; charset=utf-8');
$benutzer = $_SERVER['PHP_AUTH_USER'] ?? '';
if ($benutzer === '' && isset($_SERVER['HTTP_AUTHORIZATION'])) {
$roh = base64_decode(substr((string)$_SERVER['HTTP_AUTHORIZATION'], 6), true);
$benutzer = $roh === false ? '' : (string)strstr($roh, ':', true);
}
if ($benutzer !== 'kasse') {
http_response_code(401);
echo json_encode(['fehler' => 'Unbekannte Kennung.']);
return true;
}
if ($pfad === '/api/v1/status') {
echo json_encode(['name' => 'FSCash-Test', 'version' => '0.1']);
return true;
}
if ($pfad !== '/api/v1/buchungen') {
http_response_code(404);
echo json_encode(['fehler' => 'Unbekannter Pfad.']);
return true;
}
$betrag = (string)($_GET['betrag'] ?? '');
$von = (string)($_GET['von'] ?? '');
$bis = (string)($_GET['bis'] ?? '');
if ($betrag === '-500.00') {
http_response_code(500);
echo json_encode(['fehler' => 'Kasse steht still.']);
return true;
}
// Der Betrag mit dem Prüfdatum: Er wird nur gefunden, wenn das Fenster
// genau zehn Tage rückwärts ab dem Buchungsdatum reicht.
if ($betrag === '-7.77') {
$treffer = ($von === '2026-03-10' && $bis === '2026-03-20')
? [['id' => 'f1', 'datum' => '2026-03-15', 'betrag' => -7.77]]
: [];
echo json_encode(['anzahl' => count($treffer), 'buchungen' => $treffer]);
return true;
}
if ($betrag === '-12.50') {
echo json_encode(['anzahl' => 1, 'buchungen' => [[
'id' => 'a1',
'datum' => '2026-03-08',
'betrag' => -12.50,
'zweck' => 'Wocheneinkauf',
]]]);
return true;
}
echo json_encode(['anzahl' => 0, 'buchungen' => []]);
return true;
PHP;
}
}