Finanzmanager/src/Services/Sammelabruf.php
fds2610-bot 4d833f273b Fix #75: Knopf „Alle Umsätze abrufen“ ruft jede Bank der Reihe nach ab
Über der Tabelle der Bankverbindungen ruft ein neuer Knopf jede aktive
Bankverbindung einzeln nacheinander ab. Die Warteschlange (Services\Sammelabruf)
übersteht TAN-Runden in der Sitzung; nach der Freigabe geht es mit der nächsten
Bank weiter. Die TAN-Maske zeigt den Stand und erlaubt, eine Bank zu
überspringen. Am Ende steht je Bankverbindung eine eigene Meldung.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 12:38:24 +02:00

176 lines
5.6 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\Services;
/**
* „Alle Umsätze abrufen“: jede aktive Bankverbindung der Reihe nach.
*
* Die Klasse führt nur die Warteschlange – abgerufen wird weiterhin einzeln
* über `FinTsAbruf` beziehungsweise `PaypalAbruf`. Der Grund ist die TAN: Verlangt
* eine Bank eine Freigabe, endet der Request mit der TAN-Maske, und erst der
* nächste Request setzt den Abruf fort. Die Warteschlange muss diese Pause
* überstehen; der Controller legt ihren Zustand deshalb in die Sitzung und
* holt ihn dort wieder ab.
*
* Ein Fehler bei einer Bank hält die übrigen nicht auf: Er wird vermerkt, und
* es geht mit der nächsten weiter. Am Ende steht je Bankverbindung eine Meldung.
*/
final class Sammelabruf
{
/** Schlüssel des Zustands in der Sitzung. */
public const SITZUNG = 'banking_sammelabruf';
/**
* @param array{offen: list<array{id: int, name: string}>,
* aktuell: array{id: int, name: string}|null,
* gesamt: int,
* meldungen: list<array{typ: string, meldung: string}>} $zustand
*/
private function __construct(private array $zustand)
{
}
/**
* Eine neue Warteschlange über die übergebenen Bankverbindungen – in der
* Reihenfolge, in der sie auch in der Übersicht stehen.
*
* @param list<array<string,mixed>> $zugaenge
*/
public static function fuer(array $zugaenge): self
{
$offen = [];
foreach ($zugaenge as $zugang) {
$offen[] = ['id' => (int)$zugang['id'], 'name' => (string)$zugang['name']];
}
return new self([
'offen' => $offen,
'aktuell' => null,
'gesamt' => count($offen),
'meldungen' => [],
]);
}
/**
* Die Warteschlange aus einem gespeicherten Zustand – `null`, wenn keiner
* (mehr) da ist.
*/
public static function aus(mixed $zustand): ?self
{
if (!is_array($zustand) || !is_array($zustand['offen'] ?? null)) {
return null;
}
return new self([
'offen' => array_values($zustand['offen']),
'aktuell' => is_array($zustand['aktuell'] ?? null) ? $zustand['aktuell'] : null,
'gesamt' => (int)($zustand['gesamt'] ?? 0),
'meldungen' => is_array($zustand['meldungen'] ?? null) ? $zustand['meldungen'] : [],
]);
}
/** @return array<string,mixed> Der Zustand zum Ablegen in der Sitzung. */
public function zustand(): array
{
return $this->zustand;
}
/**
* Die nächste Bankverbindung – `null`, wenn alle durch sind. Sie gilt ab
* jetzt als die laufende, bis ihr Ergebnis vermerkt ist.
*
* @return array{id: int, name: string}|null
*/
public function naechste(): ?array
{
$this->zustand['aktuell'] = array_shift($this->zustand['offen']);
return $this->zustand['aktuell'];
}
/** @return array{id: int, name: string}|null Die gerade laufende Bankverbindung. */
public function aktuell(): ?array
{
return $this->zustand['aktuell'];
}
/**
* Das Ergebnis der laufenden Bankverbindung festhalten.
*
* @param array<string,mixed> $ergebnis wie von `FinTsAbruf`/`PaypalAbruf`
*/
public function vermerke(array $ergebnis): void
{
if ($this->zustand['aktuell'] === null) {
return;
}
$fehler = ($ergebnis['status'] ?? 'fehler') === 'fehler';
$this->notiere(
$fehler ? 'fehler' : 'erfolg',
($fehler ? 'Abruf fehlgeschlagen: ' : '') . (string)($ergebnis['meldung'] ?? '')
);
}
/** Die laufende Bankverbindung ohne Ergebnis hinter sich lassen. */
public function ueberspringe(): void
{
if ($this->zustand['aktuell'] !== null) {
$this->notiere('fehler', 'Übersprungen, die Freigabe wurde abgebrochen.');
}
}
/**
* Den ganzen Lauf abbrechen: Die laufende und alle noch offenen
* Bankverbindungen werden nicht mehr abgerufen.
*/
public function abbrechen(): void
{
$offen = count($this->zustand['offen']) + ($this->zustand['aktuell'] !== null ? 1 : 0);
$this->zustand['aktuell'] = null;
$this->zustand['offen'] = [];
if ($offen > 0) {
$this->zustand['meldungen'][] = [
'typ' => 'fehler',
'meldung' => sprintf(
'Abruf abgebrochen – %d Bankverbindung%s nicht abgerufen.',
$offen,
$offen === 1 ? '' : 'en'
),
];
}
}
/**
* Wo der Lauf steht – für die TAN-Maske, damit dort klar ist, welche Bank
* gerade fragt und wie viele noch folgen.
*
* @return array{nummer: int, gesamt: int, name: string}
*/
public function stand(): array
{
return [
'nummer' => $this->zustand['gesamt'] - count($this->zustand['offen']),
'gesamt' => $this->zustand['gesamt'],
'name' => (string)($this->zustand['aktuell']['name'] ?? ''),
];
}
/** @return list<array{typ: string, meldung: string}> Je Bankverbindung eine Meldung. */
public function meldungen(): array
{
return $this->zustand['meldungen'];
}
private function notiere(string $typ, string $meldung): void
{
$this->zustand['meldungen'][] = [
'typ' => $typ,
'meldung' => $this->zustand['aktuell']['name'] . ': ' . $meldung,
];
$this->zustand['aktuell'] = null;
}
}