Stichtag für den Umsatzabruf
Der Abrufzeitraum kannte bisher nur Spannen. Der Stichtag steht jetzt an zwei Stellen: als Vorgabe der Bankverbindung (neuer Abrufzeitraum "Ab Stichtag" mit Datum in bank_zugaenge.abruf_stichtag) und als einmalige Angabe neben "Umsätze abrufen", die der Vorgabe vorgeht und nichts speichert. 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. "Ab Stichtag" ohne Datum und unlesbare Datumsangaben werden abgewiesen; ein Stichtag in der Zukunft wird auf heute gesetzt, statt den Abruf leer ausgehen zu lassen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
423ce4122d
commit
005df48fed
12 changed files with 376 additions and 20 deletions
29
Changes.md
29
Changes.md
|
|
@ -1,5 +1,34 @@
|
|||
# Ä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)
|
||||
|
||||
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.
|
||||
* **Online-Banking** – FinTS-Abruf mit TAN-Dialog über mehrere Requests, Einrichtung in drei
|
||||
Schritten (Zugangsdaten, TAN-Verfahren, Kontenzuordnung mit Anlegen fehlender Konten),
|
||||
abgerufene Umsätze buchen sich auf Wunsch selbst, Abgleich gegen vorhandene Buchungen,
|
||||
Regeln für die automatische Kategorisierung.
|
||||
Abrufzeitraum als Spanne oder Stichtag – auch für einen einzelnen Abruf –, abgerufene
|
||||
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,
|
||||
Veräußerungsgeschäfte und Erträge; Bestandslisten der Bank lassen sich als CSV
|
||||
ü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
|
||||
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
|
||||
|
||||
* **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
|
||||
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
|
||||
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; }
|
||||
|
||||
/* 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 {
|
||||
padding: 1.25rem 1.5rem 3rem;
|
||||
flex: 1;
|
||||
|
|
|
|||
|
|
@ -363,7 +363,16 @@ class BankingController extends BaseController
|
|||
|
||||
// ── 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
|
||||
{
|
||||
$this->verlangeAnmeldung();
|
||||
|
|
@ -373,7 +382,7 @@ class BankingController extends BaseController
|
|||
// als schon vorhanden, und sie blieben für immer unsichtbar.
|
||||
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']);
|
||||
}
|
||||
|
|
@ -658,6 +667,7 @@ class BankingController extends BaseController
|
|||
'benutzerkennung' => $this->post('benutzerkennung'),
|
||||
'pin' => $this->post('pin'),
|
||||
'abrufzeitraum' => $this->post('abrufzeitraum'),
|
||||
'abruf_stichtag' => $this->post('abruf_stichtag'),
|
||||
'aktiv' => isset($_POST['aktiv']) ? 1 : 0,
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ class Bankzugang
|
|||
'sieben_tage' => 'Letzte 7 Tage',
|
||||
'dreissig_tage' => 'Letzte 30 Tage',
|
||||
'jahr' => 'Letzte 365 Tage',
|
||||
'stichtag' => 'Ab Stichtag',
|
||||
];
|
||||
|
||||
/** @return list<array<string,mixed>> */
|
||||
|
|
@ -74,13 +75,14 @@ class Bankzugang
|
|||
Database::execute(
|
||||
'INSERT INTO bank_zugaenge
|
||||
(benutzer_id, name, bankname, blz, fints_url, benutzerkennung, pin,
|
||||
tan_verfahren_id, tan_verfahren_name, tan_medium, abrufzeitraum, aktiv)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
tan_verfahren_id, tan_verfahren_name, tan_medium, abrufzeitraum,
|
||||
abruf_stichtag, aktiv)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[
|
||||
Mandant::id(), $felder['name'], $felder['bankname'], $felder['blz'],
|
||||
$felder['fints_url'], $felder['benutzerkennung'], $felder['pin'],
|
||||
$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(
|
||||
'UPDATE bank_zugaenge SET name = ?, bankname = ?, blz = ?, fints_url = ?,
|
||||
benutzerkennung = ?, pin = ?, tan_verfahren_id = ?, tan_verfahren_name = ?,
|
||||
tan_medium = ?, abrufzeitraum = ?, aktiv = ?
|
||||
tan_medium = ?, abrufzeitraum = ?, abruf_stichtag = ?, aktiv = ?
|
||||
WHERE id = ? AND benutzer_id = ?',
|
||||
[
|
||||
$felder['name'], $felder['bankname'], $felder['blz'], $felder['fints_url'],
|
||||
$felder['benutzerkennung'], $felder['pin'], $felder['tan_verfahren_id'],
|
||||
$felder['tan_verfahren_name'], $felder['tan_medium'],
|
||||
$felder['abrufzeitraum'], $felder['aktiv'],
|
||||
$felder['abrufzeitraum'], $felder['abruf_stichtag'], $felder['aktiv'],
|
||||
$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
|
||||
* Laufs – bewusst den ganzen Tag und nicht die Uhrzeit: Banken buchen
|
||||
|
|
@ -173,7 +203,8 @@ class Bankzugang
|
|||
public static function startdatum(
|
||||
?string $zeitraum,
|
||||
?string $zuletztAm,
|
||||
?\DateTimeImmutable $heute = null
|
||||
?\DateTimeImmutable $heute = null,
|
||||
?string $stichtag = null
|
||||
): array {
|
||||
$heute = $heute ?? new \DateTimeImmutable('today');
|
||||
$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'],
|
||||
'dreissig_tage' => [$heute->modify('-30 days'), 'letzte 30 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),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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} */
|
||||
private static function seitLetztemAbruf(\DateTimeImmutable $heute, ?string $zuletztAm): array
|
||||
{
|
||||
|
|
@ -223,6 +303,17 @@ class Bankzugang
|
|||
$verfahren = ($verfahren === null || $verfahren === '') ? null : (int)$verfahren;
|
||||
|
||||
$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 [
|
||||
'name' => mb_substr($name, 0, 128),
|
||||
|
|
@ -234,7 +325,8 @@ class Bankzugang
|
|||
'tan_verfahren_id' => $verfahren,
|
||||
'tan_verfahren_name' => mb_substr(trim((string)($daten['tan_verfahren_name'] ?? '')), 0, 128),
|
||||
'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,
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,11 +83,20 @@ class FinTsAbruf
|
|||
* Einrichtungsschritt – dort sollen erst die Konten zugeordnet werden, bevor
|
||||
* 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>,
|
||||
* 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);
|
||||
if ($zugang === null) {
|
||||
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();
|
||||
|
||||
$abruf = new self($zugang, Bankabruf::beginnen($zugangId), [
|
||||
'schritt' => 'anmelden',
|
||||
'modus' => $modus === 'konten' ? 'konten' : 'voll',
|
||||
'einrichtung' => $einrichtung,
|
||||
'stichtag' => $stichtag,
|
||||
'konto_nummer' => 0,
|
||||
'neu' => 0,
|
||||
'uebersprungen' => 0,
|
||||
|
|
@ -253,9 +268,9 @@ class FinTsAbruf
|
|||
private function umsaetzeAbrufen(): array
|
||||
{
|
||||
$konten = $this->abzurufendeKonten();
|
||||
[$von, $beschreibung] = Bankzugang::startdatum(
|
||||
(string)$this->zugang['abrufzeitraum'],
|
||||
$this->zugang['zuletzt_am'] === null ? null : (string)$this->zugang['zuletzt_am']
|
||||
[$von, $beschreibung] = Bankzugang::abrufBeginn(
|
||||
$this->zugang,
|
||||
(string)($this->zustand['stichtag'] ?? '')
|
||||
);
|
||||
|
||||
if ($this->zustand['konto_nummer'] === 0) {
|
||||
|
|
|
|||
|
|
@ -22,8 +22,15 @@ include __DIR__ . '/../layout/kopf.php';
|
|||
<div class="seitenaktionen">
|
||||
<a href="/banking" class="knopf">Zurück zu den Bankverbindungen</a>
|
||||
<?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) ?>">
|
||||
<?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>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
|
|
@ -72,6 +79,15 @@ include __DIR__ . '/../layout/kopf.php';
|
|||
<?php endforeach; ?>
|
||||
</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">
|
||||
<input type="checkbox" name="aktiv" value="1" <?= $neu || (int)$zugang['aktiv'] === 1 ? 'checked' : '' ?>>
|
||||
aktiv
|
||||
|
|
|
|||
|
|
@ -57,15 +57,28 @@ include __DIR__ . '/../layout/kopf.php';
|
|||
<span class="nebentext">BLZ <?= View::h($zugang['blz']) ?></span>
|
||||
<?php endif; ?>
|
||||
</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>
|
||||
<?= $zugang['zuletzt_am'] === null
|
||||
? '<span class="nebentext">noch nie</span>'
|
||||
: View::h(date('d.m.Y H:i', strtotime((string)$zugang['zuletzt_am']))) ?>
|
||||
</td>
|
||||
<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="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>
|
||||
</form>
|
||||
<a href="/banking/<?= (int)$zugang['id'] ?>" class="knopf klein">Bearbeiten</a>
|
||||
|
|
@ -116,4 +129,11 @@ include __DIR__ . '/../layout/kopf.php';
|
|||
übernommen oder verworfen werden.
|
||||
</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'; ?>
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ class BankingTest extends TestCase
|
|||
$this->tanVerfahrenLaesstSichWaehlen();
|
||||
$this->tanVerfahrenUeberstehtDasSpeichern();
|
||||
$this->zeitraumRichtetSichNachDemLetztenAbruf();
|
||||
$this->stichtagBestimmtDenAbrufbeginn();
|
||||
$this->umsaetzeKommenNurEinmalHerein();
|
||||
$this->regelnGreifenNachPrioritaet();
|
||||
$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');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
$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.
|
||||
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
|
||||
// kein Konto und bietet die Abfrage an, statt eine leere Tabelle zu zeigen.
|
||||
await page.click('a:has-text("Konten zuordnen")')
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue