Zwei Features an derselben Stelle, die im Arbeitsverzeichnis nebeneinander lagen und sich dieselben Dateien teilen (Routen, BankingController, Bankzugang, Zugangsformular, Dokumentation) – getrennte Commits ließen sich daraus nur mit einem kaputten Zwischenstand schneiden. PayPal als zweite Art von Bankverbindung ---------------------------------------- „Bankverbindung anlegen“ führt zur Wahl der Art: HBCI/FinTS oder PayPal (bank_zugaenge.typ, Migration 014). Die Abfrage läuft über die Classic-NVP-API (Services\PaypalNvp, TransactionSearch und GetBalance). Kein Dreischritt, da PayPal genau ein Guthabenkonto führt – Zugangsdaten und Kontenzuordnung stehen in einem Formular. Konten, Umsätze, Protokoll, Regeln und Abgleich bleiben dieselben. Bankleitzahlen-Verzeichnis -------------------------- Verwaltung → Bankleitzahlen (/verwaltung/bankinstitute, Migration 015) nimmt die CSV-Institutsliste der Deutschen Kreditwirtschaft auf und beantwortet damit die eine Angabe, die beim Anlegen einer FinTS-Verbindung regelmäßig fehlt: den PIN/TAN-Zugangspunkt. Reine Nachschlagedaten ohne benutzer_id; ein Import ersetzt den gesamten Bestand, statt ihn zu ergänzen. „Nur prüfen“ und „Übernehmen“ sind zwei Knöpfe an derselben Datei – abgelegt wird sie nicht, ihr Inhalt steht ja in der Tabelle. Services\FintsInstituteImport nimmt der Datei ihre Eigenheiten ab: ISO-8859-1 statt UTF-8, ein Institutsname über zwei Zeilen in Anführungszeichen, dieselbe Bankleitzahl vielfach (eine Zeile je Filiale), Hunderte leerer Zeilen am Ende, Text statt Adresse, wo es keinen PIN/TAN-Zugang gibt. Spalten werden über ihre Überschrift angesprochen; fehlen BLZ und Institut, bricht der Import ab. Gesucht wird über Bankleitzahl, BIC, Name und Ort in einem Zug: „1822“ steht bei der 1822direkt im Namen und bei der Frankfurter Sparkasse im BIC, in keiner der beiden Bankleitzahlen. Aus der Trefferliste führt „Verbindung anlegen“ ins gefüllte FinTS-Formular. Die FinTS-Adresse ist damit kein Pflichtfeld mehr – bleibt sie leer, wird sie zur Bankleitzahl nachgeschlagen. Findet auch das nichts, weist Bankzugang den Zugang ab, statt ihn ohne Gegenstelle zu speichern. Tests: 31_PaypalZugangTest, 32_BankinstituteTest, e2e/specs/banking.spec.js, e2e/specs/bankinstitute.spec.js. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
406 lines
17 KiB
PHP
406 lines
17 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
use Finanzmanager\Models\Bankumsatz;
|
||
use Finanzmanager\Models\Bankzugang;
|
||
use Finanzmanager\Models\Konto;
|
||
use Finanzmanager\Services\Crypto;
|
||
use Finanzmanager\Services\PaypalNvp;
|
||
|
||
/**
|
||
* PayPal als zweite Art von Bankverbindung – ohne PayPal.
|
||
*
|
||
* Geprüft wird alles, was um den API-Aufruf herum liegt: der Zugang mit seinen
|
||
* verschlüsselten Zugangsdaten, das eine Konto, der Fingerabdruck der Umsätze
|
||
* und die Auswertung der Antworten. Die Antworten selbst sind nachgestellt –
|
||
* genau in der Form, in der PayPal sie schickt, samt der Eigenheiten, die beim
|
||
* Anschluss aufgelaufen sind.
|
||
*/
|
||
class PaypalZugangTest extends TestCase
|
||
{
|
||
protected string $testName = 'Test 31: PayPal-Bankverbindung';
|
||
|
||
private int $zugang = 0;
|
||
|
||
public function run(): void
|
||
{
|
||
$this->mandantAnlegen();
|
||
$this->zugangAnlegen();
|
||
|
||
$this->zugangKenntSeinenTyp();
|
||
$this->geheimnisseStehenVerschluesseltInDerDatenbank();
|
||
$this->leereGeheimnisseBleibenUnveraendert();
|
||
$this->typBleibtBeimSpeichernStehen();
|
||
$this->kontoEntstehtGenauEinmal();
|
||
$this->kontoLaesstSichZuordnen();
|
||
$this->fingerabdruckHaengtAnDerTransaktionsnummer();
|
||
|
||
$this->endpunktRichtetSichNachDerSandbox();
|
||
$this->abgelehnteAntwortNenntDenGrund();
|
||
$this->sicherheitsfehlerNenntBeideUrsachen();
|
||
$this->auffaelligeAngabenWerdenBenannt();
|
||
$this->leerraumUmDieGeheimnisseFaelltWeg();
|
||
$this->saldoKommtAusBeidenAntwortformen();
|
||
$this->umsaetzeWerdenAusgelesen();
|
||
$this->nullbuchungenFallenHeraus();
|
||
$this->warnungGiltAlsErfolg();
|
||
}
|
||
|
||
private function zugangAnlegen(): void
|
||
{
|
||
$this->zugang = Bankzugang::anlegen([
|
||
'typ' => 'paypal',
|
||
'name' => 'PayPal-Test',
|
||
'bankname' => 'PayPal',
|
||
'api_benutzer' => 'test_api1.example.org',
|
||
'api_kennwort' => 'kennwort-geheim',
|
||
'api_signatur' => 'signatur-geheim',
|
||
'sandbox' => 1,
|
||
'abrufzeitraum' => 'letzter_abruf',
|
||
'aktiv' => 1,
|
||
]);
|
||
}
|
||
|
||
// ── Der Zugang ───────────────────────────────────────────────────────────
|
||
|
||
private function zugangKenntSeinenTyp(): void
|
||
{
|
||
$zugang = Bankzugang::find($this->zugang);
|
||
|
||
$this->assertEquals('paypal', $zugang['typ'], 'Der Zugang ist ein PayPal-Zugang');
|
||
$this->assertEquals('test_api1.example.org', $zugang['api_benutzer'], 'Der API-Benutzer steht im Klartext');
|
||
$this->assertEquals(1, (int)$zugang['sandbox'], 'Der Sandbox-Haken ist gesetzt');
|
||
}
|
||
|
||
private function geheimnisseStehenVerschluesseltInDerDatenbank(): void
|
||
{
|
||
if (!Crypto::einsatzbereit()) {
|
||
$this->assert(false, 'APP_KEY fehlt – ohne ihn lässt sich die Verschlüsselung nicht prüfen');
|
||
return;
|
||
}
|
||
|
||
$roh = Bankzugang::find($this->zugang);
|
||
|
||
$this->assert(
|
||
!str_contains((string)$roh['api_kennwort'], 'kennwort-geheim'),
|
||
'Das API-Kennwort steht nicht im Klartext in der Datenbank'
|
||
);
|
||
$this->assert(
|
||
!str_contains((string)$roh['api_signatur'], 'signatur-geheim'),
|
||
'Die API-Signatur steht nicht im Klartext in der Datenbank'
|
||
);
|
||
|
||
$daten = Bankzugang::zugangsdaten($this->zugang);
|
||
|
||
$this->assertEquals('kennwort-geheim', $daten['api_kennwort'], 'Das Kennwort kommt entschlüsselt zurück');
|
||
$this->assertEquals('signatur-geheim', $daten['api_signatur'], 'Die Signatur kommt entschlüsselt zurück');
|
||
}
|
||
|
||
private function leereGeheimnisseBleibenUnveraendert(): void
|
||
{
|
||
// Dieselbe Regel wie bei der FinTS-PIN: Ein leeres Feld im Formular
|
||
// heißt „unverändert“, sonst verlöre jedes Speichern die Zugangsdaten.
|
||
Bankzugang::speichern($this->zugang, [
|
||
'typ' => 'paypal',
|
||
'name' => 'PayPal-Test',
|
||
'api_benutzer' => 'test_api1.example.org',
|
||
'api_kennwort' => '',
|
||
'api_signatur' => '',
|
||
'sandbox' => 1,
|
||
'abrufzeitraum' => 'letzter_abruf',
|
||
'aktiv' => 1,
|
||
]);
|
||
|
||
$daten = Bankzugang::zugangsdaten($this->zugang);
|
||
|
||
$this->assertEquals('kennwort-geheim', $daten['api_kennwort'], 'Das Kennwort überlebt das Speichern');
|
||
$this->assertEquals('signatur-geheim', $daten['api_signatur'], 'Die Signatur überlebt das Speichern');
|
||
}
|
||
|
||
private function typBleibtBeimSpeichernStehen(): void
|
||
{
|
||
// Ein PayPal-Zugang, der sich in einen FinTS-Zugang verwandelte, hätte
|
||
// weder passende Zugangsdaten noch passende Konten.
|
||
Bankzugang::speichern($this->zugang, [
|
||
'typ' => 'fints',
|
||
'name' => 'PayPal-Test',
|
||
'api_benutzer' => 'test_api1.example.org',
|
||
'abrufzeitraum' => 'letzter_abruf',
|
||
'aktiv' => 1,
|
||
]);
|
||
|
||
$this->assertEquals(
|
||
'paypal',
|
||
Bankzugang::find($this->zugang)['typ'],
|
||
'Der Typ lässt sich nachträglich nicht umstellen'
|
||
);
|
||
}
|
||
|
||
// ── Das eine Konto ───────────────────────────────────────────────────────
|
||
|
||
private function kontoEntstehtGenauEinmal(): void
|
||
{
|
||
$erstes = Bankzugang::paypalKonto($this->zugang);
|
||
$zweites = Bankzugang::paypalKonto($this->zugang);
|
||
|
||
$this->assertEquals((int)$erstes['id'], (int)$zweites['id'], 'Der zweite Abruf findet dasselbe Konto wieder');
|
||
$this->assertEquals(
|
||
Bankzugang::PAYPAL_KENNUNG,
|
||
$erstes['kontonummer'],
|
||
'Das Konto trägt die feste PayPal-Kennung'
|
||
);
|
||
$this->assertEquals(1, count(Bankzugang::konten($this->zugang)), 'Ein PayPal-Zugang führt genau ein Konto');
|
||
}
|
||
|
||
private function kontoLaesstSichZuordnen(): void
|
||
{
|
||
$konto = Konto::anlegen([
|
||
'name' => 'PayPal-Guthaben', 'kontoart' => 'bank', 'anfangssaldo' => 0, 'aktiv' => 1,
|
||
]);
|
||
|
||
$bankkonto = Bankzugang::paypalKonto($this->zugang);
|
||
Bankzugang::ordneKontoZu((int)$bankkonto['id'], $konto);
|
||
|
||
$this->assertEquals(
|
||
$konto,
|
||
(int)Bankzugang::konten($this->zugang)[0]['konto_id'],
|
||
'Das Konto ist zugeordnet'
|
||
);
|
||
}
|
||
|
||
private function fingerabdruckHaengtAnDerTransaktionsnummer(): void
|
||
{
|
||
$bankkonto = (int)Bankzugang::paypalKonto($this->zugang)['id'];
|
||
|
||
$eine = Bankumsatz::externIdKennung($bankkonto, '3XY12345ABC678901');
|
||
$selbe = Bankumsatz::externIdKennung($bankkonto, ' 3XY12345ABC678901 ');
|
||
$andere = Bankumsatz::externIdKennung($bankkonto, '3XY12345ABC678902');
|
||
|
||
$this->assertEquals($selbe, $eine, 'Leerraum um die Nummer ändert den Fingerabdruck nicht');
|
||
$this->assert($eine !== $andere, 'Zwei Nummern ergeben zwei Fingerabdrücke');
|
||
$this->assertEquals(40, strlen($eine), 'Der Fingerabdruck passt in die Spalte');
|
||
|
||
// Derselbe Umsatz darf beim zweiten Lauf nicht noch einmal hereinkommen –
|
||
// die Abrufzeiträume überlappen sich fast immer.
|
||
$konto = (int)Bankzugang::konten($this->zugang)[0]['konto_id'];
|
||
|
||
$daten = [
|
||
'zugang_id' => $this->zugang,
|
||
'konto_id' => $konto,
|
||
'bank_konto_id' => $bankkonto,
|
||
'extern_id' => $eine,
|
||
'buchungstag' => '2026-08-01',
|
||
'wertstellung' => '2026-08-01',
|
||
'betrag' => -12.34,
|
||
'empfaenger' => 'Beispielshop',
|
||
];
|
||
|
||
$this->assertNotEmpty(Bankumsatz::anlegen($daten), 'Der Umsatz kommt herein');
|
||
$this->assertNull(Bankumsatz::anlegen($daten), 'Beim zweiten Mal nicht noch einmal');
|
||
}
|
||
|
||
// ── Die Antworten von PayPal ─────────────────────────────────────────────
|
||
|
||
private function endpunktRichtetSichNachDerSandbox(): void
|
||
{
|
||
$live = new PaypalNvp('b', 'k', 's', false);
|
||
$test = new PaypalNvp('b', 'k', 's', true);
|
||
|
||
$this->assertEquals(PaypalNvp::ENDPUNKT_LIVE, $live->endpunkt(), 'Ohne Haken der Live-Endpunkt');
|
||
$this->assertEquals(PaypalNvp::ENDPUNKT_SANDBOX, $test->endpunkt(), 'Mit Haken der Sandbox-Endpunkt');
|
||
}
|
||
|
||
private function abgelehnteAntwortNenntDenGrund(): void
|
||
{
|
||
$antwort = [
|
||
'ACK' => 'Failure',
|
||
'L_SHORTMESSAGE0' => 'Invalid argument',
|
||
'L_LONGMESSAGE0' => 'Die Suche war nicht erfolgreich.',
|
||
];
|
||
|
||
$this->assert(!PaypalNvp::erfolgreich($antwort), 'Failure gilt nicht als Erfolg');
|
||
$this->assert(
|
||
str_contains((string)PaypalNvp::fehlermeldung($antwort), 'Die Suche war nicht erfolgreich.'),
|
||
'Die ausführliche Meldung steht im Fehler'
|
||
);
|
||
$this->assertEquals([], PaypalNvp::umsaetzeAusAntwort($antwort), 'Aus einer Absage kommen keine Umsätze');
|
||
$this->assertNull(PaypalNvp::saldoAusAntwort($antwort), 'Und auch kein Kontostand');
|
||
}
|
||
|
||
private function sicherheitsfehlerNenntBeideUrsachen(): void
|
||
{
|
||
// Diese Meldung schickt PayPal bei falschen Zugangsdaten genauso wie bei
|
||
// Zugangsdaten, die zum anderen Endpunkt gehören. Nennte die Erklärung
|
||
// nur den Endpunkt, suchte man garantiert an der falschen Stelle.
|
||
$meldung = (string)PaypalNvp::fehlermeldung([
|
||
'ACK' => 'Failure',
|
||
'L_LONGMESSAGE0' => 'Security header is not valid',
|
||
]);
|
||
|
||
$this->assert(str_contains($meldung, 'Sandbox'), 'Der Sicherheitsfehler nennt den Endpunkt');
|
||
$this->assert(
|
||
str_contains($meldung, 'eine der drei Angaben stimmt nicht'),
|
||
'Und ebenso die falschen Zugangsdaten'
|
||
);
|
||
}
|
||
|
||
private function auffaelligeAngabenWerdenBenannt(): void
|
||
{
|
||
// Echte PayPal-Angaben: Name der Form name_api1.domain, Kennwort 16
|
||
// Zeichen, Signatur 56.
|
||
$this->assertEquals(
|
||
[],
|
||
PaypalNvp::auffaelligkeiten('test_api1.example.org', str_repeat('k', 16), str_repeat('s', 56)),
|
||
'An gültigen Angaben fällt nichts auf'
|
||
);
|
||
|
||
$hinweise = PaypalNvp::auffaelligkeiten('kurz', str_repeat('k', 15), str_repeat('s', 20));
|
||
|
||
$this->assertEquals(3, count($hinweise), 'Alle drei Angaben werden bemängelt');
|
||
$this->assert(
|
||
str_contains($hinweise[0], 'name_api1'),
|
||
'Der Benutzername bekommt die erwartete Form genannt'
|
||
);
|
||
$this->assert(str_contains($hinweise[1], '15'), 'Die Länge des Kennworts steht dabei');
|
||
$this->assert(str_contains($hinweise[2], '56'), 'Und die erwartete Länge der Signatur');
|
||
|
||
// Der Wert selbst darf nicht mitlesbar in der Meldung landen.
|
||
$this->assert(
|
||
!str_contains($hinweise[0], 'kurz'),
|
||
'Der Benutzername steht nur gekürzt in der Meldung'
|
||
);
|
||
|
||
// Leere Angaben bemängelt niemand hier – dafür gibt es die Prüfung auf
|
||
// Vollständigkeit vor dem Abruf.
|
||
$this->assertEquals([], PaypalNvp::auffaelligkeiten('', '', ''), 'Leeres bleibt hier unbeanstandet');
|
||
}
|
||
|
||
private function leerraumUmDieGeheimnisseFaelltWeg(): void
|
||
{
|
||
// Eine aus der Zwischenablage eingefügte Signatur bringt gern ein
|
||
// Leerzeichen mit – und PayPal antwortet darauf mit „Security header is
|
||
// not valid“, was alles Mögliche vermuten lässt, nur nicht das.
|
||
$id = Bankzugang::anlegen([
|
||
'typ' => 'paypal',
|
||
'name' => 'PayPal-Leerraum',
|
||
'api_benutzer' => ' test_api1.example.org ',
|
||
'api_kennwort' => " kennwort-mit-rand\n",
|
||
'api_signatur' => "signatur-mit-rand\t",
|
||
'abrufzeitraum' => 'letzter_abruf',
|
||
'aktiv' => 1,
|
||
]);
|
||
|
||
$daten = Bankzugang::zugangsdaten($id);
|
||
|
||
$this->assertEquals('test_api1.example.org', $daten['api_benutzer'], 'Der Benutzername steht ohne Rand');
|
||
$this->assertEquals('kennwort-mit-rand', $daten['api_kennwort'], 'Das Kennwort ebenso');
|
||
$this->assertEquals('signatur-mit-rand', $daten['api_signatur'], 'Und die Signatur');
|
||
}
|
||
|
||
private function saldoKommtAusBeidenAntwortformen(): void
|
||
{
|
||
// Die Form aus der Dokumentation …
|
||
$flach = PaypalNvp::saldoAusAntwort([
|
||
'ACK' => 'Success', 'AMT' => '42.50', 'CURRENCYCODE' => 'EUR',
|
||
]);
|
||
$this->assertEquals(42.50, $flach['betrag'], 'Der Kontostand aus AMT');
|
||
$this->assertEquals('EUR', $flach['waehrung'], 'Samt Währung');
|
||
|
||
// … und die, die Live-Konten tatsächlich liefern.
|
||
$liste = PaypalNvp::saldoAusAntwort([
|
||
'ACK' => 'Success', 'L_AMT0' => '-7.25', 'L_CURRENCYCODE0' => 'EUR',
|
||
'L_AMT1' => '13.00', 'L_CURRENCYCODE1' => 'USD',
|
||
]);
|
||
$this->assertEquals(-7.25, $liste['betrag'], 'Der Kontostand aus L_AMT0');
|
||
$this->assertEquals('EUR', $liste['waehrung'], 'Index 0 ist die Hauptwährung');
|
||
|
||
$this->assertNull(
|
||
PaypalNvp::saldoAusAntwort(['ACK' => 'Success']),
|
||
'Ohne Betrag gibt es keinen Kontostand'
|
||
);
|
||
}
|
||
|
||
private function umsaetzeWerdenAusgelesen(): void
|
||
{
|
||
$umsaetze = PaypalNvp::umsaetzeAusAntwort([
|
||
'ACK' => 'Success',
|
||
'L_TRANSACTIONID0' => '3XY12345ABC678901',
|
||
'L_TIMESTAMP0' => '2026-08-14T09:12:33Z',
|
||
'L_TYPE0' => 'Payment',
|
||
'L_STATUS0' => 'Completed',
|
||
'L_NAME0' => 'Beispielshop GmbH',
|
||
'L_EMAIL0' => 'shop@example.org',
|
||
'L_AMT0' => '-25.00',
|
||
'L_NETAMT0' => '-25.00',
|
||
'L_CURRENCYCODE0' => 'EUR',
|
||
'L_TRANSACTIONID1' => '9ZZ99999XYZ111111',
|
||
'L_TIMESTAMP1' => '2026-08-15T22:04:00Z',
|
||
'L_TYPE1' => 'Transfer',
|
||
'L_STATUS1' => 'Completed',
|
||
'L_NAME1' => '',
|
||
'L_EMAIL1' => '',
|
||
'L_AMT1' => '25.00',
|
||
'L_NETAMT1' => '25.00',
|
||
'L_CURRENCYCODE1' => 'EUR',
|
||
]);
|
||
|
||
$this->assertEquals(2, count($umsaetze), 'Beide Buchungen kommen heraus');
|
||
|
||
$this->assertEquals('3XY12345ABC678901', $umsaetze[0]['kennung'], 'Die Transaktionsnummer steht dabei');
|
||
$this->assertEquals('2026-08-14', $umsaetze[0]['datum'], 'Der Zeitstempel wird zum Buchungstag');
|
||
$this->assertEquals(-25.00, $umsaetze[0]['betrag'], 'Der Betrag trägt sein Vorzeichen');
|
||
$this->assertEquals('Beispielshop GmbH', $umsaetze[0]['empfaenger'], 'Der Name ist der Empfänger');
|
||
$this->assert(
|
||
str_contains($umsaetze[0]['verwendungszweck'], 'shop@example.org'),
|
||
'Die Adresse steht im Verwendungszweck'
|
||
);
|
||
$this->assertEquals('Payment', $umsaetze[0]['art'], 'Die Art der Buchung bleibt erhalten');
|
||
|
||
// Ohne Namen und Adresse bleibt nur die Art – etwa bei der Abbuchung vom
|
||
// hinterlegten Bankkonto.
|
||
$this->assertEquals('PayPal Transfer', $umsaetze[1]['empfaenger'], 'Namenlose Buchungen nennen ihre Art');
|
||
}
|
||
|
||
private function nullbuchungenFallenHeraus(): void
|
||
{
|
||
// Einwilligungen und Abonnements stehen als Buchung über 0,00 € in der
|
||
// Liste und bewegen nichts.
|
||
$umsaetze = PaypalNvp::umsaetzeAusAntwort([
|
||
'ACK' => 'Success',
|
||
'L_TRANSACTIONID0' => 'B-0000000000000000',
|
||
'L_TIMESTAMP0' => '2026-08-10T10:00:00Z',
|
||
'L_TYPE0' => 'Recurring Payment',
|
||
'L_AMT0' => '0.00',
|
||
'L_NETAMT0' => '0.00',
|
||
'L_TRANSACTIONID1' => '3XY00000ABC000001',
|
||
'L_TIMESTAMP1' => '2026-08-11T10:00:00Z',
|
||
'L_TYPE1' => 'Payment',
|
||
'L_AMT1' => '-9.99',
|
||
'L_NETAMT1' => '-9.99',
|
||
]);
|
||
|
||
$this->assertEquals(1, count($umsaetze), 'Die Nullbuchung fällt heraus');
|
||
$this->assertEquals('3XY00000ABC000001', $umsaetze[0]['kennung'], 'Die echte Buchung bleibt');
|
||
}
|
||
|
||
private function warnungGiltAlsErfolg(): void
|
||
{
|
||
// PayPal liefert je Aufruf höchstens 100 Buchungen und meldet das als
|
||
// Warnung. Abbrechen wäre falsch – die Antwort enthält Buchungen.
|
||
$antwort = [
|
||
'ACK' => 'SuccessWithWarning',
|
||
'L_LONGMESSAGE0' => 'The number of results were truncated.',
|
||
'L_TRANSACTIONID0' => '3XY11111ABC111111',
|
||
'L_TIMESTAMP0' => '2026-07-01T08:00:00Z',
|
||
'L_TYPE0' => 'Payment',
|
||
'L_AMT0' => '-1.00',
|
||
'L_NETAMT0' => '-1.00',
|
||
];
|
||
|
||
$this->assert(PaypalNvp::erfolgreich($antwort), 'Eine Warnung gilt als Erfolg');
|
||
$this->assertNull(PaypalNvp::fehlermeldung($antwort), 'Und ist kein Fehler');
|
||
$this->assert(PaypalNvp::abgeschnitten($antwort), 'Sie ist aber als abgeschnitten erkennbar');
|
||
$this->assertEquals(1, count(PaypalNvp::umsaetzeAusAntwort($antwort)), 'Die Buchung kommt trotzdem herein');
|
||
}
|
||
}
|