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>
This commit is contained in:
fds2610-bot 2026-08-19 08:15:45 +02:00
parent be61514093
commit aeb8254840
7 changed files with 88 additions and 3 deletions

View file

@ -1,5 +1,13 @@
# Änderungen
## Bargeld-Tool: Adresse mit `/api/v1` (19.08.2026)
Der Verbindungstest scheiterte mit „Das Bargeld-Tool kennt die Schnittstelle nicht
(HTTP 404)“, wenn die hinterlegte Adresse den Schnittstellenpfad schon enthielt – der
Finanzmanager hängt ihn selbst an und fragte dann unter `/api/v1/api/v1/status` an. Ein
abschließendes `/api` bzw. `/api/v1` kürzt er jetzt beim Speichern weg; bereits
gespeicherte Adressen räumt Migration 017 auf.
## Abgleich mit dem Bargeld-Tool (19.08.2026)
Bargeldausgaben werden in einem zweiten Programm erfasst; im Finanzmanager kommen sie als

View file

@ -262,3 +262,8 @@ nicht (404), prüft der Verbindungstest über eine Suchabfrage.
Der Betrag geht vorzeichenbehaftet über die Leitung, so wie er im Finanzmanager steht:
Ausgaben negativ. Führt das Bargeld-Tool seine Beträge positiv, muss es umrechnen.
`{adresse}` ist die Basisadresse **ohne** Schnittstellenpfad. Steht am Ende der eingetragenen
Adresse trotzdem `/api` oder `/api/v1`, kürzt der Finanzmanager sie beim Speichern – die
Adresse aus der Browserzeile zu kopieren ist der naheliegende Weg und führte sonst zu
`/api/v1/api/v1/status` und einem 404 im Verbindungstest.

View file

@ -0,0 +1,14 @@
-- Die Adresse des Bargeld-Tools ohne den Schnittstellenpfad.
--
-- Der Finanzmanager hängt `/api/v1/...` selbst an die hinterlegte Adresse.
-- Wer sie aus dem Browser kopiert hat, trug den Pfad mit ein – dann fragte
-- der Verbindungstest unter `/api/v1/api/v1/status` an und bekam ein 404.
-- Neue Eingaben kürzt der Finanzmanager beim Speichern; bereits gespeicherte
-- Adressen räumt diese Migration auf.
--
-- Idempotent: Auf einer bereits gekürzten Adresse ändert der Ausdruck nichts.
UPDATE einstellungen
SET wert = REGEXP_REPLACE(TRIM(wert), '/+api(/+v[0-9]+)?/*$', '')
WHERE schluessel = 'bargeld_url'
AND wert REGEXP '/+api(/+v[0-9]+)?/*$';

View file

@ -232,7 +232,9 @@ class VerwaltungController extends BaseController
return;
}
Einstellung::set('bargeld_url', rtrim($url, '/'), 'bargeld');
// Ein mitkopiertes „/api/v1“ am Ende fliegt raus – sonst hinge der
// Schnittstellenpfad ein zweites Mal dran.
Einstellung::set('bargeld_url', $url === '' ? '' : BargeldTool::basisadresse($url), 'bargeld');
Einstellung::set('bargeld_benutzer', trim($this->post('bargeld_benutzer')), 'bargeld');
// Wie bei der FinTS-PIN: Ein leeres Feld heißt „unverändert“, sonst

View file

@ -41,11 +41,26 @@ class BargeldTool
/** Höchstzahl der Treffer, die die Gegenstelle liefern soll. */
private const GRENZE = 20;
private string $basisUrl;
public function __construct(
private string $basisUrl,
string $basisUrl,
private string $benutzer = '',
private string $passwort = ''
) {
$this->basisUrl = self::basisadresse($basisUrl);
}
/**
* Die Basisadresse ohne den Schnittstellenpfad. Wer die Adresse aus dem
* Browser kopiert, hat `/api/v1` schon dabei – ohne diese Kürzung fragte
* der Finanzmanager unter `/api/v1/api/v1/status` an und bekam ein 404.
*/
public static function basisadresse(string $url): string
{
$url = rtrim(trim($url), '/');
return (string)preg_replace('#/api(?:/v\\d+)?$#i', '', $url);
}
/**

View file

@ -55,7 +55,8 @@ include __DIR__ . '/../layout/kopf.php';
placeholder="https://…" inputmode="url">
<p class="feldhinweis">
Die Basisadresse des Bargeld-Tools ohne Pfad; der Finanzmanager hängt
<code>/api/v1/buchungen</code> selbst an. Leer lassen schaltet den Abgleich ab –
<code>/api/v1/buchungen</code> selbst an. Ein mitkopiertes <code>/api/v1</code>
am Ende entfernt er beim Speichern. Leer lassen schaltet den Abgleich ab –
das Kontoblatt zeigt dann überall den grauen Kreis.
</p>

View file

@ -39,6 +39,7 @@ class BargeldAbgleichTest extends TestCase
$this->stammdaten();
$this->antwortAuswerten();
$this->basisadresseKuerztDenSchnittstellenpfad();
$this->kategorienauswahl();
$this->ohneAdresseKeinAbgleich();
$this->passwortStehtVerschluesselt();
@ -113,6 +114,45 @@ class BargeldAbgleichTest extends TestCase
$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