Fix #67: Übermittlungsprotokoll für Überweisungen und Bankkommunikation speichern
Der gesamte FinTS-Dialog (gesendete und empfangene Meldungen) wird je Durchlauf in bank_protokolle gespeichert, im Erfolgs- wie im Fehlerfall. Im Zahlungsverkehr öffnet der Status das Protokoll der Überweisung als modalen Dialog; unter Online-Banking › Protokoll gibt es das Gesamtprotokoll mit Filter nach Bank, Datum und Freitext. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
451a40a826
commit
3caf1dcdca
21 changed files with 756 additions and 10 deletions
19
Changes.md
19
Changes.md
|
|
@ -1,5 +1,24 @@
|
|||
# Änderungen
|
||||
|
||||
## Übermittlungsprotokoll für Überweisungen und Bankkommunikation (21.09.2026)
|
||||
|
||||
Als Nachweis gegenüber der Bank, dass eine Überweisung fristgerecht und korrekt übermittelt
|
||||
wurde, speichert der Finanzmanager jetzt den gesamten Dialog – gesendete und empfangene
|
||||
Meldungen im Wortlaut, im Erfolgs- wie im Fehlerfall. PIN und Benutzerkennung sind maskiert
|
||||
(das erledigt die FinTS-Bibliothek, bevor die Zeilen ankommen).
|
||||
|
||||
- **Zahlungsverkehr:** Ein Klick auf den Status einer Überweisung öffnet ihr
|
||||
Übermittlungsprotokoll in einem modalen Dialog. Eine Überweisung mit TAN-Freigabe hat
|
||||
mehrere Durchläufe; alle stehen darin, der älteste zuerst. Ohne JavaScript führt der
|
||||
Status auf dieselbe Ansicht als eigene Seite.
|
||||
- **Verwaltung › Online-Banking › Protokoll** (`/banking/protokoll`): das Gesamtprotokoll
|
||||
über jede Kommunikation mit den Banken – Überweisungen, Umsatzabrufe und die Abfrage der
|
||||
TAN-Verfahren. Filterbar nach Bank, Zeitraum und Freitext.
|
||||
- Neue Tabelle `bank_protokolle` (Migration `018`), Modell `Bankprotokoll`, Mitschnitt über
|
||||
`Services\BankProtokollLogger`. Das Protokoll bleibt stehen, auch wenn die Überweisung
|
||||
gelöscht wird; es wird nicht automatisch aufgeräumt.
|
||||
- Test 42 (`tests/42_UebermittlungsprotokollTest.php`).
|
||||
|
||||
## Letzter roter E2E-Test aus dem Typeahead-Umbau behoben (04.09.2026)
|
||||
|
||||
Ein Test blieb nach der Anpassung an den Typeahead (siehe unten) noch rot:
|
||||
|
|
|
|||
|
|
@ -467,6 +467,23 @@ Bei eingeschaltetem Debug-Log (Einstellungen → Diagnose) landen zusätzlich di
|
|||
FinTS-Dialogschritte in `data/logs/` – über `Services\DebugLogAdapter`, der den PSR-3-Logger
|
||||
der Bibliothek an das eigene Log hängt. PIN und TAN maskieren beide Seiten.
|
||||
|
||||
### Übermittlungsprotokoll
|
||||
|
||||
Als Nachweis, dass eine Überweisung rechtzeitig und korrekt bei der Bank ankam, speichert
|
||||
jeder Durchlauf – Überweisung, Umsatzabruf, Abfrage der TAN-Verfahren – seinen gesamten
|
||||
Dialog in `bank_protokolle`: gesendete (`>`) und empfangene (`<`) Meldungen im Wortlaut, im
|
||||
Erfolgs- wie im Fehlerfall. Den Mitschnitt macht `Services\BankProtokollLogger`, ein
|
||||
PSR-3-Logger, den die Dienste der Bibliothek statt des `DebugLogAdapter` mitgeben; er läuft
|
||||
also unabhängig vom Debug-Log. Die Bibliothek maskiert PIN und Benutzerkennung, bevor eine
|
||||
Zeile ankommt. Eine Überweisung mit TAN-Freigabe hat mehrere Durchläufe.
|
||||
|
||||
Zu sehen ist das Protokoll an zwei Stellen: im Zahlungsverkehr über den Status der
|
||||
Überweisung (modaler Dialog) und als Gesamtprotokoll unter *Verwaltung › Online-Banking ›
|
||||
Protokoll* (`/banking/protokoll`), filterbar nach Bank, Zeitraum und Freitext. Es wird nie
|
||||
automatisch gelöscht; wird ein Zahlungsauftrag entfernt, bleibt sein Protokoll stehen.
|
||||
`tests/42_UebermittlungsprotokollTest.php` prüft Mitschnitt, Zuordnung zur Überweisung, den
|
||||
Fehlerfall und die Filter.
|
||||
|
||||
## Tests
|
||||
|
||||
`tests/16_BankingTest.php` prüft alles um den Dialog herum: Verschlüsselung (Kennung, kein
|
||||
|
|
|
|||
31
migrations/018_bank_protokolle.sql
Normal file
31
migrations/018_bank_protokolle.sql
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
-- Übermittlungsprotokoll: jede Kommunikation mit einer Bank.
|
||||
--
|
||||
-- Zum Nachweis gegenüber der Bank, dass eine Überweisung fristgerecht und
|
||||
-- korrekt übermittelt wurde, bleibt der gesamte Dialog stehen – die gesendeten
|
||||
-- und die empfangenen Meldungen, im Erfolgs- wie im Fehlerfall. PIN und TAN
|
||||
-- sind vor dem Speichern maskiert (siehe Services\BankProtokollLogger).
|
||||
--
|
||||
-- Ein Eintrag ist ein Durchlauf: Eine Überweisung mit TAN-Freigabe hat deshalb
|
||||
-- mehrere. `auftrag_id` verbindet sie mit dem Zahlungsauftrag; die Verbindung
|
||||
-- fällt weg, wenn der Auftrag gelöscht wird, das Protokoll selbst bleibt.
|
||||
CREATE TABLE IF NOT EXISTS bank_protokolle (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
benutzer_id INT UNSIGNED NOT NULL,
|
||||
zugang_id INT UNSIGNED NULL,
|
||||
auftrag_id INT UNSIGNED NULL,
|
||||
art ENUM('ueberweisung','abruf','verfahren') NOT NULL,
|
||||
status ENUM('erfolg','fehler','offen') NOT NULL,
|
||||
meldung TEXT NULL,
|
||||
protokoll LONGTEXT NULL,
|
||||
erstellt_am DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_protokoll_auftrag (auftrag_id),
|
||||
KEY idx_protokoll_zugang (zugang_id, erstellt_am),
|
||||
KEY idx_protokoll_datum (benutzer_id, erstellt_am),
|
||||
CONSTRAINT fk_protokoll_benutzer FOREIGN KEY (benutzer_id)
|
||||
REFERENCES benutzer (id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_protokoll_zugang FOREIGN KEY (zugang_id)
|
||||
REFERENCES bank_zugaenge (id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_protokoll_auftrag FOREIGN KEY (auftrag_id)
|
||||
REFERENCES zahlungsauftraege (id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
|
@ -254,6 +254,7 @@ $router->post('/zahlungsverkehr/tan/abbrechen', fn() => (new ZahlungsverkehrCont
|
|||
$router->get('/zahlungsverkehr/vop', fn() => (new ZahlungsverkehrController())->vopFormular());
|
||||
$router->post('/zahlungsverkehr/vop', fn() => (new ZahlungsverkehrController())->vopBestaetigen());
|
||||
|
||||
$router->get('/zahlungsverkehr/{id}/protokoll', fn(array $p) => (new ZahlungsverkehrController())->protokoll($p));
|
||||
$router->get('/zahlungsverkehr/{id}', fn(array $p) => (new ZahlungsverkehrController())->bearbeiten($p));
|
||||
$router->post('/zahlungsverkehr/{id}', fn(array $p) => (new ZahlungsverkehrController())->speichern($p));
|
||||
$router->post('/zahlungsverkehr/{id}/senden', fn(array $p) => (new ZahlungsverkehrController())->senden($p));
|
||||
|
|
@ -305,6 +306,7 @@ $router->post('/banking/umsaetze/{id}/verwerfen',
|
|||
$router->post('/banking/umsaetze/{id}/zuruecksetzen',
|
||||
fn(array $p) => (new BankingController())->zuruecksetzen($p));
|
||||
|
||||
$router->get('/banking/protokoll', fn() => (new BankingController())->protokoll());
|
||||
$router->get('/banking/regeln', fn() => (new BankingController())->regeln());
|
||||
$router->post('/banking/regeln/neu', fn() => (new BankingController())->regelAnlegen());
|
||||
$router->post('/banking/regeln/pruefen', fn() => (new BankingController())->regelnPruefen());
|
||||
|
|
|
|||
|
|
@ -2012,6 +2012,7 @@ table.bericht .wertlink:hover {
|
|||
ein voll gefüllter Punkt zöge in einer Tabelle zu viel Aufmerksamkeit. */
|
||||
.marke-erfolg { color: var(--positiv); border-color: var(--positiv); }
|
||||
.marke-fehler { color: var(--negativ); border-color: var(--negativ); }
|
||||
.marke-offen { color: var(--warnung); border-color: var(--warnung); }
|
||||
.marke-tan_offen { color: var(--warnung); border-color: var(--warnung); }
|
||||
.marke-abgebrochen { color: var(--text-gedaempft); }
|
||||
|
||||
|
|
@ -2290,3 +2291,19 @@ table.liste tr.luecke td {
|
|||
border-bottom-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
/* Übermittlungsprotokoll: ein Block je Durchlauf, die Meldungen im Wortlaut
|
||||
klappen darunter auf. */
|
||||
.protokolldialog { width: min(60rem, 94vw); }
|
||||
|
||||
.protokolleintrag {
|
||||
margin: 0 0 .8rem;
|
||||
padding: .6rem .75rem;
|
||||
border: 1px solid var(--rand);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.protokolleintrag header { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; }
|
||||
.protokolleintrag p { margin: .4rem 0; }
|
||||
.protokolleintrag summary { cursor: pointer; font-size: .88rem; color: var(--text-gedaempft); }
|
||||
.protokolleintrag .protokolltext { max-height: 22rem; overflow: auto; word-break: break-all; }
|
||||
|
|
|
|||
|
|
@ -284,6 +284,33 @@
|
|||
});
|
||||
}
|
||||
|
||||
// Übermittlungsprotokoll im Zahlungsverkehr: Ein Klick auf den Status holt das
|
||||
// Protokoll der Überweisung in einen modalen Dialog. Ohne <dialog> oder
|
||||
// fetch bleibt der Status ein Link auf dieselbe Ansicht als eigene Seite.
|
||||
const protokolldialog = document.getElementById('protokolldialog');
|
||||
if (protokolldialog && typeof protokolldialog.showModal === 'function' && window.fetch) {
|
||||
const inhalt = protokolldialog.querySelector('[data-protokoll-inhalt]');
|
||||
|
||||
document.querySelectorAll('[data-protokoll]').forEach(function (link) {
|
||||
link.addEventListener('click', function (ereignis) {
|
||||
ereignis.preventDefault();
|
||||
|
||||
window.fetch(link.getAttribute('href'), {
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
||||
credentials: 'same-origin'
|
||||
}).then(function (antwort) {
|
||||
if (!antwort.ok) { throw new Error(antwort.status); }
|
||||
return antwort.text();
|
||||
}).then(function (html) {
|
||||
inhalt.innerHTML = html;
|
||||
protokolldialog.showModal();
|
||||
}).catch(function () {
|
||||
window.location.href = link.getAttribute('href');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Meldungen ausblenden. Über eine Klasse, nicht über element.style: Die
|
||||
// Content-Security-Policy (style-src 'self') verwirft auch per JavaScript
|
||||
// gesetzte Inline-Styles, die Meldung bliebe sonst einfach stehen.
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ use Finanzmanager\Database;
|
|||
use Finanzmanager\Models\AbgleichRegel;
|
||||
use Finanzmanager\Models\Bankabruf;
|
||||
use Finanzmanager\Models\Bankinstitut;
|
||||
use Finanzmanager\Models\Bankprotokoll;
|
||||
use Finanzmanager\Models\Bankumsatz;
|
||||
use Finanzmanager\Models\Bankzugang;
|
||||
use Finanzmanager\Models\Kategorie;
|
||||
|
|
@ -70,6 +71,26 @@ class BankingController extends BaseController
|
|||
* zwei ausgegrauten Hälften wäre für niemanden eine Hilfe. Die Wahl fällt
|
||||
* deshalb vorweg und ist danach nicht mehr umzustellen.
|
||||
*/
|
||||
/** Das Gesamtprotokoll: jede Kommunikation mit den Banken, filterbar. */
|
||||
public function protokoll(): void
|
||||
{
|
||||
$this->verlangeAnmeldung();
|
||||
|
||||
$filter = [
|
||||
'zugang_id' => (int)$this->query('zugang_id'),
|
||||
'von' => $this->query('von'),
|
||||
'bis' => $this->query('bis'),
|
||||
'text' => $this->query('text'),
|
||||
];
|
||||
|
||||
$this->render('banking/protokoll.php', [
|
||||
'titel' => 'Protokoll',
|
||||
'eintraege' => Bankprotokoll::suchen($filter),
|
||||
'zugaenge' => Bankzugang::alle(),
|
||||
'filter' => $filter,
|
||||
]);
|
||||
}
|
||||
|
||||
public function neu(): void
|
||||
{
|
||||
$this->verlangeAnmeldung();
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace Finanzmanager\Controllers;
|
||||
|
||||
use Finanzmanager\Models\Adresse;
|
||||
use Finanzmanager\Models\Bankprotokoll;
|
||||
use Finanzmanager\Models\Bankzugang;
|
||||
use Finanzmanager\Models\Kategorie;
|
||||
use Finanzmanager\Models\Klasse;
|
||||
|
|
@ -33,6 +34,30 @@ class ZahlungsverkehrController extends BaseController
|
|||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Das Übermittlungsprotokoll einer Überweisung. Das Skript holt es als
|
||||
* Fragment in den Dialog; ohne Skript ist es eine eigene Seite.
|
||||
*
|
||||
* @param array<string,string> $params
|
||||
*/
|
||||
public function protokoll(array $params): void
|
||||
{
|
||||
$this->verlangeAnmeldung();
|
||||
|
||||
$auftrag = Zahlungsauftrag::find((int)$params['id']);
|
||||
if ($auftrag === null) {
|
||||
View::redirect('/zahlungsverkehr', 'Den Zahlungsauftrag gibt es nicht.', 'fehler');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->render('zahlungsverkehr/protokoll.php', [
|
||||
'titel' => 'Übermittlungsprotokoll',
|
||||
'auftrag' => $auftrag,
|
||||
'eintraege' => Bankprotokoll::fuerAuftrag((int)$auftrag['id']),
|
||||
'fragment' => $this->istAjax(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function neu(): void
|
||||
{
|
||||
$this->verlangeAnmeldung();
|
||||
|
|
|
|||
115
src/Models/Bankprotokoll.php
Normal file
115
src/Models/Bankprotokoll.php
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Finanzmanager\Models;
|
||||
|
||||
use Finanzmanager\Database;
|
||||
use Finanzmanager\Mandant;
|
||||
|
||||
/**
|
||||
* Das Übermittlungsprotokoll: der vollständige Dialog mit der Bank, gesendete
|
||||
* und empfangene Meldungen.
|
||||
*
|
||||
* Es dient als Nachweis, dass eine Überweisung rechtzeitig und korrekt
|
||||
* übermittelt wurde – deshalb wird im Erfolgs- wie im Fehlerfall gespeichert
|
||||
* und nichts davon automatisch gelöscht.
|
||||
*/
|
||||
class Bankprotokoll
|
||||
{
|
||||
public const ARTEN = [
|
||||
'ueberweisung' => 'Überweisung',
|
||||
'abruf' => 'Umsatzabruf',
|
||||
'verfahren' => 'TAN-Verfahren',
|
||||
];
|
||||
|
||||
public const STATUS = [
|
||||
'erfolg' => 'erfolgreich',
|
||||
'fehler' => 'fehlgeschlagen',
|
||||
'offen' => 'wartet auf Freigabe',
|
||||
];
|
||||
|
||||
public static function speichern(
|
||||
?int $zugangId,
|
||||
string $art,
|
||||
string $status,
|
||||
string $meldung,
|
||||
string $protokoll,
|
||||
?int $auftragId = null
|
||||
): int {
|
||||
Database::execute(
|
||||
'INSERT INTO bank_protokolle
|
||||
(benutzer_id, zugang_id, auftrag_id, art, status, meldung, protokoll)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||||
[
|
||||
Mandant::id(),
|
||||
$zugangId === 0 ? null : $zugangId,
|
||||
$auftragId === 0 ? null : $auftragId,
|
||||
array_key_exists($art, self::ARTEN) ? $art : 'abruf',
|
||||
array_key_exists($status, self::STATUS) ? $status : 'fehler',
|
||||
mb_substr($meldung, 0, 2000),
|
||||
$protokoll,
|
||||
]
|
||||
);
|
||||
|
||||
return Database::lastInsertId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle Durchläufe zu einer Überweisung, der älteste zuerst.
|
||||
*
|
||||
* @return list<array<string,mixed>>
|
||||
*/
|
||||
public static function fuerAuftrag(int $auftragId): array
|
||||
{
|
||||
return Database::fetchAll(
|
||||
'SELECT p.*, z.name AS zugangsname
|
||||
FROM bank_protokolle p
|
||||
LEFT JOIN bank_zugaenge z ON z.id = p.zugang_id
|
||||
WHERE p.auftrag_id = ? AND p.benutzer_id = ?
|
||||
ORDER BY p.erstellt_am, p.id',
|
||||
[$auftragId, Mandant::id()]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Das Gesamtprotokoll, das neueste zuerst.
|
||||
*
|
||||
* @param array{zugang_id?: int, von?: string, bis?: string, text?: string} $filter
|
||||
* @return list<array<string,mixed>>
|
||||
*/
|
||||
public static function suchen(array $filter = [], int $limit = 200): array
|
||||
{
|
||||
$params = [Mandant::id()];
|
||||
$sql = 'SELECT p.*, z.name AS zugangsname, a.empfaenger, a.betrag
|
||||
FROM bank_protokolle p
|
||||
LEFT JOIN bank_zugaenge z ON z.id = p.zugang_id
|
||||
LEFT JOIN zahlungsauftraege a ON a.id = p.auftrag_id
|
||||
WHERE p.benutzer_id = ?';
|
||||
|
||||
if ((int)($filter['zugang_id'] ?? 0) > 0) {
|
||||
$sql .= ' AND p.zugang_id = ?';
|
||||
$params[] = (int)$filter['zugang_id'];
|
||||
}
|
||||
if (Buchung::istDatum((string)($filter['von'] ?? ''))) {
|
||||
$sql .= ' AND p.erstellt_am >= ?';
|
||||
$params[] = $filter['von'] . ' 00:00:00';
|
||||
}
|
||||
if (Buchung::istDatum((string)($filter['bis'] ?? ''))) {
|
||||
$sql .= ' AND p.erstellt_am <= ?';
|
||||
$params[] = $filter['bis'] . ' 23:59:59';
|
||||
}
|
||||
|
||||
$text = trim((string)($filter['text'] ?? ''));
|
||||
if ($text !== '') {
|
||||
$muster = '%' . addcslashes($text, '\\%_') . '%';
|
||||
$sql .= ' AND (p.protokoll LIKE ? OR p.meldung LIKE ? OR a.empfaenger LIKE ?)';
|
||||
array_push($params, $muster, $muster, $muster);
|
||||
}
|
||||
|
||||
return Database::fetchAll(
|
||||
$sql . ' ORDER BY p.erstellt_am DESC, p.id DESC LIMIT ' . max(1, $limit),
|
||||
$params
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -27,7 +27,8 @@ class Zahlungsauftrag
|
|||
public static function alle(string $status = ''): array
|
||||
{
|
||||
$params = [Mandant::id()];
|
||||
$sql = 'SELECT z.*, k.name AS kontoname, kat.pfad AS kategorie_pfad
|
||||
$sql = 'SELECT z.*, k.name AS kontoname, kat.pfad AS kategorie_pfad,
|
||||
(SELECT COUNT(*) FROM bank_protokolle p WHERE p.auftrag_id = z.id) AS protokolle
|
||||
FROM zahlungsauftraege z
|
||||
JOIN konten k ON k.id = z.konto_id
|
||||
LEFT JOIN kategorien kat ON kat.id = z.kategorie_id
|
||||
|
|
|
|||
63
src/Services/BankProtokollLogger.php
Normal file
63
src/Services/BankProtokollLogger.php
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Finanzmanager\Services;
|
||||
|
||||
use Psr\Log\AbstractLogger;
|
||||
use Stringable;
|
||||
|
||||
/**
|
||||
* Schneidet den FinTS-Dialog für das Übermittlungsprotokoll mit.
|
||||
*
|
||||
* Die Bibliothek meldet jede gesendete (`>`) und empfangene (`<`) Nachricht an
|
||||
* ihren Logger – unabhängig davon, ob das Debug-Log an ist. Dieser Logger
|
||||
* sammelt alles im Speicher und reicht es zusätzlich an das Debug-Log weiter.
|
||||
* Was ankommt, hat die Bibliothek bereits von PIN und Benutzerkennung befreit
|
||||
* (`SanitizingLogger`).
|
||||
*/
|
||||
class BankProtokollLogger extends AbstractLogger
|
||||
{
|
||||
/** @var list<string> */
|
||||
private array $zeilen = [];
|
||||
|
||||
private DebugLogAdapter $debug;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->debug = new DebugLogAdapter();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $level
|
||||
* @param array<string,mixed> $context
|
||||
*/
|
||||
public function log($level, string|Stringable $message, array $context = []): void
|
||||
{
|
||||
$this->debug->log($level, $message, $context);
|
||||
|
||||
$this->vermerke('[' . (is_scalar($level) ? (string)$level : 'info') . '] ' . (string)$message);
|
||||
}
|
||||
|
||||
/** Eine eigene Zeile (etwa ein Schritt der Anwendung) ins Protokoll. */
|
||||
public function vermerke(string $zeile): void
|
||||
{
|
||||
$this->zeilen[] = date('Y-m-d H:i:s') . ' ' . self::alsUtf8($zeile);
|
||||
}
|
||||
|
||||
public function text(): string
|
||||
{
|
||||
return implode("\n", $this->zeilen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Wire-Nachrichten kommen in ISO-8859-1; die Datenbank nimmt nur
|
||||
* gültiges UTF-8.
|
||||
*/
|
||||
private static function alsUtf8(string $text): string
|
||||
{
|
||||
return mb_check_encoding($text, 'UTF-8')
|
||||
? $text
|
||||
: mb_convert_encoding($text, 'UTF-8', 'ISO-8859-1');
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ use Fhp\Options\FinTsOptions;
|
|||
use Finanzmanager\Auth;
|
||||
use Finanzmanager\Models\AbgleichRegel;
|
||||
use Finanzmanager\Models\Bankabruf;
|
||||
use Finanzmanager\Models\Bankprotokoll;
|
||||
use Finanzmanager\Models\Bankumsatz;
|
||||
use Finanzmanager\Models\Bankzugang;
|
||||
use Finanzmanager\Models\Einstellung;
|
||||
|
|
@ -67,12 +68,16 @@ class FinTsAbruf
|
|||
|
||||
private FinTs $fints;
|
||||
|
||||
/** Schneidet den Dialog dieses Durchlaufs für das Übermittlungsprotokoll mit. */
|
||||
private BankProtokollLogger $mitschnitt;
|
||||
|
||||
/** @param array<string,mixed> $zugang mit entschlüsselter PIN */
|
||||
private function __construct(
|
||||
private array $zugang,
|
||||
private int $abrufId,
|
||||
private array $zustand
|
||||
) {
|
||||
$this->mitschnitt = new BankProtokollLogger();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -194,6 +199,32 @@ class FinTsAbruf
|
|||
* @return array<string,mixed>
|
||||
*/
|
||||
private function lauf(?string $tan): array
|
||||
{
|
||||
$ergebnis = $this->durchlauf($tan);
|
||||
|
||||
$this->mitschnitt->vermerke('Ergebnis: ' . $ergebnis['meldung']);
|
||||
try {
|
||||
Bankprotokoll::speichern(
|
||||
(int)$this->zugang['id'],
|
||||
'abruf',
|
||||
match ($ergebnis['status']) {
|
||||
'erfolg' => 'erfolg',
|
||||
'tan' => 'offen',
|
||||
default => 'fehler',
|
||||
},
|
||||
(string)$ergebnis['meldung'],
|
||||
$this->mitschnitt->text()
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
// Ein Fehler im Protokoll darf den Abruf nicht verdecken.
|
||||
DebugLog::fehler($e);
|
||||
}
|
||||
|
||||
return $ergebnis;
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private function durchlauf(?string $tan): array
|
||||
{
|
||||
$this->protokoll = $this->zustand['protokoll'] ?? [];
|
||||
|
||||
|
|
@ -568,7 +599,7 @@ class FinTsAbruf
|
|||
);
|
||||
|
||||
$fints = FinTs::new($optionen, $zugangsdaten, $this->zustand['dialog'] ?? null);
|
||||
$fints->setLogger(new DebugLogAdapter());
|
||||
$fints->setLogger($this->mitschnitt);
|
||||
|
||||
// Beim ersten Lauf steht das Verfahren noch nicht im Dialog – es kommt
|
||||
// aus der Einstellung des Zugangs (siehe `Services\FinTsVerfahren`).
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ use Fhp\Options\Credentials;
|
|||
use Fhp\Options\FinTsOptions;
|
||||
use Fhp\Segment\SPA\HISPAS;
|
||||
use Finanzmanager\Auth;
|
||||
use Finanzmanager\Models\Bankprotokoll;
|
||||
use Finanzmanager\Models\Bankzugang;
|
||||
use Finanzmanager\Models\Einstellung;
|
||||
use Finanzmanager\Models\Konto;
|
||||
|
|
@ -52,6 +53,9 @@ class FinTsUeberweisung
|
|||
/** … und so lange warten wir dazwischen höchstens (Sekunden). */
|
||||
private const POLLING_WARTEZEIT = 6;
|
||||
|
||||
/** Schneidet den Dialog dieses Durchlaufs für das Übermittlungsprotokoll mit. */
|
||||
private BankProtokollLogger $mitschnitt;
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $zugang mit entschlüsselter PIN
|
||||
* @param array<string,mixed> $zustand
|
||||
|
|
@ -61,6 +65,7 @@ class FinTsUeberweisung
|
|||
private int $auftragId,
|
||||
private array $zustand
|
||||
) {
|
||||
$this->mitschnitt = new BankProtokollLogger();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -168,8 +173,56 @@ class FinTsUeberweisung
|
|||
|
||||
// ── Ablauf ───────────────────────────────────────────────────────────────
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
/**
|
||||
* Ein Durchlauf samt Übermittlungsprotokoll: Was zwischen Finanzmanager und
|
||||
* Bank gesendet und empfangen wurde, wird gespeichert – ob es klappt oder
|
||||
* nicht.
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function lauf(?string $tan, bool $vopBestaetigt = false): array
|
||||
{
|
||||
$auftrag = Zahlungsauftrag::find($this->auftragId);
|
||||
if ($auftrag !== null) {
|
||||
$this->mitschnitt->vermerke(sprintf(
|
||||
'Überweisung %d: %s, IBAN %s, %s %s, Ausführung %s',
|
||||
$this->auftragId,
|
||||
$auftrag['empfaenger'],
|
||||
$auftrag['iban'],
|
||||
number_format((float)$auftrag['betrag'], 2, ',', '.'),
|
||||
$auftrag['waehrung'],
|
||||
$auftrag['ausfuehrung_am'] ?? 'sofort'
|
||||
));
|
||||
}
|
||||
|
||||
$ergebnis = $this->durchlauf($tan, $vopBestaetigt);
|
||||
|
||||
$status = match ($ergebnis['status']) {
|
||||
'erfolg' => 'erfolg',
|
||||
'tan', 'vop' => 'offen',
|
||||
default => 'fehler',
|
||||
};
|
||||
$this->mitschnitt->vermerke('Ergebnis: ' . $ergebnis['meldung']);
|
||||
|
||||
try {
|
||||
Bankprotokoll::speichern(
|
||||
(int)$this->zugang['id'],
|
||||
'ueberweisung',
|
||||
$status,
|
||||
(string)$ergebnis['meldung'],
|
||||
$this->mitschnitt->text(),
|
||||
$this->auftragId
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
// Ein Fehler im Protokoll darf die Überweisung nicht verdecken.
|
||||
DebugLog::fehler($e);
|
||||
}
|
||||
|
||||
return $ergebnis;
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private function durchlauf(?string $tan, bool $vopBestaetigt): array
|
||||
{
|
||||
$auftrag = Zahlungsauftrag::find($this->auftragId);
|
||||
if ($auftrag === null) {
|
||||
|
|
@ -600,7 +653,7 @@ class FinTsUeberweisung
|
|||
),
|
||||
$this->zustand['dialog'] ?? null
|
||||
);
|
||||
$fints->setLogger(new DebugLogAdapter());
|
||||
$fints->setLogger($this->mitschnitt);
|
||||
|
||||
// Dieselbe Wahl wie beim Umsatzabruf – eine Überweisung soll nicht
|
||||
// plötzlich ein anderes Verfahren verlangen als der Abruf.
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ use Fhp\FinTs;
|
|||
use Fhp\Model\TanMode;
|
||||
use Fhp\Options\Credentials;
|
||||
use Fhp\Options\FinTsOptions;
|
||||
use Finanzmanager\Models\Bankprotokoll;
|
||||
use Finanzmanager\Models\Bankzugang;
|
||||
use Finanzmanager\Models\Einstellung;
|
||||
|
||||
|
|
@ -44,6 +45,31 @@ class FinTsVerfahren
|
|||
* hinweis: string}>}
|
||||
*/
|
||||
public static function abfragen(int $zugangId): array
|
||||
{
|
||||
$mitschnitt = new BankProtokollLogger();
|
||||
$ergebnis = self::abfrageDurchfuehren($zugangId, $mitschnitt);
|
||||
|
||||
if (Bankzugang::zugangsdaten($zugangId) !== null) {
|
||||
$mitschnitt->vermerke('Ergebnis: ' . $ergebnis['meldung']);
|
||||
try {
|
||||
Bankprotokoll::speichern(
|
||||
$zugangId,
|
||||
'verfahren',
|
||||
$ergebnis['status'] === 'erfolg' ? 'erfolg' : 'fehler',
|
||||
$ergebnis['meldung'],
|
||||
$mitschnitt->text()
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
// Ein Fehler im Protokoll darf die Abfrage nicht verdecken.
|
||||
DebugLog::fehler($e);
|
||||
}
|
||||
}
|
||||
|
||||
return $ergebnis;
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private static function abfrageDurchfuehren(int $zugangId, BankProtokollLogger $mitschnitt): array
|
||||
{
|
||||
$zugang = Bankzugang::zugangsdaten($zugangId);
|
||||
if ($zugang === null) {
|
||||
|
|
@ -63,7 +89,7 @@ class FinTsVerfahren
|
|||
}
|
||||
|
||||
try {
|
||||
$fints = self::verbindung($zugang);
|
||||
$fints = self::verbindung($zugang, $mitschnitt);
|
||||
$modi = $fints->getTanModes();
|
||||
} catch (\Throwable $e) {
|
||||
DebugLog::fehler($e);
|
||||
|
|
@ -270,7 +296,7 @@ class FinTsVerfahren
|
|||
}
|
||||
|
||||
/** @param array<string,mixed> $zugang */
|
||||
private static function verbindung(array $zugang): FinTs
|
||||
private static function verbindung(array $zugang, BankProtokollLogger $mitschnitt): FinTs
|
||||
{
|
||||
$optionen = new FinTsOptions();
|
||||
$optionen->url = (string)$zugang['fints_url'];
|
||||
|
|
@ -288,7 +314,7 @@ class FinTsVerfahren
|
|||
$optionen,
|
||||
Credentials::create((string)$zugang['benutzerkennung'], (string)$zugang['pin'])
|
||||
);
|
||||
$fints->setLogger(new DebugLogAdapter());
|
||||
$fints->setLogger($mitschnitt);
|
||||
|
||||
return $fints;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ include __DIR__ . '/../layout/kopf.php';
|
|||
<span class="tab aktiv">Bankverbindungen</span>
|
||||
<a href="/banking/umsaetze" class="tab">Umsätze abgleichen<?= $offen > 0 ? ' (' . $offen . ')' : '' ?></a>
|
||||
<a href="/banking/regeln" class="tab">Regeln</a>
|
||||
<a href="/banking/protokoll" class="tab">Protokoll</a>
|
||||
</div>
|
||||
|
||||
<?php if (!$schluessel): ?>
|
||||
|
|
|
|||
57
src/Views/banking/protokoll.php
Normal file
57
src/Views/banking/protokoll.php
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
use Finanzmanager\View;
|
||||
|
||||
/**
|
||||
* Das Gesamtprotokoll: jede Kommunikation mit den Banken – Überweisungen,
|
||||
* Umsatzabrufe, Abfragen der TAN-Verfahren. Nach Bank, Zeitraum und Freitext
|
||||
* filterbar.
|
||||
*
|
||||
* @var list<array<string,mixed>> $eintraege
|
||||
* @var list<array<string,mixed>> $zugaenge
|
||||
* @var array{zugang_id: int, von: string, bis: string, text: string} $filter
|
||||
*/
|
||||
|
||||
$mitAuftrag = true;
|
||||
|
||||
include __DIR__ . '/../layout/kopf.php';
|
||||
?>
|
||||
|
||||
<div class="tableiste">
|
||||
<a href="/banking" class="tab">Bankverbindungen</a>
|
||||
<a href="/banking/umsaetze" class="tab">Umsätze abgleichen</a>
|
||||
<a href="/banking/regeln" class="tab">Regeln</a>
|
||||
<span class="tab aktiv">Protokoll</span>
|
||||
</div>
|
||||
|
||||
<form method="get" action="/banking/protokoll" class="filterleiste">
|
||||
<input type="search" name="text" placeholder="Freitext in den Meldungen"
|
||||
value="<?= View::h($filter['text']) ?>">
|
||||
<select name="zugang_id" aria-label="Bank">
|
||||
<option value="">Alle Banken</option>
|
||||
<?php foreach ($zugaenge as $zugang): ?>
|
||||
<option value="<?= (int)$zugang['id'] ?>"
|
||||
<?= $filter['zugang_id'] === (int)$zugang['id'] ? 'selected' : '' ?>>
|
||||
<?= View::h($zugang['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<label>von <input type="date" name="von" value="<?= View::h($filter['von']) ?>"></label>
|
||||
<label>bis <input type="date" name="bis" value="<?= View::h($filter['bis']) ?>"></label>
|
||||
<button type="submit" class="knopf klein primaer">Filtern</button>
|
||||
<a href="/banking/protokoll" class="knopf klein">Zurücksetzen</a>
|
||||
</form>
|
||||
|
||||
<p class="hinweis">
|
||||
Das Protokoll hält fest, was der Finanzmanager an die Bank gesendet und was sie geantwortet
|
||||
hat – als Nachweis, dass eine Überweisung rechtzeitig übermittelt wurde. PIN und TAN sind
|
||||
maskiert.
|
||||
</p>
|
||||
|
||||
<?php if ($eintraege === []): ?>
|
||||
<p class="platzhalter">Keine Einträge.</p>
|
||||
<?php else: ?>
|
||||
<?php include __DIR__ . '/protokolleintraege.php'; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php include __DIR__ . '/../layout/fuss.php'; ?>
|
||||
39
src/Views/banking/protokolleintraege.php
Normal file
39
src/Views/banking/protokolleintraege.php
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
use Finanzmanager\Models\Bankprotokoll;
|
||||
use Finanzmanager\View;
|
||||
|
||||
/**
|
||||
* Die Durchläufe des Übermittlungsprotokolls. Jeder steht in einem eigenen
|
||||
* Block; die Meldungen im Wortlaut liegen aufgeklappt darunter.
|
||||
*
|
||||
* @var list<array<string,mixed>> $eintraege
|
||||
* @var bool $mitAuftrag Ob zu jedem Eintrag der Auftrag genannt wird
|
||||
*/
|
||||
?>
|
||||
<?php foreach ($eintraege as $eintrag): ?>
|
||||
<section class="protokolleintrag">
|
||||
<header>
|
||||
<strong><?= View::h(date('d.m.Y H:i:s', strtotime((string)$eintrag['erstellt_am']))) ?></strong>
|
||||
<span class="marke marke-<?= View::h($eintrag['status']) ?>">
|
||||
<?= View::h(Bankprotokoll::STATUS[$eintrag['status']] ?? $eintrag['status']) ?>
|
||||
</span>
|
||||
<span class="nebentext">
|
||||
<?= View::h(Bankprotokoll::ARTEN[$eintrag['art']] ?? $eintrag['art']) ?>
|
||||
<?php if (($eintrag['zugangsname'] ?? '') !== ''): ?>
|
||||
· <?= View::h($eintrag['zugangsname']) ?>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($mitAuftrag) && ($eintrag['empfaenger'] ?? null) !== null): ?>
|
||||
· an <?= View::h($eintrag['empfaenger']) ?>, <?= View::h(View::betrag($eintrag['betrag'])) ?>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
</header>
|
||||
<?php if (($eintrag['meldung'] ?? '') !== ''): ?>
|
||||
<p><?= View::h($eintrag['meldung']) ?></p>
|
||||
<?php endif; ?>
|
||||
<details>
|
||||
<summary>Gesendete und empfangene Meldungen</summary>
|
||||
<pre class="protokolltext"><?= View::h($eintrag['protokoll'] ?? '') ?></pre>
|
||||
</details>
|
||||
</section>
|
||||
<?php endforeach; ?>
|
||||
|
|
@ -68,6 +68,7 @@ include __DIR__ . '/../layout/kopf.php';
|
|||
<div class="knopfleiste">
|
||||
<a href="/banking" class="knopf klein primaer">Bankverbindungen</a>
|
||||
<a href="/banking/regeln" class="knopf klein">Regeln</a>
|
||||
<a href="/banking/protokoll" class="knopf klein">Protokoll</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -71,9 +71,19 @@ include __DIR__ . '/../layout/kopf.php';
|
|||
: View::h(View::datum($auftrag['ausfuehrung_am'])) ?>
|
||||
</td>
|
||||
<td>
|
||||
<span class="marke marke-<?= View::h($auftrag['status']) ?>">
|
||||
<?= View::h(Zahlungsauftrag::STATUS[$auftrag['status']]) ?>
|
||||
</span>
|
||||
<?php if ((int)$auftrag['protokolle'] > 0): ?>
|
||||
<?php /* Der Status öffnet das Übermittlungsprotokoll; ohne
|
||||
Skript führt der Link auf dieselbe Ansicht als Seite. */ ?>
|
||||
<a href="/zahlungsverkehr/<?= (int)$auftrag['id'] ?>/protokoll"
|
||||
class="marke marke-<?= View::h($auftrag['status']) ?>" data-protokoll
|
||||
title="Übermittlungsprotokoll anzeigen">
|
||||
<?= View::h(Zahlungsauftrag::STATUS[$auftrag['status']]) ?>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<span class="marke marke-<?= View::h($auftrag['status']) ?>">
|
||||
<?= View::h(Zahlungsauftrag::STATUS[$auftrag['status']]) ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<?php if (($auftrag['meldung'] ?? '') !== '' && $auftrag['status'] === 'fehler'): ?>
|
||||
<span class="nebentext"><?= View::h($auftrag['meldung']) ?></span>
|
||||
<?php endif; ?>
|
||||
|
|
@ -99,4 +109,11 @@ include __DIR__ . '/../layout/kopf.php';
|
|||
</table>
|
||||
<?php endif; ?>
|
||||
|
||||
<dialog class="splitdialog protokolldialog" id="protokolldialog">
|
||||
<div data-protokoll-inhalt></div>
|
||||
<form method="dialog" class="knopfleiste">
|
||||
<button type="submit" class="knopf klein">Schließen</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<?php include __DIR__ . '/../layout/fuss.php'; ?>
|
||||
|
|
|
|||
35
src/Views/zahlungsverkehr/protokoll.php
Normal file
35
src/Views/zahlungsverkehr/protokoll.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
use Finanzmanager\Models\Zahlungsauftrag;
|
||||
use Finanzmanager\View;
|
||||
|
||||
/**
|
||||
* Das Übermittlungsprotokoll einer Überweisung – als Fragment für den Dialog
|
||||
* im Zahlungsverkehr, sonst als eigene Seite.
|
||||
*
|
||||
* @var array<string,mixed> $auftrag
|
||||
* @var list<array<string,mixed>> $eintraege
|
||||
* @var bool $fragment
|
||||
*/
|
||||
|
||||
if (!$fragment) {
|
||||
include __DIR__ . '/../layout/kopf.php';
|
||||
}
|
||||
?>
|
||||
|
||||
<h2>Übermittlungsprotokoll</h2>
|
||||
<p class="nebentext">
|
||||
Überweisung über <?= View::h(View::betrag($auftrag['betrag'])) ?> an
|
||||
<?= View::h($auftrag['empfaenger']) ?> – <?= View::h(Zahlungsauftrag::STATUS[$auftrag['status']]) ?>
|
||||
</p>
|
||||
|
||||
<?php if ($eintraege === []): ?>
|
||||
<p class="platzhalter">Zu dieser Überweisung liegt kein Protokoll vor.</p>
|
||||
<?php else: ?>
|
||||
<?php $mitAuftrag = false; include __DIR__ . '/../banking/protokolleintraege.php'; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!$fragment): ?>
|
||||
<p><a href="/zahlungsverkehr" class="knopf">Zurück zum Zahlungsverkehr</a></p>
|
||||
<?php include __DIR__ . '/../layout/fuss.php'; ?>
|
||||
<?php endif; ?>
|
||||
148
tests/42_UebermittlungsprotokollTest.php
Normal file
148
tests/42_UebermittlungsprotokollTest.php
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Finanzmanager\Database;
|
||||
use Finanzmanager\Models\Bankprotokoll;
|
||||
use Finanzmanager\Models\Bankzugang;
|
||||
use Finanzmanager\Models\Konto;
|
||||
use Finanzmanager\Models\Zahlungsauftrag;
|
||||
use Finanzmanager\Services\BankProtokollLogger;
|
||||
use Finanzmanager\Services\FinTsUeberweisung;
|
||||
|
||||
/**
|
||||
* Übermittlungsprotokoll: Nachweis gegenüber der Bank.
|
||||
*
|
||||
* Geprüft wird, dass der Dialog mit der Bank gespeichert wird – im Erfolgs- wie
|
||||
* im Fehlerfall –, dass er zur Überweisung wiederzufinden ist und dass das
|
||||
* Gesamtprotokoll nach Bank, Datum und Freitext filtert. Für den Fehlerfall geht
|
||||
* die Überweisung an eine Adresse auf dem eigenen Rechner, auf der niemand
|
||||
* lauscht: Es verlässt nichts den Container.
|
||||
*/
|
||||
class UebermittlungsprotokollTest extends TestCase
|
||||
{
|
||||
protected string $testName = 'Test 42: Übermittlungsprotokoll';
|
||||
|
||||
private int $giro = 0;
|
||||
private int $zugang = 0;
|
||||
private int $zweiterZugang = 0;
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$this->mandantAnlegen();
|
||||
$this->stammdaten();
|
||||
|
||||
$this->derMitschnittSammeltGesendetUndEmpfangen();
|
||||
$this->einProtokollHaengtAmAuftrag();
|
||||
$this->einFehlgeschlagenerVersuchHinterlaesstEinProtokoll();
|
||||
$this->dasGesamtprotokollFiltert();
|
||||
}
|
||||
|
||||
private function stammdaten(): void
|
||||
{
|
||||
$this->giro = Konto::anlegen([
|
||||
'name' => 'Protokoll-Giro', 'kontoart' => 'bank', 'anfangssaldo' => 0, 'aktiv' => 1,
|
||||
]);
|
||||
|
||||
$zugang = static fn(string $name): array => [
|
||||
'name' => $name,
|
||||
'bankname' => $name . ' AG',
|
||||
'blz' => '12030000',
|
||||
'fints_url' => 'https://banking.example.org/fints',
|
||||
'benutzerkennung' => 'testkunde',
|
||||
'pin' => 'geheim1234',
|
||||
'abrufzeitraum' => 'letzter_abruf',
|
||||
'aktiv' => 1,
|
||||
];
|
||||
|
||||
$this->zugang = Bankzugang::anlegen($zugang('Protokoll-Bank'));
|
||||
$this->zweiterZugang = Bankzugang::anlegen($zugang('Andere-Bank'));
|
||||
}
|
||||
|
||||
private function auftrag(): int
|
||||
{
|
||||
return Zahlungsauftrag::anlegen([
|
||||
'konto_id' => $this->giro,
|
||||
'zugang_id' => $this->zugang,
|
||||
'empfaenger' => 'Finanzamt Musterstadt',
|
||||
'iban' => 'DE02120300000000202051',
|
||||
'betrag' => 42.5,
|
||||
]);
|
||||
}
|
||||
|
||||
private function derMitschnittSammeltGesendetUndEmpfangen(): void
|
||||
{
|
||||
$logger = new BankProtokollLogger();
|
||||
$logger->debug('> HNHBK:1:3+000000000123');
|
||||
$logger->debug("< HNHBK:1:3+000000000456 Ümlaut\xE4");
|
||||
$logger->vermerke('Ergebnis: angenommen');
|
||||
|
||||
$text = $logger->text();
|
||||
$this->assert(str_contains($text, '> HNHBK:1:3+000000000123'), 'Die gesendete Meldung steht im Mitschnitt');
|
||||
$this->assert(str_contains($text, '< HNHBK:1:3+000000000456'), 'Die empfangene Meldung steht im Mitschnitt');
|
||||
$this->assert(str_contains($text, 'Ergebnis: angenommen'), 'Auch die eigenen Vermerke stehen darin');
|
||||
$this->assert(mb_check_encoding($text, 'UTF-8'), 'Der Mitschnitt ist gültiges UTF-8, auch nach Latin-1');
|
||||
}
|
||||
|
||||
private function einProtokollHaengtAmAuftrag(): void
|
||||
{
|
||||
$auftragId = $this->auftrag();
|
||||
|
||||
Bankprotokoll::speichern($this->zugang, 'ueberweisung', 'offen', 'TAN nötig', '> Anfrage', $auftragId);
|
||||
Bankprotokoll::speichern($this->zugang, 'ueberweisung', 'erfolg', 'angenommen', '< Antwort', $auftragId);
|
||||
|
||||
$eintraege = Bankprotokoll::fuerAuftrag($auftragId);
|
||||
$this->assertEquals(2, count($eintraege), 'Beide Durchläufe gehören zur Überweisung');
|
||||
$this->assertEquals('offen', $eintraege[0]['status'], 'Der älteste Durchlauf steht zuerst');
|
||||
$this->assertEquals('< Antwort', $eintraege[1]['protokoll'], 'Das Protokoll bleibt vollständig erhalten');
|
||||
|
||||
$liste = Zahlungsauftrag::alle();
|
||||
$this->assertEquals(2, (int)$liste[0]['protokolle'], 'Die Liste zählt die Protokolle je Überweisung');
|
||||
}
|
||||
|
||||
private function einFehlgeschlagenerVersuchHinterlaesstEinProtokoll(): void
|
||||
{
|
||||
// Auf dem eigenen Rechner lauscht niemand – der Versuch scheitert sofort.
|
||||
Database::execute(
|
||||
'UPDATE bank_zugaenge SET fints_url = ? WHERE id = ?',
|
||||
['http://127.0.0.1:9/fints', $this->zugang]
|
||||
);
|
||||
|
||||
$auftragId = $this->auftrag();
|
||||
$ergebnis = FinTsUeberweisung::senden($auftragId);
|
||||
FinTsUeberweisung::abbrechen();
|
||||
|
||||
$this->assertEquals('fehler', $ergebnis['status'], 'Ohne erreichbare Bank scheitert das Senden');
|
||||
|
||||
$eintraege = Bankprotokoll::fuerAuftrag($auftragId);
|
||||
$this->assertEquals(1, count($eintraege), 'Auch der Fehlerfall wird protokolliert');
|
||||
$this->assertEquals('fehler', $eintraege[0]['status'], 'Der Eintrag trägt den Status „fehler“');
|
||||
$this->assert(
|
||||
str_contains((string)$eintraege[0]['protokoll'], 'Finanzamt Musterstadt'),
|
||||
'Das Protokoll nennt die Überweisung'
|
||||
);
|
||||
$this->assert(
|
||||
!str_contains((string)$eintraege[0]['protokoll'], 'geheim1234'),
|
||||
'Die PIN steht nicht im Protokoll'
|
||||
);
|
||||
}
|
||||
|
||||
private function dasGesamtprotokollFiltert(): void
|
||||
{
|
||||
Bankprotokoll::speichern($this->zweiterZugang, 'abruf', 'erfolg', 'Umsätze geholt', 'Kontoauszug Nummer 77');
|
||||
|
||||
$alle = Bankprotokoll::suchen();
|
||||
$this->assertGreaterThan(3, count($alle), 'Das Gesamtprotokoll führt alle Durchläufe');
|
||||
|
||||
$andere = Bankprotokoll::suchen(['zugang_id' => $this->zweiterZugang]);
|
||||
$this->assertEquals(1, count($andere), 'Der Bankfilter zeigt nur diese Bank');
|
||||
|
||||
$treffer = Bankprotokoll::suchen(['text' => 'Nummer 77']);
|
||||
$this->assertEquals(1, count($treffer), 'Die Freitextsuche findet den Wortlaut der Meldungen');
|
||||
|
||||
$heute = date('Y-m-d');
|
||||
$this->assertGreaterThan(0, count(Bankprotokoll::suchen(['von' => $heute, 'bis' => $heute])), 'Heute ist im Zeitraum');
|
||||
$this->assertEquals(0, count(Bankprotokoll::suchen(['bis' => '2000-01-01'])), 'Ein Zeitraum in der Vergangenheit ist leer');
|
||||
$this->assertEquals(0, count(Bankprotokoll::suchen(['text' => '100%'])), 'Sonderzeichen der Suche sind wörtlich gemeint');
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue