Finanzmanager/src/Controllers/BankingController.php
fds2610-bot 005df48fed 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>
2026-08-13 10:41:05 +02:00

805 lines
28 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
namespace Finanzmanager\Controllers;
use Finanzmanager\Auth;
use Finanzmanager\Database;
use Finanzmanager\Models\AbgleichRegel;
use Finanzmanager\Models\Bankabruf;
use Finanzmanager\Models\Bankumsatz;
use Finanzmanager\Models\Bankzugang;
use Finanzmanager\Models\Kategorie;
use Finanzmanager\Models\Klasse;
use Finanzmanager\Models\Konto;
use Finanzmanager\Services\Bankabgleich;
use Finanzmanager\Services\Crypto;
use Finanzmanager\Services\FinTsAbruf;
use Finanzmanager\Services\FinTsVerfahren;
use Finanzmanager\View;
/**
* Online-Banking: Bankverbindungen verwalten, Umsätze abrufen, abgleichen und
* Regeln pflegen.
*
* Der Abruf selbst steckt in `Services\FinTsAbruf`; dieser Controller reicht nur
* durch und übersetzt das Ergebnis in Seiten – vor allem in die TAN-Maske, die
* zwischen zwei Requests stehen bleibt.
*/
class BankingController extends BaseController
{
/** Schlüssel der abgefragten TAN-Verfahren in der Sitzung. */
private const SITZUNG_VERFAHREN = 'banking_verfahren';
/** Wert der Kontenauswahl, der für „neues Konto anlegen“ steht. */
public const KONTO_NEU = 'neu';
// ── Bankverbindungen ─────────────────────────────────────────────────────
public function index(): void
{
$this->verlangeAnmeldung();
Bankabruf::raeumeAlteAuf();
$this->render('banking/index.php', [
'titel' => 'Bankverbindungen',
'zugaenge' => Bankzugang::alle(),
'abrufe' => Bankabruf::letzte(10),
'offen' => Bankumsatz::anzahlOffen(),
'schluessel' => Crypto::einsatzbereit(),
]);
}
public function neu(): void
{
$this->verlangeAnmeldung();
$this->render('banking/formular.php', [
'titel' => 'Bankverbindung anlegen',
'zugang' => null,
'konten' => [],
]);
}
public function anlegen(): void
{
$this->verlangeAnmeldung();
try {
$id = Bankzugang::anlegen($this->formulardaten());
} catch (\Throwable $e) {
View::redirect('/banking/neu', $e->getMessage(), 'fehler');
return;
}
// Weiter zum zweiten Schritt: Erst jetzt – mit gespeicherten Zugangsdaten –
// lässt sich die Bank nach ihren TAN-Verfahren fragen.
View::redirect(
'/banking/' . $id . '/verfahren?einrichtung=1',
'Zugangsdaten gespeichert. Jetzt das TAN-Verfahren wählen.'
);
}
/** @param array<string,string> $params */
public function bearbeiten(array $params): void
{
$this->verlangeAnmeldung();
$zugang = Bankzugang::find((int)$params['id']);
if ($zugang === null) {
View::redirect('/banking', 'Die Bankverbindung gibt es nicht.', 'fehler');
return;
}
$this->render('banking/formular.php', [
'titel' => $zugang['name'],
'zugang' => $zugang,
'konten' => Bankzugang::konten((int)$zugang['id']),
'abrufe' => Bankabruf::letzte(5, (int)$zugang['id']),
]);
}
/** @param array<string,string> $params */
public function speichern(array $params): void
{
$this->verlangeAnmeldung();
$id = (int)$params['id'];
try {
Bankzugang::speichern($id, $this->formulardaten());
} catch (\Throwable $e) {
View::redirect('/banking/' . $id, $e->getMessage(), 'fehler');
return;
}
View::redirect('/banking/' . $id, 'Bankverbindung gespeichert.');
}
/** @param array<string,string> $params */
public function loeschen(array $params): void
{
$this->verlangeAnmeldung();
Bankzugang::loeschen((int)$params['id']);
View::redirect('/banking', 'Bankverbindung gelöscht.');
}
// ── TAN-Verfahren ────────────────────────────────────────────────────────
/**
* Der zweite Schritt der Einrichtung: das TAN-Verfahren.
*
* 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
*/
public function verfahren(array $params): void
{
$this->verlangeAnmeldung();
$id = (int)$params['id'];
$zugang = Bankzugang::find($id);
if ($zugang === null) {
View::redirect('/banking', 'Die Bankverbindung gibt es nicht.', 'fehler');
return;
}
$abfrage = $this->gemerkteAbfrage($id) ?? $this->verfahrenHolen($id);
$this->render('banking/verfahren.php', [
'titel' => 'TAN-Verfahren – ' . $zugang['name'],
'zugang' => $zugang,
'medium' => Crypto::entschluessle((string)($zugang['tan_medium'] ?? '')),
'verfahren' => $abfrage['liste'],
'fehler' => $abfrage['status'] === 'fehler' ? $abfrage['meldung'] : '',
'einrichtung' => $this->query('einrichtung') === '1',
]);
}
/**
* Ein erneuter Anlauf, wenn der erste scheiterte oder sich etwas geändert hat.
*
* @param array<string,string> $params
*/
public function verfahrenAbfragen(array $params): void
{
$this->verlangeAnmeldung();
$id = (int)$params['id'];
$abfrage = $this->verfahrenHolen($id);
$ziel = '/banking/' . $id . '/verfahren' . ($this->post('einrichtung') === '1' ? '?einrichtung=1' : '');
View::redirect($ziel, $abfrage['meldung'], $abfrage['status'] === 'fehler' ? 'fehler' : 'erfolg');
}
/** @param array<string,string> $params */
public function verfahrenWaehlen(array $params): void
{
$this->verlangeAnmeldung();
$id = (int)$params['id'];
$verfahren = (int)$this->post('verfahren_id');
$abfrage = $this->gemerkteAbfrage($id);
try {
FinTsVerfahren::uebernehmen(
$id,
$verfahren,
// Das Medium des angekreuzten Verfahrens, nicht das irgendeiner
// anderen Zeile.
$this->post('tan_medium_' . $verfahren),
$abfrage === null ? [] : $abfrage['liste']
);
} catch (\Throwable $e) {
View::redirect('/banking/' . $id . '/verfahren', $e->getMessage(), 'fehler');
return;
}
// Dritter Schritt: Die Konten holen wir gleich hier – erst mit
// gewähltem Verfahren lässt sich die Bank danach fragen, und ohne
// Zuordnung wäre jeder Umsatz eines fremden Kontos wertlos.
if ($this->post('einrichtung') === '1') {
$this->zeigeAbruf(FinTsAbruf::starten($id, 'konten', true), $id);
return;
}
View::redirect('/banking/' . $id, 'TAN-Verfahren übernommen.');
}
// ── Kontenzuordnung ──────────────────────────────────────────────────────
/**
* Der dritte Schritt der Einrichtung: Welches Bankkonto gehört zu welchem
* eigenen Konto?
*
* Ohne diese Zuordnung landet jeder abgerufene Umsatz in der
* Vorschlagsliste, ohne dass sich sagen ließe, wohin er gebucht gehört.
* Fehlt ein passendes eigenes Konto, lässt es sich hier gleich anlegen –
* mit IBAN und Bankname aus dem, was die Bank gemeldet hat.
*
* @param array<string,string> $params
*/
public function konten(array $params): void
{
$this->verlangeAnmeldung();
$zugang = Bankzugang::find((int)$params['id']);
if ($zugang === null) {
View::redirect('/banking', 'Die Bankverbindung gibt es nicht.', 'fehler');
return;
}
$this->render('banking/konten.php', [
'titel' => 'Konten zuordnen – ' . $zugang['name'],
'zugang' => $zugang,
'konten' => Bankzugang::konten((int)$zugang['id']),
'eigene' => Konto::alle(),
'einrichtung' => $this->query('einrichtung') === '1',
]);
}
/**
* Die Konten (erneut) bei der Bank erfragen. Ein eigener Knopf, weil dafür
* eine Anmeldung nötig ist – und die verlangt bei den meisten Banken eine
* TAN, die niemand beim bloßen Öffnen einer Seite erwartet.
*
* @param array<string,string> $params
*/
public function kontenAbfragen(array $params): void
{
$this->verlangeAnmeldung();
$id = (int)$params['id'];
$this->zeigeAbruf(
FinTsAbruf::starten($id, 'konten', $this->post('einrichtung') === '1'),
$id
);
}
/** @param array<string,string> $params */
public function kontenSpeichern(array $params): void
{
$this->verlangeAnmeldung();
$id = (int)$params['id'];
$zugang = Bankzugang::find($id);
if ($zugang === null) {
View::redirect('/banking', 'Die Bankverbindung gibt es nicht.', 'fehler');
return;
}
$einrichtung = $this->post('einrichtung') === '1';
$ziel = '/banking/' . $id . '/konten' . ($einrichtung ? '?einrichtung=1' : '');
try {
$neue = $this->kontenzuordnungSpeichern($zugang);
} catch (\Throwable $e) {
View::redirect($ziel, $e->getMessage(), 'fehler');
return;
}
// Was schon abgerufen wurde, hat mit der Zuordnung eben sein Konto
// bekommen – jetzt kann es dort auch gebucht werden.
$gebucht = Bankabgleich::automatisch();
$meldung = $einrichtung ? 'Einrichtung abgeschlossen.' : 'Zuordnung gespeichert.';
if ($neue > 0) {
$meldung .= sprintf(' %d Konto/Konten neu angelegt.', $neue);
}
if ($gebucht !== null && $gebucht['uebernommen'] + $gebucht['verbunden'] > 0) {
$meldung .= ' ' . Bankabgleich::bericht($gebucht);
}
View::redirect(self::zielNachZuordnung($id, $einrichtung), $meldung);
}
/**
* Wohin nach dem Speichern der Zuordnung?
*
* Am Ende der Einrichtung in die Übersicht der Bankverbindungen: Dort steht
* die neue Verbindung fertig zwischen den anderen. Die Zugangsmaske hätte
* nichts mehr zu zeigen, was gerade nicht schon durchlaufen wäre.
*
* Sonst zurück auf die Zuordnungsseite – wer sie einzeln aufruft, will das
* Ergebnis an derselben Stelle sehen.
*/
public static function zielNachZuordnung(int $zugangId, bool $einrichtung): string
{
return $einrichtung ? '/banking' : '/banking/' . $zugangId . '/konten';
}
/**
* 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.
*
* @return array{status: string, meldung: string, liste: list<array<string,mixed>>}|null
*/
private function gemerkteAbfrage(int $zugangId): ?array
{
Auth::startSession();
$gemerkt = $_SESSION[self::SITZUNG_VERFAHREN] ?? null;
if (!is_array($gemerkt) || (int)($gemerkt['zugang_id'] ?? 0) !== $zugangId) {
return null;
}
return [
'status' => (string)($gemerkt['status'] ?? 'fehler'),
'meldung' => (string)($gemerkt['meldung'] ?? ''),
'liste' => is_array($gemerkt['liste'] ?? null) ? $gemerkt['liste'] : [],
];
}
// ── Abruf ────────────────────────────────────────────────────────────────
/**
* 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();
// Umsätze, deren Buchung hinter unserem Rücken verschwunden ist, gehören
// zurück in die Liste – sonst überspringt der Abruf sie gleich wieder
// als schon vorhanden, und sie blieben für immer unsichtbar.
Bankumsatz::verwaisteOeffnen();
$ergebnis = FinTsAbruf::starten((int)$params['id'], 'voll', false, $this->post('stichtag'));
$this->zeigeAbruf($ergebnis, (int)$params['id']);
}
/** Die TAN-Maske nach einem Neuladen der Seite. */
public function tanFormular(): void
{
$this->verlangeAnmeldung();
$tan = FinTsAbruf::offeneTanAnfrage();
if ($tan === null) {
View::redirect('/banking', 'Es wartet kein Abruf auf eine Freigabe.');
return;
}
$this->render('banking/tan.php', [
'titel' => 'Freigabe erforderlich',
'tan' => $tan,
'meldung' => 'Die Bank verlangt eine Freigabe.',
'protokoll' => [],
]);
}
public function tanEinreichen(): void
{
$this->verlangeAnmeldung();
$ergebnis = FinTsAbruf::tanEinreichen($this->post('tan'));
$this->zeigeAbruf($ergebnis, null);
}
public function tanAbbrechen(): void
{
$this->verlangeAnmeldung();
FinTsAbruf::abbrechen();
View::redirect('/banking', 'Abruf abgebrochen.');
}
/**
* Ein Abrufergebnis anzeigen: entweder die TAN-Maske oder zurück zur
* Übersicht mit Meldung.
*
* @param array<string,mixed> $ergebnis
*/
private function zeigeAbruf(array $ergebnis, ?int $zugangId): void
{
if ($ergebnis['status'] === 'tan') {
$this->render('banking/tan.php', [
'titel' => 'Freigabe erforderlich',
'tan' => $ergebnis['tan'] ?? [],
'meldung' => $ergebnis['meldung'],
'protokoll' => $ergebnis['protokoll'],
]);
return;
}
// Nach einer TAN-Runde kennt nur das Ergebnis noch den Zugang: Der
// Aufrufer war dann `tanEinreichen()` ohne jeden Zusammenhang.
$zugangId ??= isset($ergebnis['zugang_id']) ? (int)$ergebnis['zugang_id'] : null;
$kontenlauf = ($ergebnis['modus'] ?? 'voll') === 'konten';
$kontenseite = $zugangId === null
? '/banking'
: '/banking/' . $zugangId . '/konten' . (($ergebnis['einrichtung'] ?? false) ? '?einrichtung=1' : '');
if ($ergebnis['status'] === 'fehler') {
View::redirect(
$kontenlauf ? $kontenseite : ($zugangId === null ? '/banking' : '/banking/' . $zugangId),
'Abruf fehlgeschlagen: ' . $ergebnis['meldung'],
'fehler'
);
return;
}
if ($kontenlauf) {
View::redirect($kontenseite, $ergebnis['meldung'] . ' Jetzt die Konten zuordnen.');
return;
}
// Automatisch gebuchte Umsätze stehen im Kontoblatt, nicht mehr in der
// Vorschlagsliste – dorthin lohnt der Sprung nur, wenn dort etwas liegt.
View::redirect(
Bankumsatz::anzahlOffen() > 0 ? '/banking/umsaetze' : '/banking',
$ergebnis['meldung']
);
}
// ── Abgleich ─────────────────────────────────────────────────────────────
public function umsaetze(): void
{
$this->verlangeAnmeldung();
Bankumsatz::verwaisteOeffnen();
$status = $this->query('status', 'offen');
if (!array_key_exists($status, Bankumsatz::STATUS)) {
$status = 'offen';
}
$regeln = AbgleichRegel::alle(true);
$umsaetze = [];
foreach (Bankumsatz::liste(['status' => $status]) as $umsatz) {
$umsatz['vorschlag'] = $status === 'offen'
? Bankumsatz::vorschlag($umsatz, $regeln)
: null;
$umsaetze[] = $umsatz;
}
$this->render('banking/umsaetze.php', [
'titel' => 'Umsätze abgleichen',
'status' => $status,
'umsaetze' => $umsaetze,
'kategorien' => Kategorie::baumFlach(null, true),
'klassen' => Klasse::alle(),
'konten' => Konto::alle(true),
'offen' => Bankumsatz::anzahlOffen(),
]);
}
/** @param array<string,string> $params */
public function uebernehmen(array $params): void
{
$this->verlangeAnmeldung();
$id = (int)$params['id'];
try {
// Eine vorgeschlagene Buchung wird verbunden statt neu angelegt –
// sonst stünde dieselbe Zahlung zweimal im Kontoblatt.
$buchungId = (int)$this->post('buchung_id');
if ($buchungId > 0) {
Bankumsatz::verbinden($id, $buchungId);
} else {
Bankumsatz::uebernehmen($id, [
'konto_id' => $this->post('konto_id') !== '' ? (int)$this->post('konto_id') : null,
'kategorie_id' => $this->post('kategorie_id') !== '' ? (int)$this->post('kategorie_id') : null,
'klasse_id' => $this->post('klasse_id') !== '' ? (int)$this->post('klasse_id') : null,
'gegenkonto_id' => $this->post('gegenkonto_id') !== '' ? (int)$this->post('gegenkonto_id') : null,
'splits' => $this->splitsAusFormular(),
]);
}
} catch (\Throwable $e) {
View::redirect('/banking/umsaetze', $e->getMessage(), 'fehler');
return;
}
View::redirect('/banking/umsaetze', 'Umsatz übernommen.');
}
/** @param array<string,string> $params */
public function verwerfen(array $params): void
{
$this->verlangeAnmeldung();
Bankumsatz::verwerfen((int)$params['id']);
View::redirect('/banking/umsaetze', 'Umsatz verworfen.');
}
/** @param array<string,string> $params */
public function zuruecksetzen(array $params): void
{
$this->verlangeAnmeldung();
Bankumsatz::zuruecksetzen((int)$params['id']);
View::redirect('/banking/umsaetze?status=offen', 'Umsatz steht wieder zum Abgleich bereit.');
}
/**
* Alles übernehmen, was einen eindeutigen Vorschlag hat.
*
* Ohne Regel bleibt ein Umsatz hier liegen – anders als beim automatischen
* Lauf nach dem Abruf. Wer den Dialog öffnet, will die Zuordnung selbst
* treffen; eine Buchung ohne Kategorie wäre ihm nur ungefragt aufgehalst.
*/
public function alleUebernehmen(): void
{
$this->verlangeAnmeldung();
View::redirect(
'/banking/umsaetze',
Bankabgleich::bericht(Bankabgleich::laufenLassen(false))
);
}
// ── Regeln ───────────────────────────────────────────────────────────────
public function regeln(): void
{
$this->verlangeAnmeldung();
$this->render('banking/regeln.php', [
'titel' => 'Regeln für den Abgleich',
'regeln' => AbgleichRegel::alle(),
'kategorien' => Kategorie::baumFlach(null, true),
'klassen' => Klasse::alle(),
'konten' => Konto::alle(true),
]);
}
public function regelAnlegen(): void
{
$this->verlangeAnmeldung();
try {
AbgleichRegel::anlegen($this->regeldaten());
} catch (\Throwable $e) {
View::redirect('/banking/regeln', $e->getMessage(), 'fehler');
return;
}
View::redirect('/banking/regeln', 'Regel angelegt.');
}
/** @param array<string,string> $params */
public function regelSpeichern(array $params): void
{
$this->verlangeAnmeldung();
try {
AbgleichRegel::speichern((int)$params['id'], $this->regeldaten());
} catch (\Throwable $e) {
View::redirect('/banking/regeln', $e->getMessage(), 'fehler');
return;
}
View::redirect('/banking/regeln', 'Regel gespeichert.');
}
/** @param array<string,string> $params */
public function regelLoeschen(array $params): void
{
$this->verlangeAnmeldung();
AbgleichRegel::loeschen((int)$params['id']);
View::redirect('/banking/regeln', 'Regel gelöscht.');
}
/**
* Wendet die Regeln auf die offenen Umsätze an, ohne sie zu übernehmen –
* damit lässt sich eine neue Regel ausprobieren, bevor sie bucht.
*/
public function regelnPruefen(): void
{
$this->verlangeAnmeldung();
$regeln = AbgleichRegel::alle(true);
$treffer = 0;
$gesamt = 0;
foreach (Bankumsatz::liste(['status' => 'offen']) as $umsatz) {
$gesamt++;
if (AbgleichRegel::passendeRegel($umsatz, $regeln) !== null) {
$treffer++;
}
}
View::redirect('/banking/regeln', sprintf(
'Von %d offenen Umsätzen treffen die Regeln auf %d.',
$gesamt,
$treffer
));
}
// ── Formulardaten ────────────────────────────────────────────────────────
/** @return array<string,mixed> */
private function formulardaten(): array
{
return [
'name' => $this->post('name'),
'bankname' => $this->post('bankname'),
'blz' => $this->post('blz'),
'fints_url' => $this->post('fints_url'),
'benutzerkennung' => $this->post('benutzerkennung'),
'pin' => $this->post('pin'),
'abrufzeitraum' => $this->post('abrufzeitraum'),
'abruf_stichtag' => $this->post('abruf_stichtag'),
'aktiv' => isset($_POST['aktiv']) ? 1 : 0,
];
}
/** @return array<string,mixed> */
private function regeldaten(): array
{
return [
'name' => $this->post('name'),
'feld' => $this->post('feld'),
'vergleich' => $this->post('vergleich'),
'muster' => $this->post('muster'),
'betrag_von' => $this->post('betrag_von'),
'betrag_bis' => $this->post('betrag_bis'),
'kategorie_id' => $this->post('kategorie_id'),
'klasse_id' => $this->post('klasse_id'),
'gegenkonto_id' => $this->post('gegenkonto_id'),
'prioritaet' => $this->post('prioritaet', '100'),
'aktiv' => isset($_POST['aktiv']) ? 1 : 0,
];
}
/**
* Die Zuordnung Bankkonto → eigenes Konto aus dem Formular.
*
* Nur Bankkonten dieses Zugangs werden angefasst: Die Kennungen kommen aus
* dem Formular, und eine fremde darf weder etwas zuordnen noch ein Konto
* anlegen.
*
* Alles in einer Transaktion: Scheitert eine Zeile – meist ein neues Konto
* ohne Namen –, soll die Seite wieder so dastehen wie vorher und nicht halb
* zugeordnet.
*
* @param array<string,mixed> $zugang
* @return int Zahl der dabei neu angelegten eigenen Konten
*/
private function kontenzuordnungSpeichern(array $zugang): int
{
$zuordnung = $_POST['bankkonto'] ?? [];
if (!is_array($zuordnung)) {
return 0;
}
$bekannt = [];
foreach (Bankzugang::konten((int)$zugang['id']) as $eintrag) {
$bekannt[(int)$eintrag['id']] = $eintrag;
}
return (int)Database::transaktion(function () use ($zugang, $zuordnung, $bekannt): int {
$neue = 0;
foreach ($zuordnung as $bankKontoId => $werte) {
$bankKontoId = (int)$bankKontoId;
if (!is_array($werte) || !isset($bekannt[$bankKontoId])) {
continue;
}
$wahl = (string)($werte['konto_id'] ?? '');
if ($wahl === self::KONTO_NEU) {
$kontoId = $this->kontoAnlegen($zugang, $bekannt[$bankKontoId], $werte);
$neue++;
} else {
$kontoId = $wahl === '' ? null : (int)$wahl;
}
Bankzugang::ordneKontoZu($bankKontoId, $kontoId, !empty($werte['abrufen']));
}
return $neue;
});
}
/**
* Legt das eigene Konto zu einem Bankkonto an. IBAN, Kontonummer und
* Bankname kommen aus dem, was die Bank gemeldet hat – abtippen muss der
* Nutzer nur den Namen, unter dem das Konto in seiner Liste stehen soll.
*
* @param array<string,mixed> $zugang
* @param array<string,mixed> $bankkonto
* @param array<string,mixed> $werte
*/
private function kontoAnlegen(array $zugang, array $bankkonto, array $werte): int
{
$name = trim((string)($werte['neuer_name'] ?? ''));
if ($name === '') {
throw new \InvalidArgumentException(
'Für „' . ($bankkonto['iban'] ?: $bankkonto['kontonummer'])
. '“ fehlt der Name des neuen Kontos.'
);
}
return Konto::anlegen([
'name' => $name,
'kontoart' => (string)($werte['neue_kontoart'] ?? 'bank'),
'bankname' => (string)($zugang['bankname'] !== '' ? $zugang['bankname'] : $zugang['name']),
'iban' => (string)$bankkonto['iban'],
'kontonummer' => (string)$bankkonto['kontonummer'],
// Was die Bank an Umsätzen liefert, reicht selten bis zur
// Kontoeröffnung zurück – der Anfangssaldo schließt die Lücke.
'anfangssaldo' => KontenController::zahl((string)($werte['anfangssaldo'] ?? '0')),
'aktiv' => 1,
'in_summe' => 1,
]);
}
/**
* Splitzeilen aus der Abgleichmaske.
*
* @return list<array<string,mixed>>
*/
private function splitsAusFormular(): array
{
$roh = $_POST['split'] ?? [];
if (!is_array($roh)) {
return [];
}
$splits = [];
foreach ($roh as $zeile) {
if (!is_array($zeile) || ($zeile['betrag'] ?? '') === '') {
continue;
}
$splits[] = [
'kategorie_id' => ($zeile['kategorie_id'] ?? '') === '' ? null : (int)$zeile['kategorie_id'],
'klasse_id' => ($zeile['klasse_id'] ?? '') === '' ? null : (int)$zeile['klasse_id'],
'betrag' => (float)str_replace(',', '.', (string)$zeile['betrag']),
'notiz' => (string)($zeile['notiz'] ?? ''),
];
}
return $splits;
}
}