Online-Banking: Einrichtung in zwei Schritten

Die Bank lässt sich erst nach ihren TAN-Verfahren fragen, wenn die
Zugangsdaten gespeichert sind. Das Anlegen führt deshalb über „Weiter“
direkt auf die Verfahrensseite, und die holt die Liste selbst – ein
Knopf davor wäre ein Schritt, dessen Ergebnis feststeht.

Scheitert die Abfrage, bleibt das Ergebnis in der Sitzung, damit nicht
jeder Seitenaufruf wieder in dieselbe Zeitüberschreitung läuft; die ist
für die Abfrage kürzer gesetzt als in der Bibliothek.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
fds2610-bot 2026-08-12 08:39:44 +02:00
parent 4bd7ffed1b
commit 9111eeeb91
8 changed files with 207 additions and 94 deletions

View file

@ -60,9 +60,18 @@ Damit ist der geplante Funktionsumfang beisammen: Von der Kontenübersicht bis z
- Umsätze, die vor einer TAN-Unterbrechung geschrieben wurden, bleiben erhalten. - Umsätze, die vor einer TAN-Unterbrechung geschrieben wurden, bleiben erhalten.
**Wahl des TAN-Verfahrens** (`Services\FinTsVerfahren`, Migration `009_tan_verfahren.sql`) **Wahl des TAN-Verfahrens** (`Services\FinTsVerfahren`, Migration `009_tan_verfahren.sql`)
- Eigene Seite am Zugang (`/banking/{id}/verfahren`): Der Knopf *Verfahren bei der Bank - **Einrichtung in zwei Schritten**: Zugangsdaten erfassen und mit *Weiter* speichern, dann
abfragen* holt die Liste über `getTanModes()` – das kommt ohne Anmeldung aus und verlangt das TAN-Verfahren. Vorher lässt sich die Bank nicht fragen – ohne gespeicherte
selbst keine TAN. TAN-Medien werden nur für die Verfahren geholt, die eines brauchen. Zugangsdaten gibt es keine Verbindung.
- Der zweite Schritt holt die Liste beim Aufruf selbst (`getTanModes()`, ohne Anmeldung und
ohne TAN); ein Knopf davor wäre ein Schritt, dessen Ergebnis feststeht. Ein erneuter
Anlauf steht daneben. TAN-Medien werden nur für die Verfahren geholt, die eines brauchen.
- Das Ergebnis liegt in der Sitzung, auch ein gescheitertes – sonst liefe jeder
Seitenaufruf wieder in dieselbe Zeitüberschreitung. Die sind für die Abfrage kürzer
gesetzt als in der Bibliothek (8/20 statt 15/30 Sekunden), weil sie an einem
Seitenaufruf hängt.
- Scheitert die Abfrage, erklärt die Seite, wo der Fehler meist steckt, und verlinkt zurück
auf die Zugangsdaten; überspringen lässt sich der Schritt ebenfalls.
- Abgefragt statt eingetippt: Die Kennungen sind bankspezifisch, 913 heißt nicht überall - Abgefragt statt eingetippt: Die Kennungen sind bankspezifisch, 913 heißt nicht überall
chipTAN QR. Der Name wird mitgespeichert (`tan_verfahren_name`), damit das Formular ihn chipTAN QR. Der Name wird mitgespeichert (`tan_verfahren_name`), damit das Formular ihn
ohne Bankverbindung zeigen kann. ohne Bankverbindung zeigen kann.

View file

@ -58,22 +58,39 @@ ausdrücklich aus. Sie wird bei jedem Request neu aus der Datenbank geholt und e
fragt die Anwendung auf Knopfdruck nach (`checkDecoupledSubmission`) – steht die Freigabe fragt die Anwendung auf Knopfdruck nach (`checkDecoupledSubmission`) – steht die Freigabe
noch aus, bleibt die Maske stehen. noch aus, bleibt die Maske stehen.
### Das Verfahren wählen ### Einrichtung in zwei Schritten
Fast jede Bank bietet mehrere Verfahren an. Welches gilt, steht am Zugang unter Fast jede Bank bietet mehrere Verfahren an, und welche das sind, weiß nur sie. Fragen lässt
*TAN-Verfahren einstellen* (`/banking/{id}/verfahren`). sie sich aber erst, wenn Bankleitzahl, FinTS-Adresse, Benutzerkennung und PIN vorliegen –
deshalb zerfällt das Anlegen einer Bankverbindung in zwei Schritte:
1. **Zugangsdaten** (`/banking/neu`) – *Weiter* speichert sie.
2. **TAN-Verfahren** (`/banking/{id}/verfahren`) – die Seite fragt die Bank beim Aufruf
selbst und zeigt die Liste zur Wahl.
Der zweite Schritt holt die Liste **ohne Knopfdruck**: Wer hier landet, will genau das, und
ein Knopf, dessen Ergebnis feststeht, wäre ein Schritt zu viel. Ein erneuter Anlauf steht
trotzdem bereit, wenn sich etwas geändert hat.
Später erreicht man dieselbe Seite über *TAN-Verfahren einstellen* am Zugang.
**Abgefragt, nicht eingetippt.** Die Kennungen sind bankspezifisch – 913 heißt nicht **Abgefragt, nicht eingetippt.** Die Kennungen sind bankspezifisch – 913 heißt nicht
überall chipTAN QR –, und niemand soll sie in einer PDF der Bank suchen müssen. Der Knopf überall chipTAN QR –, und niemand soll sie in einer PDF der Bank suchen müssen.
*Verfahren bei der Bank abfragen* holt die Liste über `getTanModes()`. Das kommt ohne `getTanModes()` kommt ohne Anmeldung aus (die Bibliothek braucht dafür nur die
Anmeldung aus (die Bibliothek braucht dafür nur die Bankparameterdaten) und verlangt Bankparameterdaten) und verlangt deshalb selbst keine TAN.
deshalb selbst keine TAN.
`getTanMedia()` wird nur für die Verfahren aufgerufen, die ein Medium brauchen: Jeder `getTanMedia()` wird nur für die Verfahren aufgerufen, die ein Medium brauchen: Jeder
Aufruf ist ein eigener Dialog mit der Bank. Aufruf ist ein eigener Dialog mit der Bank.
Die abgefragte Liste liegt in der Sitzung, nicht in der Datenbank – sie ist eine Die Zeitüberschreitungen sind kürzer gesetzt als in der Bibliothek (8 statt 15 Sekunden zum
Momentaufnahme. Beim Übernehmen wird gegen sie geprüft: Eine Kennung, die die Bank nicht Verbinden, 20 statt 30 zur Antwort): Die Abfrage hängt an einem Seitenaufruf.
Das Ergebnis liegt in der Sitzung, **auch ein gescheitertes** – sonst liefe jeder
Seitenaufruf erneut in dieselbe Zeitüberschreitung. Scheitert die Abfrage, erklärt die
Seite, wo der Fehler meist steckt, und der Weg zurück zu den Zugangsdaten steht daneben.
Die Wahl lässt sich überspringen; die Verbindung ist auch ohne sie benutzbar.
Beim Übernehmen wird gegen die abgefragte Liste geprüft: Eine Kennung, die die Bank nicht
angeboten hat, wäre beim nächsten Abruf wirkungslos. angeboten hat, wäre beim nächsten Abruf wirkungslos.
**Ohne Wahl** nimmt der erste Abruf das erste Verfahren, das die Bank meldet, und hält es **Ohne Wahl** nimmt der erste Abruf das erste Verfahren, das die Bank meldet, und hält es
@ -81,8 +98,7 @@ fest; sonst stünde im Formular nichts. Eine **ausdrückliche** Wahl wird dagege
überschrieben: Bietet die Bank sie einmal nicht an, sagt das Protokoll das und die überschrieben: Bietet die Bank sie einmal nicht an, sagt das Protokoll das und die
Einstellung bleibt stehen. Einstellung bleibt stehen.
Das Verfahren steht bewusst nicht im Zugangsformular – dort ließe es sich nur als Zahl Das Zugangsformular fasst Verfahren und Medium nicht an: Ohne diesen Vorbehalt setzte jedes
eintragen. Das Formular fasst es deshalb auch nicht an: Ohne diesen Vorbehalt setzte jedes
Speichern die Wahl zurück. Speichern die Wahl zurück.
Abruf und Überweisung benutzen dieselbe Wahl (`Services\FinTsVerfahren::anwenden()`) – eine Abruf und Überweisung benutzen dieselbe Wahl (`Services\FinTsVerfahren::anwenden()`) – eine

View file

@ -307,7 +307,10 @@ form.inline { display: inline; margin: 0; }
font-weight: 600; font-weight: 600;
} }
.phasenmarke { /* Kleine Marke: Zustand (Monatsabschluss), Dateiname (Import) oder der Schritt
eines mehrteiligen Dialogs (Einrichtung einer Bankverbindung). */
.phasenmarke,
.schrittmarke {
display: inline-block; display: inline-block;
background: var(--akzent-schwach); background: var(--akzent-schwach);
color: var(--akzent); color: var(--akzent);

View file

@ -70,9 +70,11 @@ class BankingController extends BaseController
return; return;
} }
// Weiter zum zweiten Schritt: Erst jetzt – mit gespeicherten Zugangsdaten –
// lässt sich die Bank nach ihren TAN-Verfahren fragen.
View::redirect( View::redirect(
'/banking/' . $id, '/banking/' . $id . '/verfahren?einrichtung=1',
'Bankverbindung angelegt. Beim ersten Abruf werden die Konten der Bank ermittelt.' 'Zugangsdaten gespeichert. Jetzt das TAN-Verfahren wählen.'
); );
} }
@ -127,9 +129,15 @@ class BankingController extends BaseController
// ── TAN-Verfahren ──────────────────────────────────────────────────────── // ── TAN-Verfahren ────────────────────────────────────────────────────────
/** /**
* Die abgefragte Verfahrensliste. Sie liegt in der Sitzung, damit ein * Der zweite Schritt der Einrichtung: das TAN-Verfahren.
* erneutes Anzeigen der Seite – etwa nach einer Fehlermeldung – nicht jedes *
* Mal wieder die Bank behelligt. * Die Liste wird beim Aufruf **selbst geholt** – erst mit gespeicherten
* Zugangsdaten lässt sich die Bank fragen, und genau dann steht der Nutzer
* hier. Ein eigener Knopf davor wäre ein Schritt, dessen Ergebnis ohnehin
* feststeht.
*
* Das Ergebnis bleibt in der Sitzung, auch ein gescheitertes: Sonst liefe
* jeder Seitenaufruf erneut in dieselbe Zeitüberschreitung.
* *
* @param array<string,string> $params * @param array<string,string> $params
*/ */
@ -144,33 +152,32 @@ class BankingController extends BaseController
return; return;
} }
$abfrage = $this->gemerkteAbfrage($id) ?? $this->verfahrenHolen($id);
$this->render('banking/verfahren.php', [ $this->render('banking/verfahren.php', [
'titel' => 'TAN-Verfahren – ' . $zugang['name'], 'titel' => 'TAN-Verfahren – ' . $zugang['name'],
'zugang' => $zugang, 'zugang' => $zugang,
'medium' => Crypto::entschluessle((string)($zugang['tan_medium'] ?? '')), 'medium' => Crypto::entschluessle((string)($zugang['tan_medium'] ?? '')),
'verfahren' => $this->gemerkteVerfahren($id), 'verfahren' => $abfrage['liste'],
'fehler' => $abfrage['status'] === 'fehler' ? $abfrage['meldung'] : '',
'einrichtung' => $this->query('einrichtung') === '1',
]); ]);
} }
/** @param array<string,string> $params */ /**
* Ein erneuter Anlauf, wenn der erste scheiterte oder sich etwas geändert hat.
*
* @param array<string,string> $params
*/
public function verfahrenAbfragen(array $params): void public function verfahrenAbfragen(array $params): void
{ {
$this->verlangeAnmeldung(); $this->verlangeAnmeldung();
$id = (int)$params['id']; $id = (int)$params['id'];
$ergebnis = FinTsVerfahren::abfragen($id); $abfrage = $this->verfahrenHolen($id);
$ziel = '/banking/' . $id . '/verfahren' . ($this->post('einrichtung') === '1' ? '?einrichtung=1' : '');
Auth::startSession(); View::redirect($ziel, $abfrage['meldung'], $abfrage['status'] === 'fehler' ? 'fehler' : 'erfolg');
$_SESSION[self::SITZUNG_VERFAHREN] = [
'zugang_id' => $id,
'liste' => $ergebnis['verfahren'],
];
View::redirect(
'/banking/' . $id . '/verfahren',
$ergebnis['meldung'],
$ergebnis['status'] === 'fehler' ? 'fehler' : 'erfolg'
);
} }
/** @param array<string,string> $params */ /** @param array<string,string> $params */
@ -180,6 +187,7 @@ class BankingController extends BaseController
$id = (int)$params['id']; $id = (int)$params['id'];
$verfahren = (int)$this->post('verfahren_id'); $verfahren = (int)$this->post('verfahren_id');
$abfrage = $this->gemerkteAbfrage($id);
try { try {
FinTsVerfahren::uebernehmen( FinTsVerfahren::uebernehmen(
@ -188,31 +196,61 @@ class BankingController extends BaseController
// Das Medium des angekreuzten Verfahrens, nicht das irgendeiner // Das Medium des angekreuzten Verfahrens, nicht das irgendeiner
// anderen Zeile. // anderen Zeile.
$this->post('tan_medium_' . $verfahren), $this->post('tan_medium_' . $verfahren),
$this->gemerkteVerfahren($id) $abfrage === null ? [] : $abfrage['liste']
); );
} catch (\Throwable $e) { } catch (\Throwable $e) {
View::redirect('/banking/' . $id . '/verfahren', $e->getMessage(), 'fehler'); View::redirect('/banking/' . $id . '/verfahren', $e->getMessage(), 'fehler');
return; return;
} }
View::redirect('/banking/' . $id, 'TAN-Verfahren übernommen.'); View::redirect(
'/banking/' . $id,
$this->post('einrichtung') === '1'
? 'Bankverbindung eingerichtet. Der erste Abruf ermittelt jetzt die Konten.'
: 'TAN-Verfahren übernommen.'
);
}
/**
* Fragt die Bank und merkt sich das Ergebnis für diese Sitzung.
*
* @return array{status: string, meldung: string, liste: list<array<string,mixed>>}
*/
private function verfahrenHolen(int $zugangId): array
{
$ergebnis = FinTsVerfahren::abfragen($zugangId);
$abfrage = [
'status' => $ergebnis['status'],
'meldung' => $ergebnis['meldung'],
'liste' => $ergebnis['verfahren'],
];
Auth::startSession();
$_SESSION[self::SITZUNG_VERFAHREN] = ['zugang_id' => $zugangId] + $abfrage;
return $abfrage;
} }
/** /**
* Die zuletzt abgefragte Liste – aber nur die zu diesem Zugang. * Die zuletzt abgefragte Liste – aber nur die zu diesem Zugang.
* *
* @return list<array<string,mixed>> * @return array{status: string, meldung: string, liste: list<array<string,mixed>>}|null
*/ */
private function gemerkteVerfahren(int $zugangId): array private function gemerkteAbfrage(int $zugangId): ?array
{ {
Auth::startSession(); Auth::startSession();
$gemerkt = $_SESSION[self::SITZUNG_VERFAHREN] ?? null; $gemerkt = $_SESSION[self::SITZUNG_VERFAHREN] ?? null;
if (!is_array($gemerkt) || (int)($gemerkt['zugang_id'] ?? 0) !== $zugangId) { if (!is_array($gemerkt) || (int)($gemerkt['zugang_id'] ?? 0) !== $zugangId) {
return []; return null;
} }
return is_array($gemerkt['liste'] ?? null) ? $gemerkt['liste'] : []; return [
'status' => (string)($gemerkt['status'] ?? 'fehler'),
'meldung' => (string)($gemerkt['meldung'] ?? ''),
'liste' => is_array($gemerkt['liste'] ?? null) ? $gemerkt['liste'] : [],
];
} }
// ── Abruf ──────────────────────────────────────────────────────────────── // ── Abruf ────────────────────────────────────────────────────────────────

View file

@ -278,6 +278,12 @@ class FinTsVerfahren
$optionen->productName = Einstellung::get('banking_produkt_id') ?: self::PRODUKT; $optionen->productName = Einstellung::get('banking_produkt_id') ?: self::PRODUKT;
$optionen->productVersion = self::PRODUKT_VERSION; $optionen->productVersion = self::PRODUKT_VERSION;
// Kürzer als die Vorgabe der Bibliothek (15/30): Die Abfrage hängt an
// einem Seitenaufruf, und eine Bank, die nicht antwortet, soll die Seite
// nicht minutenlang aufhalten.
$optionen->timeoutConnect = 8;
$optionen->timeoutResponse = 20;
$fints = FinTs::new( $fints = FinTs::new(
$optionen, $optionen,
Credentials::create((string)$zugang['benutzerkennung'], (string)$zugang['pin']) Credentials::create((string)$zugang['benutzerkennung'], (string)$zugang['pin'])

View file

@ -16,6 +16,10 @@ $wert = static fn(string $feld, string $standard = ''): string => (string)($zu
include __DIR__ . '/../layout/kopf.php'; include __DIR__ . '/../layout/kopf.php';
?> ?>
<?php if ($neu): ?>
<p class="schrittmarke">Schritt 1 von 2 · Zugangsdaten</p>
<?php endif; ?>
<div class="seitenaktionen"> <div class="seitenaktionen">
<a href="/banking" class="knopf">Zurück zu den Bankverbindungen</a> <a href="/banking" class="knopf">Zurück zu den Bankverbindungen</a>
<?php if (!$neu): ?> <?php if (!$neu): ?>
@ -95,9 +99,7 @@ include __DIR__ . '/../layout/kopf.php';
Die Wahl steht auf einer eigenen Seite, weil dafür die Bank gefragt werden muss: Die Wahl steht auf einer eigenen Seite, weil dafür die Bank gefragt werden muss:
<a href="/banking/<?= (int)$zugang['id'] ?>/verfahren">TAN-Verfahren einstellen</a>. <a href="/banking/<?= (int)$zugang['id'] ?>/verfahren">TAN-Verfahren einstellen</a>.
</p> </p>
<?php endif; ?>
<?php if (!$neu): ?>
<h3>Konten bei dieser Bank</h3> <h3>Konten bei dieser Bank</h3>
<?php if ($konten === []): ?> <?php if ($konten === []): ?>
<p class="platzhalter"> <p class="platzhalter">
@ -153,8 +155,15 @@ include __DIR__ . '/../layout/kopf.php';
<?php endif; ?> <?php endif; ?>
<?php endif; ?> <?php endif; ?>
<?php if ($neu): ?>
<p class="feldhinweis">
Nach dem Speichern fragt der Finanzmanager die Bank, welche TAN-Verfahren sie für
diese Benutzerkennung anbietet – ohne gespeicherte Zugangsdaten geht das nicht.
</p>
<?php endif; ?>
<div class="knopfleiste"> <div class="knopfleiste">
<button type="submit" class="knopf primaer"><?= $neu ? 'Anlegen' : 'Speichern' ?></button> <button type="submit" class="knopf primaer"><?= $neu ? 'Weiter' : 'Speichern' ?></button>
</div> </div>
</form> </form>

View file

@ -3,74 +3,81 @@
use Finanzmanager\View; use Finanzmanager\View;
/** /**
* Die Wahl des TAN-Verfahrens für einen Bankzugang. * Schritt 2 der Einrichtung: das TAN-Verfahren.
*
* Die Liste hat der Controller schon geholt – hier steht nur noch die Wahl.
* *
* @var array<string,mixed> $zugang * @var array<string,mixed> $zugang
* @var string $medium Das gespeicherte TAN-Medium im Klartext * @var string $medium Das gespeicherte TAN-Medium im Klartext
* @var list<array{id:int, name:string, entkoppelt:bool, medium_noetig:bool, * @var list<array{id:int, name:string, entkoppelt:bool, medium_noetig:bool,
* medien:list<string>, hinweis:string}> $verfahren * medien:list<string>, hinweis:string}> $verfahren
* @var string $fehler Meldung, wenn die Abfrage scheiterte
* @var bool $einrichtung Teil des Einrichtungsablaufs?
* @var string $csrfToken * @var string $csrfToken
*/ */
$gewaehlt = $zugang['tan_verfahren_id'] === null ? null : (int)$zugang['tan_verfahren_id']; $gewaehlt = $zugang['tan_verfahren_id'] === null ? null : (int)$zugang['tan_verfahren_id'];
$zugangId = (int)$zugang['id'];
include __DIR__ . '/../layout/kopf.php'; include __DIR__ . '/../layout/kopf.php';
?> ?>
<?php if ($einrichtung): ?>
<p class="schrittmarke">Schritt 2 von 2 · TAN-Verfahren</p>
<?php endif; ?>
<div class="seitenaktionen"> <div class="seitenaktionen">
<a href="/banking/<?= (int)$zugang['id'] ?>" class="knopf">Zurück zur Bankverbindung</a> <a href="/banking/<?= $zugangId ?>" class="knopf">
<form method="post" action="/banking/<?= (int)$zugang['id'] ?>/verfahren/abfragen" class="inline"> <?= $einrichtung ? 'Später einstellen' : 'Zurück zur Bankverbindung' ?>
</a>
<form method="post" action="/banking/<?= $zugangId ?>/verfahren/abfragen" class="inline">
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>"> <input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
<button type="submit" class="knopf primaer"> <input type="hidden" name="einrichtung" value="<?= $einrichtung ? '1' : '0' ?>">
<?= $verfahren === [] ? 'Verfahren bei der Bank abfragen' : 'Erneut abfragen' ?> <button type="submit" class="knopf">Erneut bei der Bank abfragen</button>
</button>
</form> </form>
</div> </div>
<?php if ($fehler !== ''): ?>
<p class="meldung meldung-fehler" role="status">
<?= View::h($fehler) ?>
</p>
<p class="feldhinweis"> <p class="feldhinweis">
Welche Verfahren zur Verfügung stehen, weiß nur die Bank – die Kennungen sind Meist stimmt eine Angabe im ersten Schritt nicht – Bankleitzahl, FinTS-Adresse,
von Haus zu Haus verschieden. Die Abfrage kommt ohne Anmeldung aus und verlangt Benutzerkennung oder PIN.
deshalb selbst keine TAN. <a href="/banking/<?= $zugangId ?>">Zugangsdaten prüfen</a>.
</p> Die Wahl lässt sich auch später nachholen; ohne sie nimmt der erste Abruf das erste
Verfahren, das die Bank meldet.
<h3>Zurzeit eingestellt</h3>
<?php if ($gewaehlt === null): ?>
<p class="platzhalter">
Noch nichts gewählt. Beim nächsten Abruf nimmt der Finanzmanager das erste
Verfahren, das die Bank meldet, und merkt es sich – das muss nicht das sein,
mit dem du sonst arbeitest.
</p>
<?php else: ?>
<p>
<strong>
<?= View::h(($zugang['tan_verfahren_name'] ?? '') !== ''
? $zugang['tan_verfahren_name'] : 'Verfahren ' . $gewaehlt) ?>
</strong>
<span class="nebentext">Kennung <?= $gewaehlt ?></span>
</p> </p>
<?php endif; ?> <?php endif; ?>
<?php if ($verfahren === []): ?> <?php if ($verfahren === []): ?>
<p class="platzhalter"> <?php if ($fehler === ''): ?>
Noch keine Liste abgefragt. Der Knopf oben holt sie – dafür müssen <p class="platzhalter">Die Bank hat kein Verfahren genannt.</p>
Bankleitzahl, FinTS-Adresse, Benutzerkennung und PIN gespeichert sein. <?php endif; ?>
</p>
<?php else: ?> <?php else: ?>
<h3>Von der Bank angeboten</h3> <p class="feldhinweis">
<form method="post" action="/banking/<?= (int)$zugang['id'] ?>/verfahren" class="formular breit"> Welche Verfahren zur Verfügung stehen, weiß nur die Bank – die Kennungen sind von
Haus zu Haus verschieden. Zu wählen ist das, mit dem du sonst arbeitest.
</p>
<form method="post" action="/banking/<?= $zugangId ?>/verfahren" class="formular breit">
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>"> <input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
<input type="hidden" name="einrichtung" value="<?= $einrichtung ? '1' : '0' ?>">
<table class="liste"> <table class="liste">
<thead> <thead>
<tr><th>Wahl</th><th>Verfahren</th><th>Kennung</th><th>TAN-Medium</th></tr> <tr><th>Wahl</th><th>Verfahren</th><th>Kennung</th><th>TAN-Medium</th></tr>
</thead> </thead>
<tbody> <tbody>
<?php foreach ($verfahren as $eintrag): ?> <?php foreach ($verfahren as $nummer => $eintrag): ?>
<tr> <tr>
<td> <td>
<?php /* Ohne Vorwahl bliebe das Formular leer abschickbar; die
erste Zeile ist die naheliegende Voreinstellung. */ ?>
<input type="radio" name="verfahren_id" id="verfahren-<?= (int)$eintrag['id'] ?>" <input type="radio" name="verfahren_id" id="verfahren-<?= (int)$eintrag['id'] ?>"
value="<?= (int)$eintrag['id'] ?>" value="<?= (int)$eintrag['id'] ?>"
<?= $gewaehlt === (int)$eintrag['id'] ? 'checked' : '' ?>> <?= $gewaehlt === (int)$eintrag['id'] || ($gewaehlt === null && $nummer === 0)
? 'checked' : '' ?>>
</td> </td>
<td> <td>
<label for="verfahren-<?= (int)$eintrag['id'] ?>"> <label for="verfahren-<?= (int)$eintrag['id'] ?>">
@ -107,9 +114,30 @@ include __DIR__ . '/../layout/kopf.php';
</table> </table>
<div class="knopfleiste"> <div class="knopfleiste">
<button type="submit" class="knopf primaer">Verfahren übernehmen</button> <button type="submit" class="knopf primaer">
<?= $einrichtung ? 'Einrichtung abschließen' : 'Verfahren übernehmen' ?>
</button>
</div> </div>
</form> </form>
<?php endif; ?> <?php endif; ?>
<?php if (!$einrichtung): ?>
<h3>Zurzeit eingestellt</h3>
<?php if ($gewaehlt === null): ?>
<p class="platzhalter">
Noch nichts gewählt. Beim nächsten Abruf nimmt der Finanzmanager das erste
Verfahren, das die Bank meldet, und merkt es sich.
</p>
<?php else: ?>
<p>
<strong>
<?= View::h(($zugang['tan_verfahren_name'] ?? '') !== ''
? $zugang['tan_verfahren_name'] : 'Verfahren ' . $gewaehlt) ?>
</strong>
<span class="nebentext">Kennung <?= $gewaehlt ?><?= $medium !== ''
? ', Medium ' . View::h($medium) : '' ?></span>
</p>
<?php endif; ?>
<?php endif; ?>
<?php include __DIR__ . '/../layout/fuss.php'; ?> <?php include __DIR__ . '/../layout/fuss.php'; ?>

View file

@ -46,11 +46,11 @@ test.describe('Module der Phasen 4 bis 7', () => {
['/verwaltung', 'Verwaltung'], ['/verwaltung', 'Verwaltung'],
] ]
// Jeder Bereich hat sein Modul: Die Überschrift gehört zum Modul, nicht
// zur früheren Platzhalterseite.
for (const [pfad, titel] of punkte) { for (const [pfad, titel] of punkte) {
await page.goto(pfad) await page.goto(pfad)
await expect(page.locator('h1')).toContainText(titel) await expect(page.locator('h1')).toContainText(titel)
// Kein Platzhalter mehr: Alle Bereiche haben ihr Modul.
await expect(page.locator('.phasenmarke')).toHaveCount(0)
} }
}) })
@ -94,24 +94,28 @@ test.describe('Module der Phasen 4 bis 7', () => {
await page.fill('#fints_url', 'https://banking.example.org/fints') await page.fill('#fints_url', 'https://banking.example.org/fints')
await page.fill('#benutzerkennung', 'e2e-kunde') await page.fill('#benutzerkennung', 'e2e-kunde')
await page.fill('#pin', 'geheim') await page.fill('#pin', 'geheim')
await page.click('button:has-text("Anlegen")')
await expect(page.locator('.meldung')).toContainText('angelegt') // Schritt 1 speichert nur die Zugangsdaten.
await expect(page.locator('.schrittmarke')).toContainText('Schritt 1 von 2')
await page.click('button:has-text("Weiter")')
// Schritt 2 fragt die Bank selbst nach ihren TAN-Verfahren. Die Adresse
// im Test führt nirgendwohin – geprüft wird deshalb der Fehlerweg: Die
// Seite erklärt, was zu tun ist, statt stehen zu bleiben.
await expect(page.locator('.schrittmarke')).toContainText('Schritt 2 von 2')
await expect(page.locator('h1')).toContainText('E2E-Testbank')
await expect(page.locator('.meldung-fehler')).toBeVisible()
await expect(page.locator('a:has-text("Zugangsdaten prüfen")')).toHaveCount(1)
await page.click('a:has-text("Später einstellen")')
await expect(page.locator('h1')).toContainText('E2E-Testbank') await expect(page.locator('h1')).toContainText('E2E-Testbank')
// Die PIN wird nie wieder angezeigt. // Die PIN wird nie wieder angezeigt.
await expect(page.locator('#pin')).toHaveValue('') await expect(page.locator('#pin')).toHaveValue('')
await expect(page.locator('#pin')).toHaveAttribute('placeholder', /unverändert/) await expect(page.locator('#pin')).toHaveAttribute('placeholder', /unverändert/)
// Das TAN-Verfahren steht auf einer eigenen Seite. Ohne Abfrage bei der // Ohne Wahl bleibt der Hinweis stehen, und die Seite ist wieder erreichbar.
// Bank ist dort nichts gewählt – abgefragt wird hier nicht, das bräuchte await expect(page.locator('a:has-text("TAN-Verfahren einstellen")')).toHaveCount(1)
// eine echte Bankverbindung.
await page.click('a:has-text("TAN-Verfahren einstellen")')
await expect(page.locator('h1')).toContainText('TAN-Verfahren')
await expect(page.locator('.platzhalter').first()).toContainText('Noch nichts gewählt')
await expect(page.locator('button:has-text("Verfahren bei der Bank abfragen")')).toHaveCount(1)
await page.click('a:has-text("Zurück zur Bankverbindung")')
page.once('dialog', dialog => dialog.accept()) page.once('dialog', dialog => dialog.accept())
await page.click('button:has-text("Bankverbindung löschen")') await page.click('button:has-text("Bankverbindung löschen")')