Merge pull request 'Stichtag für den Umsatzabruf' (#13) from fds2610-bot/Finanzmanager:main into main
Reviewed-on: https://codeberg.org/fds2610/Finanzmanager/pulls/13
This commit is contained in:
commit
dfbc9a9b8e
12 changed files with 376 additions and 20 deletions
29
Changes.md
29
Changes.md
|
|
@ -1,5 +1,34 @@
|
||||||
# Änderungen
|
# Änderungen
|
||||||
|
|
||||||
|
## Stichtag für den Umsatzabruf (13.08.2026)
|
||||||
|
|
||||||
|
Der Abrufzeitraum kannte bisher nur Spannen: seit dem letzten Abruf, 7, 30 oder 365 Tage.
|
||||||
|
Wer einen Bestand ab einem festen Datum aufbauen wollte – etwa ab dem Wechsel vom Lexware
|
||||||
|
FinanzManager –, hatte dafür keine Angabe. Der Stichtag steht jetzt an zwei Stellen.
|
||||||
|
|
||||||
|
**Vorgabe der Bankverbindung** (`Models\Bankzugang`, `Views/banking/formular.php`,
|
||||||
|
`migrations/011_abruf_stichtag.sql`)
|
||||||
|
- Neuer Abrufzeitraum *Ab Stichtag* und das dazugehörige Datumsfeld im Zugangsformular
|
||||||
|
(neue Spalte `bank_zugaenge.abruf_stichtag`). Gewählt, wird jeder Abruf ab diesem Tag
|
||||||
|
geholt.
|
||||||
|
- *Ab Stichtag* ohne Datum wird abgewiesen – eine Vorgabe, die nichts vorgibt, führte beim
|
||||||
|
Abruf nur zu einem anderen Zeitraum als gedacht. Ein unlesbares Datum ebenso.
|
||||||
|
- Die Übersicht der Bankverbindungen zeigt den Stichtag neben dem Zeitraum.
|
||||||
|
|
||||||
|
**Stichtag für einen einzelnen Abruf** (`Services\FinTsAbruf`, `BankingController`,
|
||||||
|
`Views/banking/index.php`)
|
||||||
|
- Neben *Umsätze abrufen* steht ein Datumsfeld. Ausgefüllt geht es dem eingestellten
|
||||||
|
Abrufzeitraum vor und gilt **nur für diesen Lauf**: Wer einmalig weiter zurückgreifen
|
||||||
|
will, muss die Einstellung des Zugangs nicht ändern und hinterher zurückstellen.
|
||||||
|
- Der Stichtag liegt im Abrufzustand, nicht in der Datenbank – nach einer TAN-Runde läuft
|
||||||
|
der Abruf im nächsten Request weiter und holt dort denselben Zeitraum wie beim Start.
|
||||||
|
- Ein Stichtag in der Zukunft wird auf heute gesetzt, statt den Abruf leer ausgehen zu
|
||||||
|
lassen; das Protokoll vermerkt es.
|
||||||
|
|
||||||
|
Angenommen werden das Format des Datumsfelds (`2026-01-01`) und die deutsche Schreibweise
|
||||||
|
(`01.01.2026`); alles andere gilt als Fehleingabe. Wie weit die Bank zurückreicht,
|
||||||
|
entscheidet sie selbst – 90 Tage sind üblich, mehr liefert nicht jede.
|
||||||
|
|
||||||
## Termine und Verträge wandern unter die Verwaltung (13.08.2026)
|
## Termine und Verträge wandern unter die Verwaltung (13.08.2026)
|
||||||
|
|
||||||
Die Seitenleiste war lang geworden. *Termine* und *Verträge* sind kein täglicher Einstieg –
|
Die Seitenleiste war lang geworden. *Termine* und *Verträge* sind kein täglicher Einstieg –
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,9 @@ 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),
|
||||||
abgerufene Umsätze buchen sich auf Wunsch selbst, Abgleich gegen vorhandene Buchungen,
|
Abrufzeitraum als Spanne oder Stichtag – auch für einen einzelnen Abruf –, abgerufene
|
||||||
Regeln für die automatische Kategorisierung.
|
Umsätze buchen sich auf Wunsch selbst, Abgleich gegen vorhandene Buchungen, Regeln für
|
||||||
|
die automatische Kategorisierung.
|
||||||
* **Wertpapiere** – Depotbestände mit Einstand und Bewertung, Depotbuchungen, Kursabruf,
|
* **Wertpapiere** – Depotbestände mit Einstand und Bewertung, Depotbuchungen, Kursabruf,
|
||||||
Veräußerungsgeschäfte und Erträge; Bestandslisten der Bank lassen sich als CSV
|
Veräußerungsgeschäfte und Erträge; Bestandslisten der Bank lassen sich als CSV
|
||||||
übernehmen.
|
übernehmen.
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,41 @@ Platz, und ein Request, der fünf Minuten auf eine TAN wartet, blockiert einen W
|
||||||
Was in der Sitzung liegt, enthält keine PIN – die Bibliothek schließt sie beim Persistieren
|
Was in der Sitzung liegt, enthält keine PIN – die Bibliothek schließt sie beim Persistieren
|
||||||
ausdrücklich aus. Sie wird bei jedem Request neu aus der Datenbank geholt und entschlüsselt.
|
ausdrücklich aus. Sie wird bei jedem Request neu aus der Datenbank geholt und entschlüsselt.
|
||||||
|
|
||||||
|
### Wie weit zurück: Zeitraum und Stichtag
|
||||||
|
|
||||||
|
Ab wann abgerufen wird, bestimmt `Bankzugang::abrufBeginn()`. Zwei Angaben spielen hinein:
|
||||||
|
|
||||||
|
* Der **Abrufzeitraum** der Bankverbindung (`bank_zugaenge.abrufzeitraum`): seit dem letzten
|
||||||
|
Abruf, letzte 7/30/365 Tage – oder *Ab Stichtag* mit dem Datum aus
|
||||||
|
`bank_zugaenge.abruf_stichtag`.
|
||||||
|
* Ein **Stichtag für diesen einen Abruf**, angegeben im Datumsfeld neben *Umsätze abrufen*.
|
||||||
|
Er geht der Vorgabe vor und wird **nirgends gespeichert**.
|
||||||
|
|
||||||
|
Beides nebeneinander, weil beides gebraucht wird: Die Vorgabe beantwortet den Alltag („immer
|
||||||
|
seit dem letzten Abruf“), der einmalige Stichtag den Sonderfall – einen Bestand ab einem
|
||||||
|
festen Datum aufbauen, eine Lücke nachholen. Ohne ihn müsste man die Einstellung des Zugangs
|
||||||
|
ändern und hinterher zurückstellen, mit dem üblichen Ausgang, dass man das Zurückstellen
|
||||||
|
vergisst.
|
||||||
|
|
||||||
|
Der einmalige Stichtag liegt im **Abrufzustand**, nicht in der Datenbank: Nach einer
|
||||||
|
TAN-Runde läuft der Abruf im nächsten Request weiter und muss dort denselben Zeitraum holen
|
||||||
|
wie beim Start.
|
||||||
|
|
||||||
|
„Seit dem letzten Abruf“ nimmt den **Tag** des letzten Laufs, nicht die Uhrzeit: Banken
|
||||||
|
buchen nachträglich, und ein Umsatz, der schon da war, fällt ohnehin über seinen
|
||||||
|
Fingerabdruck heraus. Beim allerersten Abruf gilt ein Jahr zurück – sonst käme entweder
|
||||||
|
nichts oder alles.
|
||||||
|
|
||||||
|
Ein Stichtag **in der Zukunft** wird auf heute gesetzt statt abgewiesen: Aus der Zukunft
|
||||||
|
liefert keine Bank etwas, und ein verrutschtes Datum soll den Abruf nicht ergebnislos machen
|
||||||
|
– das Protokoll vermerkt die Korrektur. *Ab Stichtag* **ohne** Datum lässt sich dagegen gar
|
||||||
|
nicht erst speichern; eine Vorgabe, die nichts vorgibt, führte beim Abruf nur zu einem
|
||||||
|
anderen Zeitraum als gedacht.
|
||||||
|
|
||||||
|
Wie weit die Bank tatsächlich zurückreicht, entscheidet sie selbst – 90 Tage sind üblich,
|
||||||
|
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.
|
||||||
|
|
||||||
### 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
|
||||||
|
|
@ -244,7 +279,9 @@ der Bibliothek an das eigene Log hängt. PIN und TAN maskieren beide Seiten.
|
||||||
|
|
||||||
`tests/16_BankingTest.php` prüft alles um den Dialog herum: Verschlüsselung (Kennung, kein
|
`tests/16_BankingTest.php` prüft alles um den Dialog herum: Verschlüsselung (Kennung, kein
|
||||||
Klartext, wechselnder Geheimtext, kein Verschachteln), PIN bleibt beim Speichern erhalten,
|
Klartext, wechselnder Geheimtext, kein Verschachteln), PIN bleibt beim Speichern erhalten,
|
||||||
Abrufzeiträume, Dublettenerkennung, Regeln nach Priorität und Feld, Splitschema mit
|
Abrufzeiträume samt Stichtag (gespeicherte Vorgabe, einmalige Angabe geht ihr vor und lässt
|
||||||
|
die Vorgabe stehen, Zukunft wird auf heute gesetzt, unlesbare Datumsangaben werden
|
||||||
|
abgewiesen), Dublettenerkennung, Regeln nach Priorität und Feld, Splitschema mit
|
||||||
Restausgleich, Abweisung ungültiger Ausdrücke, Abgleich gegen vorhandene Buchungen samt
|
Restausgleich, Abweisung ungültiger Ausdrücke, Abgleich gegen vorhandene Buchungen samt
|
||||||
Toleranz sowie Übernehmen, Verwerfen und Zurücksetzen.
|
Toleranz sowie Übernehmen, Verwerfen und Zurücksetzen.
|
||||||
|
|
||||||
|
|
|
||||||
10
migrations/011_abruf_stichtag.sql
Normal file
10
migrations/011_abruf_stichtag.sql
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
-- Stichtag für den Umsatzabruf.
|
||||||
|
--
|
||||||
|
-- Bisher konnte der Abrufzeitraum nur relativ gewählt werden (letzter Abruf,
|
||||||
|
-- 7/30/365 Tage). Wer einen Bestand ab einem festen Datum aufbauen will – etwa
|
||||||
|
-- ab dem Wechsel vom Lexware FinanzManager –, braucht ein Datum statt einer
|
||||||
|
-- Spanne. Der Wert ist die Vorgabe des Zugangs; beim einzelnen Abruf lässt sich
|
||||||
|
-- ein abweichender Stichtag angeben, der nicht gespeichert wird.
|
||||||
|
|
||||||
|
ALTER TABLE bank_zugaenge
|
||||||
|
ADD COLUMN IF NOT EXISTS abruf_stichtag DATE NULL AFTER abrufzeitraum;
|
||||||
|
|
@ -219,6 +219,32 @@ code {
|
||||||
|
|
||||||
form.inline { display: inline; margin: 0; }
|
form.inline { display: inline; margin: 0; }
|
||||||
|
|
||||||
|
/* Der Abrufknopf mit dem Stichtagsfeld davor: Beide gehören zusammen und sollen
|
||||||
|
auf einer Zeile bleiben – auch in der rechtsbündigen Aktionsspalte. */
|
||||||
|
.abrufform {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stichtagwahl {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .35rem;
|
||||||
|
font-size: .88rem;
|
||||||
|
color: var(--text-gedaempft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stichtagfeld {
|
||||||
|
padding: .25rem .4rem;
|
||||||
|
border: 1px solid var(--rand);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--flaeche);
|
||||||
|
color: var(--text);
|
||||||
|
font: inherit;
|
||||||
|
font-size: .85rem;
|
||||||
|
}
|
||||||
|
|
||||||
.inhalt {
|
.inhalt {
|
||||||
padding: 1.25rem 1.5rem 3rem;
|
padding: 1.25rem 1.5rem 3rem;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
|
||||||
|
|
@ -363,7 +363,16 @@ class BankingController extends BaseController
|
||||||
|
|
||||||
// ── Abruf ────────────────────────────────────────────────────────────────
|
// ── Abruf ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/** @param array<string,string> $params */
|
/**
|
||||||
|
* Umsätze abrufen.
|
||||||
|
*
|
||||||
|
* Das Stichtagsfeld neben dem Knopf gilt **nur für diesen Lauf**: Wer
|
||||||
|
* einmalig weiter zurückgreifen will, soll dafür nicht die Einstellung des
|
||||||
|
* Zugangs ändern und hinterher zurückstellen müssen. Bleibt es leer, gilt
|
||||||
|
* der Abrufzeitraum der Bankverbindung.
|
||||||
|
*
|
||||||
|
* @param array<string,string> $params
|
||||||
|
*/
|
||||||
public function abrufen(array $params): void
|
public function abrufen(array $params): void
|
||||||
{
|
{
|
||||||
$this->verlangeAnmeldung();
|
$this->verlangeAnmeldung();
|
||||||
|
|
@ -373,7 +382,7 @@ 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();
|
||||||
|
|
||||||
$ergebnis = FinTsAbruf::starten((int)$params['id']);
|
$ergebnis = FinTsAbruf::starten((int)$params['id'], 'voll', false, $this->post('stichtag'));
|
||||||
|
|
||||||
$this->zeigeAbruf($ergebnis, (int)$params['id']);
|
$this->zeigeAbruf($ergebnis, (int)$params['id']);
|
||||||
}
|
}
|
||||||
|
|
@ -658,6 +667,7 @@ class BankingController extends BaseController
|
||||||
'benutzerkennung' => $this->post('benutzerkennung'),
|
'benutzerkennung' => $this->post('benutzerkennung'),
|
||||||
'pin' => $this->post('pin'),
|
'pin' => $this->post('pin'),
|
||||||
'abrufzeitraum' => $this->post('abrufzeitraum'),
|
'abrufzeitraum' => $this->post('abrufzeitraum'),
|
||||||
|
'abruf_stichtag' => $this->post('abruf_stichtag'),
|
||||||
'aktiv' => isset($_POST['aktiv']) ? 1 : 0,
|
'aktiv' => isset($_POST['aktiv']) ? 1 : 0,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ class Bankzugang
|
||||||
'sieben_tage' => 'Letzte 7 Tage',
|
'sieben_tage' => 'Letzte 7 Tage',
|
||||||
'dreissig_tage' => 'Letzte 30 Tage',
|
'dreissig_tage' => 'Letzte 30 Tage',
|
||||||
'jahr' => 'Letzte 365 Tage',
|
'jahr' => 'Letzte 365 Tage',
|
||||||
|
'stichtag' => 'Ab Stichtag',
|
||||||
];
|
];
|
||||||
|
|
||||||
/** @return list<array<string,mixed>> */
|
/** @return list<array<string,mixed>> */
|
||||||
|
|
@ -74,13 +75,14 @@ class Bankzugang
|
||||||
Database::execute(
|
Database::execute(
|
||||||
'INSERT INTO bank_zugaenge
|
'INSERT INTO bank_zugaenge
|
||||||
(benutzer_id, name, bankname, blz, fints_url, benutzerkennung, pin,
|
(benutzer_id, name, bankname, blz, fints_url, benutzerkennung, pin,
|
||||||
tan_verfahren_id, tan_verfahren_name, tan_medium, abrufzeitraum, aktiv)
|
tan_verfahren_id, tan_verfahren_name, tan_medium, abrufzeitraum,
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
abruf_stichtag, aktiv)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||||
[
|
[
|
||||||
Mandant::id(), $felder['name'], $felder['bankname'], $felder['blz'],
|
Mandant::id(), $felder['name'], $felder['bankname'], $felder['blz'],
|
||||||
$felder['fints_url'], $felder['benutzerkennung'], $felder['pin'],
|
$felder['fints_url'], $felder['benutzerkennung'], $felder['pin'],
|
||||||
$felder['tan_verfahren_id'], $felder['tan_verfahren_name'], $felder['tan_medium'],
|
$felder['tan_verfahren_id'], $felder['tan_verfahren_name'], $felder['tan_medium'],
|
||||||
$felder['abrufzeitraum'], $felder['aktiv'],
|
$felder['abrufzeitraum'], $felder['abruf_stichtag'], $felder['aktiv'],
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -117,13 +119,13 @@ class Bankzugang
|
||||||
Database::execute(
|
Database::execute(
|
||||||
'UPDATE bank_zugaenge SET name = ?, bankname = ?, blz = ?, fints_url = ?,
|
'UPDATE bank_zugaenge SET name = ?, bankname = ?, blz = ?, fints_url = ?,
|
||||||
benutzerkennung = ?, pin = ?, tan_verfahren_id = ?, tan_verfahren_name = ?,
|
benutzerkennung = ?, pin = ?, tan_verfahren_id = ?, tan_verfahren_name = ?,
|
||||||
tan_medium = ?, abrufzeitraum = ?, aktiv = ?
|
tan_medium = ?, abrufzeitraum = ?, abruf_stichtag = ?, aktiv = ?
|
||||||
WHERE id = ? AND benutzer_id = ?',
|
WHERE id = ? AND benutzer_id = ?',
|
||||||
[
|
[
|
||||||
$felder['name'], $felder['bankname'], $felder['blz'], $felder['fints_url'],
|
$felder['name'], $felder['bankname'], $felder['blz'], $felder['fints_url'],
|
||||||
$felder['benutzerkennung'], $felder['pin'], $felder['tan_verfahren_id'],
|
$felder['benutzerkennung'], $felder['pin'], $felder['tan_verfahren_id'],
|
||||||
$felder['tan_verfahren_name'], $felder['tan_medium'],
|
$felder['tan_verfahren_name'], $felder['tan_medium'],
|
||||||
$felder['abrufzeitraum'], $felder['aktiv'],
|
$felder['abrufzeitraum'], $felder['abruf_stichtag'], $felder['aktiv'],
|
||||||
$id, Mandant::id(),
|
$id, Mandant::id(),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
@ -162,6 +164,34 @@ class Bankzugang
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ab wann für diesen Zugang abgerufen wird.
|
||||||
|
*
|
||||||
|
* Ein **beim Abruf angegebener Stichtag** geht der gespeicherten Vorgabe vor
|
||||||
|
* und wird nirgends festgehalten: Er gilt für diesen einen Lauf – etwa um
|
||||||
|
* einmalig weiter zurückzugreifen, ohne die Einstellung des Zugangs zu
|
||||||
|
* ändern.
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $zugang
|
||||||
|
* @return array{0: \DateTimeImmutable, 1: string} Startdatum und Beschreibung
|
||||||
|
*/
|
||||||
|
public static function abrufBeginn(
|
||||||
|
array $zugang,
|
||||||
|
?string $stichtag = null,
|
||||||
|
?\DateTimeImmutable $heute = null
|
||||||
|
): array {
|
||||||
|
if (trim((string)$stichtag) !== '') {
|
||||||
|
return self::startdatum('stichtag', null, $heute, $stichtag);
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::startdatum(
|
||||||
|
(string)($zugang['abrufzeitraum'] ?? ''),
|
||||||
|
($zugang['zuletzt_am'] ?? null) === null ? null : (string)$zugang['zuletzt_am'],
|
||||||
|
$heute,
|
||||||
|
($zugang['abruf_stichtag'] ?? null) === null ? null : (string)$zugang['abruf_stichtag']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ab wann abgerufen wird. „Seit dem letzten Abruf“ nimmt den Tag des letzten
|
* Ab wann abgerufen wird. „Seit dem letzten Abruf“ nimmt den Tag des letzten
|
||||||
* Laufs – bewusst den ganzen Tag und nicht die Uhrzeit: Banken buchen
|
* Laufs – bewusst den ganzen Tag und nicht die Uhrzeit: Banken buchen
|
||||||
|
|
@ -173,7 +203,8 @@ class Bankzugang
|
||||||
public static function startdatum(
|
public static function startdatum(
|
||||||
?string $zeitraum,
|
?string $zeitraum,
|
||||||
?string $zuletztAm,
|
?string $zuletztAm,
|
||||||
?\DateTimeImmutable $heute = null
|
?\DateTimeImmutable $heute = null,
|
||||||
|
?string $stichtag = null
|
||||||
): array {
|
): array {
|
||||||
$heute = $heute ?? new \DateTimeImmutable('today');
|
$heute = $heute ?? new \DateTimeImmutable('today');
|
||||||
$zeitraum = array_key_exists((string)$zeitraum, self::ZEITRAEUME) ? (string)$zeitraum : 'letzter_abruf';
|
$zeitraum = array_key_exists((string)$zeitraum, self::ZEITRAEUME) ? (string)$zeitraum : 'letzter_abruf';
|
||||||
|
|
@ -182,10 +213,59 @@ class Bankzugang
|
||||||
'sieben_tage' => [$heute->modify('-7 days'), 'letzte 7 Tage'],
|
'sieben_tage' => [$heute->modify('-7 days'), 'letzte 7 Tage'],
|
||||||
'dreissig_tage' => [$heute->modify('-30 days'), 'letzte 30 Tage'],
|
'dreissig_tage' => [$heute->modify('-30 days'), 'letzte 30 Tage'],
|
||||||
'jahr' => [$heute->modify('-365 days'), 'letzte 365 Tage'],
|
'jahr' => [$heute->modify('-365 days'), 'letzte 365 Tage'],
|
||||||
|
// Ohne brauchbares Datum wäre „Ab Stichtag“ eine Angabe ohne Inhalt –
|
||||||
|
// dann gilt dieselbe Regel wie ohne Vorgabe.
|
||||||
|
'stichtag' => self::abStichtag($heute, $stichtag) ?? self::seitLetztemAbruf($heute, $zuletztAm),
|
||||||
default => self::seitLetztemAbruf($heute, $zuletztAm),
|
default => self::seitLetztemAbruf($heute, $zuletztAm),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ein Stichtag als Datum – oder null, wenn sich keines daraus lesen lässt.
|
||||||
|
*
|
||||||
|
* Angenommen werden das Format des Datumsfelds (`2026-08-13`) und die
|
||||||
|
* deutsche Schreibweise (`13.08.2026`); alles andere gilt als Fehleingabe
|
||||||
|
* statt als „irgendein Tag“.
|
||||||
|
*/
|
||||||
|
public static function stichtagDatum(?string $roh): ?\DateTimeImmutable
|
||||||
|
{
|
||||||
|
$roh = trim((string)$roh);
|
||||||
|
if ($roh === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (['Y-m-d', 'd.m.Y'] as $format) {
|
||||||
|
$datum = \DateTimeImmutable::createFromFormat('!' . $format, $roh);
|
||||||
|
$fehler = \DateTimeImmutable::getLastErrors();
|
||||||
|
|
||||||
|
// `createFromFormat` biegt Unsinn wie den 31.02. still zurecht – die
|
||||||
|
// Warnungen sind das Einzige, woran sich das erkennen lässt.
|
||||||
|
if ($datum !== false
|
||||||
|
&& ($fehler === false || ($fehler['warning_count'] === 0 && $fehler['error_count'] === 0))) {
|
||||||
|
return $datum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array{0: \DateTimeImmutable, 1: string}|null */
|
||||||
|
private static function abStichtag(\DateTimeImmutable $heute, ?string $stichtag): ?array
|
||||||
|
{
|
||||||
|
$datum = self::stichtagDatum($stichtag);
|
||||||
|
if ($datum === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aus der Zukunft liefert keine Bank etwas; ein verrutschter Stichtag
|
||||||
|
// soll den Abruf nicht ergebnislos machen.
|
||||||
|
if ($datum > $heute) {
|
||||||
|
return [$heute, 'ab Stichtag ' . $heute->format('d.m.Y') . ' (in der Zukunft, auf heute gesetzt)'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$datum, 'ab Stichtag ' . $datum->format('d.m.Y')];
|
||||||
|
}
|
||||||
|
|
||||||
/** @return array{0: \DateTimeImmutable, 1: string} */
|
/** @return array{0: \DateTimeImmutable, 1: string} */
|
||||||
private static function seitLetztemAbruf(\DateTimeImmutable $heute, ?string $zuletztAm): array
|
private static function seitLetztemAbruf(\DateTimeImmutable $heute, ?string $zuletztAm): array
|
||||||
{
|
{
|
||||||
|
|
@ -223,6 +303,17 @@ class Bankzugang
|
||||||
$verfahren = ($verfahren === null || $verfahren === '') ? null : (int)$verfahren;
|
$verfahren = ($verfahren === null || $verfahren === '') ? null : (int)$verfahren;
|
||||||
|
|
||||||
$zeitraum = (string)($daten['abrufzeitraum'] ?? 'letzter_abruf');
|
$zeitraum = (string)($daten['abrufzeitraum'] ?? 'letzter_abruf');
|
||||||
|
$zeitraum = array_key_exists($zeitraum, self::ZEITRAEUME) ? $zeitraum : 'letzter_abruf';
|
||||||
|
|
||||||
|
// Ein leeres Datumsfeld ist kein Fehler; „Ab Stichtag“ ohne Datum schon –
|
||||||
|
// sonst stünde eine Vorgabe da, die nichts vorgibt.
|
||||||
|
$stichtag = self::stichtagDatum((string)($daten['abruf_stichtag'] ?? ''));
|
||||||
|
if ($stichtag === null && trim((string)($daten['abruf_stichtag'] ?? '')) !== '') {
|
||||||
|
throw new \InvalidArgumentException('Der Stichtag ist kein gültiges Datum.');
|
||||||
|
}
|
||||||
|
if ($stichtag === null && $zeitraum === 'stichtag') {
|
||||||
|
throw new \InvalidArgumentException('Für „Ab Stichtag“ fehlt das Datum.');
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'name' => mb_substr($name, 0, 128),
|
'name' => mb_substr($name, 0, 128),
|
||||||
|
|
@ -234,7 +325,8 @@ class Bankzugang
|
||||||
'tan_verfahren_id' => $verfahren,
|
'tan_verfahren_id' => $verfahren,
|
||||||
'tan_verfahren_name' => mb_substr(trim((string)($daten['tan_verfahren_name'] ?? '')), 0, 128),
|
'tan_verfahren_name' => mb_substr(trim((string)($daten['tan_verfahren_name'] ?? '')), 0, 128),
|
||||||
'tan_medium' => Crypto::verschluessle(trim((string)($daten['tan_medium'] ?? ''))),
|
'tan_medium' => Crypto::verschluessle(trim((string)($daten['tan_medium'] ?? ''))),
|
||||||
'abrufzeitraum' => array_key_exists($zeitraum, self::ZEITRAEUME) ? $zeitraum : 'letzter_abruf',
|
'abrufzeitraum' => $zeitraum,
|
||||||
|
'abruf_stichtag' => $stichtag?->format('Y-m-d'),
|
||||||
'aktiv' => !empty($daten['aktiv']) ? 1 : 0,
|
'aktiv' => !empty($daten['aktiv']) ? 1 : 0,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -83,11 +83,20 @@ class FinTsAbruf
|
||||||
* Einrichtungsschritt – dort sollen erst die Konten zugeordnet werden, bevor
|
* Einrichtungsschritt – dort sollen erst die Konten zugeordnet werden, bevor
|
||||||
* Umsätze hereinkommen, die sonst ohne Konto dastünden.
|
* Umsätze hereinkommen, die sonst ohne Konto dastünden.
|
||||||
*
|
*
|
||||||
|
* `$stichtag` gilt nur für diesen Lauf und geht dem Abrufzeitraum des Zugangs
|
||||||
|
* vor. Er liegt im Zustand, nicht in der Datenbank: Nach einer TAN-Runde
|
||||||
|
* läuft der Abruf im nächsten Request weiter und muss dort denselben Zeitraum
|
||||||
|
* holen wie beim Start.
|
||||||
|
*
|
||||||
* @return array{status: string, meldung: string, protokoll: list<string>,
|
* @return array{status: string, meldung: string, protokoll: list<string>,
|
||||||
* neu: int, uebersprungen: int, tan?: array<string,mixed>}
|
* neu: int, uebersprungen: int, tan?: array<string,mixed>}
|
||||||
*/
|
*/
|
||||||
public static function starten(int $zugangId, string $modus = 'voll', bool $einrichtung = false): array
|
public static function starten(
|
||||||
{
|
int $zugangId,
|
||||||
|
string $modus = 'voll',
|
||||||
|
bool $einrichtung = false,
|
||||||
|
?string $stichtag = null
|
||||||
|
): array {
|
||||||
$zugang = Bankzugang::zugangsdaten($zugangId);
|
$zugang = Bankzugang::zugangsdaten($zugangId);
|
||||||
if ($zugang === null) {
|
if ($zugang === null) {
|
||||||
return self::ergebnis('fehler', 'Den Bankzugang gibt es nicht.', []);
|
return self::ergebnis('fehler', 'Den Bankzugang gibt es nicht.', []);
|
||||||
|
|
@ -100,12 +109,18 @@ class FinTsAbruf
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$stichtag = trim((string)$stichtag);
|
||||||
|
if ($stichtag !== '' && Bankzugang::stichtagDatum($stichtag) === null) {
|
||||||
|
return self::ergebnis('fehler', 'Der Stichtag ist kein gültiges Datum.', []);
|
||||||
|
}
|
||||||
|
|
||||||
self::vergessen();
|
self::vergessen();
|
||||||
|
|
||||||
$abruf = new self($zugang, Bankabruf::beginnen($zugangId), [
|
$abruf = new self($zugang, Bankabruf::beginnen($zugangId), [
|
||||||
'schritt' => 'anmelden',
|
'schritt' => 'anmelden',
|
||||||
'modus' => $modus === 'konten' ? 'konten' : 'voll',
|
'modus' => $modus === 'konten' ? 'konten' : 'voll',
|
||||||
'einrichtung' => $einrichtung,
|
'einrichtung' => $einrichtung,
|
||||||
|
'stichtag' => $stichtag,
|
||||||
'konto_nummer' => 0,
|
'konto_nummer' => 0,
|
||||||
'neu' => 0,
|
'neu' => 0,
|
||||||
'uebersprungen' => 0,
|
'uebersprungen' => 0,
|
||||||
|
|
@ -253,9 +268,9 @@ class FinTsAbruf
|
||||||
private function umsaetzeAbrufen(): array
|
private function umsaetzeAbrufen(): array
|
||||||
{
|
{
|
||||||
$konten = $this->abzurufendeKonten();
|
$konten = $this->abzurufendeKonten();
|
||||||
[$von, $beschreibung] = Bankzugang::startdatum(
|
[$von, $beschreibung] = Bankzugang::abrufBeginn(
|
||||||
(string)$this->zugang['abrufzeitraum'],
|
$this->zugang,
|
||||||
$this->zugang['zuletzt_am'] === null ? null : (string)$this->zugang['zuletzt_am']
|
(string)($this->zustand['stichtag'] ?? '')
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($this->zustand['konto_nummer'] === 0) {
|
if ($this->zustand['konto_nummer'] === 0) {
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,15 @@ include __DIR__ . '/../layout/kopf.php';
|
||||||
<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): ?>
|
||||||
<form method="post" action="/banking/<?= (int)$zugang['id'] ?>/abrufen" class="inline">
|
<form method="post" action="/banking/<?= (int)$zugang['id'] ?>/abrufen" class="inline abrufform">
|
||||||
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
|
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
|
||||||
|
<?php /* Nur für diesen Lauf – die Vorgabe des Zugangs steht unten im
|
||||||
|
Formular und bleibt davon unberührt. */ ?>
|
||||||
|
<label class="stichtagwahl">
|
||||||
|
Stichtag
|
||||||
|
<input type="date" name="stichtag" class="stichtagfeld" max="<?= date('Y-m-d') ?>"
|
||||||
|
title="Ab diesem Tag abrufen – leer: eingestellter Abrufzeitraum">
|
||||||
|
</label>
|
||||||
<button type="submit" class="knopf primaer">Umsätze abrufen</button>
|
<button type="submit" class="knopf primaer">Umsätze abrufen</button>
|
||||||
</form>
|
</form>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
@ -72,6 +79,15 @@ include __DIR__ . '/../layout/kopf.php';
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<label for="abruf_stichtag">Stichtag</label>
|
||||||
|
<input type="date" id="abruf_stichtag" name="abruf_stichtag"
|
||||||
|
value="<?= View::h($wert('abruf_stichtag')) ?>" max="<?= date('Y-m-d') ?>">
|
||||||
|
<p class="feldhinweis">
|
||||||
|
Gilt, wenn oben <em>Ab Stichtag</em> gewählt ist: Abgerufen wird dann jedes Mal ab
|
||||||
|
diesem Tag. Für einen einzelnen Abruf lässt sich oben neben <em>Umsätze abrufen</em>
|
||||||
|
ein abweichender Stichtag angeben, ohne diese Einstellung zu ändern.
|
||||||
|
</p>
|
||||||
|
|
||||||
<label class="ankreuzfeld">
|
<label class="ankreuzfeld">
|
||||||
<input type="checkbox" name="aktiv" value="1" <?= $neu || (int)$zugang['aktiv'] === 1 ? 'checked' : '' ?>>
|
<input type="checkbox" name="aktiv" value="1" <?= $neu || (int)$zugang['aktiv'] === 1 ? 'checked' : '' ?>>
|
||||||
aktiv
|
aktiv
|
||||||
|
|
|
||||||
|
|
@ -57,15 +57,28 @@ include __DIR__ . '/../layout/kopf.php';
|
||||||
<span class="nebentext">BLZ <?= View::h($zugang['blz']) ?></span>
|
<span class="nebentext">BLZ <?= View::h($zugang['blz']) ?></span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
<td><?= View::h(Bankzugang::ZEITRAEUME[$zugang['abrufzeitraum']] ?? $zugang['abrufzeitraum']) ?></td>
|
<td>
|
||||||
|
<?= View::h(Bankzugang::ZEITRAEUME[$zugang['abrufzeitraum']] ?? $zugang['abrufzeitraum']) ?>
|
||||||
|
<?php if ($zugang['abrufzeitraum'] === 'stichtag' && $zugang['abruf_stichtag'] !== null): ?>
|
||||||
|
<span class="nebentext">
|
||||||
|
<?= View::h(date('d.m.Y', strtotime((string)$zugang['abruf_stichtag']))) ?>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<?= $zugang['zuletzt_am'] === null
|
<?= $zugang['zuletzt_am'] === null
|
||||||
? '<span class="nebentext">noch nie</span>'
|
? '<span class="nebentext">noch nie</span>'
|
||||||
: View::h(date('d.m.Y H:i', strtotime((string)$zugang['zuletzt_am']))) ?>
|
: View::h(date('d.m.Y H:i', strtotime((string)$zugang['zuletzt_am']))) ?>
|
||||||
</td>
|
</td>
|
||||||
<td class="rechts nowrap">
|
<td class="rechts nowrap">
|
||||||
<form method="post" action="/banking/<?= (int)$zugang['id'] ?>/abrufen" class="inline">
|
<?php /* Der Stichtag gilt nur für diesen Lauf; leer heißt: der
|
||||||
|
eingestellte Abrufzeitraum der Verbindung. */ ?>
|
||||||
|
<form method="post" action="/banking/<?= (int)$zugang['id'] ?>/abrufen" class="inline abrufform">
|
||||||
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
|
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
|
||||||
|
<input type="date" name="stichtag" class="stichtagfeld"
|
||||||
|
max="<?= date('Y-m-d') ?>"
|
||||||
|
aria-label="Stichtag für diesen Abruf"
|
||||||
|
title="Ab diesem Tag abrufen – leer: eingestellter Abrufzeitraum">
|
||||||
<button type="submit" class="knopf klein primaer">Umsätze abrufen</button>
|
<button type="submit" class="knopf klein primaer">Umsätze abrufen</button>
|
||||||
</form>
|
</form>
|
||||||
<a href="/banking/<?= (int)$zugang['id'] ?>" class="knopf klein">Bearbeiten</a>
|
<a href="/banking/<?= (int)$zugang['id'] ?>" class="knopf klein">Bearbeiten</a>
|
||||||
|
|
@ -116,4 +129,11 @@ include __DIR__ . '/../layout/kopf.php';
|
||||||
übernommen oder verworfen werden.
|
übernommen oder verworfen werden.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p class="hinweis">
|
||||||
|
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
|
||||||
|
ändern. Leer gelassen gilt der Zeitraum der Bankverbindung. Wie weit die Bank
|
||||||
|
zurückreicht, entscheidet sie selbst – 90 Tage sind üblich, mehr liefert nicht jede.
|
||||||
|
</p>
|
||||||
|
|
||||||
<?php include __DIR__ . '/../layout/fuss.php'; ?>
|
<?php include __DIR__ . '/../layout/fuss.php'; ?>
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ class BankingTest extends TestCase
|
||||||
$this->tanVerfahrenLaesstSichWaehlen();
|
$this->tanVerfahrenLaesstSichWaehlen();
|
||||||
$this->tanVerfahrenUeberstehtDasSpeichern();
|
$this->tanVerfahrenUeberstehtDasSpeichern();
|
||||||
$this->zeitraumRichtetSichNachDemLetztenAbruf();
|
$this->zeitraumRichtetSichNachDemLetztenAbruf();
|
||||||
|
$this->stichtagBestimmtDenAbrufbeginn();
|
||||||
$this->umsaetzeKommenNurEinmalHerein();
|
$this->umsaetzeKommenNurEinmalHerein();
|
||||||
$this->regelnGreifenNachPrioritaet();
|
$this->regelnGreifenNachPrioritaet();
|
||||||
$this->regelnRechnenSplitsAus();
|
$this->regelnRechnenSplitsAus();
|
||||||
|
|
@ -296,6 +297,90 @@ class BankingTest extends TestCase
|
||||||
$this->assertEquals('2025-08-11', $von->format('Y-m-d'), 'Unbekannte Vorgabe fällt zurück');
|
$this->assertEquals('2025-08-11', $von->format('Y-m-d'), 'Unbekannte Vorgabe fällt zurück');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Der Stichtag: als Vorgabe des Zugangs und als einmalige Angabe beim Abruf.
|
||||||
|
*
|
||||||
|
* Die einmalige Angabe darf die gespeicherte Einstellung weder verwenden noch
|
||||||
|
* verändern – sonst wäre der nächste reguläre Abruf ein anderer als gedacht.
|
||||||
|
*/
|
||||||
|
private function stichtagBestimmtDenAbrufbeginn(): void
|
||||||
|
{
|
||||||
|
$heute = new \DateTimeImmutable('2026-08-11');
|
||||||
|
|
||||||
|
[$von, $text] = Bankzugang::startdatum('stichtag', null, $heute, '2026-01-01');
|
||||||
|
$this->assertEquals('2026-01-01', $von->format('Y-m-d'), 'Ab dem Stichtag');
|
||||||
|
$this->assert(str_contains($text, '01.01.2026'), 'Und das Protokoll sagt es');
|
||||||
|
|
||||||
|
// Aus der Zukunft liefert keine Bank etwas.
|
||||||
|
[$von] = Bankzugang::startdatum('stichtag', null, $heute, '2027-03-01');
|
||||||
|
$this->assertEquals('2026-08-11', $von->format('Y-m-d'), 'Ein Stichtag in der Zukunft wird auf heute gesetzt');
|
||||||
|
|
||||||
|
// Ohne brauchbares Datum bleibt nur die Regel ohne Vorgabe.
|
||||||
|
[$von] = Bankzugang::startdatum('stichtag', '2026-08-09 22:15:00', $heute, '');
|
||||||
|
$this->assertEquals('2026-08-09', $von->format('Y-m-d'), 'Ohne Datum wie „seit dem letzten Abruf“');
|
||||||
|
|
||||||
|
$this->assertNull(Bankzugang::stichtagDatum('31.02.2026'), 'Den 31. Februar gibt es nicht');
|
||||||
|
$this->assertNull(Bankzugang::stichtagDatum('irgendwann'), 'Text ist kein Datum');
|
||||||
|
$this->assertEquals(
|
||||||
|
'2026-08-13',
|
||||||
|
Bankzugang::stichtagDatum('13.08.2026')?->format('Y-m-d'),
|
||||||
|
'Auch die deutsche Schreibweise wird gelesen'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Die Vorgabe des Zugangs …
|
||||||
|
Bankzugang::speichern($this->zugang, [
|
||||||
|
'name' => 'Testbank',
|
||||||
|
'blz' => '12030000',
|
||||||
|
'fints_url' => 'https://banking.example.org/fints',
|
||||||
|
'benutzerkennung' => 'testkunde',
|
||||||
|
'pin' => '',
|
||||||
|
'abrufzeitraum' => 'stichtag',
|
||||||
|
'abruf_stichtag' => '2026-01-01',
|
||||||
|
'aktiv' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$zugang = Bankzugang::find($this->zugang);
|
||||||
|
$this->assertEquals('2026-01-01', (string)$zugang['abruf_stichtag'], 'Der Stichtag wurde gespeichert');
|
||||||
|
|
||||||
|
[$von] = Bankzugang::abrufBeginn($zugang, null, $heute);
|
||||||
|
$this->assertEquals('2026-01-01', $von->format('Y-m-d'), 'Der gespeicherte Stichtag gilt');
|
||||||
|
|
||||||
|
// … und die einmalige Angabe, die ihr vorgeht.
|
||||||
|
[$von] = Bankzugang::abrufBeginn($zugang, '2026-07-01', $heute);
|
||||||
|
$this->assertEquals('2026-07-01', $von->format('Y-m-d'), 'Der Stichtag des Laufs geht vor');
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
'2026-01-01',
|
||||||
|
(string)Bankzugang::find($this->zugang)['abruf_stichtag'],
|
||||||
|
'Und lässt die gespeicherte Vorgabe stehen'
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assert(
|
||||||
|
$this->wirftFehler(fn() => Bankzugang::speichern($this->zugang, [
|
||||||
|
'name' => 'Testbank',
|
||||||
|
'blz' => '12030000',
|
||||||
|
'fints_url' => 'https://banking.example.org/fints',
|
||||||
|
'benutzerkennung' => 'testkunde',
|
||||||
|
'pin' => '',
|
||||||
|
'abrufzeitraum' => 'stichtag',
|
||||||
|
'abruf_stichtag' => '',
|
||||||
|
'aktiv' => 1,
|
||||||
|
])),
|
||||||
|
'„Ab Stichtag“ ohne Datum wird abgewiesen'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Zurück auf die Vorgabe – die folgenden Prüfungen rechnen damit.
|
||||||
|
Bankzugang::speichern($this->zugang, [
|
||||||
|
'name' => 'Testbank',
|
||||||
|
'blz' => '12030000',
|
||||||
|
'fints_url' => 'https://banking.example.org/fints',
|
||||||
|
'benutzerkennung' => 'testkunde',
|
||||||
|
'pin' => '',
|
||||||
|
'abrufzeitraum' => 'letzter_abruf',
|
||||||
|
'aktiv' => 1,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
private function umsaetzeKommenNurEinmalHerein(): void
|
private function umsaetzeKommenNurEinmalHerein(): void
|
||||||
{
|
{
|
||||||
$erste = $this->umsatzAnlegen([
|
$erste = $this->umsatzAnlegen([
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,21 @@ test.describe('Module der Phasen 4 bis 7', () => {
|
||||||
// Ohne Wahl bleibt der Hinweis stehen, und die Seite ist wieder erreichbar.
|
// Ohne Wahl bleibt der Hinweis stehen, und die Seite ist wieder erreichbar.
|
||||||
await expect(page.locator('a:has-text("TAN-Verfahren einstellen")')).toHaveCount(1)
|
await expect(page.locator('a:has-text("TAN-Verfahren einstellen")')).toHaveCount(1)
|
||||||
|
|
||||||
|
// Der Stichtag gibt es zweimal: als Vorgabe des Zugangs …
|
||||||
|
await page.selectOption('#abrufzeitraum', 'stichtag')
|
||||||
|
await page.fill('#abruf_stichtag', '2026-01-01')
|
||||||
|
await page.click('button:has-text("Speichern")')
|
||||||
|
await expect(page.locator('.meldung')).toContainText('gespeichert')
|
||||||
|
await expect(page.locator('#abruf_stichtag')).toHaveValue('2026-01-01')
|
||||||
|
|
||||||
|
// … und als einmalige Angabe neben dem Abrufknopf, die nichts speichert.
|
||||||
|
await expect(page.locator('.abrufform input[name="stichtag"]')).toHaveCount(1)
|
||||||
|
|
||||||
|
await page.goto('/banking')
|
||||||
|
await expect(page.locator('tr:has-text("E2E-Testbank")')).toContainText('Ab Stichtag')
|
||||||
|
await expect(page.locator('tr:has-text("E2E-Testbank")')).toContainText('01.01.2026')
|
||||||
|
await page.click('td a:has-text("E2E-Testbank")')
|
||||||
|
|
||||||
// Schritt 3: die Kontenzuordnung. Ohne erreichbare Bank kennt sie noch
|
// Schritt 3: die Kontenzuordnung. Ohne erreichbare Bank kennt sie noch
|
||||||
// kein Konto und bietet die Abfrage an, statt eine leere Tabelle zu zeigen.
|
// kein Konto und bietet die Abfrage an, statt eine leere Tabelle zu zeigen.
|
||||||
await page.click('a:has-text("Konten zuordnen")')
|
await page.click('a:has-text("Konten zuordnen")')
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue