Merge pull request 'Fix #75: Knopf „Alle Umsätze abrufen“ ruft jede Bank der Reihe nach ab' (#87) from fds2610-bot/Finanzmanager:main into main
Reviewed-on: https://codeberg.org/fds2610/Finanzmanager/pulls/87
This commit is contained in:
commit
e080e8f6dd
11 changed files with 569 additions and 21 deletions
13
Changes.md
13
Changes.md
|
|
@ -1,5 +1,18 @@
|
||||||
# Änderungen
|
# Änderungen
|
||||||
|
|
||||||
|
## Online-Banking: Alle Umsätze abrufen (28.09.2026)
|
||||||
|
|
||||||
|
Über der Tabelle der Bankverbindungen steht ein Knopf *Alle Umsätze abrufen*, der jede
|
||||||
|
aktive Bankverbindung der Reihe nach einzeln abruft (#75).
|
||||||
|
|
||||||
|
- Neuer Dienst `Services\Sammelabruf` führt die Warteschlange; sie liegt während einer
|
||||||
|
TAN-Runde in der Sitzung, nach der Freigabe geht es mit der nächsten Bank weiter.
|
||||||
|
- Die TAN-Maske zeigt im Sammelabruf, welche Bankverbindung gerade fragt, und bietet
|
||||||
|
*Diese Bank überspringen* neben *Abruf abbrechen*.
|
||||||
|
- Ein Fehler bei einer Bank hält die übrigen nicht auf; am Ende steht je Bankverbindung
|
||||||
|
eine eigene Meldung. Dafür neu: `View::flash()` für mehrere Meldungen auf einmal.
|
||||||
|
- Neuer Test `tests/46_SammelabrufTest.php`, E2E-Prüfung in `banking.spec.js`.
|
||||||
|
|
||||||
## Einnahme/Ausgabe entscheidet der Geldfluss, nicht die Kategorie-Art (28.09.2026)
|
## Einnahme/Ausgabe entscheidet der Geldfluss, nicht die Kategorie-Art (28.09.2026)
|
||||||
|
|
||||||
Cockpit und Berichte zählten eine Bewegung nach der Art ihrer Kategorie zu Einnahmen
|
Cockpit und Berichte zählten eine Bewegung nach der Art ihrer Kategorie zu Einnahmen
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,8 @@ Haushalt wird beides parallel genutzt.
|
||||||
Monat, Ausgaben nach Kategorie, Kontostände, offene Vorgänge.
|
Monat, Ausgaben nach Kategorie, Kontostände, offene Vorgänge.
|
||||||
* **Online-Banking** – FinTS-Abruf mit TAN-Dialog über mehrere Requests, Einrichtung in drei
|
* **Online-Banking** – FinTS-Abruf mit TAN-Dialog über mehrere Requests, Einrichtung in drei
|
||||||
Schritten (Zugangsdaten, TAN-Verfahren, Kontenzuordnung mit Anlegen fehlender Konten),
|
Schritten (Zugangsdaten, TAN-Verfahren, Kontenzuordnung mit Anlegen fehlender Konten),
|
||||||
Abrufzeitraum als Spanne oder Stichtag – auch für einen einzelnen Abruf –, abgerufene
|
Abrufzeitraum als Spanne oder Stichtag – auch für einen einzelnen Abruf –, *Alle Umsätze
|
||||||
|
abrufen* für alle Bankverbindungen der Reihe nach, abgerufene
|
||||||
Umsätze buchen sich auf Wunsch selbst, Abgleich gegen vorhandene Buchungen, Regeln für
|
Umsätze buchen sich auf Wunsch selbst, Abgleich gegen vorhandene Buchungen, Regeln für
|
||||||
die automatische Kategorisierung. Alternativ **PayPal** über dessen NVP-Schnittstelle:
|
die automatische Kategorisierung. Alternativ **PayPal** über dessen NVP-Schnittstelle:
|
||||||
ein Formular statt dreier Schritte, da PayPal genau ein Guthabenkonto führt.
|
ein Formular statt dreier Schritte, da PayPal genau ein Guthabenkonto führt.
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,24 @@ Wie weit die Bank tatsächlich zurückreicht, entscheidet sie selbst – 90 Tage
|
||||||
mehr liefert nicht jede. Ein früherer Stichtag holt deshalb nicht zwingend mehr; die Lücke
|
mehr liefert nicht jede. Ein früherer Stichtag holt deshalb nicht zwingend mehr; die Lücke
|
||||||
davor schließt der Anfangssaldo des Kontos oder der QIF-Import.
|
davor schließt der Anfangssaldo des Kontos oder der QIF-Import.
|
||||||
|
|
||||||
|
### Alle Umsätze abrufen
|
||||||
|
|
||||||
|
Über der Tabelle der Bankverbindungen ruft *Alle Umsätze abrufen* jede **aktive**
|
||||||
|
Bankverbindung der Reihe nach ab – in der Reihenfolge der Übersicht und jede mit ihrem
|
||||||
|
eingestellten Abrufzeitraum (ein einmaliger Stichtag gilt hier nicht). Die Warteschlange
|
||||||
|
führt `Services\Sammelabruf`; abgerufen wird weiterhin einzeln über `FinTsAbruf`
|
||||||
|
bzw. `PaypalAbruf`.
|
||||||
|
|
||||||
|
Was ohne Rückfrage geht, läuft in einem Request durch. Verlangt eine Bank eine Freigabe,
|
||||||
|
liegt die Warteschlange in der Sitzung (`banking_sammelabruf`), und die TAN-Maske zeigt,
|
||||||
|
welche Bankverbindung gerade fragt („Bankverbindung 2 von 4“). Nach der Freigabe geht es in
|
||||||
|
`tanEinreichen()` mit der nächsten weiter. In der Maske lässt *Diese Bank überspringen* nur
|
||||||
|
die fragende Bank aus; *Abruf abbrechen* beendet den ganzen Lauf.
|
||||||
|
|
||||||
|
Ein Fehler bei einer Bank hält die übrigen nicht auf. Am Ende steht je Bankverbindung eine
|
||||||
|
eigene Meldung – Fehler rot –, und es geht wie beim Einzelabruf in *Umsätze abgleichen*,
|
||||||
|
wenn dort etwas liegt. Ein einzelner Abruf löst einen liegengebliebenen Sammelabruf ab.
|
||||||
|
|
||||||
### TAN-Verfahren
|
### TAN-Verfahren
|
||||||
|
|
||||||
* **Klassisch** (chipTAN, photoTAN): Die Maske zeigt den Hinweistext der Bank und, wenn
|
* **Klassisch** (chipTAN, photoTAN): Die Maske zeigt den Hinweistext der Bank und, wenn
|
||||||
|
|
@ -511,6 +529,12 @@ ein PayPal-Zugang aber ohne sie auskommt. Der Weg im Browser (Upload, Prüfberic
|
||||||
steht in `tests/e2e/specs/bankinstitute.spec.js`; übernommen wird dort nichts, weil ein
|
steht in `tests/e2e/specs/bankinstitute.spec.js`; übernommen wird dort nichts, weil ein
|
||||||
Import den ganzen Bestand der Instanz ersetzte.
|
Import den ganzen Bestand der Instanz ersetzte.
|
||||||
|
|
||||||
|
`tests/46_SammelabrufTest.php` prüft die Warteschlange von *Alle Umsätze abrufen*: nur
|
||||||
|
aktive Bankverbindungen in der Reihenfolge der Übersicht, ein Fehler hält die übrigen nicht
|
||||||
|
auf, der Zustand übersteht die TAN-Pause in der Sitzung, Überspringen und Abbrechen samt
|
||||||
|
ihren Meldungen. Dass der Knopf über der Tabelle steht, prüft
|
||||||
|
`tests/e2e/specs/banking.spec.js`.
|
||||||
|
|
||||||
`tests/28_UmsaetzeLoeschenTest.php` prüft das Sammellöschen: Es trifft genau die
|
`tests/28_UmsaetzeLoeschenTest.php` prüft das Sammellöschen: Es trifft genau die
|
||||||
ausgewählten Zeilen, eine schon erzeugte Buchung überlebt es, jeder Status lässt sich
|
ausgewählten Zeilen, eine schon erzeugte Buchung überlebt es, jeder Status lässt sich
|
||||||
löschen, leere und unbekannte Auswahl bleiben folgenlos, eine doppelt gesendete Nummer zählt
|
löschen, leere und unbekannte Auswahl bleiben folgenlos, eine doppelt gesendete Nummer zählt
|
||||||
|
|
|
||||||
|
|
@ -300,6 +300,7 @@ $router->post('/banking/neu/{typ}', fn(array $p) => (new BankingController())->a
|
||||||
// „/banking/umsaetze“ und „/banking/regeln“.
|
// „/banking/umsaetze“ und „/banking/regeln“.
|
||||||
$router->get('/banking/umsaetze', fn() => (new BankingController())->umsaetze());
|
$router->get('/banking/umsaetze', fn() => (new BankingController())->umsaetze());
|
||||||
$router->post('/banking/umsaetze/alle', fn() => (new BankingController())->alleUebernehmen());
|
$router->post('/banking/umsaetze/alle', fn() => (new BankingController())->alleUebernehmen());
|
||||||
|
$router->post('/banking/alle-abrufen', fn() => (new BankingController())->alleAbrufen());
|
||||||
$router->post('/banking/umsaetze/loeschen', fn() => (new BankingController())->umsaetzeLoeschen());
|
$router->post('/banking/umsaetze/loeschen', fn() => (new BankingController())->umsaetzeLoeschen());
|
||||||
$router->post('/banking/umsaetze/{id}/uebernehmen',
|
$router->post('/banking/umsaetze/{id}/uebernehmen',
|
||||||
fn(array $p) => (new BankingController())->uebernehmen($p));
|
fn(array $p) => (new BankingController())->uebernehmen($p));
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ use Finanzmanager\Services\Crypto;
|
||||||
use Finanzmanager\Services\FinTsAbruf;
|
use Finanzmanager\Services\FinTsAbruf;
|
||||||
use Finanzmanager\Services\FinTsVerfahren;
|
use Finanzmanager\Services\FinTsVerfahren;
|
||||||
use Finanzmanager\Services\PaypalAbruf;
|
use Finanzmanager\Services\PaypalAbruf;
|
||||||
|
use Finanzmanager\Services\Sammelabruf;
|
||||||
use Finanzmanager\View;
|
use Finanzmanager\View;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -535,6 +536,10 @@ class BankingController extends BaseController
|
||||||
// als schon vorhanden, und sie blieben für immer unsichtbar.
|
// als schon vorhanden, und sie blieben für immer unsichtbar.
|
||||||
Bankumsatz::verwaisteOeffnen();
|
Bankumsatz::verwaisteOeffnen();
|
||||||
|
|
||||||
|
// Ein einzelner Abruf löst einen liegengebliebenen Sammelabruf ab –
|
||||||
|
// sonst liefe dessen Warteschlange nach der nächsten TAN weiter.
|
||||||
|
$this->sammelabrufMerken(null);
|
||||||
|
|
||||||
$id = (int)$params['id'];
|
$id = (int)$params['id'];
|
||||||
$zugang = Bankzugang::find($id);
|
$zugang = Bankzugang::find($id);
|
||||||
if ($zugang === null) {
|
if ($zugang === null) {
|
||||||
|
|
@ -542,11 +547,100 @@ class BankingController extends BaseController
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$ergebnis = (string)$zugang['typ'] === 'paypal'
|
$this->zeigeAbruf($this->abrufStarten($zugang, $this->post('stichtag')), $id);
|
||||||
? PaypalAbruf::starten($id, $this->post('stichtag'))
|
}
|
||||||
: FinTsAbruf::starten($id, 'voll', false, $this->post('stichtag'));
|
|
||||||
|
|
||||||
$this->zeigeAbruf($ergebnis, $id);
|
/**
|
||||||
|
* „Alle Umsätze abrufen“: jede aktive Bankverbindung der Reihe nach, mit
|
||||||
|
* dem jeweils eingestellten Abrufzeitraum.
|
||||||
|
*
|
||||||
|
* Was ohne Rückfrage geht, läuft in diesem einen Request durch. Verlangt
|
||||||
|
* eine Bank eine Freigabe, steht die TAN-Maske da; nach der Freigabe geht
|
||||||
|
* es in `tanEinreichen()` mit der nächsten Bank weiter.
|
||||||
|
*/
|
||||||
|
public function alleAbrufen(): void
|
||||||
|
{
|
||||||
|
$this->verlangeAnmeldung();
|
||||||
|
|
||||||
|
Bankumsatz::verwaisteOeffnen();
|
||||||
|
|
||||||
|
$zugaenge = Bankzugang::alle(true);
|
||||||
|
if ($zugaenge === []) {
|
||||||
|
View::redirect('/banking', 'Es gibt keine aktive Bankverbindung zum Abrufen.', 'fehler');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->sammelabrufFortsetzen(Sammelabruf::fuer($zugaenge));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die Warteschlange abarbeiten, bis sie leer ist oder eine Bank eine
|
||||||
|
* Freigabe verlangt.
|
||||||
|
*/
|
||||||
|
private function sammelabrufFortsetzen(Sammelabruf $lauf): void
|
||||||
|
{
|
||||||
|
while (($naechste = $lauf->naechste()) !== null) {
|
||||||
|
$zugang = Bankzugang::find($naechste['id']);
|
||||||
|
$ergebnis = $zugang === null
|
||||||
|
? ['status' => 'fehler', 'meldung' => 'Die Bankverbindung gibt es nicht mehr.']
|
||||||
|
: $this->abrufStarten($zugang, null);
|
||||||
|
|
||||||
|
if ($ergebnis['status'] === 'tan') {
|
||||||
|
$this->sammelabrufMerken($lauf);
|
||||||
|
$this->zeigeTan($ergebnis['tan'] ?? [], $ergebnis['meldung'], $ergebnis['protokoll'], $lauf);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lauf->vermerke($ergebnis);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->sammelabrufBeenden($lauf);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Je Bankverbindung eine Meldung, dann dorthin, wo es weitergeht. */
|
||||||
|
private function sammelabrufBeenden(Sammelabruf $lauf): void
|
||||||
|
{
|
||||||
|
$this->sammelabrufMerken(null);
|
||||||
|
|
||||||
|
foreach ($lauf->meldungen() as $meldung) {
|
||||||
|
View::flash($meldung['meldung'], $meldung['typ']);
|
||||||
|
}
|
||||||
|
|
||||||
|
View::redirect(Bankumsatz::anzahlOffen() > 0 ? '/banking/umsaetze' : '/banking');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Der Sammelabruf aus der Sitzung – `null`, wenn keiner läuft. */
|
||||||
|
private function sammelabruf(): ?Sammelabruf
|
||||||
|
{
|
||||||
|
Auth::startSession();
|
||||||
|
|
||||||
|
return Sammelabruf::aus($_SESSION[Sammelabruf::SITZUNG] ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Den Sammelabruf für den nächsten Request ablegen; `null` vergisst ihn. */
|
||||||
|
private function sammelabrufMerken(?Sammelabruf $lauf): void
|
||||||
|
{
|
||||||
|
Auth::startSession();
|
||||||
|
if ($lauf === null) {
|
||||||
|
unset($_SESSION[Sammelabruf::SITZUNG]);
|
||||||
|
} else {
|
||||||
|
$_SESSION[Sammelabruf::SITZUNG] = $lauf->zustand();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Einen Abruf je nach Art der Bankverbindung starten.
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $zugang
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
private function abrufStarten(array $zugang, ?string $stichtag): array
|
||||||
|
{
|
||||||
|
$id = (int)$zugang['id'];
|
||||||
|
|
||||||
|
return (string)$zugang['typ'] === 'paypal'
|
||||||
|
? PaypalAbruf::starten($id, $stichtag)
|
||||||
|
: FinTsAbruf::starten($id, 'voll', false, $stichtag);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Die TAN-Maske nach einem Neuladen der Seite. */
|
/** Die TAN-Maske nach einem Neuladen der Seite. */
|
||||||
|
|
@ -560,12 +654,7 @@ class BankingController extends BaseController
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->render('banking/tan.php', [
|
$this->zeigeTan($tan, 'Die Bank verlangt eine Freigabe.', [], $this->sammelabruf());
|
||||||
'titel' => 'Freigabe erforderlich',
|
|
||||||
'tan' => $tan,
|
|
||||||
'meldung' => 'Die Bank verlangt eine Freigabe.',
|
|
||||||
'protokoll' => [],
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function tanEinreichen(): void
|
public function tanEinreichen(): void
|
||||||
|
|
@ -574,16 +663,64 @@ class BankingController extends BaseController
|
||||||
|
|
||||||
$ergebnis = FinTsAbruf::tanEinreichen($this->post('tan'));
|
$ergebnis = FinTsAbruf::tanEinreichen($this->post('tan'));
|
||||||
|
|
||||||
|
// Gehört die Freigabe zu einem Sammelabruf, geht es nach ihr mit der
|
||||||
|
// nächsten Bank weiter – nicht zurück in die Übersicht.
|
||||||
|
$lauf = $this->sammelabruf();
|
||||||
|
if ($lauf !== null && $ergebnis['status'] !== 'tan'
|
||||||
|
&& ($lauf->aktuell()['id'] ?? null) === (int)($ergebnis['zugang_id'] ?? 0)) {
|
||||||
|
$lauf->vermerke($ergebnis);
|
||||||
|
$this->sammelabrufFortsetzen($lauf);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$this->zeigeAbruf($ergebnis, null);
|
$this->zeigeAbruf($ergebnis, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Den wartenden Abruf abbrechen.
|
||||||
|
*
|
||||||
|
* Im Sammelabruf gibt es zwei Wege: `weiter` lässt nur die Bank aus, die
|
||||||
|
* gerade fragt, und macht mit der nächsten weiter – für die Bank, deren
|
||||||
|
* Banking-Gerät gerade nicht zur Hand ist. Sonst endet der ganze Lauf.
|
||||||
|
*/
|
||||||
public function tanAbbrechen(): void
|
public function tanAbbrechen(): void
|
||||||
{
|
{
|
||||||
$this->verlangeAnmeldung();
|
$this->verlangeAnmeldung();
|
||||||
|
|
||||||
FinTsAbruf::abbrechen();
|
FinTsAbruf::abbrechen();
|
||||||
|
|
||||||
|
$lauf = $this->sammelabruf();
|
||||||
|
if ($lauf === null) {
|
||||||
View::redirect('/banking', 'Abruf abgebrochen.');
|
View::redirect('/banking', 'Abruf abgebrochen.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->post('weiter') === '1') {
|
||||||
|
$lauf->ueberspringe();
|
||||||
|
$this->sammelabrufFortsetzen($lauf);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lauf->abbrechen();
|
||||||
|
$this->sammelabrufBeenden($lauf);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die TAN-Maske – im Sammelabruf mit dem Hinweis, welche Bank fragt und wie
|
||||||
|
* viele noch folgen.
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $tan
|
||||||
|
* @param list<string> $protokoll
|
||||||
|
*/
|
||||||
|
private function zeigeTan(array $tan, string $meldung, array $protokoll, ?Sammelabruf $lauf): void
|
||||||
|
{
|
||||||
|
$this->render('banking/tan.php', [
|
||||||
|
'titel' => 'Freigabe erforderlich',
|
||||||
|
'tan' => $tan,
|
||||||
|
'meldung' => $meldung,
|
||||||
|
'protokoll' => $protokoll,
|
||||||
|
'sammelabruf' => $lauf?->aktuell() === null ? null : $lauf->stand(),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -595,12 +732,13 @@ class BankingController extends BaseController
|
||||||
private function zeigeAbruf(array $ergebnis, ?int $zugangId): void
|
private function zeigeAbruf(array $ergebnis, ?int $zugangId): void
|
||||||
{
|
{
|
||||||
if ($ergebnis['status'] === 'tan') {
|
if ($ergebnis['status'] === 'tan') {
|
||||||
$this->render('banking/tan.php', [
|
// Den Stand eines Sammelabrufs nur zeigen, wenn die Freigabe auch
|
||||||
'titel' => 'Freigabe erforderlich',
|
// zu dessen laufender Bank gehört.
|
||||||
'tan' => $ergebnis['tan'] ?? [],
|
$lauf = $this->sammelabruf();
|
||||||
'meldung' => $ergebnis['meldung'],
|
if (($lauf?->aktuell()['id'] ?? null) !== (int)($ergebnis['zugang_id'] ?? 0)) {
|
||||||
'protokoll' => $ergebnis['protokoll'],
|
$lauf = null;
|
||||||
]);
|
}
|
||||||
|
$this->zeigeTan($ergebnis['tan'] ?? [], $ergebnis['meldung'], $ergebnis['protokoll'], $lauf);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
176
src/Services/Sammelabruf.php
Normal file
176
src/Services/Sammelabruf.php
Normal file
|
|
@ -0,0 +1,176 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Finanzmanager\Services;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* „Alle Umsätze abrufen“: jede aktive Bankverbindung der Reihe nach.
|
||||||
|
*
|
||||||
|
* Die Klasse führt nur die Warteschlange – abgerufen wird weiterhin einzeln
|
||||||
|
* über `FinTsAbruf` beziehungsweise `PaypalAbruf`. Der Grund ist die TAN: Verlangt
|
||||||
|
* eine Bank eine Freigabe, endet der Request mit der TAN-Maske, und erst der
|
||||||
|
* nächste Request setzt den Abruf fort. Die Warteschlange muss diese Pause
|
||||||
|
* überstehen; der Controller legt ihren Zustand deshalb in die Sitzung und
|
||||||
|
* holt ihn dort wieder ab.
|
||||||
|
*
|
||||||
|
* Ein Fehler bei einer Bank hält die übrigen nicht auf: Er wird vermerkt, und
|
||||||
|
* es geht mit der nächsten weiter. Am Ende steht je Bankverbindung eine Meldung.
|
||||||
|
*/
|
||||||
|
final class Sammelabruf
|
||||||
|
{
|
||||||
|
/** Schlüssel des Zustands in der Sitzung. */
|
||||||
|
public const SITZUNG = 'banking_sammelabruf';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{offen: list<array{id: int, name: string}>,
|
||||||
|
* aktuell: array{id: int, name: string}|null,
|
||||||
|
* gesamt: int,
|
||||||
|
* meldungen: list<array{typ: string, meldung: string}>} $zustand
|
||||||
|
*/
|
||||||
|
private function __construct(private array $zustand)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eine neue Warteschlange über die übergebenen Bankverbindungen – in der
|
||||||
|
* Reihenfolge, in der sie auch in der Übersicht stehen.
|
||||||
|
*
|
||||||
|
* @param list<array<string,mixed>> $zugaenge
|
||||||
|
*/
|
||||||
|
public static function fuer(array $zugaenge): self
|
||||||
|
{
|
||||||
|
$offen = [];
|
||||||
|
foreach ($zugaenge as $zugang) {
|
||||||
|
$offen[] = ['id' => (int)$zugang['id'], 'name' => (string)$zugang['name']];
|
||||||
|
}
|
||||||
|
|
||||||
|
return new self([
|
||||||
|
'offen' => $offen,
|
||||||
|
'aktuell' => null,
|
||||||
|
'gesamt' => count($offen),
|
||||||
|
'meldungen' => [],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die Warteschlange aus einem gespeicherten Zustand – `null`, wenn keiner
|
||||||
|
* (mehr) da ist.
|
||||||
|
*/
|
||||||
|
public static function aus(mixed $zustand): ?self
|
||||||
|
{
|
||||||
|
if (!is_array($zustand) || !is_array($zustand['offen'] ?? null)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new self([
|
||||||
|
'offen' => array_values($zustand['offen']),
|
||||||
|
'aktuell' => is_array($zustand['aktuell'] ?? null) ? $zustand['aktuell'] : null,
|
||||||
|
'gesamt' => (int)($zustand['gesamt'] ?? 0),
|
||||||
|
'meldungen' => is_array($zustand['meldungen'] ?? null) ? $zustand['meldungen'] : [],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string,mixed> Der Zustand zum Ablegen in der Sitzung. */
|
||||||
|
public function zustand(): array
|
||||||
|
{
|
||||||
|
return $this->zustand;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die nächste Bankverbindung – `null`, wenn alle durch sind. Sie gilt ab
|
||||||
|
* jetzt als die laufende, bis ihr Ergebnis vermerkt ist.
|
||||||
|
*
|
||||||
|
* @return array{id: int, name: string}|null
|
||||||
|
*/
|
||||||
|
public function naechste(): ?array
|
||||||
|
{
|
||||||
|
$this->zustand['aktuell'] = array_shift($this->zustand['offen']);
|
||||||
|
|
||||||
|
return $this->zustand['aktuell'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array{id: int, name: string}|null Die gerade laufende Bankverbindung. */
|
||||||
|
public function aktuell(): ?array
|
||||||
|
{
|
||||||
|
return $this->zustand['aktuell'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Das Ergebnis der laufenden Bankverbindung festhalten.
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $ergebnis wie von `FinTsAbruf`/`PaypalAbruf`
|
||||||
|
*/
|
||||||
|
public function vermerke(array $ergebnis): void
|
||||||
|
{
|
||||||
|
if ($this->zustand['aktuell'] === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fehler = ($ergebnis['status'] ?? 'fehler') === 'fehler';
|
||||||
|
$this->notiere(
|
||||||
|
$fehler ? 'fehler' : 'erfolg',
|
||||||
|
($fehler ? 'Abruf fehlgeschlagen: ' : '') . (string)($ergebnis['meldung'] ?? '')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Die laufende Bankverbindung ohne Ergebnis hinter sich lassen. */
|
||||||
|
public function ueberspringe(): void
|
||||||
|
{
|
||||||
|
if ($this->zustand['aktuell'] !== null) {
|
||||||
|
$this->notiere('fehler', 'Übersprungen, die Freigabe wurde abgebrochen.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Den ganzen Lauf abbrechen: Die laufende und alle noch offenen
|
||||||
|
* Bankverbindungen werden nicht mehr abgerufen.
|
||||||
|
*/
|
||||||
|
public function abbrechen(): void
|
||||||
|
{
|
||||||
|
$offen = count($this->zustand['offen']) + ($this->zustand['aktuell'] !== null ? 1 : 0);
|
||||||
|
$this->zustand['aktuell'] = null;
|
||||||
|
$this->zustand['offen'] = [];
|
||||||
|
|
||||||
|
if ($offen > 0) {
|
||||||
|
$this->zustand['meldungen'][] = [
|
||||||
|
'typ' => 'fehler',
|
||||||
|
'meldung' => sprintf(
|
||||||
|
'Abruf abgebrochen – %d Bankverbindung%s nicht abgerufen.',
|
||||||
|
$offen,
|
||||||
|
$offen === 1 ? '' : 'en'
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wo der Lauf steht – für die TAN-Maske, damit dort klar ist, welche Bank
|
||||||
|
* gerade fragt und wie viele noch folgen.
|
||||||
|
*
|
||||||
|
* @return array{nummer: int, gesamt: int, name: string}
|
||||||
|
*/
|
||||||
|
public function stand(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'nummer' => $this->zustand['gesamt'] - count($this->zustand['offen']),
|
||||||
|
'gesamt' => $this->zustand['gesamt'],
|
||||||
|
'name' => (string)($this->zustand['aktuell']['name'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<array{typ: string, meldung: string}> Je Bankverbindung eine Meldung. */
|
||||||
|
public function meldungen(): array
|
||||||
|
{
|
||||||
|
return $this->zustand['meldungen'];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function notiere(string $typ, string $meldung): void
|
||||||
|
{
|
||||||
|
$this->zustand['meldungen'][] = [
|
||||||
|
'typ' => $typ,
|
||||||
|
'meldung' => $this->zustand['aktuell']['name'] . ': ' . $meldung,
|
||||||
|
];
|
||||||
|
$this->zustand['aktuell'] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/View.php
10
src/View.php
|
|
@ -30,13 +30,19 @@ class View
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function redirect(string $pfad, string $meldung = '', string $typ = 'erfolg'): void
|
public static function redirect(string $pfad, string $meldung = '', string $typ = 'erfolg'): void
|
||||||
|
{
|
||||||
|
self::flash($meldung, $typ);
|
||||||
|
header('Location: ' . $pfad);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Eine Meldung für die nächste Seite vormerken – es dürfen mehrere sein. */
|
||||||
|
public static function flash(string $meldung, string $typ = 'erfolg'): void
|
||||||
{
|
{
|
||||||
Auth::startSession();
|
Auth::startSession();
|
||||||
if ($meldung !== '') {
|
if ($meldung !== '') {
|
||||||
$_SESSION['flash'][] = ['typ' => $typ, 'meldung' => $meldung];
|
$_SESSION['flash'][] = ['typ' => $typ, 'meldung' => $meldung];
|
||||||
}
|
}
|
||||||
header('Location: ' . $pfad);
|
|
||||||
exit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function h(mixed $wert): string
|
public static function h(mixed $wert): string
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,15 @@ include __DIR__ . '/../layout/kopf.php';
|
||||||
<div class="seitenaktionen">
|
<div class="seitenaktionen">
|
||||||
<a href="/banking/neu" class="knopf primaer">Bankverbindung anlegen</a>
|
<a href="/banking/neu" class="knopf primaer">Bankverbindung anlegen</a>
|
||||||
<a href="/banking/umsaetze" class="knopf">Umsätze abgleichen</a>
|
<a href="/banking/umsaetze" class="knopf">Umsätze abgleichen</a>
|
||||||
|
<?php if (array_filter($zugaenge, fn(array $z): bool => (int)$z['aktiv'] === 1) !== []): ?>
|
||||||
|
<?php /* Ruft jede aktive Bankverbindung der Reihe nach ab, jede mit
|
||||||
|
ihrem eingestellten Abrufzeitraum. Verlangt eine Bank eine
|
||||||
|
Freigabe, geht es nach der TAN mit der nächsten weiter. */ ?>
|
||||||
|
<form method="post" action="/banking/alle-abrufen" class="inline" id="alle-abrufen">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
|
||||||
|
<button type="submit" class="knopf">Alle Umsätze abrufen</button>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php if ($zugaenge === []): ?>
|
<?php if ($zugaenge === []): ?>
|
||||||
|
|
@ -137,6 +146,12 @@ include __DIR__ . '/../layout/kopf.php';
|
||||||
übernommen oder verworfen werden.
|
übernommen oder verworfen werden.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p class="hinweis">
|
||||||
|
<em>Alle Umsätze abrufen</em> holt jede aktive Bankverbindung der Reihe nach mit ihrem
|
||||||
|
eingestellten Abrufzeitraum ab. Verlangt eine Bank eine Freigabe, geht es nach der TAN
|
||||||
|
mit der nächsten weiter; am Ende steht je Bankverbindung eine Meldung.
|
||||||
|
</p>
|
||||||
|
|
||||||
<p class="hinweis">
|
<p class="hinweis">
|
||||||
Das Datumsfeld neben <em>Umsätze abrufen</em> ist ein <strong>Stichtag für diesen einen
|
Das Datumsfeld neben <em>Umsätze abrufen</em> ist ein <strong>Stichtag für diesen einen
|
||||||
Abruf</strong>: Es wird ab diesem Tag geholt, ohne den eingestellten Abrufzeitraum zu
|
Abruf</strong>: Es wird ab diesem Tag geholt, ohne den eingestellten Abrufzeitraum zu
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,12 @@ use Finanzmanager\View;
|
||||||
* @var array<string,mixed> $tan
|
* @var array<string,mixed> $tan
|
||||||
* @var string $meldung
|
* @var string $meldung
|
||||||
* @var list<string> $protokoll
|
* @var list<string> $protokoll
|
||||||
|
* @var array{nummer: int, gesamt: int, name: string}|null $sammelabruf
|
||||||
* @var string $csrfToken
|
* @var string $csrfToken
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$entkoppelt = ($tan['entkoppelt'] ?? false) === true;
|
$entkoppelt = ($tan['entkoppelt'] ?? false) === true;
|
||||||
|
$sammelabruf = $sammelabruf ?? null;
|
||||||
|
|
||||||
include __DIR__ . '/../layout/kopf.php';
|
include __DIR__ . '/../layout/kopf.php';
|
||||||
?>
|
?>
|
||||||
|
|
@ -24,6 +26,13 @@ include __DIR__ . '/../layout/kopf.php';
|
||||||
<p class="kachelhinweis"><?= View::h($tan['bezeichnung'] ?? 'Abruf') ?></p>
|
<p class="kachelhinweis"><?= View::h($tan['bezeichnung'] ?? 'Abruf') ?></p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<?php if ($sammelabruf !== null): ?>
|
||||||
|
<p class="feldhinweis sammelabrufstand">
|
||||||
|
Alle Umsätze abrufen: Bankverbindung <?= (int)$sammelabruf['nummer'] ?>
|
||||||
|
von <?= (int)$sammelabruf['gesamt'] ?> – <strong><?= View::h($sammelabruf['name']) ?></strong>
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<p class="hinweis"><?= View::h($meldung) ?></p>
|
<p class="hinweis"><?= View::h($meldung) ?></p>
|
||||||
|
|
||||||
<?php if (($tan['hinweis'] ?? '') !== ''): ?>
|
<?php if (($tan['hinweis'] ?? '') !== ''): ?>
|
||||||
|
|
@ -66,6 +75,10 @@ include __DIR__ . '/../layout/kopf.php';
|
||||||
<form method="post" action="/banking/tan/abbrechen" class="formular">
|
<form method="post" action="/banking/tan/abbrechen" class="formular">
|
||||||
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
|
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
|
||||||
<div class="knopfleiste">
|
<div class="knopfleiste">
|
||||||
|
<?php if ($sammelabruf !== null && $sammelabruf['nummer'] < $sammelabruf['gesamt']): ?>
|
||||||
|
<?php /* Nur diese Bank auslassen – die übrigen werden trotzdem abgerufen. */ ?>
|
||||||
|
<button type="submit" name="weiter" value="1" class="knopf">Diese Bank überspringen</button>
|
||||||
|
<?php endif; ?>
|
||||||
<button type="submit" class="knopf">Abruf abbrechen</button>
|
<button type="submit" class="knopf">Abruf abbrechen</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
138
tests/46_SammelabrufTest.php
Normal file
138
tests/46_SammelabrufTest.php
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Finanzmanager\Models\Bankzugang;
|
||||||
|
use Finanzmanager\Services\Sammelabruf;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* „Alle Umsätze abrufen“: die Warteschlange über alle aktiven Bankverbindungen.
|
||||||
|
*
|
||||||
|
* Der Abruf selbst bleibt außen vor – geprüft wird, dass jede Bank der Reihe
|
||||||
|
* nach drankommt, ein Fehler die übrigen nicht aufhält, die Warteschlange eine
|
||||||
|
* TAN-Pause in der Sitzung übersteht und am Ende je Bank eine Meldung steht.
|
||||||
|
*/
|
||||||
|
class SammelabrufTest extends TestCase
|
||||||
|
{
|
||||||
|
protected string $testName = 'Test 46: Alle Umsätze abrufen';
|
||||||
|
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$this->mandantAnlegen();
|
||||||
|
|
||||||
|
$this->nurAktiveKommenDran();
|
||||||
|
$this->jedeBankDerReiheNach();
|
||||||
|
$this->derZustandUeberstehtDieTanPause();
|
||||||
|
$this->ueberspringenUndAbbrechen();
|
||||||
|
$this->kaputterZustandIstKeinLauf();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function nurAktiveKommenDran(): void
|
||||||
|
{
|
||||||
|
foreach (['Sammel B' => 1, 'Sammel A' => 1, 'Sammel ruht' => 0] as $name => $aktiv) {
|
||||||
|
Bankzugang::anlegen([
|
||||||
|
'name' => $name,
|
||||||
|
'blz' => '12030000',
|
||||||
|
'fints_url' => 'https://banking.example.org/fints',
|
||||||
|
'benutzerkennung' => 'kunde',
|
||||||
|
'pin' => 'geheim',
|
||||||
|
'abrufzeitraum' => 'letzter_abruf',
|
||||||
|
'aktiv' => $aktiv,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$lauf = Sammelabruf::fuer(Bankzugang::alle(true));
|
||||||
|
$namen = [];
|
||||||
|
while (($naechste = $lauf->naechste()) !== null) {
|
||||||
|
$namen[] = $naechste['name'];
|
||||||
|
$lauf->vermerke(['status' => 'erfolg', 'meldung' => 'ok']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertEquals(['Sammel A', 'Sammel B'], $namen,
|
||||||
|
'Nur aktive Bankverbindungen, in der Reihenfolge der Übersicht');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function jedeBankDerReiheNach(): void
|
||||||
|
{
|
||||||
|
$lauf = Sammelabruf::fuer([
|
||||||
|
['id' => 1, 'name' => 'Girobank'],
|
||||||
|
['id' => 2, 'name' => 'Kaputtbank'],
|
||||||
|
['id' => 3, 'name' => 'PayPal'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertNull($lauf->aktuell(), 'Vor dem Start läuft keine Bank');
|
||||||
|
|
||||||
|
$this->assertEquals(1, $lauf->naechste()['id'] ?? null, 'Die erste Bank kommt zuerst');
|
||||||
|
$this->assertEquals(['nummer' => 1, 'gesamt' => 3, 'name' => 'Girobank'], $lauf->stand(),
|
||||||
|
'Der Stand nennt Bank 1 von 3');
|
||||||
|
$lauf->vermerke(['status' => 'erfolg', 'meldung' => '4 neue Umsätze.']);
|
||||||
|
$this->assertNull($lauf->aktuell(), 'Nach dem Vermerk läuft keine Bank mehr');
|
||||||
|
|
||||||
|
$this->assertEquals(2, $lauf->naechste()['id'] ?? null, 'Dann die zweite');
|
||||||
|
$lauf->vermerke(['status' => 'fehler', 'meldung' => 'PIN falsch.']);
|
||||||
|
|
||||||
|
$this->assertEquals(3, $lauf->naechste()['id'] ?? null,
|
||||||
|
'Ein Fehler hält die übrigen Banken nicht auf');
|
||||||
|
$lauf->vermerke(['status' => 'erfolg', 'meldung' => 'Nichts Neues.']);
|
||||||
|
|
||||||
|
$this->assertNull($lauf->naechste(), 'Danach ist die Warteschlange leer');
|
||||||
|
|
||||||
|
$this->assertEquals([
|
||||||
|
['typ' => 'erfolg', 'meldung' => 'Girobank: 4 neue Umsätze.'],
|
||||||
|
['typ' => 'fehler', 'meldung' => 'Kaputtbank: Abruf fehlgeschlagen: PIN falsch.'],
|
||||||
|
['typ' => 'erfolg', 'meldung' => 'PayPal: Nichts Neues.'],
|
||||||
|
], $lauf->meldungen(), 'Je Bankverbindung eine Meldung, Fehler als Fehler');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function derZustandUeberstehtDieTanPause(): void
|
||||||
|
{
|
||||||
|
$lauf = Sammelabruf::fuer([
|
||||||
|
['id' => 7, 'name' => 'TAN-Bank'],
|
||||||
|
['id' => 8, 'name' => 'Folgebank'],
|
||||||
|
]);
|
||||||
|
$lauf->naechste();
|
||||||
|
|
||||||
|
// Wie in der Sitzung: serialisiert und im nächsten Request zurückgeholt.
|
||||||
|
$weiter = Sammelabruf::aus(unserialize(serialize($lauf->zustand())));
|
||||||
|
|
||||||
|
$this->assert($weiter !== null, 'Der Zustand lässt sich wiederherstellen');
|
||||||
|
$this->assertEquals(7, $weiter->aktuell()['id'] ?? null, 'Die fragende Bank ist noch die laufende');
|
||||||
|
|
||||||
|
$weiter->vermerke(['status' => 'erfolg', 'meldung' => 'Freigegeben.']);
|
||||||
|
$this->assertEquals(8, $weiter->naechste()['id'] ?? null, 'Nach der Freigabe geht es mit der nächsten weiter');
|
||||||
|
$this->assertEquals(['nummer' => 2, 'gesamt' => 2, 'name' => 'Folgebank'], $weiter->stand(),
|
||||||
|
'Der Stand zählt weiter');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ueberspringenUndAbbrechen(): void
|
||||||
|
{
|
||||||
|
$lauf = Sammelabruf::fuer([
|
||||||
|
['id' => 1, 'name' => 'Erste'],
|
||||||
|
['id' => 2, 'name' => 'Zweite'],
|
||||||
|
['id' => 3, 'name' => 'Dritte'],
|
||||||
|
['id' => 4, 'name' => 'Vierte'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$lauf->naechste();
|
||||||
|
$lauf->ueberspringe();
|
||||||
|
$this->assertEquals(2, $lauf->naechste()['id'] ?? null, 'Überspringen macht mit der nächsten weiter');
|
||||||
|
|
||||||
|
$lauf->abbrechen();
|
||||||
|
$this->assertNull($lauf->aktuell(), 'Nach dem Abbruch läuft nichts mehr');
|
||||||
|
$this->assertNull($lauf->naechste(), 'Nach dem Abbruch folgt keine Bank mehr');
|
||||||
|
|
||||||
|
$meldungen = $lauf->meldungen();
|
||||||
|
$this->assertEquals(2, count($meldungen), 'Übersprungene Bank und Abbruch je eine Meldung');
|
||||||
|
$this->assertEquals('fehler', $meldungen[0]['typ'], 'Übersprungen gilt als nicht abgerufen');
|
||||||
|
$this->assert(str_starts_with($meldungen[0]['meldung'], 'Erste: Übersprungen'),
|
||||||
|
'Die Meldung nennt die übersprungene Bank');
|
||||||
|
$this->assertEquals('Abruf abgebrochen – 3 Bankverbindungen nicht abgerufen.',
|
||||||
|
$meldungen[1]['meldung'], 'Der Abbruch zählt die laufende und die offenen Banken');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function kaputterZustandIstKeinLauf(): void
|
||||||
|
{
|
||||||
|
$this->assertNull(Sammelabruf::aus(null), 'Ohne Zustand kein Lauf');
|
||||||
|
$this->assertNull(Sammelabruf::aus(['gesamt' => 3]), 'Ein unvollständiger Zustand ist kein Lauf');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -130,6 +130,29 @@ test.describe('Bankverbindungen', () => {
|
||||||
await expect(page.locator('#konto_id option:checked')).toHaveText('– nicht zugeordnet –')
|
await expect(page.locator('#konto_id option:checked')).toHaveText('– nicht zugeordnet –')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('„Alle Umsätze abrufen“ steht über der Tabelle der Bankverbindungen', async ({ page }) => {
|
||||||
|
const name = `E2E PayPal Sammelabruf ${MARKE}`
|
||||||
|
|
||||||
|
await page.goto('/banking/neu/paypal')
|
||||||
|
await page.fill('#name', name)
|
||||||
|
await page.fill('#api_benutzer', 'e2e_api1.example.org')
|
||||||
|
await page.fill('#api_kennwort', 'e2e-kennwort')
|
||||||
|
await page.fill('#api_signatur', 'e2e-signatur')
|
||||||
|
await page.click('.formular button[type="submit"]')
|
||||||
|
await expect(page).toHaveURL(/\/banking$/)
|
||||||
|
|
||||||
|
// Abgeschickt wird hier nicht: Der Lauf ginge an echte Gegenstellen.
|
||||||
|
const form = page.locator('.seitenaktionen form#alle-abrufen')
|
||||||
|
await expect(form).toHaveAttribute('method', 'post')
|
||||||
|
await expect(form).toHaveAttribute('action', '/banking/alle-abrufen')
|
||||||
|
await expect(form.locator('button[type="submit"]')).toHaveText('Alle Umsätze abrufen')
|
||||||
|
|
||||||
|
// Der Knopf steht über der Tabelle, nicht in einer ihrer Zeilen.
|
||||||
|
const knopf = await form.boundingBox()
|
||||||
|
const tabelle = await page.locator('table.liste').first().boundingBox()
|
||||||
|
expect(knopf.y).toBeLessThan(tabelle.y)
|
||||||
|
})
|
||||||
|
|
||||||
test('Ein PayPal-Zugang kennt kein TAN-Verfahren', async ({ page }) => {
|
test('Ein PayPal-Zugang kennt kein TAN-Verfahren', async ({ page }) => {
|
||||||
const name = `E2E PayPal ohne TAN ${MARKE}`
|
const name = `E2E PayPal ohne TAN ${MARKE}`
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue