Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
892ce81
fix(fints): accept POST on the bank-access pages again
lukas-staab Aug 11, 2026
42e2d8c
fix(fints): stop mangling the PIN, the TAN and the free-text labels
lukas-staab Aug 11, 2026
779c505
fix(fints): stop convertToCent() flipping the sign of negative amounts
lukas-staab Aug 11, 2026
9e78219
fix(fints): never re-import silently when the resume point is not found
lukas-staab Aug 11, 2026
e83f024
fix(fints): send an expired session back to the login instead of erro…
lukas-staab Aug 11, 2026
3eb77ed
fix(fints): catch the bank responses that ended in an error page
lukas-staab Aug 11, 2026
927f0c1
fix(fints): only resume a statement request for the account it was ma…
lukas-staab Aug 11, 2026
1918ff9
fix(fints): report the installed version to the bank, not always "-dev"
lukas-staab Aug 11, 2026
fb3085e
fix(fints): validate an account before registering it for synchronisa…
lukas-staab Aug 11, 2026
803d31d
chore(fints): remove the dead Hibiscus connector and the broken REST …
lukas-staab Aug 11, 2026
84698c8
fix(fints): verify the CSRF nonce on the bank-access actions
lukas-staab Aug 11, 2026
fb45df5
style(fints): apply rector to the new FinTS conversion test
lukas-staab Aug 11, 2026
08f68be
chore: bump version to 4.4.4
lukas-staab Aug 11, 2026
cf38557
fix(fints): work through the 4.4.4 review feedback
lukas-staab Aug 11, 2026
5d81891
Fix: changelog
DieMichii Aug 11, 2026
0e90269
feat(konto): explain what the account IBAN is used for
lukas-staab Aug 11, 2026
e7c6025
fix(logging): rotate the application log instead of growing one file
lukas-staab Aug 11, 2026
78521dc
chore(dependencies): update composer.lock to latest package versions
lukas-staab Aug 11, 2026
fdb7769
feat(fints): sync the FinTS bank list instead of maintaining konto_ba…
lukas-staab Aug 14, 2026
d202f7a
feat(konto): drop the Kasse wording when a bank access hands the acco…
lukas-staab Aug 14, 2026
f7e868b
fix(validation): stop the "name" message naming the project everywhere
lukas-staab Aug 14, 2026
dfaa1d3
fix(fints): refuse a PIN/TAN endpoint that is not HTTPS
lukas-staab Aug 14, 2026
d3f0345
fix(legacy): close every output buffer a broken-off page left open
lukas-staab Aug 14, 2026
75a66b8
feat(fints): let a bank access be deleted again
lukas-staab Aug 14, 2026
ad6e7c6
chore(session): recommend encrypting the session, which holds the ban…
lukas-staab Aug 14, 2026
49c3128
docs(changelog): record the 4.4.4 additions
lukas-staab Aug 14, 2026
57fad9c
fix(fints): stop preselecting the first bank when creating an access
lukas-staab Aug 14, 2026
6f0bea4
fix(fints): stop restarting a statement import that already got its TAN
lukas-staab Aug 14, 2026
fde7da9
feat(fints): support decoupled TAN confirmation from the banking app
lukas-staab Aug 14, 2026
2c63b67
fix(fints): name the account in the breadcrumb during a TAN prompt
lukas-staab Aug 14, 2026
01e9bfa
fix(fints): name the account on the TAN pages themselves
lukas-staab Aug 14, 2026
35b9c94
fix(booking): download the booking archive instead of erroring
lukas-staab Aug 23, 2026
2015b68
fix(lang): fill the three empty translations that fail the CI check
lukas-staab Aug 23, 2026
fc5e0e5
fix(export): hand the legacy CSV downloads over as a response too
lukas-staab Aug 23, 2026
e929d0f
feat(booking): offer the DATEV export under the booking history
lukas-staab Aug 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/feature-changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
# v4.4.4
* Bankimport (FinTS): Das Absenden der Formulare auf den Seiten des Bankzugangs führte zu einer Fehlerseite – betroffen waren das Anlegen eines Zugangs, die Auswahl des TAN-Verfahrens und jede TAN-Eingabe. Die Seiten nehmen ihre eigenen Formulare nun wieder an.
* Bankimport (FinTS): Die eingegebene Online-Banking-PIN wurde vor dem Login verändert – alle Sonderzeichen und Umlaute wurden stillschweigend entfernt. Wer eine PIN mit Sonderzeichen nutzt, konnte sich dadurch nicht anmelden und riskierte nach drei Versuchen die Sperrung des Bankzugangs. Die PIN wird nun unverändert übernommen. Bei der TAN werden weiterhin nur Leerzeichen entfernt, damit sie sich wie aufgedruckt („123 456“) eingeben lässt.
* Bankimport (FinTS): In der Bezeichnung eines Bankzugangs und eines Kontos gingen Ziffern und Leerzeichen verloren („Konto 2024“ wurde zu „Konto“). Beide Bezeichnungen werden nun so gespeichert, wie sie eingegeben wurden.
* Bankimport (FinTS): Beim Abholen der Kontoauszüge konnten bereits importierte Umsätze ein zweites Mal angelegt werden – ohne Hinweis, mit der Erfolgsmeldung „N Einträge importiert“. Ursache war, dass der Anknüpfungspunkt an die bereits vorhandenen Umsätze über die SEPA-Referenz gesucht wurde, die bei vielen Banken leer bleibt. Der Import erkennt die bekannten Umsätze nun zuverlässig und bricht mit einer klaren Meldung ab, wenn er den Anknüpfungspunkt nicht findet, anstatt Buchungen zu verdoppeln.
* Bankimport (FinTS): Wurde ein Umsatzabruf durch die TAN-Abfrage unterbrochen und danach ein anderes Konto geöffnet, konnten die Umsätze des ursprünglich abgefragten Kontos dem falschen Konto zugeordnet werden. Ein unterbrochener Abruf wird nun nur noch für genau das Konto und den Zeitraum fortgesetzt, für den er begonnen wurde.
* Bankimport (FinTS): Eine von der Bank abgelehnte TAN führte zu einer Fehlerseite, sodass der Vorgang abgebrochen war. Nun erscheint der Hinweis „TAN nicht akzeptiert“ und die Eingabe kann wiederholt werden. Ebenso führen gestörte Antworten der Bank beim Abrufen der TAN-Verfahren, der TAN-Medien und beim Abmelden nicht mehr zu einer Fehlerseite. Wird ein Freigabe-Verfahren ohne TAN-Eingabe genutzt (z. B. pushTAN-Freigabe in der Banking-App), wird nun verständlich gemeldet, dass StuFiS dieses Verfahren noch nicht unterstützt.
* Bankimport (FinTS): War die Sitzung abgelaufen, endete jeder Klick auf den Seiten des Bankzugangs – etwa „Kontostand aktualisieren“ – in einer Fehlerseite. Stattdessen erscheint nun ein Hinweis und die erneute Anmeldung beim Bankzugang wird angeboten.
* Bankimport (FinTS): Der Kontostand der Bank wird nun tatsächlich gegen den zuletzt gespeicherten Stand geprüft. Diese Prüfung war zwar vorhanden, lief aber nie – Abweichungen (z. B. durch nachträglich geänderte Umsätze) blieben dadurch unbemerkt. Meldungen zu Saldo-Abweichungen und Lücken zwischen zwei Auszügen sind jetzt außerdem verständlich formuliert und nennen die betroffenen Beträge und Daten.

---

# v4.4.3
* Projekte mit mehreren Posten ließen sich nicht mehr speichern, wenn vor dem Speichern in jeder Postenzeile etwas geändert wurde – das Speichern brach mit einer Fehlerseite ab. Die Beträge gingen dabei auf dem Weg zum Server verloren; sie werden nun wieder zuverlässig als Geldbeträge erkannt.
* Fehlermeldungen beim Speichern eines Projekts erscheinen jetzt direkt an dem Feld, das sie ausgelöst hat – und zwar alle. Bisher wurde nur die erste Meldung als einzelne Zeile über dem Formular angezeigt, sodass unklar blieb, welche Zeile oder welches Feld gemeint war.
Expand Down
1 change: 1 addition & 0 deletions lang/de/general.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
'tan-mode' => 'Tan-Modus',
'sepa' => 'Konten',
'import-konto' => 'Neu',
'import-transactions' => 'Aktualisieren',
],
'sitzung' => 'Sitzung',
'budget-plan' => 'Haushaltsplan',
Expand Down
76 changes: 65 additions & 11 deletions legacy/lib/booking/konto/FintsConnectionHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,12 @@ public function logout(): bool
$this->finTs->close(); // logout @ server
$this->forgetCachedCredentials($this->credentialId);
HTMLPageRenderer::addFlash(BT::TYPE_SUCCESS, 'Erfolgreich ausgeloggt');
} catch (ServerException $e) {
} catch (CurlException|ServerException|UnexpectedResponseException $e) {
// A logout that cannot reach the bank is not worth an error page - the local
// session data is dropped either way below.
$this->logger->error('Logout failed', ['exception' => $e]);
HTMLPageRenderer::addFlash(BT::TYPE_DANGER, 'Logout fehlgeschlagen', $e->getMessage());
$this->forgetCachedCredentials($this->credentialId);

return false;
}
Expand All @@ -156,7 +159,7 @@ public function getUserTanModes(): array
try {
$this->logger->info('Fetch TAN Modes', ['credId' => $this->credentialId]);
$tanModes = $this->finTs->getTanModes();
} catch (CurlException|ServerException $e) {
} catch (CurlException|ServerException|UnexpectedResponseException $e) {
$this->logger->info('Fetch TAN Modes failed', ['exception' => $e]);
ErrorHandler::handleException($e, 'TAN Modi können nicht empfangen werden - Verbringung zur Bank gestört');
}
Expand Down Expand Up @@ -185,7 +188,7 @@ public function getTanMedias(int $tanModeId): array
}

return $tanMediumNames;
} catch (CurlException|ServerException $e) {
} catch (CurlException|ServerException|UnexpectedResponseException $e) {
$this->logger->error('Tan kann nicht empfangen werden - Verbindung zur Bank gestört', ['exception' => $e]);
ErrorHandler::handleException($e, 'TAN Modi können nicht empfangen werden - Verbindung zur Bank gestört');
}
Expand Down Expand Up @@ -284,6 +287,7 @@ private function saveAction(?BaseAction $action = null): void
} else {
// delete it from cache otherwise
$this->setCache('action', null);
$this->setCache('action-scope', null);
}
// save persist in cache
$this->setCache('persist', $this->finTs->persist());
Expand Down Expand Up @@ -342,11 +346,24 @@ public static function load(int $credentialId): self

$credentials = Credentials::create($username, self::getPassword($credentialId));

if (trim((string) FINTS_REGNR) === '') {
// FinTsOptions::validate() would raise "Product name required!" as an
// uncaught InvalidArgumentException, i.e. an error page with no clue.
throw new LegacyDieException(
500,
'Für den Bankzugang fehlt die FinTS-Registrierungsnummer (FINTS_REG_NR in der Konfiguration). '.
'Bitte wende dich an die Administration.'
);
}

$options = new FinTsOptions;
$options->url = $res['bank.url'];
$options->bankCode = $res['bank.blz'];
$options->productName = FINTS_REGNR;
$options->productVersion = InstalledVersions::getRootPackage()['version'].DEV ? '-dev' : '';
// The concatenation binds tighter than ?:, so this used to evaluate as
// (('4.4.3'.DEV) ? '-dev' : '') - an always-truthy string, which reported the
// version to the bank as literally "-dev" regardless of what is installed.
$options->productVersion = InstalledVersions::getRootPackage()['version'].(DEV ? '-dev' : '');

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use the one out of the config instead

@lukas-staab lukas-staab Aug 11, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Claude Code (posting via @lukas-staab's account)

Done — config('stufis.version') now, in cf38557f.

Worth noting it was wrong twice over: getRootPackage()['version'] returns the normalised version, so even with the precedence fixed the bank would have been told 4.4.4.0. The config value goes through getPrettyVersion() and gives 4.4.4. Verified locally:

config(stufis.version)           = '4.5.0-beta'
getRootPackage()['version']      = '4.5.0.0-beta'   <- was being sent
getRootPackage()['pretty_version'] = '4.5.0-beta'

The Composer\InstalledVersions import is gone with it.


$tanModeInt = null;
if ($res['tan_mode'] !== 'null' && ! is_null($res['tan_mode'])) {
Expand Down Expand Up @@ -374,7 +391,7 @@ private function execute(BaseAction $action): void
// TODO decoupled tan stuff here
throw new NeedsTanException($action);
}
} catch (CurlException|ServerException $e) {
} catch (CurlException|ServerException|UnexpectedResponseException $e) {
$this->logger->error('Aktion nicht ausgeführt', ['exception' => $e]);
ErrorHandler::handleException($e, 'Verbindung zur Bank gestört - Aktion nicht ausgeführt');
}
Expand Down Expand Up @@ -423,10 +440,27 @@ public function submitTan(string $tan): bool
HTMLPageRenderer::addFlash(BT::TYPE_DANGER, 'Konnte keine Verbindung zum Server aufbauen', $e->getMessage());

return false;
} catch (ServerException $e) {
} catch (ServerException|UnexpectedResponseException $e) {
// A rejected TAN arrives as UnexpectedResponseException("Bank has not accepted
// TAN: ...") from FinTs::submitTan(). That extends RuntimeException, while
// ServerException extends Exception - two unrelated hierarchies, so catching
// only the latter turned a mistyped TAN into an error page.
$this->logger->error('Wrong Tan', ['exception' => $e]);
HTMLPageRenderer::addFlash(BT::TYPE_DANGER, 'TAN nicht akzeptiert', $e->getMessage());

return false;
} catch (InvalidArgumentException $e) {
// The library refuses to take a TAN for a decoupled TAN mode (confirmation
// happens in the banking app instead). Supporting that properly is its own
// work package; until then, say so rather than showing an error page.
$this->logger->error('TAN submission rejected by the library', ['exception' => $e]);
HTMLPageRenderer::addFlash(
BT::TYPE_DANGER,
'Dieses TAN-Verfahren kann StuFiS derzeit nicht abschließen',
'Bei Freigabe-Verfahren ohne TAN-Eingabe (z. B. pushTAN-Freigabe in der Banking-App) '.
'fehlt die Unterstützung noch. Bitte wähle ein TAN-Verfahren mit TAN-Eingabe.'
);

return false;
}

Expand All @@ -447,7 +481,7 @@ public function setTanMode(int $tanModeId, ?string $tanMediumName = null): bool
}
$this->saveAction();
$this->logger->info('Set TAN Mode', ['credId' => $this->credentialId, 'tanMode' => $tanModeId, 'tanMedium' => $tanMediumName]);
} catch (CurlException|ServerException $e) {
} catch (CurlException|ServerException|UnexpectedResponseException $e) {
$this->logger->error('BPB fetch failed', ['exception' => $e]);
ErrorHandler::handleException($e, 'Kann keine Verbindung zum Bank Server aufbauen', 'BPB fetch failed');
}
Expand All @@ -466,25 +500,45 @@ public function setTanMode(int $tanModeId, ?string $tanMediumName = null): bool

public function getStatements(string $iban, DateTime $start, DateTime $end): StatementOfAccount
{
// What a pending statement request was created for. While it waits for a TAN the
// action sits in the session, and it used to be resumed on nothing but its type:
// asking for account A, then opening account B's import URL and entering the TAN
// there returned A's statements, which the caller then stored under B's konto_id.
$scope = $this->statementScope($iban, $start, $end);
$action = $this->resumableAction();
if ($action instanceof GetStatementOfAccount) {
if ($action->isDone()) {
$this->saveAction();
if ($this->getCache('action-scope') === $scope) {
if ($action->isDone()) {
$this->saveAction();

return $action->getStatement();
return $action->getStatement();
}
throw new NeedsTanException($action);
}
throw new NeedsTanException($action);
$this->logger->warning('Discarding a pending statement request made for something else', [
'credId' => $this->credentialId,
'requested' => $scope,
]);
$this->saveAction(); // drops the stale action and its scope
}
$this->logger->info('Start Get SEPA Statements', ['credId' => $this->credentialId, $iban]);
$account = $this->getSepaAccount($iban);
$account = clone $account; // weird fix, without the clone the session var is changed to DateTime object
// might be a bug in fints TODO: see if minimal example with the same bug can be found
$action = GetStatementOfAccount::create($account, $start, $end);
// Has to be recorded before execute(), which caches the action and then throws
// NeedsTanException, ending this request.
$this->setCache('action-scope', $scope);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this ever read?

@lukas-staab lukas-staab Aug 11, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Claude Code (posting via @lukas-staab's account)

Written, yes — the logger level is Info in production (DEV ? Debug : Info), and this is a warning. But read, realistically no: it goes to legacy/runtime/logs/fints.log, which nobody opens, and that was the actual problem — from the user's side the TAN they were about to enter just silently stops applying.

So in cf38557f it also says so on the page:

Der noch offene Umsatzabruf gehörte zu einem anderen Konto oder Zeitraum und wurde verworfen — Der Abruf für dieses Konto wird neu gestartet, dafür ist eine neue TAN nötig.

Separate question your comment raises: that whole fints.log channel sits outside Laravel's logging stack, so nothing there reaches central monitoring. Happy to fold it into Laravel's channels as its own work package if you want — say the word and I'll file it.

$this->execute($action);

return $action->getStatement();
}

private function statementScope(string $iban, DateTime $start, DateTime $end): string
{
return $iban.'|'.$start->format('Y-m-d').'|'.$end->format('Y-m-d');
}

public function getLogger(): LoggerInterface
{
return $this->finTs->getLogger();
Expand Down
Loading