feat: HKEKA-Implementierung, PDF-Bugfixes, Sortierung, Umsatz-Umbenennung

- HKEKA v3/v4/v5 Segmente fuer phpFinTS implementiert (VR Bank unterstuetzt kein HKEKP)
- GetElectronicStatement Action mit Base64-Erkennung und Quittungscode
- PDF-Deduplizierung per MD5 (Bank sendet identische Saldenmitteilungen)
- Saldenmitteilungen ohne Auszugsnummer werden uebersprungen
- Datums-Validierung: 30.02. (Bank-Konvention) wird auf 28.02. korrigiert
- Numerische Sortierung fuer statement_number (CAST statt String-Sort)
- Jahr-Filter: statement_year=0 ausgeschlossen
- Menue/Button: "Kontoauszuege" -> "Umsaetze" (statements.php zeigt MT940, nicht PDFs)
- Redirect nach FinTS-Abruf auf aktuelles Jahr statt year=0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-03-06 07:10:59 +01:00
parent 8b64fd24d3
commit 014a943f78
425 changed files with 2359 additions and 4223 deletions

View file

@ -2,66 +2,6 @@
Alle wesentlichen Änderungen an diesem Projekt werden in dieser Datei dokumentiert.
## [3.5] - 2026-03-05
### Hinzugefügt
- **PDF-Kontoauszüge per FinTS**: Elektronische Kontoauszüge direkt von der Bank abrufen
- **HKEKP**: Direkt-Abruf (für Banken die dies unterstützen)
- **HKKAA**: Fallback über Bank-Archiv/Postfach (VR Banken, etc.)
- Automatische Methodenwahl: System prüft BPD und wählt beste verfügbare Methode
- Neue Segmente für php-fints: EKP/* (HKEKP) und KAA/* (HKKAA)
- Integration in bestehende PDF-Kontoauszüge-Seite
- **Cronjob für automatischen PDF-Abruf**: Neue geplante Aufgabe `doAutoFetchPdf`
- Aktivierbar über Konstante `BANKIMPORT_PDF_AUTO_ENABLED`
- Ruft automatisch neue PDF-Kontoauszüge ab und speichert sie
### Geändert
- PDF-Kontoauszüge-Seite: Neues Layout mit zwei Spalten (FinTS-Abruf links, Upload rechts)
- fints.class.php: Neue Methoden für PDF-Abruf mit automatischer Methodenwahl
### Technisch
- Erweiterung der php-fints Bibliothek:
- HKEKP-Unterstützung (Segment/EKP/*): HKEKPv2, HIEKPv2, HIEKPSv2
- HKKAA-Unterstützung (Segment/KAA/*): HKKAAv2, HIKAAv2, HIKAASv1
- Action-Klassen: GetStatementPDF, GetStatementFromArchive
- Neue Methoden in BankImportFinTS:
- `getPdfStatementMethod()`: Prüft welche Methode die Bank unterstützt
- `getStatementPDFAuto()`: Automatische Methodenwahl
- `supportsArchiveStatements()`: Prüft HKKAA-Support
## [3.1] - 2026-03-05
### Hinzugefügt
- **Skonto-Erkennung**: Multi-Invoice-Matching erkennt jetzt Skonto-Abzüge bis 3%
- **Automatische Skonto-Verarbeitung**: Zahlungen werden proportional auf Rechnungen verteilt
- **Skonto-Vermerk**: Rechnungen werden mit `close_code='discount_vat'` als bezahlt markiert
- **Skonto-Dokumentation**: Notiz zeigt Skonto-Betrag pro Rechnung
### Verbessert
- **Match-Sortierung**: Matches mit näherem Betrag werden jetzt bevorzugt angezeigt
- **Multi-Match Score**: Von 90 auf 98 erhöht, damit Sammelzahlungen vor Einzelmatches erscheinen
- Sammelzahlungen mit Skonto werden jetzt korrekt als Gruppe erkannt (statt einzelner Rechnungen)
### Beispiel
Transaktion -523,40€ mit 3 Rechnungen (Summe 529,69€):
- Erkennt 6,29€ Skonto-Abzug
- Verteilt proportional: 59,37€ + 206,72€ + 257,32€
- Markiert alle 3 als "bezahlt mit Skonto"
## [3.0] - 2026-03-05
### Hinzugefügt
- **Repair-Seite**: Neue Admin-Seite zum Reparieren verwaister Transaktionen
- Findet Transaktionen die als "Neu" markiert sind, obwohl Zahlung bereits existiert
- Ermöglicht manuelles oder Batch-Reparieren
- Erreichbar über Admin-Setup → Admin-Werkzeuge
- **Admin-Werkzeuge Sektion**: Neuer Bereich im Setup für Wartungsfunktionen
### Geändert
- **Filter für bezahlte Rechnungen**: Zeigt jetzt alle bezahlten Rechnungen die noch nicht über BankImport verknüpft sind
- Prüft nur BankImport-Verknüpfung (nicht mehr payment.fk_bank)
- Ermöglicht nachträgliche Verknüpfung von extern bezahlten Rechnungen
## [2.9] - 2026-02-23
### Entfernt

175
CLAUDE.md
View file

@ -1,175 +0,0 @@
# BankImport Modul - Entwicklernotizen
## Modul-Übersicht
- **Name**: BankImport
- **Version**: 3.5
- **Pfad**: `/srv/http/dolibarr/custom/bankimport/`
- **Funktion**: FinTS/HBCI Kontoauszüge importieren und mit Dolibarr-Rechnungen abgleichen
## Wichtige Dateien
| Datei | Funktion |
|-------|----------|
| `class/banktransaction.class.php` | Hauptklasse für Transaktionen, findMatches(), confirmMultiplePayment() |
| `confirm.php` | Übersicht aller Matches zur Bestätigung |
| `card.php` | Einzelansicht einer Transaktion mit manueller Zuordnung |
| `repair.php` | Admin-Seite für verwaiste Transaktionen |
| `cron/bankimport.cron.php` | Cronjob für automatischen Import |
| `admin/cronmonitor.php` | Cron-Monitoring und Pause/Resume |
| `pdfstatements.php` | PDF-Kontoauszüge hochladen und per FinTS abrufen |
| `vendor/.../Segment/EKP/*` | HKEKP-Segmente für PDF-Abruf (direkt) |
| `vendor/.../Segment/KAA/*` | HKKAA-Segmente für PDF-Abruf (Archiv) |
| `vendor/.../Action/GetStatementPDF.php` | Action-Klasse für HKEKP |
| `vendor/.../Action/GetStatementFromArchive.php` | Action-Klasse für HKKAA |
## PDF-Kontoauszüge per FinTS
### Übersicht
Seit Version 3.5 können PDF-Kontoauszüge von der Bank abgerufen werden. Es gibt zwei Methoden:
| Methode | Segment | Beschreibung | Bank-Beispiele |
|---------|---------|--------------|----------------|
| **HKEKP** | Elektronischer Kontoauszug PDF | Direkt-Abruf | Sparkassen, einige Volksbanken |
| **HKKAA** | Kontoauszug aus Archiv | Abruf aus Bank-Postfach | VR Banken (z.B. VR Bank Schleswig-Holstein) |
Das System wählt automatisch die beste verfügbare Methode.
### Neue Dateien in php-fints
```
vendor/nemiah/php-fints/lib/Fhp/
├── Action/
│ ├── GetStatementPDF.php # HKEKP Action
│ └── GetStatementFromArchive.php # HKKAA Action
└── Segment/
├── EKP/ # HKEKP Segmente
│ ├── HKEKPv2.php
│ ├── HIEKPv2.php
│ └── ...
└── KAA/ # HKKAA Segmente
├── HKKAAv2.php
├── HIKAAv2.php
├── HIKAASv1.php
└── ParameterKontoauszugArchiv.php
```
### Verwendung (empfohlen: Auto-Modus)
```php
$fints = new BankImportFinTS($db);
$fints->login();
// Automatische Methodenwahl
$method = $fints->getPdfStatementMethod(); // 'HKEKP', 'HKKAA' oder false
if ($method) {
$result = $fints->getStatementPDFAuto(0);
if (is_array($result) && !empty($result['pdfData'])) {
$pdfData = $result['pdfData'];
$info = $result['info'];
$usedMethod = $result['method']; // 'HKEKP' oder 'HKKAA'
}
}
```
### Cronjob
- Aktivieren: `BANKIMPORT_PDF_AUTO_ENABLED = 1`
- Klasse: `BankImportCron::doAutoFetchPdf()`
- Frequenz: Wie Transaktions-Import konfigurierbar
## Multi-Invoice Matching (Sammelzahlungen)
### Ablauf
1. `findMatches()` sucht Einzelmatches und Multi-Matches
2. `findSupplierForMultiMatch()` findet Lieferant über IBAN oder Name (Similarity > 70%)
3. `findMultipleSupplierInvoiceMatches()` kombiniert Rechnungen deren ref_supplier im Buchungstext steht
4. Toleranz: **3% des Betrags** (für Skonto)
### Sortierung
Matches werden sortiert nach:
1. Betragsnähe (innerhalb 10% Threshold bevorzugt)
2. Score (Multi-Match: 98-100, Einzelmatch: variabel)
### Skonto-Verarbeitung
In `confirmMultiplePayment()`:
1. Wenn gezahlter Betrag < Rechnungssumme (max 5% Abweichung)
2. Zahlungen werden proportional verteilt (`discountFactor = actualAmount / totalPayment`)
3. Rechnungen werden mit `setPaid($user, 'discount_vat', 'Skonto (X EUR)')` geschlossen
## Datenbank-Struktur
### llx_bankimport_transaction
| Feld | Bedeutung |
|------|-----------|
| `status` | 0=Neu, 1=Zugeordnet, 2=Abgestimmt, 9=Ignoriert |
| `fk_facture_fourn` | Verknüpfte Lieferantenrechnung (erste bei Multi) |
| `fk_paiementfourn` | Verknüpfte Zahlung |
| `fk_bank` | Verknüpfter Bank-Eintrag |
| `note_private` | Multi-invoice payment: SI..., SI..., SI... (Skonto: X EUR) |
### Rechnung (close_code)
| Code | Bedeutung |
|------|-----------|
| `NULL` | Normal bezahlt |
| `discount_vat` | Mit Skonto bezahlt |
| `badsupplier` | Zahlungsausfall |
| `abandon` | Aufgegeben |
## Bekannte Edge Cases
### Problem: Nur 1 statt 3 Rechnungen bestätigt
- **Ursache**: Multi-Match hatte niedrigeren Score als Einzelmatch
- **Lösung**: Score auf 98 erhöht + Sortierung nach Betragsnähe
### Problem: Skonto nicht erkannt
- **Ursache**: Toleranz war 5€ fix, Skonto war 6,29€
- **Lösung**: Toleranz auf 3% des Betrags geändert
### Problem: Bezahlte Rechnungen nicht in Liste
- **Ursache**: Filter prüfte payment.fk_bank statt BankImport-Verknüpfung
- **Lösung**: Nur `llx_bankimport_transaction.status > 0` prüfen
## Test-Datenbank
- **Host**: 192.168.155.1 (Produktiv) / 192.168.155.11 (Test)
- **User**: dolibarr
- **Passwort**: 8715
## Typische Debug-Queries
```sql
-- Transaktion mit Verknüpfungen
SELECT rowid, ref, amount, status, fk_facture_fourn, fk_paiementfourn, fk_bank, note_private
FROM llx_bankimport_transaction WHERE rowid = X;
-- Zahlungsverteilung prüfen
SELECT pf.fk_paiementfourn, pf.fk_facturefourn, pf.amount, f.ref
FROM llx_paiementfourn_facturefourn pf
JOIN llx_facture_fourn f ON f.rowid = pf.fk_facturefourn
WHERE pf.fk_paiementfourn = X;
-- Rechnungen mit Skonto-Status
SELECT rowid, ref, total_ttc, fk_statut, paye, close_code, close_note
FROM llx_facture_fourn WHERE rowid IN (X, Y, Z);
```
## Zurücksetzen einer Transaktion (für Tests)
```sql
-- 1. Zahlung-Rechnung Verknüpfung löschen
DELETE FROM llx_paiementfourn_facturefourn WHERE fk_paiementfourn = X;
-- 2. Bank-URL löschen
DELETE FROM llx_bank_url WHERE url_id = X AND type = 'payment_supplier';
-- 3. Zahlung löschen
DELETE FROM llx_paiementfourn WHERE rowid = X;
-- 4. Bank-Eintrag löschen
DELETE FROM llx_bank WHERE rowid = Y;
-- 5. Rechnung(en) auf offen setzen
UPDATE llx_facture_fourn SET fk_statut = 1, paye = 0, close_code = NULL, close_note = NULL WHERE rowid IN (...);
-- 6. BankImport-Transaktion zurücksetzen
UPDATE llx_bankimport_transaction
SET status = 0, fk_facture_fourn = NULL, fk_paiementfourn = NULL, fk_bank = NULL,
fk_user_match = NULL, date_match = NULL
WHERE rowid = Z;
```

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -16,7 +16,6 @@
require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
dol_include_once('/bankimport/class/fints.class.php');
dol_include_once('/bankimport/class/banktransaction.class.php');
dol_include_once('/bankimport/class/bankstatement.class.php');
/**
* Class BankImportCron
@ -733,184 +732,4 @@ class BankImportCron
return $this->doAutoImport();
}
/**
* Execute automatic PDF statement fetch via HKEKP
* Called by Dolibarr's scheduled task system
*
* @return int 0 if OK, < 0 if error
*/
public function doAutoFetchPdf()
{
global $conf, $langs, $user;
// Initialize timing
$this->startTime = microtime(true);
$langs->load('bankimport@bankimport');
$this->cronLog("========== PDF FETCH CRON START ==========");
$this->recordCronStatus('started', 'PDF fetch cron started');
// Check if PDF fetch is enabled
if (!getDolGlobalInt('BANKIMPORT_PDF_AUTO_ENABLED')) {
$this->output = $langs->trans('AutoPdfFetchDisabled');
$this->cronLog("Auto PDF fetch is disabled - exiting");
$this->recordCronStatus('completed', 'Auto PDF fetch disabled');
return 0;
}
// Check cron pause status (shared with main cron)
$pausedUntil = getDolGlobalInt('BANKIMPORT_CRON_PAUSED_UNTIL');
if ($pausedUntil > 0 && $pausedUntil > time()) {
$pauseReason = getDolGlobalString('BANKIMPORT_CRON_PAUSE_REASON');
$this->output = "Cron pausiert: {$pauseReason}";
$this->cronLog("Cron is PAUSED - skipping PDF fetch", 'WARNING');
return 0;
}
$this->cronLog("Initializing FinTS for PDF fetch");
// Initialize FinTS
$fints = new BankImportFinTS($this->db);
if (!$fints->isConfigured()) {
$this->error = $langs->trans('AutoImportNotConfigured');
$this->cronLog("FinTS not configured", 'WARNING');
return -1;
}
try {
// Login to bank
$this->cronLog("Logging in to bank");
$loginResult = $fints->login();
if ($loginResult < 0) {
$this->error = $fints->error;
$this->cronLog("Login failed: ".$fints->error, 'ERROR');
return -1;
}
if ($loginResult == 0) {
// TAN required
$this->output = $langs->trans('TANRequired');
$this->cronLog("TAN required for PDF fetch", 'WARNING');
$this->recordCronStatus('completed', 'TAN required');
return 0;
}
// Check if bank supports PDF statements
if (!$fints->supportsPdfStatements()) {
$this->output = $langs->trans('ErrorBankDoesNotSupportPdfStatements');
$this->cronLog("Bank does not support PDF statements (HKEKP)", 'WARNING');
$fints->close();
return 0;
}
// Fetch PDF statement
$this->cronLog("Fetching PDF statement via HKEKP");
$pdfResult = $fints->getStatementPDF(0);
if ($pdfResult === 0) {
// TAN required
$this->output = $langs->trans('TANRequired');
$this->cronLog("TAN required for PDF statement", 'WARNING');
$fints->close();
return 0;
}
if ($pdfResult === -1) {
$this->error = $fints->error;
$this->cronLog("PDF fetch failed: ".$fints->error, 'ERROR');
$fints->close();
return -1;
}
// Check if we got any data
if (empty($pdfResult['pdfData'])) {
$this->output = $langs->trans('NoPdfStatementsAvailable');
$this->cronLog("No new PDF statements available");
$fints->close();
return 0;
}
// Save the PDF
$info = $pdfResult['info'];
$pdfData = $pdfResult['pdfData'];
$this->cronLog("Received PDF statement #".$info['statementNumber'].'/'.$info['statementYear']);
// Check if statement already exists
$stmt = new BankImportStatement($this->db);
$stmt->statement_number = $info['statementNumber'];
$stmt->statement_year = $info['statementYear'];
$stmt->iban = $info['iban'] ?: getDolGlobalString('BANKIMPORT_IBAN');
if ($stmt->exists()) {
$this->output = $langs->trans("StatementAlreadyExists").': '.$stmt->statement_number.'/'.$stmt->statement_year;
$this->cronLog("Statement already exists - skipping");
$fints->close();
return 0;
}
// Save PDF to file
$dir = BankImportStatement::getStorageDir();
$ibanPart = preg_replace('/[^A-Z0-9]/', '', strtoupper($stmt->iban ?: 'KONTO'));
$filename = sprintf('Kontoauszug_%s_%d_%s.pdf',
$ibanPart,
$stmt->statement_year,
str_pad($stmt->statement_number, 3, '0', STR_PAD_LEFT)
);
$filepath = $dir.'/'.$filename;
if (file_put_contents($filepath, $pdfData) === false) {
$this->error = $langs->trans("ErrorSavingPdfFile");
$this->cronLog("Failed to save PDF file", 'ERROR');
$fints->close();
return -1;
}
// Create database record
$stmt->filename = $filename;
$stmt->filepath = $filepath;
$stmt->filesize = strlen($pdfData);
$stmt->statement_date = $info['creationDate'] ? $info['creationDate']->getTimestamp() : dol_now();
$stmt->import_key = 'fints_cron_'.date('YmdHis');
// Get system user
$importUser = new User($this->db);
$importUser->fetch(1);
$result = $stmt->create($importUser);
if ($result > 0) {
$this->output = $langs->trans("PdfStatementFetched", $stmt->statement_number.'/'.$stmt->statement_year);
$this->cronLog("PDF statement saved successfully: ".$stmt->statement_number.'/'.$stmt->statement_year);
// Update last fetch timestamp
dolibarr_set_const($this->db, 'BANKIMPORT_PDF_LAST_FETCH', time(), 'chaine', 0, '', $conf->entity);
} else {
$this->error = $stmt->error;
$this->cronLog("Failed to create database record: ".$stmt->error, 'ERROR');
@unlink($filepath);
$fints->close();
return -1;
}
$fints->close();
$duration = round(microtime(true) - $this->startTime, 2);
$this->cronLog("PDF fetch completed successfully in {$duration}s");
$this->recordCronStatus('completed', "PDF fetched: {$stmt->statement_number}/{$stmt->statement_year}");
$this->cronLog("========== PDF FETCH CRON END ==========");
return 0;
} catch (Exception $e) {
$this->error = 'Exception: '.$e->getMessage();
$this->cronLog("EXCEPTION: ".$e->getMessage(), 'ERROR');
$this->recordCronStatus('error', 'Exception: '.$e->getMessage());
return -1;
}
}
}

View file

@ -299,7 +299,13 @@ class BankImportStatement extends CommonObject
}
// Sort and limit
$sql .= $this->db->order($sortfield, $sortorder);
if (strpos($sortfield, 'statement_number') !== false) {
// statement_number numerisch sortieren (db->order() sanitiert CAST-Ausdruecke)
$dir = strtoupper($sortorder) === 'DESC' ? 'DESC' : 'ASC';
$sql .= " ORDER BY t.statement_year ".$dir.", CAST(t.statement_number AS UNSIGNED) ".$dir;
} else {
$sql .= $this->db->order($sortfield, $sortorder);
}
if ($limit > 0) {
$sql .= $this->db->plimit($limit, $offset);
}
@ -747,6 +753,7 @@ class BankImportStatement extends CommonObject
$sql = "SELECT DISTINCT statement_year";
$sql .= " FROM ".MAIN_DB_PREFIX."bankimport_statement";
$sql .= " WHERE entity = ".((int) $this->entity);
$sql .= " AND statement_year > 0";
$sql .= " ORDER BY statement_year DESC";
$result = array();
@ -1166,6 +1173,14 @@ class BankImportStatement extends CommonObject
$txType = trim($rest);
}
// Ungueltige Daten korrigieren (z.B. 30.02. bei Monatsabschluss)
if (!checkdate($bookMonth, $bookDay, $bookYear)) {
$bookDay = (int) date('t', mktime(0, 0, 0, $bookMonth, 1, $bookYear));
}
if (!checkdate($valMonth, $valDay, $valYear)) {
$valDay = (int) date('t', mktime(0, 0, 0, $valMonth, 1, $valYear));
}
// Build date strings
$dateBooking = sprintf('%04d-%02d-%02d', $bookYear, $bookMonth, $bookDay);
$dateValue = sprintf('%04d-%02d-%02d', $valYear, $valMonth, $valDay);

View file

@ -38,8 +38,8 @@ use Fhp\Options\Credentials;
use Fhp\Action\GetSEPAAccounts;
use Fhp\Action\GetStatementOfAccount;
use Fhp\Action\GetStatementOfAccountXML;
use Fhp\Action\GetStatementPDF;
use Fhp\Action\GetStatementFromArchive;
use Fhp\Action\GetBankStatement;
use Fhp\Action\GetElectronicStatement;
use Fhp\Model\StatementOfAccount\Statement;
use Fhp\Model\StatementOfAccount\Transaction;
@ -296,16 +296,28 @@ class BankImportFinTS
*/
public function login($tanModeId = null)
{
dol_syslog("BankImport FinTS: login() gestartet, tanModeId=".($tanModeId ?: 'auto'), LOG_DEBUG);
if ($this->fints === null) {
dol_syslog("BankImport FinTS: Keine bestehende Verbindung, rufe initConnection() auf", LOG_DEBUG);
$result = $this->initConnection();
dol_syslog("BankImport FinTS: initConnection() Ergebnis=".$result, LOG_DEBUG);
if ($result < 0) {
dol_syslog("BankImport FinTS: initConnection() FEHLGESCHLAGEN: ".$this->error, LOG_ERR);
return -1;
}
}
try {
// Get and select TAN mode
dol_syslog("BankImport FinTS: Rufe getTanModes() ab...", LOG_DEBUG);
$tanModes = $this->fints->getTanModes();
dol_syslog("BankImport FinTS: ".count($tanModes)." TAN-Modi verfuegbar", LOG_DEBUG);
foreach ($tanModes as $mode) {
dol_syslog("BankImport FinTS: TAN-Modus: ID=".$mode->getId()." Name=".$mode->getName()
." Decoupled=".($mode->isDecoupled() ? 'JA' : 'NEIN'), LOG_DEBUG);
}
if ($tanModeId !== null) {
foreach ($tanModes as $mode) {
@ -330,24 +342,34 @@ class BankImportFinTS
if ($this->selectedTanMode === null) {
$this->error = 'No TAN mode available';
dol_syslog("BankImport FinTS: KEIN TAN-Modus ausgewaehlt", LOG_ERR);
return -1;
}
dol_syslog("BankImport FinTS: Ausgewaehlter TAN-Modus: ID=".$this->selectedTanMode->getId()
." Name=".$this->selectedTanMode->getName()
." Decoupled=".($this->selectedTanMode->isDecoupled() ? 'JA' : 'NEIN'), LOG_DEBUG);
$this->fints->selectTanMode($this->selectedTanMode);
dol_syslog("BankImport FinTS: TAN-Modus gesetzt, starte fints->login()...", LOG_DEBUG);
// Perform login
$login = $this->fints->login();
dol_syslog("BankImport FinTS: fints->login() abgeschlossen, needsTan=".($login->needsTan() ? 'JA' : 'NEIN'), LOG_DEBUG);
if ($login->needsTan()) {
$this->pendingAction = $login;
$tanRequest = $login->getTanRequest();
$this->tanChallenge = $tanRequest->getChallenge();
dol_syslog("BankImport FinTS: TAN benoetigt, Challenge-Laenge=".strlen($this->tanChallenge), LOG_DEBUG);
return 0; // TAN required
}
dol_syslog("BankImport FinTS: Login erfolgreich (kein TAN benoetigt)", LOG_DEBUG);
return 1;
} catch (Exception $e) {
$this->error = $e->getMessage();
dol_syslog("BankImport FinTS: Login EXCEPTION: ".$e->getMessage()."\n".$e->getTraceAsString(), LOG_ERR);
return -1;
}
}
@ -417,7 +439,10 @@ class BankImportFinTS
*/
public function getBankParameters()
{
dol_syslog("BankImport FinTS: getBankParameters() aufgerufen", LOG_DEBUG);
if ($this->fints === null) {
dol_syslog("BankImport FinTS: getBankParameters() - fints===null", LOG_WARNING);
return array('error' => 'Not connected');
}
@ -429,22 +454,30 @@ class BankImportFinTS
$bpd = $bpdProperty->getValue($this->fints);
if ($bpd === null) {
dol_syslog("BankImport FinTS: getBankParameters() - BPD ist null (noch kein Login?)", LOG_WARNING);
return array('error' => 'BPD not available');
}
dol_syslog("BankImport FinTS: BPD-Klasse: ".get_class($bpd), LOG_DEBUG);
// Get parameters property from BPD
$bpdReflection = new \ReflectionClass($bpd);
$paramsProperty = $bpdReflection->getProperty('parameters');
$paramsProperty->setAccessible(true);
$parameters = $paramsProperty->getValue($bpd);
dol_syslog("BankImport FinTS: BPD enthaelt ".count($parameters)." Segment-Typen", LOG_DEBUG);
$result = array();
foreach ($parameters as $type => $versions) {
$result[$type] = array_keys($versions);
}
dol_syslog("BankImport FinTS: BPD-Segmente: ".implode(', ', array_keys($result)), LOG_DEBUG);
return $result;
} catch (Exception $e) {
dol_syslog("BankImport FinTS: getBankParameters() Exception: ".$e->getMessage(), LOG_ERR);
return array('error' => $e->getMessage());
}
}
@ -910,6 +943,224 @@ class BankImportFinTS
}
}
/**
* Prueft ob die Bank elektronische Kontoauszuege (PDF) unterstuetzt
*
* Prueft sowohl HKEKP (HIEPS) als auch HKEKA (HIEKAS).
*
* @return bool True wenn HIEPS oder HIEKAS in den BPD vorhanden
*/
public function supportsStatementPdf()
{
if ($this->fints === null) {
return false;
}
try {
$params = $this->getBankParameters();
return isset($params['HIEPS']) || isset($params['HIEKAS']);
} catch (Exception $e) {
return false;
}
}
/**
* Elektronische Kontoauszuege als PDF von der Bank abrufen (HKEKP)
*
* @param string|null $statementNumber Optionale Auszugsnummer
* @param string|null $year Optionales Jahr (JJJJ)
* @return array|int Array mit PDF-Daten oder -1 bei Fehler, 0 wenn TAN benoetigt
*/
public function fetchBankStatements($statementNumber = null, $year = null)
{
dol_syslog("BankImport HKEKP: ===== fetchBankStatements() START =====", LOG_DEBUG);
dol_syslog("BankImport HKEKP: Parameter: statementNumber=".($statementNumber ?: 'null').", year=".($year ?: 'null'), LOG_DEBUG);
if (!$this->isConfigured()) {
$this->error = 'Konfiguration unvollstaendig';
dol_syslog("BankImport HKEKP: ABBRUCH - nicht konfiguriert", LOG_ERR);
return -1;
}
if ($this->fints === null) {
$this->error = 'Nicht verbunden. Bitte zuerst login() aufrufen.';
dol_syslog("BankImport HKEKP: ABBRUCH - fints===null, kein Login", LOG_ERR);
return -1;
}
try {
// Konten abrufen
dol_syslog("BankImport HKEKP: Rufe GetSEPAAccounts ab...", LOG_DEBUG);
$getAccounts = GetSEPAAccounts::create();
$this->fints->execute($getAccounts);
dol_syslog("BankImport HKEKP: GetSEPAAccounts ausgefuehrt", LOG_DEBUG);
if ($getAccounts->needsTan()) {
dol_syslog("BankImport HKEKP: GetSEPAAccounts benoetigt TAN", LOG_DEBUG);
$this->pendingAction = $getAccounts;
$tanRequest = $getAccounts->getTanRequest();
$this->tanChallenge = $tanRequest->getChallenge();
return 0;
}
$accounts = $getAccounts->getAccounts();
dol_syslog("BankImport HKEKP: ".count($accounts)." SEPA-Konten gefunden", LOG_DEBUG);
foreach ($accounts as $idx => $acc) {
dol_syslog("BankImport HKEKP: Konto ".($idx+1).": IBAN=".$acc->getIban()
.", BIC=".$acc->getBic()
.", Ktonr=".$acc->getAccountNumber()
.", BLZ=".$acc->getSubAccount(), LOG_DEBUG);
}
// Passendes Konto per IBAN finden
$selectedAccount = null;
dol_syslog("BankImport HKEKP: Suche Konto mit IBAN=".$this->iban, LOG_DEBUG);
foreach ($accounts as $account) {
if ($account->getIban() === $this->iban) {
$selectedAccount = $account;
dol_syslog("BankImport HKEKP: IBAN-Match gefunden!", LOG_DEBUG);
break;
}
}
if ($selectedAccount === null) {
if (!empty($accounts)) {
$selectedAccount = $accounts[0];
dol_syslog("BankImport HKEKP: KEIN IBAN-Match! Verwende erstes Konto: ".$selectedAccount->getIban(), LOG_WARNING);
} else {
$this->error = 'Keine Konten gefunden';
dol_syslog("BankImport HKEKP: ABBRUCH - keine Konten gefunden", LOG_ERR);
return -1;
}
}
// BPD (Bank Parameter Data) abfragen und loggen
dol_syslog("BankImport HKEKP: Rufe getBankParameters() ab...", LOG_DEBUG);
$params = $this->getBankParameters();
dol_syslog("BankImport HKEKP: BPD enthaelt ".count($params)." Geschaeftsvorfaelle", LOG_DEBUG);
dol_syslog("BankImport HKEKP: BPD-Segmente: ".implode(', ', array_keys($params)), LOG_DEBUG);
// Detailliert loggen welche Versionen unterstuetzt werden
foreach ($params as $segName => $versions) {
if (in_array($segName, ['HIEPS', 'HIEKAS', 'HIKEP', 'HIKAS', 'HIKAZS', 'HICAZS', 'HIPINS'])) {
dol_syslog("BankImport: BPD Detail: ".$segName." Versionen=[".implode(',', $versions)."]", LOG_DEBUG);
}
}
// Strategie: HKEKP (PDF-spezifisch) bevorzugen, Fallback auf HKEKA (generisch)
$useHkekp = isset($params['HIEPS']);
$useHkeka = isset($params['HIEKAS']);
dol_syslog("BankImport: HIEPS (HKEKP)=".($useHkekp ? 'JA' : 'NEIN')
.", HIEKAS (HKEKA)=".($useHkeka ? 'JA' : 'NEIN'), LOG_DEBUG);
if (!$useHkekp && !$useHkeka) {
$this->error = 'Die Bank unterstuetzt keine elektronischen Kontoauszuege (weder HKEKP/HIEPS noch HKEKA/HIEKAS in BPD)';
dol_syslog("BankImport: Weder HIEPS noch HIEKAS in BPD! Kontoauszuege nicht unterstuetzt.", LOG_WARNING);
dol_syslog("BankImport: Alle verfuegbaren BPD-Parameter: ".implode(', ', array_keys($params)), LOG_WARNING);
return -1;
}
dol_syslog("BankImport: Starte Abruf fuer ".$selectedAccount->getIban()
.($statementNumber ? " Nr.".$statementNumber : " (alle)")
.($year ? " Jahr ".$year : " (alle Jahre)"), LOG_DEBUG);
$pdfStatements = [];
if ($useHkekp) {
// === HKEKP: PDF-spezifischer Kontoauszug ===
dol_syslog("BankImport HKEKP: HIEPS gefunden! Versionen: [".implode(',', $params['HIEPS'])."]", LOG_DEBUG);
dol_syslog("BankImport HKEKP: Erstelle GetBankStatement Action...", LOG_DEBUG);
$getBankStatement = GetBankStatement::create($selectedAccount, $statementNumber, $year);
dol_syslog("BankImport HKEKP: Fuehre GetBankStatement aus (fints->execute)...", LOG_DEBUG);
$this->fints->execute($getBankStatement);
dol_syslog("BankImport HKEKP: GetBankStatement ausgefuehrt", LOG_DEBUG);
if ($getBankStatement->needsTan()) {
dol_syslog("BankImport HKEKP: GetBankStatement benoetigt TAN", LOG_DEBUG);
$this->pendingAction = $getBankStatement;
$tanRequest = $getBankStatement->getTanRequest();
$this->tanChallenge = $tanRequest->getChallenge();
return 0;
}
$pdfStatements = $getBankStatement->getPdfStatements();
dol_syslog("BankImport HKEKP: ".count($pdfStatements)." PDFs empfangen", LOG_DEBUG);
} else {
// === HKEKA: Generischer Kontoauszug (mit Format=PDF) ===
dol_syslog("BankImport HKEKA: HIEKAS gefunden! Versionen: [".implode(',', $params['HIEKAS'])."]", LOG_DEBUG);
dol_syslog("BankImport HKEKA: Verwende HKEKA mit Format=3 (PDF)", LOG_DEBUG);
dol_syslog("BankImport HKEKA: Erstelle GetElectronicStatement Action...", LOG_DEBUG);
$getStatement = GetElectronicStatement::create(
$selectedAccount,
GetElectronicStatement::FORMAT_PDF,
$statementNumber,
$year
);
dol_syslog("BankImport HKEKA: Fuehre GetElectronicStatement aus...", LOG_DEBUG);
$this->fints->execute($getStatement);
dol_syslog("BankImport HKEKA: GetElectronicStatement ausgefuehrt", LOG_DEBUG);
if ($getStatement->needsTan()) {
dol_syslog("BankImport HKEKA: GetElectronicStatement benoetigt TAN", LOG_DEBUG);
$this->pendingAction = $getStatement;
$tanRequest = $getStatement->getTanRequest();
$this->tanChallenge = $tanRequest->getChallenge();
return 0;
}
// Alle Statements holen, nach PDF filtern
$allStatements = $getStatement->getStatements();
dol_syslog("BankImport HKEKA: ".count($allStatements)." Statements empfangen", LOG_DEBUG);
foreach ($allStatements as $sIdx => $stmt) {
$format = $stmt['format'] ?? 'unbekannt';
$data = $stmt['data'];
dol_syslog("BankImport HKEKA: Statement ".($sIdx+1).": Format=".$format
.", ".strlen($data)." Bytes, Anfang=".substr($data, 0, 10), LOG_DEBUG);
}
// PDFs extrahieren (getPdfStatements filtert nach Format=3 oder %PDF-)
$pdfStatements = $getStatement->getPdfStatements();
dol_syslog("BankImport HKEKA: Davon ".count($pdfStatements)." PDFs", LOG_DEBUG);
// Falls keine PDFs, aber andere Formate: Hinweis
if (empty($pdfStatements) && !empty($allStatements)) {
$formats = array_map(function($s) { return $s['format'] ?? '?'; }, $allStatements);
dol_syslog("BankImport HKEKA: Keine PDFs! Empfangene Formate: ".implode(', ', $formats), LOG_WARNING);
$this->error = 'Kontoauszuege empfangen, aber nicht im PDF-Format (Formate: '.implode(', ', $formats).')';
return -1;
}
}
foreach ($pdfStatements as $pIdx => $pdf) {
dol_syslog("BankImport: PDF ".($pIdx+1).": ".strlen($pdf)." Bytes, beginnt mit: ".substr($pdf, 0, 10), LOG_DEBUG);
}
dol_syslog("BankImport: ===== fetchBankStatements() ERFOLG - ".count($pdfStatements)." PDFs =====", LOG_DEBUG);
return array(
'count' => count($pdfStatements),
'pdfs' => $pdfStatements,
'iban' => $selectedAccount->getIban(),
);
} catch (\Fhp\Protocol\UnexpectedResponseException $e) {
// Bank unterstuetzt den Geschaeftsvorfall nicht
$this->error = 'Kontoauszug-Abruf nicht unterstuetzt: '.$e->getMessage();
dol_syslog("BankImport HKEKP: UnexpectedResponseException: ".$e->getMessage(), LOG_WARNING);
dol_syslog("BankImport HKEKP: Stack-Trace: ".$e->getTraceAsString(), LOG_DEBUG);
return -1;
} catch (Exception $e) {
$this->error = 'Fehler beim Kontoauszug-Abruf: '.$e->getMessage();
dol_syslog("BankImport HKEKP: Exception (".get_class($e)."): ".$e->getMessage(), LOG_ERR);
dol_syslog("BankImport HKEKP: Stack-Trace: ".$e->getTraceAsString(), LOG_DEBUG);
return -1;
}
}
/**
* Close FinTS connection
*
@ -1019,301 +1270,4 @@ class BankImportFinTS
{
return $this->iban;
}
/**
* Get PDF bank statement via HKEKP
*
* @param int $accountIndex Index of account to use (default 0)
* @param int|null $statementNumber Optional: specific statement number
* @param int|null $statementYear Optional: statement year
* @return array|int Array with 'pdfData' and 'info', or 0 if TAN required, or -1 on error
*/
public function getStatementPDF($accountIndex = 0, $statementNumber = null, $statementYear = null)
{
global $conf;
$this->error = '';
if (!$this->fints) {
$this->error = 'Not connected';
return -1;
}
try {
// Get accounts if not cached
if (empty($this->accounts)) {
$getAccounts = GetSEPAAccounts::create();
$this->fints->execute($getAccounts);
if ($getAccounts->needsTan()) {
$this->pendingAction = $getAccounts;
$tanRequest = $getAccounts->getTanRequest();
$this->tanChallenge = $tanRequest->getChallenge();
return 0;
}
$this->accounts = $getAccounts->getAccounts();
}
if (empty($this->accounts) || !isset($this->accounts[$accountIndex])) {
$this->error = 'No accounts available or invalid account index';
return -1;
}
$selectedAccount = $this->accounts[$accountIndex];
dol_syslog("BankImport: Fetching PDF statement via HKEKP", LOG_DEBUG);
$getPdf = GetStatementPDF::create(
$selectedAccount,
$statementNumber,
$statementYear
);
$this->fints->execute($getPdf);
if ($getPdf->needsTan()) {
$this->pendingAction = $getPdf;
$tanRequest = $getPdf->getTanRequest();
$this->tanChallenge = $tanRequest->getChallenge();
return 0;
}
$pdfData = $getPdf->getPdfData();
$info = $getPdf->getStatementInfo();
if (empty($pdfData)) {
dol_syslog("BankImport: No PDF data received (no new statements available)", LOG_DEBUG);
return array(
'pdfData' => '',
'info' => array(),
'message' => 'No new statements available'
);
}
dol_syslog("BankImport: Received PDF statement #".$info['statementNumber'].'/'.$info['statementYear'], LOG_DEBUG);
return array(
'pdfData' => $pdfData,
'info' => $info
);
} catch (Exception $e) {
$this->error = $e->getMessage();
dol_syslog("BankImport: HKEKP failed: ".$this->error, LOG_ERR);
// Check if bank doesn't support HKEKP
if (stripos($this->error, 'HKEKP') !== false || stripos($this->error, 'not support') !== false) {
$this->error = 'Bank does not support PDF statements (HKEKP)';
}
return -1;
}
}
/**
* Check if bank supports PDF statements (HKEKP)
*
* @return bool
*/
public function supportsPdfStatements()
{
if (!$this->fints) {
return false;
}
try {
// Try to get BPD and check for HIEKPS
$reflection = new ReflectionClass($this->fints);
$bpdProperty = $reflection->getProperty('bpd');
$bpdProperty->setAccessible(true);
$bpd = $bpdProperty->getValue($this->fints);
if ($bpd === null) {
return false;
}
$hiekps = $bpd->getLatestSupportedParameters('HIEKPS');
return $hiekps !== null;
} catch (Exception $e) {
return false;
}
}
/**
* Check if bank supports archive statements (HKKAA) with PDF format
*
* @return bool
*/
public function supportsArchiveStatements()
{
if (!$this->fints) {
return false;
}
try {
$reflection = new ReflectionClass($this->fints);
$bpdProperty = $reflection->getProperty('bpd');
$bpdProperty->setAccessible(true);
$bpd = $bpdProperty->getValue($this->fints);
if ($bpd === null) {
return false;
}
$hikaas = $bpd->getLatestSupportedParameters('HIKAAS');
if ($hikaas === null) {
return false;
}
// Check if PDF format is supported
$param = $hikaas->getParameter();
return $param->supportsPdf();
} catch (Exception $e) {
dol_syslog("BankImport: supportsArchiveStatements exception: ".$e->getMessage(), LOG_DEBUG);
return false;
}
}
/**
* Check if bank supports any PDF statement method (HKEKP or HKKAA)
*
* @return string|false Method name ('HKEKP', 'HKKAA') or false if none supported
*/
public function getPdfStatementMethod()
{
// First try HKEKP (direct PDF)
if ($this->supportsPdfStatements()) {
return 'HKEKP';
}
// Fallback to HKKAA (archive)
if ($this->supportsArchiveStatements()) {
return 'HKKAA';
}
return false;
}
/**
* Get PDF bank statement from archive via HKKAA
*
* @param int $accountIndex Index of account to use (default 0)
* @param \DateTime|null $fromDate Optional: start date
* @param \DateTime|null $toDate Optional: end date
* @return array|int Array with 'pdfData' and 'info', or 0 if TAN required, or -1 on error
*/
public function getStatementFromArchive($accountIndex = 0, $fromDate = null, $toDate = null)
{
global $conf;
$this->error = '';
if (!$this->fints) {
$this->error = 'Not connected';
return -1;
}
try {
// Get accounts if not cached
if (empty($this->accounts)) {
$getAccounts = GetSEPAAccounts::create();
$this->fints->execute($getAccounts);
if ($getAccounts->needsTan()) {
$this->pendingAction = $getAccounts;
$tanRequest = $getAccounts->getTanRequest();
$this->tanChallenge = $tanRequest->getChallenge();
return 0;
}
$this->accounts = $getAccounts->getAccounts();
}
if (empty($this->accounts) || !isset($this->accounts[$accountIndex])) {
$this->error = 'No accounts available or invalid account index';
return -1;
}
$selectedAccount = $this->accounts[$accountIndex];
dol_syslog("BankImport: Fetching PDF statement from archive via HKKAA", LOG_DEBUG);
$getArchive = GetStatementFromArchive::create(
$selectedAccount,
GetStatementFromArchive::FORMAT_PDF,
$fromDate,
$toDate
);
$this->fints->execute($getArchive);
if ($getArchive->needsTan()) {
$this->pendingAction = $getArchive;
$tanRequest = $getArchive->getTanRequest();
$this->tanChallenge = $tanRequest->getChallenge();
return 0;
}
$pdfData = $getArchive->getPdfData();
$info = $getArchive->getStatementInfo();
if (empty($pdfData)) {
dol_syslog("BankImport: No PDF data received from archive", LOG_DEBUG);
return array(
'pdfData' => '',
'info' => array(),
'message' => 'No statements available in archive'
);
}
dol_syslog("BankImport: Received PDF statement from archive #".($info['statementNumber'] ?? '?').'/'.($info['statementYear'] ?? '?'), LOG_DEBUG);
return array(
'pdfData' => $pdfData,
'info' => $info
);
} catch (Exception $e) {
$this->error = $e->getMessage();
dol_syslog("BankImport: HKKAA failed: ".$this->error, LOG_ERR);
dol_syslog("BankImport: HKKAA Exception class: ".get_class($e), LOG_DEBUG);
// Keep original error message for better debugging
return -1;
}
}
/**
* Get PDF statement using best available method (HKEKP or HKKAA)
*
* @param int $accountIndex Index of account to use (default 0)
* @param int|null $statementNumber Optional: specific statement number (only for HKEKP)
* @param int|null $statementYear Optional: statement year (only for HKEKP)
* @return array|int Array with 'pdfData', 'info' and 'method', or 0 if TAN required, or -1 on error
*/
public function getStatementPDFAuto($accountIndex = 0, $statementNumber = null, $statementYear = null)
{
$method = $this->getPdfStatementMethod();
if ($method === false) {
$this->error = 'Bank does not support PDF statements (neither HKEKP nor HKKAA)';
return -1;
}
if ($method === 'HKEKP') {
$result = $this->getStatementPDF($accountIndex, $statementNumber, $statementYear);
} else {
// HKKAA doesn't support statement number, use date range instead
$result = $this->getStatementFromArchive($accountIndex);
}
if (is_array($result)) {
$result['method'] = $method;
}
return $result;
}
}

View file

@ -5,7 +5,7 @@
"license": "GPL-3.0-or-later",
"require": {
"php": ">=8.0",
"nemiah/php-fints": "^4.0"
"nemiah/php-fints": "^3.2"
},
"replace": {
"psr/log": "*"

16
composer.lock generated
View file

@ -4,26 +4,26 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "32eb1d84f3157a4dee83ef5a81763257",
"content-hash": "cfc07b7e6c4a3dcfdcd6e754983b1a9b",
"packages": [
{
"name": "nemiah/php-fints",
"version": "4.0.0",
"version": "3.7.0",
"source": {
"type": "git",
"url": "https://github.com/nemiah/phpFinTS.git",
"reference": "b37e6df7efd39b4e757537e782241d5abb6b2bb5"
"reference": "08257e10229db2d4ca8c54ed7fec0f390b332519"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nemiah/phpFinTS/zipball/b37e6df7efd39b4e757537e782241d5abb6b2bb5",
"reference": "b37e6df7efd39b4e757537e782241d5abb6b2bb5",
"url": "https://api.github.com/repos/nemiah/phpFinTS/zipball/08257e10229db2d4ca8c54ed7fec0f390b332519",
"reference": "08257e10229db2d4ca8c54ed7fec0f390b332519",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-mbstring": "*",
"php": ">=8.3",
"php": ">=8.0",
"psr/log": "^1|^2|^3"
},
"require-dev": {
@ -51,9 +51,9 @@
"homepage": "https://github.com/nemiah/phpFinTS",
"support": {
"issues": "https://github.com/nemiah/phpFinTS/issues",
"source": "https://github.com/nemiah/phpFinTS/tree/4.0"
"source": "https://github.com/nemiah/phpFinTS/tree/3.7"
},
"time": "2026-01-16T07:56:30+00:00"
"time": "2025-10-14T15:05:56+00:00"
}
],
"packages-dev": [],

View file

@ -76,7 +76,7 @@ class modBankImport extends DolibarrModules
$this->editor_squarred_logo = ''; // Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@bankimport'
// Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z'
$this->version = '3.5';
$this->version = '3.4';
// Url to the file with your last numberversion of this module
//$this->url_last_version = 'http://www.example.com/versionmodule.txt';
@ -281,20 +281,6 @@ class modBankImport extends DolibarrModules
'test' => 'isModEnabled("bankimport") && getDolGlobalInt("BANKIMPORT_AUTO_ENABLED")',
'priority' => 50,
),
1 => array(
'label' => 'BankImportPdfFetch',
'jobtype' => 'method',
'class' => '/bankimport/class/bankimportcron.class.php',
'objectname' => 'BankImportCron',
'method' => 'doAutoFetchPdf',
'parameters' => '',
'comment' => 'Automatic PDF statement fetch via HKEKP',
'frequency' => 1,
'unitfrequency' => 86400, // Daily
'status' => 0, // Disabled by default
'test' => 'isModEnabled("bankimport") && getDolGlobalInt("BANKIMPORT_PDF_AUTO_ENABLED")',
'priority' => 51,
),
);
/* END MODULEBUILDER CRON */
// Example: $this->cronjobs=array(

View file

@ -55,10 +55,10 @@ SecurityInfo = Sicherheitshinweis
SecurityInfoText = Die PIN wird verschlüsselt in der Datenbank gespeichert. Für zusätzliche Sicherheit sollte Ihre Dolibarr-Installation HTTPS verwenden.
#
# Kontoauszüge Seite
# Umsaetze Seite
#
BankStatements = Kontoauszüge
FetchStatements = Kontoauszüge abrufen
BankStatements = Umsätze
FetchStatements = Umsätze abrufen
Transactions = Buchungen
TransactionsFound = %s Buchungen gefunden
NoTransactionsFound = Keine Buchungen im ausgewählten Zeitraum gefunden
@ -377,26 +377,22 @@ Open = Öffnen
CashDiscount = Skonto
#
# PDF Statement Fetch (HKEKP)
# FinTS Kontoauszug-Abruf (HKEKP)
#
AutoFetchStatements = Kontoauszüge automatisch von der Bank abrufen
AutoFetchStatementsDesc = Lädt die neuesten elektronischen Kontoauszüge (PDF) direkt von der Bank per FinTS herunter.
FetchFromBank = Von Bank abrufen
FetchNewStatements = Neue Auszüge abrufen
FetchNewStatementsDesc = Ruft alle noch nicht abgerufenen PDF-Kontoauszüge von der Bank ab
FetchSpecificStatement = Bestimmten Auszug abrufen
Fetch = Abrufen
ErrorFinTSNotConfigured = FinTS-Verbindung nicht konfiguriert
ErrorFinTSConnection = FinTS-Verbindungsfehler
ErrorBankDoesNotSupportPdfStatements = Bank unterstützt keine PDF-Kontoauszüge (HKEKP)
ErrorFetchingPdfStatement = Fehler beim Abrufen des PDF-Kontoauszugs
NoPdfStatementsAvailable = Keine neuen PDF-Kontoauszüge verfügbar
PdfStatementFetched = PDF-Kontoauszug %s erfolgreich abgerufen
ErrorSavingPdfFile = Fehler beim Speichern der PDF-Datei
FinTSNotConfiguredForPdf = FinTS nicht konfiguriert - PDF-Abruf nicht möglich
AutoPdfFetchDisabled = Automatischer PDF-Abruf ist deaktiviert
BankImportPdfFetch = PDF-Kontoauszüge abrufen
PdfFetchCronDescription = Automatischer Abruf von PDF-Kontoauszügen via FinTS (HKEKP)
PdfStatementsFetched = %s PDF-Kontoauszüge abgerufen
ManualUpload = Manuell hochladen
Source = Quelle
SourceUpload = Hochgeladen
SourceFinTS = FinTS-Abruf
FinTSLibraryMissing = phpFinTS-Bibliothek nicht installiert
WaitingForTanConfirmation = Warte auf TAN-Bestätigung in der SecureGo Plus App...
TanCheckFailed = TAN-Prüfung fehlgeschlagen
TanTimeout = TAN-Bestätigung: Zeitüberschreitung (2 Minuten)
TanRequiredForStatements = TAN-Bestätigung für Kontoauszug-Abruf erforderlich
FetchStatementsFailed = Kontoauszug-Abruf fehlgeschlagen
NoStatementsAvailable = Keine neuen Kontoauszüge bei der Bank verfügbar
StatementsDownloaded = %s von %s Kontoauszügen erfolgreich heruntergeladen und gespeichert
StatementsDownloadErrors = %s Kontoauszüge konnten nicht gespeichert werden
StatementsSkippedNoNumber = %s PDF(s) ohne Auszugsnummer übersprungen (vermutlich Saldenmitteilungen)
StatementsNotPdfFormat = Kontoauszüge empfangen, aber nicht im PDF-Format
StatementsUsingHKEKA = Nutze HKEKA (generischer Kontoauszug) statt HKEKP
StatementsUsingHKEKP = Nutze HKEKP (PDF-Kontoauszug)
NeitherHKEKPnorHKEKA = Die Bank unterstützt weder HKEKP noch HKEKA für elektronische Kontoauszüge

View file

@ -55,10 +55,10 @@ SecurityInfo = Security Information
SecurityInfoText = The PIN is stored encrypted in the database. For additional security, ensure your Dolibarr installation uses HTTPS.
#
# Statements page
# Transactions page
#
BankStatements = Bank Statements
FetchStatements = Fetch Statements
BankStatements = Transactions
FetchStatements = Fetch Transactions
Transactions = Transactions
TransactionsFound = %s transactions found
NoTransactionsFound = No transactions found in the selected period
@ -271,3 +271,24 @@ Open = Open
# Cash Discount / Skonto
#
CashDiscount = Cash Discount
#
# FinTS Bank Statement Retrieval (HKEKP)
#
AutoFetchStatements = Automatically fetch bank statements
AutoFetchStatementsDesc = Downloads the latest electronic bank statements (PDF) directly from the bank via FinTS.
FetchFromBank = Fetch from bank
FinTSLibraryMissing = phpFinTS library not installed
WaitingForTanConfirmation = Waiting for TAN confirmation in SecureGo Plus app...
TanCheckFailed = TAN check failed
TanTimeout = TAN confirmation: Timeout (2 minutes)
TanRequiredForStatements = TAN confirmation required for statement retrieval
FetchStatementsFailed = Bank statement retrieval failed
NoStatementsAvailable = No new bank statements available at the bank
StatementsDownloaded = %s of %s bank statements successfully downloaded and saved
StatementsDownloadErrors = %s bank statements could not be saved
StatementsSkippedNoNumber = %s PDF(s) without statement number skipped (probably balance notifications)
StatementsNotPdfFormat = Bank statements received but not in PDF format
StatementsUsingHKEKA = Using HKEKA (generic statement) instead of HKEKP
StatementsUsingHKEKP = Using HKEKP (PDF statement)
NeitherHKEKPnorHKEKA = The bank supports neither HKEKP nor HKEKA for electronic statements

View file

@ -73,103 +73,296 @@ if (!$user->hasRight('bankimport', 'read')) {
$statement = new BankImportStatement($db);
// Fetch PDF statement from bank via HKEKP
if ($action == 'fetchpdf' || $action == 'fetchpdf_single') {
// FinTS: Elektronische Kontoauszuege automatisch abrufen (HKEKP)
if ($action == 'fetchfints') {
dol_syslog("BankImport HKEKP: ========== START fetchfints Action ==========", LOG_DEBUG);
dol_syslog("BankImport HKEKP: User=".$user->login." (ID ".$user->id."), Zeitpunkt=".date('Y-m-d H:i:s'), LOG_DEBUG);
if (!$user->hasRight('bankimport', 'write')) {
dol_syslog("BankImport HKEKP: Zugriff verweigert - User hat kein Schreibrecht", LOG_WARNING);
accessforbidden();
}
$fetchNumber = ($action == 'fetchpdf_single') ? GETPOSTINT('fetch_number') : null;
$fetchYear = ($action == 'fetchpdf_single') ? GETPOSTINT('fetch_year') : null;
$fints = new BankImportFinTS($db);
// Check FinTS configuration
$fintsUrl = getDolGlobalString('BANKIMPORT_FINTS_URL');
$fintsBlz = getDolGlobalString('BANKIMPORT_FINTS_BLZ');
$fintsUser = getDolGlobalString('BANKIMPORT_FINTS_USERNAME');
$fintsPin = getDolGlobalString('BANKIMPORT_FINTS_PIN');
dol_syslog("BankImport HKEKP: FinTS-Objekt erstellt, isConfigured=".($fints->isConfigured() ? 'JA' : 'NEIN')
.", isLibraryAvailable=".($fints->isLibraryAvailable() ? 'JA' : 'NEIN'), LOG_DEBUG);
dol_syslog("BankImport HKEKP: Konfiguration - URL=".getDolGlobalString('BANKIMPORT_FINTS_URL')
.", BLZ=".getDolGlobalString('BANKIMPORT_FINTS_BLZ')
.", IBAN=".getDolGlobalString('BANKIMPORT_FINTS_IBAN')
.", User=".getDolGlobalString('BANKIMPORT_FINTS_USERNAME'), LOG_DEBUG);
if (empty($fintsUrl) || empty($fintsBlz) || empty($fintsUser) || empty($fintsPin)) {
setEventMessages($langs->trans("ErrorFinTSNotConfigured"), null, 'errors');
if (!$fints->isConfigured()) {
dol_syslog("BankImport HKEKP: ABBRUCH - FinTS nicht konfiguriert", LOG_WARNING);
setEventMessages($langs->trans("FinTSNotConfigured"), null, 'errors');
} elseif (!$fints->isLibraryAvailable()) {
dol_syslog("BankImport HKEKP: ABBRUCH - phpFinTS Library fehlt", LOG_WARNING);
setEventMessages($langs->trans("FinTSLibraryMissing"), null, 'errors');
} else {
$fints = new BankImportFinTS($db);
// Login
dol_syslog("BankImport HKEKP: Starte login()...", LOG_DEBUG);
$loginResult = $fints->login();
dol_syslog("BankImport HKEKP: login() Ergebnis=".$loginResult." (1=OK, 0=TAN, -1=Fehler)", LOG_DEBUG);
if ($loginResult < 0) {
setEventMessages($langs->trans("ErrorFinTSConnection").': '.$fints->error, null, 'errors');
} elseif ($loginResult === 0) {
// TAN required for login
setEventMessages($langs->trans("TANRequired").($fints->tanChallenge ? ': '.$fints->tanChallenge : ''), null, 'warnings');
$fints->close();
} else {
// Check if bank supports any PDF statement method (HKEKP or HKKAA)
$pdfMethod = $fints->getPdfStatementMethod();
if ($pdfMethod === false) {
setEventMessages($langs->trans("ErrorBankDoesNotSupportPdfStatements"), null, 'errors');
$fints->close();
if ($loginResult == -1) {
dol_syslog("BankImport HKEKP: Login FEHLGESCHLAGEN - ".$fints->error, LOG_ERR);
setEventMessages('FinTS Login fehlgeschlagen: '.$fints->error, null, 'errors');
}
if ($loginResult == 0) {
// TAN benoetigt - Decoupled-Polling
$tanConfirmed = false;
$maxWait = 120; // Max 2 Minuten warten
$waited = 0;
if ($fints->selectedTanMode && $fints->selectedTanMode->isDecoupled()) {
dol_syslog("BankImport HKEKP: TAN-Modus: ".$fints->selectedTanMode->getName()
." (ID ".$fints->selectedTanMode->getId().", Decoupled=JA)", LOG_DEBUG);
dol_syslog("BankImport HKEKP: Starte Decoupled-TAN-Polling (max ".$maxWait."s, Intervall 3s)...", LOG_DEBUG);
setEventMessages($langs->trans("WaitingForTanConfirmation"), null, 'mesgs');
while ($waited < $maxWait) {
sleep(3);
$waited += 3;
$tanStatus = $fints->checkDecoupledTan();
dol_syslog("BankImport HKEKP: TAN-Poll nach ".$waited."s - Status=".$tanStatus." (1=OK, 0=Wartend, -1=Fehler)", LOG_DEBUG);
if ($tanStatus == 1) {
$tanConfirmed = true;
dol_syslog("BankImport HKEKP: TAN BESTAETIGT nach ".$waited."s", LOG_DEBUG);
break;
} elseif ($tanStatus < 0) {
dol_syslog("BankImport HKEKP: TAN-Pruefung FEHLGESCHLAGEN: ".$fints->error, LOG_ERR);
setEventMessages($langs->trans("TanCheckFailed").': '.$fints->error, null, 'errors');
break;
}
}
if (!$tanConfirmed && $waited >= $maxWait) {
dol_syslog("BankImport HKEKP: TAN-TIMEOUT nach ".$waited."s", LOG_WARNING);
setEventMessages($langs->trans("TanTimeout"), null, 'errors');
}
} else {
dol_syslog("BankImport: Using PDF method: ".$pdfMethod, LOG_DEBUG);
$tanModeName = $fints->selectedTanMode ? $fints->selectedTanMode->getName() : 'UNBEKANNT';
$tanModeDecoupled = $fints->selectedTanMode ? ($fints->selectedTanMode->isDecoupled() ? 'JA' : 'NEIN') : '?';
dol_syslog("BankImport HKEKP: TAN-Modus: ".$tanModeName." (Decoupled=".$tanModeDecoupled.") - Manuell benoetigt!", LOG_DEBUG);
dol_syslog("BankImport HKEKP: TAN-Challenge: ".$fints->tanChallenge, LOG_DEBUG);
setEventMessages($langs->trans("TanRequired").': '.$fints->tanChallenge, null, 'warnings');
}
// Fetch PDF using auto method (tries HKEKP first, falls back to HKKAA)
$pdfResult = $fints->getStatementPDFAuto(0, $fetchNumber, $fetchYear);
if (!$tanConfirmed) {
dol_syslog("BankImport HKEKP: TAN nicht bestaetigt, schliesse Verbindung", LOG_DEBUG);
$fints->close();
$action = '';
} else {
$loginResult = 1; // Weiter mit Abruf
}
}
if ($pdfResult === 0) {
// TAN required - save to session and show TAN form
$_SESSION['bankimport_pending_action'] = serialize($fints);
setEventMessages($langs->trans("TANRequired").': '.$fints->tanChallenge, null, 'warnings');
} elseif ($pdfResult === -1) {
setEventMessages($langs->trans("ErrorFetchingPdfStatement").': '.$fints->error, null, 'errors');
} elseif (empty($pdfResult['pdfData'])) {
setEventMessages($langs->trans("NoPdfStatementsAvailable"), null, 'warnings');
if ($loginResult == 1) {
// Kontoauszuege abrufen
$fetchYear = GETPOST('fetch_year', 'alpha') ?: null;
dol_syslog("BankImport HKEKP: Login erfolgreich, starte fetchBankStatements(year="
.($fetchYear ?: 'ALLE').")", LOG_DEBUG);
$result = $fints->fetchBankStatements(null, $fetchYear);
dol_syslog("BankImport HKEKP: fetchBankStatements() Ergebnis-Typ=".gettype($result)
.(is_array($result) ? " count=".$result['count'] : " val=".$result), LOG_DEBUG);
if ($result === 0) {
dol_syslog("BankImport HKEKP: fetchBankStatements benoetigt TAN", LOG_WARNING);
setEventMessages($langs->trans("TanRequiredForStatements"), null, 'warnings');
} elseif ($result === -1) {
dol_syslog("BankImport HKEKP: fetchBankStatements FEHLGESCHLAGEN: ".$fints->error, LOG_ERR);
setEventMessages($langs->trans("FetchStatementsFailed").': '.$fints->error, null, 'errors');
} elseif (is_array($result)) {
$pdfCount = $result['count'];
$savedCount = 0;
$errorCountFints = 0;
dol_syslog("BankImport HKEKP: ".$pdfCount." PDFs empfangen, IBAN=".$result['iban'], LOG_DEBUG);
if ($pdfCount == 0) {
dol_syslog("BankImport HKEKP: Keine Auszuege verfuegbar", LOG_DEBUG);
setEventMessages($langs->trans("NoStatementsAvailable"), null, 'warnings');
} else {
// Save PDF
$info = $pdfResult['info'];
$pdfData = $pdfResult['pdfData'];
// PDFs speichern ueber die bestehende bankstatement-Logik
$dir = BankImportStatement::getStorageDir();
dol_syslog("BankImport HKEKP: Speicher-Verzeichnis: ".$dir, LOG_DEBUG);
// Check if statement already exists
$stmt = new BankImportStatement($db);
$stmt->statement_number = $info['statementNumber'];
$stmt->statement_year = $info['statementYear'];
$stmt->iban = $info['iban'] ?: getDolGlobalString('BANKIMPORT_IBAN');
if ($stmt->exists()) {
setEventMessages($langs->trans("StatementAlreadyExists").': '.$stmt->statement_number.'/'.$stmt->statement_year, null, 'warnings');
} else {
// Save PDF to file
$dir = BankImportStatement::getStorageDir();
$ibanPart = preg_replace('/[^A-Z0-9]/', '', strtoupper($stmt->iban ?: 'KONTO'));
$filename = sprintf('Kontoauszug_%s_%d_%s.pdf',
$ibanPart,
$stmt->statement_year,
str_pad($stmt->statement_number, 3, '0', STR_PAD_LEFT)
);
$filepath = $dir.'/'.$filename;
if (file_put_contents($filepath, $pdfData) !== false) {
$stmt->filename = $filename;
$stmt->filepath = $filepath;
$stmt->filesize = strlen($pdfData);
$stmt->statement_date = $info['creationDate'] ? $info['creationDate']->getTimestamp() : dol_now();
$stmt->import_key = 'fints_'.date('YmdHis');
$result = $stmt->create($user);
if ($result > 0) {
setEventMessages($langs->trans("PdfStatementFetched", $stmt->statement_number.'/'.$stmt->statement_year), null, 'mesgs');
$year = $stmt->statement_year;
} else {
setEventMessages($stmt->error, null, 'errors');
@unlink($filepath);
}
// Identische PDFs deduplizieren (Bank sendet teilweise Duplikate)
$seenHashes = [];
$uniquePdfs = [];
foreach ($result['pdfs'] as $pdfData) {
$hash = md5($pdfData);
if (!isset($seenHashes[$hash])) {
$seenHashes[$hash] = true;
$uniquePdfs[] = $pdfData;
} else {
setEventMessages($langs->trans("ErrorSavingPdfFile"), null, 'errors');
dol_syslog("BankImport HKEKP: Duplikat-PDF uebersprungen (Hash=".substr($hash, 0, 8)."...)", LOG_DEBUG);
}
}
$result['pdfs'] = $uniquePdfs;
$pdfCount = count($uniquePdfs);
$skippedCount = 0;
foreach ($result['pdfs'] as $idx => $pdfData) {
dol_syslog("BankImport HKEKP: --- PDF ".($idx+1)."/".$pdfCount." ---", LOG_DEBUG);
dol_syslog("BankImport HKEKP: PDF-Groesse=".strlen($pdfData)." Bytes, Erste 20 Bytes=".bin2hex(substr($pdfData, 0, 20)), LOG_DEBUG);
dol_syslog("BankImport HKEKP: PDF startet mit: ".substr($pdfData, 0, 10), LOG_DEBUG);
// PDF in Temp-Datei schreiben fuer Metadaten-Extraktion
$tmpFile = tempnam(sys_get_temp_dir(), 'fints_stmt_');
file_put_contents($tmpFile, $pdfData);
dol_syslog("BankImport HKEKP: Temp-Datei: ".$tmpFile." (".filesize($tmpFile)." Bytes)", LOG_DEBUG);
// Metadaten aus PDF parsen
dol_syslog("BankImport HKEKP: Starte parsePdfMetadata()...", LOG_DEBUG);
$parsed = BankImportStatement::parsePdfMetadata($tmpFile);
if ($parsed) {
dol_syslog("BankImport HKEKP: Metadaten erkannt: IBAN=".$parsed['iban']
.", Nr=".$parsed['statement_number'].", Jahr=".$parsed['statement_year']
.", Datum=".$parsed['statement_date']
.", Von=".$parsed['date_from'].", Bis=".$parsed['date_to']
.", Saldo_Start=".$parsed['opening_balance'].", Saldo_Ende=".$parsed['closing_balance'], LOG_DEBUG);
} else {
dol_syslog("BankImport HKEKP: Metadaten NICHT erkannt - verwende Fallback", LOG_WARNING);
}
$stmt = new BankImportStatement($db);
if ($parsed && !empty($parsed['statement_number'])) {
// Vollstaendige Metadaten mit Auszugsnummer
$stmt->iban = $parsed['iban'] ?: $result['iban'];
$stmt->statement_number = $parsed['statement_number'];
$stmt->statement_year = $parsed['statement_year'];
$stmt->statement_date = $parsed['statement_date'];
$stmt->date_from = $parsed['date_from'];
$stmt->date_to = $parsed['date_to'];
$stmt->opening_balance = $parsed['opening_balance'];
$stmt->closing_balance = $parsed['closing_balance'];
} elseif ($parsed && !empty($parsed['iban'])) {
// IBAN erkannt aber keine Auszugsnummer (z.B. Saldenmitteilung)
// Ueberspringe solche PDFs - ohne Nummer nicht sinnvoll speicherbar
dol_syslog("BankImport HKEKP: PDF uebersprungen - IBAN erkannt aber keine Auszugsnummer (wahrscheinlich Saldenmitteilung)", LOG_WARNING);
$skippedCount++;
@unlink($tmpFile);
continue;
} else {
// Keinerlei Metadaten - Fallback mit Index
$stmt->iban = $result['iban'];
$stmt->statement_number = (string) ($idx + 1);
$stmt->statement_year = (int) date('Y');
}
$stmt->import_key = 'fints_'.date('YmdHis').'_'.$user->id;
dol_syslog("BankImport HKEKP: import_key=".$stmt->import_key, LOG_DEBUG);
// Duplikat-Pruefung
if ($stmt->statement_number && $stmt->exists()) {
dol_syslog("BankImport HKEKP: DUPLIKAT - Auszug ".$stmt->statement_number."/".$stmt->statement_year." existiert bereits, ueberspringe", LOG_DEBUG);
@unlink($tmpFile);
continue;
}
// Dateiname generieren
if ($parsed) {
$newFilename = BankImportStatement::generateFilename($parsed);
} else {
$newFilename = sprintf('Kontoauszug_FinTS_%s_%d_%03d.pdf',
preg_replace('/[^A-Z0-9]/', '', strtoupper($stmt->iban)),
$stmt->statement_year,
$idx + 1
);
}
dol_syslog("BankImport HKEKP: Dateiname=".$newFilename, LOG_DEBUG);
$stmt->filepath = $dir.'/'.$newFilename;
// Kollisionsvermeidung
if (file_exists($stmt->filepath)) {
$newFilename = pathinfo($newFilename, PATHINFO_FILENAME).'_'.date('His').'.pdf';
$stmt->filepath = $dir.'/'.$newFilename;
dol_syslog("BankImport HKEKP: Datei existiert bereits, neuer Name: ".$newFilename, LOG_DEBUG);
}
$stmt->filename = $newFilename;
// PDF von Temp nach Ziel verschieben
if (!rename($tmpFile, $stmt->filepath)) {
dol_syslog("BankImport HKEKP: rename() fehlgeschlagen, verwende copy()", LOG_DEBUG);
copy($tmpFile, $stmt->filepath);
@unlink($tmpFile);
}
$stmt->filesize = filesize($stmt->filepath);
dol_syslog("BankImport HKEKP: Datei gespeichert: ".$stmt->filepath." (".$stmt->filesize." Bytes)", LOG_DEBUG);
// In DB speichern
dol_syslog("BankImport HKEKP: Starte DB create()...", LOG_DEBUG);
$dbResult = $stmt->create($user);
dol_syslog("BankImport HKEKP: DB create() Ergebnis=".$dbResult." (>0=ID, <0=Fehler)", LOG_DEBUG);
if ($dbResult > 0) {
dol_syslog("BankImport HKEKP: DB-Eintrag erstellt mit ID=".$dbResult, LOG_DEBUG);
// FinTS-Transaktionen verknuepfen
dol_syslog("BankImport HKEKP: Starte linkTransactions()...", LOG_DEBUG);
$linkResult = $stmt->linkTransactions();
dol_syslog("BankImport HKEKP: linkTransactions() Ergebnis=".$linkResult, LOG_DEBUG);
// PDF-Einzelbuchungen parsen
dol_syslog("BankImport HKEKP: Starte parsePdfTransactions()...", LOG_DEBUG);
$pdfLines = $stmt->parsePdfTransactions();
dol_syslog("BankImport HKEKP: parsePdfTransactions() ergab ".(is_array($pdfLines) ? count($pdfLines) : 0)." Buchungszeilen", LOG_DEBUG);
if (!empty($pdfLines)) {
$stmt->saveStatementLines($pdfLines);
dol_syslog("BankImport HKEKP: saveStatementLines() abgeschlossen", LOG_DEBUG);
}
// PDF in Dolibarr Bank-Verzeichnis kopieren
$uploadBankAccountId = getDolGlobalInt('BANKIMPORT_BANK_ACCOUNT_ID');
dol_syslog("BankImport HKEKP: BANKIMPORT_BANK_ACCOUNT_ID=".$uploadBankAccountId, LOG_DEBUG);
if ($uploadBankAccountId > 0) {
dol_syslog("BankImport HKEKP: Starte copyToDolibarrStatementDir()...", LOG_DEBUG);
$stmt->copyToDolibarrStatementDir($uploadBankAccountId);
dol_syslog("BankImport HKEKP: Starte reconcileBankEntries()...", LOG_DEBUG);
$reconcileResult = $stmt->reconcileBankEntries($user, $uploadBankAccountId);
dol_syslog("BankImport HKEKP: reconcileBankEntries() Ergebnis=".$reconcileResult, LOG_DEBUG);
}
$savedCount++;
} else {
$errorCountFints++;
dol_syslog("BankImport HKEKP: DB-FEHLER bei create(): ".$stmt->error, LOG_ERR);
if (file_exists($stmt->filepath)) {
@unlink($stmt->filepath);
}
}
}
dol_syslog("BankImport HKEKP: Zusammenfassung: ".$savedCount." gespeichert, ".$errorCountFints." Fehler, ".$skippedCount." uebersprungen von ".$pdfCount." PDFs", LOG_DEBUG);
if ($savedCount > 0) {
setEventMessages($langs->trans("StatementsDownloaded", $savedCount, $pdfCount), null, 'mesgs');
}
if ($errorCountFints > 0) {
setEventMessages($langs->trans("StatementsDownloadErrors", $errorCountFints), null, 'warnings');
}
if ($skippedCount > 0) {
setEventMessages($langs->trans("StatementsSkippedNoNumber", $skippedCount), null, 'warnings');
}
}
}
$fints->close();
dol_syslog("BankImport HKEKP: FinTS-Verbindung geschlossen", LOG_DEBUG);
}
dol_syslog("BankImport HKEKP: ========== ENDE fetchfints Action ==========", LOG_DEBUG);
header("Location: ".$_SERVER['PHP_SELF']."?year=".date('Y'));
exit;
}
$action = '';
}
// Upload PDF (supports multiple files)
@ -622,6 +815,18 @@ print '<strong>'.$langs->trans("PDFStatementsInfo").'</strong><br>';
print $langs->trans("PDFStatementsInfoDesc");
print '</div>';
// FinTS-Abruf Button (wenn konfiguriert)
$fintsCheck = new BankImportFinTS($db);
if ($fintsCheck->isConfigured() && $fintsCheck->isLibraryAvailable() && $user->hasRight('bankimport', 'write')) {
print '<div class="center" style="margin-bottom: 15px; padding: 10px; background: #f0f8ff; border: 1px solid #b0d4f1; border-radius: 5px;">';
print '<strong>'.img_picto('', 'bank', 'class="pictofixedwidth"').$langs->trans("AutoFetchStatements").'</strong><br>';
print '<span class="opacitymedium">'.$langs->trans("AutoFetchStatementsDesc").'</span><br><br>';
print '<a class="butAction" href="'.$_SERVER["PHP_SELF"].'?action=fetchfints&token='.newToken().'">';
print img_picto('', 'download', 'class="pictofixedwidth"').$langs->trans("FetchFromBank");
print '</a>';
print '</div>';
}
// Delete confirmation dialog
if ($action == 'delete') {
$id = GETPOSTINT('id');
@ -640,76 +845,20 @@ if ($action == 'delete') {
print $formconfirm;
}
// Check if FinTS is configured for PDF fetch
$fintsConfigured = !empty(getDolGlobalString('BANKIMPORT_FINTS_URL'))
&& !empty(getDolGlobalString('BANKIMPORT_FINTS_BLZ'))
&& !empty(getDolGlobalString('BANKIMPORT_FINTS_USERNAME'))
&& !empty(getDolGlobalString('BANKIMPORT_FINTS_PIN'));
print '<div class="fichecenter">';
// Left side: Fetch from bank (if FinTS configured)
print '<div class="fichehalfleft">';
if ($fintsConfigured) {
print '<table class="noborder centpercent">';
print '<tr class="liste_titre">';
print '<td colspan="2">'.img_picto('', 'download', 'class="pictofixedwidth"').$langs->trans("FetchFromBank").'</td>';
print '</tr>';
// Fetch all new statements
print '<tr class="oddeven">';
print '<td colspan="2">';
print '<a class="butAction" href="'.$_SERVER["PHP_SELF"].'?action=fetchpdf&token='.newToken().'">';
print img_picto('', 'refresh', 'class="pictofixedwidth"').$langs->trans("FetchNewStatements");
print '</a>';
print '<br><span class="opacitymedium small">'.$langs->trans("FetchNewStatementsDesc").'</span>';
print '</td>';
print '</tr>';
// Fetch specific statement
print '<tr class="oddeven">';
print '<td colspan="2">';
print '<form method="POST" action="'.$_SERVER["PHP_SELF"].'" style="display: inline;">';
print '<input type="hidden" name="token" value="'.newToken().'">';
print '<input type="hidden" name="action" value="fetchpdf_single">';
print $langs->trans("FetchSpecificStatement").': ';
print '<input type="text" name="fetch_number" class="flat width50" placeholder="Nr." maxlength="5">';
print ' / ';
$years = array();
for ($y = (int) date('Y'); $y >= ((int) date('Y') - 5); $y--) {
$years[$y] = $y;
}
print $form->selectarray('fetch_year', $years, (int) date('Y'), 0, 0, 0, '', 0, 0, 0, '', 'minwidth75');
print ' <input type="submit" class="button smallpaddingimp" value="'.$langs->trans("Fetch").'">';
print '</form>';
print '</td>';
print '</tr>';
print '</table>';
} else {
print '<div class="opacitymedium">';
print img_warning().' '.$langs->trans("FinTSNotConfiguredForPdf");
print ' <a href="'.dol_buildpath('/bankimport/admin/setup.php', 1).'">'.$langs->trans("GoToSetup").'</a>';
print '</div>';
}
print '</div>'; // fichehalfleft
// Right side: Manual upload
print '<div class="fichehalfright">';
// Upload form
$defaultMode = getDolGlobalString('BANKIMPORT_UPLOAD_MODE') ?: 'auto';
$uploadMode = GETPOST('upload_mode', 'alpha') ?: $defaultMode;
print '<div class="fichecenter">';
print '<div class="fichehalfleft">';
print '<form method="POST" action="'.$_SERVER["PHP_SELF"].'" enctype="multipart/form-data" id="uploadform">';
print '<input type="hidden" name="token" value="'.newToken().'">';
print '<input type="hidden" name="action" value="upload">';
print '<table class="noborder centpercent">';
print '<tr class="liste_titre">';
print '<td colspan="2">'.img_picto('', 'upload', 'class="pictofixedwidth"').$langs->trans("UploadPDFStatement").'</td>';
print '<td colspan="2">'.$langs->trans("UploadPDFStatement").'</td>';
print '</tr>';
// Upload mode selection
@ -834,7 +983,7 @@ function toggleUploadMode() {
document.addEventListener("DOMContentLoaded", function() { toggleUploadMode(); });
</script>';
print '</div>'; // fichehalfright (upload form)
print '</div>'; // fichehalfleft
print '</div>'; // fichecenter
print '<div class="clearboth"></div><br>';

View file

@ -6,9 +6,6 @@ $vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'BankImportCron' => $baseDir . '/class/bankimportcron.class.php',
'BankImportFinTS' => $baseDir . '/class/fints.class.php',
'BankImportStatement' => $baseDir . '/class/bankstatement.class.php',
'BankImportTransaction' => $baseDir . '/class/banktransaction.class.php',
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
);

View file

@ -24,10 +24,7 @@ class ComposerStaticInitcfc07b7e6c4a3dcfdcd6e754983b1a9b
);
public static $classMap = array (
'BankImportCron' => __DIR__ . '/../..' . '/class/bankimportcron.class.php',
'BankImportFinTS' => __DIR__ . '/../..' . '/class/fints.class.php',
'BankImportStatement' => __DIR__ . '/../..' . '/class/bankstatement.class.php',
'BankImportTransaction' => __DIR__ . '/../..' . '/class/banktransaction.class.php',
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
);

View file

@ -2,23 +2,23 @@
"packages": [
{
"name": "nemiah/php-fints",
"version": "4.0.0",
"version_normalized": "4.0.0.0",
"version": "3.7.0",
"version_normalized": "3.7.0.0",
"source": {
"type": "git",
"url": "https://github.com/nemiah/phpFinTS.git",
"reference": "b37e6df7efd39b4e757537e782241d5abb6b2bb5"
"reference": "08257e10229db2d4ca8c54ed7fec0f390b332519"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nemiah/phpFinTS/zipball/b37e6df7efd39b4e757537e782241d5abb6b2bb5",
"reference": "b37e6df7efd39b4e757537e782241d5abb6b2bb5",
"url": "https://api.github.com/repos/nemiah/phpFinTS/zipball/08257e10229db2d4ca8c54ed7fec0f390b332519",
"reference": "08257e10229db2d4ca8c54ed7fec0f390b332519",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-mbstring": "*",
"php": ">=8.3",
"php": ">=8.0",
"psr/log": "^1|^2|^3"
},
"require-dev": {
@ -31,7 +31,7 @@
"monolog/monolog": "Allow sending log messages to a variety of different handlers",
"nemiah/php-sepa-xml": "dev-master"
},
"time": "2026-01-16T07:56:30+00:00",
"time": "2025-10-14T15:05:56+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@ -48,7 +48,7 @@
"homepage": "https://github.com/nemiah/phpFinTS",
"support": {
"issues": "https://github.com/nemiah/phpFinTS/issues",
"source": "https://github.com/nemiah/phpFinTS/tree/4.0"
"source": "https://github.com/nemiah/phpFinTS/tree/3.7"
},
"install-path": "../nemiah/php-fints"
}

View file

@ -1,9 +1,9 @@
<?php return array(
'root' => array(
'name' => 'dolibarr/bankimport',
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => 'fc380892f035d3a48038c3c0cedef76fd0fec404',
'pretty_version' => '1.0.0+no-version-set',
'version' => '1.0.0.0',
'reference' => null,
'type' => 'dolibarr-module',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@ -11,18 +11,18 @@
),
'versions' => array(
'dolibarr/bankimport' => array(
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => 'fc380892f035d3a48038c3c0cedef76fd0fec404',
'pretty_version' => '1.0.0+no-version-set',
'version' => '1.0.0.0',
'reference' => null,
'type' => 'dolibarr-module',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev_requirement' => false,
),
'nemiah/php-fints' => array(
'pretty_version' => '4.0.0',
'version' => '4.0.0.0',
'reference' => 'b37e6df7efd39b4e757537e782241d5abb6b2bb5',
'pretty_version' => '3.7.0',
'version' => '3.7.0.0',
'reference' => '08257e10229db2d4ca8c54ed7fec0f390b332519',
'type' => 'library',
'install_path' => __DIR__ . '/../nemiah/php-fints',
'aliases' => array(),

View file

@ -4,8 +4,8 @@
$issues = array();
if (!(PHP_VERSION_ID >= 80300)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.3.0". You are running ' . PHP_VERSION . '.';
if (!(PHP_VERSION_ID >= 80000)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.0.0". You are running ' . PHP_VERSION . '.';
}
if ($issues) {

View file

@ -1,50 +0,0 @@
# .github/workflows/tests.yml
name: tests
on:
push:
branches:
- master
pull_request:
jobs:
phpunit:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: [ '8.0', '8.1', '8.2', '8.3', '8.4' ]
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: mbstring
- name: Install dependencies
run: composer install --no-interaction --prefer-dist --no-progress
- name: Run PHPUnit
run: ./vendor/bin/phpunit
php-cs-fixer:
name: PHP-CS-Fixer
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/cache@v4
with:
path: .php-cs-fixer.cache
key: ${{ runner.OS }}-${{ github.repository }}-phpcsfixer-${{ github.sha }}
restore-keys: |
${{ runner.OS }}-${{ github.repository }}-phpcsfixer-
- name: PHP-CS-Fixer
uses: docker://oskarstark/php-cs-fixer-ga
with:
args: -v --diff --dry-run

View file

@ -1,22 +0,0 @@
.DS_Store
.idea/
.vscode/
vendor/
develop/
coverage/
test.php
/nbproject/private/
/nbproject/
/composer.lock
/composer.phar
/Samples/tan.txt
/Samples/*.test.php
/Samples/*.log
/Samples/analyzeLogs.php
/Samplesstate.txt
/Samples/state.txt
/Samples/session_*
/doc/
.phpunit.result.cache
.php_cs.cache
.php-cs-fixer.cache

20
vendor/nemiah/php-fints/.php-cs-fixer.php vendored Normal file → Executable file
View file

@ -12,19 +12,19 @@ return (new PhpCsFixer\Config())
// But then we have some exclusions, i.e. we disable some of the checks/rules from Symfony:
// Logic
'yoda_style' => false, // Allow both Yoda-style and regular comparisons.
'yoda_style' => FALSE, // Allow both Yoda-style and regular comparisons.
// Whitespace
'blank_line_before_statement' => false, // Don't put blank lines before `return` statements.
'concat_space' => false, // Allow spaces around string concatenation operator.
'blank_line_after_opening_tag' => false, // Allow file-level @noinspection suppressions to live on the `<?php` line.
'single_line_throw' => false, // Allow `throw` statements to span multiple lines.
'blank_line_before_statement' => FALSE, // Don't put blank lines before `return` statements.
'concat_space' => FALSE, // Allow spaces around string concatenation operator.
'blank_line_after_opening_tag' => FALSE, // Allow file-level @noinspection suppressions to live on the `<?php` line.
'single_line_throw' => FALSE, // Allow `throw` statements to span multiple lines.
// phpDoc
'phpdoc_align' => false, // Don't add spaces within phpDoc just to make parameter names / descriptions align.
'phpdoc_annotation_without_dot' => false, // Allow terminating dot on @param and such.
'phpdoc_no_alias_tag' => false, // Allow @link in addition to @see.
'phpdoc_separation' => false, // Don't put blank line between @params, @throws and @return.
'phpdoc_summary' => false, // Don't force terminating dot on the first line.
'phpdoc_align' => FALSE, // Don't add spaces within phpDoc just to make parameter names / descriptions align.
'phpdoc_annotation_without_dot' => FALSE, // Allow terminating dot on @param and such.
'phpdoc_no_alias_tag' => FALSE, // Allow @link in addition to @see.
'phpdoc_separation' => FALSE, // Don't put blank line between @params, @throws and @return.
'phpdoc_summary' => FALSE, // Don't force terminating dot on the first line.
])
->setFinder($finder);

12
vendor/nemiah/php-fints/.travis.yml vendored Executable file
View file

@ -0,0 +1,12 @@
language: php
install: composer install
script:
- ./disallowtabs.sh
- ./csfixer-check.sh
- ./phplint.sh ./lib/
- ./vendor/bin/phpunit
dist: bionic
php:
- '8.0'
- '8.1.0'
- '8.2.0'

0
vendor/nemiah/php-fints/DEVELOPER-GUIDE.md vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/LICENSE vendored Normal file → Executable file
View file

2
vendor/nemiah/php-fints/README.md vendored Normal file → Executable file
View file

@ -1,6 +1,6 @@
# PHP FinTS/HBCI library
[![CI status](https://github.com/nemiah/phpFinTS/actions/workflows/tests.yml/badge.svg)](https://github.com/nemiah/phpFinTS/actions/workflows/tests.yml)
[![Build Status](https://travis-ci.org/nemiah/phpFinTS.svg?branch=master)](https://travis-ci.org/nemiah/phpFinTS)
A PHP library implementing the following functions of the FinTS/HBCI protocol:

0
vendor/nemiah/php-fints/Samples/accounts.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/Samples/balance.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/Samples/bpd.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/Samples/browser.php vendored Normal file → Executable file
View file

9
vendor/nemiah/php-fints/Samples/directDebit_Sephpa.php vendored Normal file → Executable file
View file

@ -46,9 +46,6 @@ $xml = $directDebitFile->generateOutput(['zipToOneFile' => false])[0]['data'];
$sendSEPADirectDebit = \Fhp\Action\SendSEPADirectDebit::create($oneAccount, $xml);
$fints->execute($sendSEPADirectDebit);
require_once 'vop.php';
handleVopAndAuthentication($sendSEPADirectDebit);
// Debit requests don't produce any result we could receive through a getter, but we still need to make sure it's done.
$sendSEPADirectDebit->ensureDone();
if ($sendSEPADirectDebit->needsTan()) {
handleStrongAuthentication($sendSEPADirectDebit); // See login.php for the implementation.
}

9
vendor/nemiah/php-fints/Samples/directDebit_phpSepaXml.php vendored Normal file → Executable file
View file

@ -62,9 +62,6 @@ $oneAccount = $getSepaAccounts->getAccounts()[0];
$sendSEPADirectDebit = \Fhp\Action\SendSEPADirectDebit::create($oneAccount, $sepaDD->toXML('pain.008.001.02'));
$fints->execute($sendSEPADirectDebit);
require_once 'vop.php';
handleVopAndAuthentication($sendSEPADirectDebit);
// Debit requests don't produce any result we could receive through a getter, but we still need to make sure it's done.
$sendSEPADirectDebit->ensureDone();
if ($sendSEPADirectDebit->needsTan()) {
handleStrongAuthentication($sendSEPADirectDebit); // See login.php for the implementation.
}

0
vendor/nemiah/php-fints/Samples/init.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/Samples/login.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/Samples/statementOfAccount.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/Samples/statementOfHoldings.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/Samples/tanModesAndMedia.php vendored Normal file → Executable file
View file

18
vendor/nemiah/php-fints/Samples/transfer.php vendored Normal file → Executable file
View file

@ -21,15 +21,6 @@ use nemiah\phpSepaXml\SEPATransfer;
/** @var \Fhp\FinTs $fints */
$fints = require_once 'login.php';
// Just pick the first account, for demonstration purposes. You could also have the user choose, or have SEPAAccount
// hard-coded and not call getSEPAAccounts() at all.
$getSepaAccounts = \Fhp\Action\GetSEPAAccounts::create();
$fints->execute($getSepaAccounts);
if ($getSepaAccounts->needsTan()) {
handleStrongAuthentication($getSepaAccounts); // See login.php for the implementation.
}
$oneAccount = $getSepaAccounts->getAccounts()[0];
$dt = new \DateTime();
$dt->add(new \DateInterval('P1D'));
@ -58,9 +49,6 @@ $sepaDD->addCreditor(new SEPACreditor([ //this is who you want to send money to
$sendSEPATransfer = \Fhp\Action\SendSEPATransfer::create($oneAccount, $sepaDD->toXML());
$fints->execute($sendSEPATransfer);
require_once 'vop.php';
handleVopAndAuthentication($sendSEPATransfer);
// SEPA transfers don't produce any result we could receive through a getter, but we still need to make sure it's done.
$sendSEPATransfer->ensureDone();
if ($sendSEPATransfer->needsTan()) {
handleStrongAuthentication($sendSEPATransfer); // See login.php for the implementation.
}

View file

@ -1,138 +0,0 @@
<?php
use Fhp\CurlException;
use Fhp\Protocol\ServerException;
use Fhp\Protocol\UnexpectedResponseException;
/**
* SAMPLE - Helper functions for Verification of Payee. To be used together with init.php.
*/
/** @var \Fhp\FinTs $fints */
$fints = require_once 'init.php';
/**
* To be called after the $action was already executed, this function takes care of asking the user for a TAN and VOP
* confirmation, if necessary.
* @param \Fhp\BaseAction $action The action, which must already have been run through {@link \Fhp\FinTs::execute()}.
* @throws CurlException|UnexpectedResponseException|ServerException See {@link FinTs::execute()} for details.
*/
function handleVopAndAuthentication(\Fhp\BaseAction $action): void
{
// NOTE: This is implemented as a `while` loop here, because this sample script runs entirely in one PHP process.
// If you want to make real use of the serializations demonstrated below, in order to resume processing in a new
// PHP process later (once the user has responded via your browser/client-side application), then you won't have a
// loop like this, but instead you'll just run the code within each time you get a new request from the user.
while (!$action->isDone()) {
if ($action->needsTan()) {
handleStrongAuthentication($action); // See login.php for the implementation.
} elseif ($action->needsPollingWait()) {
handlePollingWait($action);
} elseif ($action->needsVopConfirmation()) {
handleVopConfirmation($action);
} else {
throw new \AssertionError(
'Action is not done but also does not need anything to be done. Did you execute() it?'
);
}
}
}
/**
* Waits for the amount of time that the bank prescribed and then polls the server for a status update.
* @param \Fhp\BaseAction $action An action for which {@link \Fhp\BaseAction::needsPollingWait()} returns true.
* @throws CurlException|UnexpectedResponseException|ServerException See {@link FinTs::execute()} for details.
*/
function handlePollingWait(\Fhp\BaseAction $action): void
{
global $fints, $options, $credentials; // From login.php
// Tell the user what the bank had to say (if anything).
$pollingInfo = $action->getPollingInfo();
if ($infoText = $pollingInfo->getInformationForUser()) {
echo $infoText . PHP_EOL;
}
// Optional: If the wait is too long for your PHP process to remain alive (i.e. your server would kill the process),
// you can persist the state as shown here and instead send a response to the client-side application indicating
// that the operation is still ongoing. Then after an appropriate amount of time, the client can send another
// request, spawning a new PHP process, where you can restore the state as shown below.
if ($optionallyPersistEverything = false) {
$persistedAction = serialize($action);
$persistedFints = $fints->persist();
// These are two strings (watch out, they are NOT necessarily UTF-8 encoded), which you can store anywhere.
// This example code stores them in a text file, but you might write them to your database (use a BLOB, not a
// CHAR/TEXT field to allow for arbitrary encoding) or in some other storage (possibly base64-encoded to make it
// ASCII).
file_put_contents(__DIR__ . '/state.txt', serialize([$persistedFints, $persistedAction]));
}
// Wait for (at least) the prescribed amount of time. --------------------------------------------------------------
// Note: In your real application, you may be doing this waiting on the client and then send a fresh request to your
// server.
$waitSecs = $pollingInfo->getNextAttemptInSeconds() ?: 5;
echo "Waiting for $waitSecs seconds before polling the bank server again..." . PHP_EOL;
sleep($waitSecs);
// Optional: If the state was persisted above, we can restore it now (imagine this is a new PHP process).
if ($optionallyPersistEverything) {
$restoredState = file_get_contents(__DIR__ . '/state.txt');
list($persistedInstance, $persistedAction) = unserialize($restoredState);
$fints = \Fhp\FinTs::new($options, $credentials, $persistedInstance);
$action = unserialize($persistedAction);
}
$fints->pollAction($action);
// Now the action is in a new state, which the caller of this function (handleVopAndAuthentication) will deal with.
}
/**
* Asks the user to confirm
* @param \Fhp\BaseAction $action An action for which {@link \Fhp\BaseAction::needsVopConfirmation()} returns true.
* @throws CurlException|UnexpectedResponseException|ServerException See {@link FinTs::execute()} for details.
*/
function handleVopConfirmation(\Fhp\BaseAction $action): void
{
global $fints, $options, $credentials; // From login.php
$vopConfirmationRequest = $action->getVopConfirmationRequest();
if ($infoText = $vopConfirmationRequest->getInformationForUser()) {
echo $infoText . PHP_EOL;
}
echo match ($vopConfirmationRequest->getVerificationResult()) {
\Fhp\Model\VopVerificationResult::CompletedFullMatch =>
'The bank says the payee information matched perfectly, but still wants you to confirm.',
\Fhp\Model\VopVerificationResult::CompletedCloseMatch =>
'The bank says the payee information does not match exactly, so please confirm.',
\Fhp\Model\VopVerificationResult::CompletedPartialMatch =>
'The bank says the payee information does not match for all transfers, so please confirm.',
\Fhp\Model\VopVerificationResult::CompletedNoMatch =>
'The bank says the payee information does not match, but you can still confirm the transfer if you want.',
\Fhp\Model\VopVerificationResult::NotApplicable =>
$vopConfirmationRequest->getVerificationNotApplicableReason() == null
? 'The bank did not provide any information about payee verification, but you can still confirm.'
: 'The bank says: ' . $vopConfirmationRequest->getVerificationNotApplicableReason(),
default => 'The bank failed to provide information about payee verification, but you can still confirm.',
} . PHP_EOL;
// Just like in handleTan(), handleDecoupledSubmission() or handlePollingWait(), we have the option to interrupt the
// PHP process at this point, so that we can ask the user in a client application for their confirmation.
if ($optionallyPersistEverything = false) {
$persistedAction = serialize($action);
$persistedFints = $fints->persist();
// See handlePollingWait() for how to deal with this in practice.
file_put_contents(__DIR__ . '/state.txt', serialize([$persistedFints, $persistedAction]));
}
echo "In light of the information provided above, do you want to confirm the execution of the transfer?" . PHP_EOL;
// Note: We currently have no way canceling the transfer; the only thing we can do is never to confirm it.
echo "If so, please type 'confirm' and hit Return. Otherwise, please kill this PHP process." . PHP_EOL;
while (trim(fgets(STDIN)) !== 'confirm') {
echo "Try again." . PHP_EOL;
}
echo "Confirming the transfer." . PHP_EOL;
$fints->confirmVop($action);
echo "Confirmed" . PHP_EOL;
// Now the action is in a new state, which the caller of this function (handleVopAndAuthentication) will deal with.
}

4
vendor/nemiah/php-fints/composer.json vendored Normal file → Executable file
View file

@ -2,7 +2,7 @@
"name": "nemiah/php-fints",
"description": "PHP Library for the protocols fints and hbci",
"homepage": "https://github.com/nemiah/phpFinTS",
"version": "4.0.0",
"version": "3.7.0",
"license": "MIT",
"autoload": {
"psr-0": {
@ -11,7 +11,7 @@
}
},
"require": {
"php": ">=8.3",
"php": ">=8.0",
"psr/log": "^1|^2|^3",
"ext-curl": "*",
"ext-mbstring": "*"

36
vendor/nemiah/php-fints/csfixer-check.sh vendored Executable file
View file

@ -0,0 +1,36 @@
#!/bin/bash
#
# When this is run as part of a Travis test for a pull request, then it ensures that none of the touched files has any
# PHP CS Fixer warnings.
# From: https://github.com/FriendsOfPHP/PHP-CS-Fixer#using-php-cs-fixer-on-ci
if [ -z "$TRAVIS_COMMIT_RANGE" ]
then
# TRAVIS_COMMIT_RANGE "is empty for builds triggered by the initial commit of a new branch"
# From: https://docs.travis-ci.com/user/environment-variables/
echo "Variable TRAVIS_COMMIT_RANGE not set, falling back to full git diff"
TRAVIS_COMMIT_RANGE=.
fi
IFS='
'
CHANGED_FILES=$(git diff --name-only --diff-filter=ACMRTUXB "$TRAVIS_COMMIT_RANGE")
if [ "$?" -ne "0" ]
then
echo "Error: git diff response code > 0, aborting"
exit 1
fi
if [ -z "${CHANGED_FILES}" ]
then
echo "0 changed files found, exiting"
exit 0
fi
# February 2022: PHP CS FIXER is currently not PHP 8.1 compatible:
# "you may experience code modified in a wrong way"
# "To ignore this requirement please set `PHP_CS_FIXER_IGNORE_ENV`."
export PHP_CS_FIXER_IGNORE_ENV="1"
if ! echo "${CHANGED_FILES}" | grep -qE "^(\\.php_cs(\\.dist)?|composer\\.lock)$"; then EXTRA_ARGS=$(printf -- '--path-mode=intersection\n--\n%s' "${CHANGED_FILES}"); else EXTRA_ARGS=''; fi
vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php -v --dry-run --stop-on-violation --using-cache=no ${EXTRA_ARGS} || (echo "php-cs-fixer failed" && exit 1)

37
vendor/nemiah/php-fints/disallowtabs.sh vendored Executable file
View file

@ -0,0 +1,37 @@
#!/bin/bash
#
# When this is run as part of a Travis test for a pull request, then it ensures that none of the added lines (compared
# to the base branch of the pull request) use tabs for indentations.
# Adapted from https://github.com/mrc/git-hook-library/blob/master/pre-commit.no-tabs
# Abort if any of the inner commands (particularly the git commands) fails.
set -e
set -o pipefail
if [ -z ${TRAVIS_PULL_REQUEST} ]; then
echo "Expected environment variable TRAVIS_PULL_REQUEST"
exit 2
elif [ "${TRAVIS_PULL_REQUEST}" == "false" ]; then
echo "Not a Travis pull request, skipping."
exit 0
fi
# Make sure that we have a local copy of the relevant commits (otherwise git diff won't work).
git remote set-branches --add origin ${TRAVIS_BRNACH}
git fetch
# Compute the diff from the PR's target branch to its HEAD commit.
target_branch="origin/${TRAVIS_BRANCH}"
the_diff=$(git diff "${target_branch}...HEAD")
# Make sure that there are no tabs in the indentation part of added lines.
if echo "${the_diff}" | egrep '^\+\s* ' >/dev/null; then
echo -e "\e[31mError: The changes contain a tab for indentation\e[0m, which is against this repo's policy."
echo "Target branch: origin/${TRAVIS_BRANCH}"
echo "Commit range: ${TRAVIS_COMMIT_RANGE}"
echo "The following tabs were detected:"
echo "${the_diff}" | egrep '^(\+\s* |\+\+\+|@@)'
exit 1
else
echo "No new tabs detected."
fi

6
vendor/nemiah/php-fints/lib/Fhp/Action/GetBalance.php vendored Normal file → Executable file
View file

@ -24,7 +24,7 @@ use Fhp\UnsupportedException;
*/
class GetBalance extends PaginateableAction
{
// Request (if you add a field here, update __serialize() and __unserialize() as well).
// Request (not available after serialization, i.e. not available in processResponse()).
/** @var SEPAAccount */
private $account;
/** @var bool */
@ -79,7 +79,7 @@ class GetBalance extends PaginateableAction
{
list(
$parentSerialized,
$this->account, $this->allAccounts,
$this->account, $this->allAccounts
) = $serialized;
is_array($parentSerialized) ?
@ -96,6 +96,7 @@ class GetBalance extends PaginateableAction
return $this->response;
}
/** {@inheritdoc} */
protected function createRequest(BPD $bpd, ?UPD $upd)
{
/** @var BaseSegment $hisals */
@ -114,6 +115,7 @@ class GetBalance extends PaginateableAction
}
}
/** {@inheritdoc} */
public function processResponse(Message $response)
{
parent::processResponse($response);

View file

@ -0,0 +1,211 @@
<?php
namespace Fhp\Action;
use Fhp\Model\SEPAAccount;
use Fhp\PaginateableAction;
use Fhp\Protocol\BPD;
use Fhp\Protocol\Message;
use Fhp\Protocol\UnexpectedResponseException;
use Fhp\Protocol\UPD;
use Fhp\Segment\Common\Kti;
use Fhp\Segment\Common\KtvV3;
use Fhp\Segment\HIEPS\HIEPS;
use Fhp\Segment\HIKEP\HIKEP;
use Fhp\Segment\HKEKP\HKEKPv1;
use Fhp\Segment\HKEKP\HKEKPv2;
use Fhp\Segment\HIRMS\Rueckmeldungscode;
use Fhp\UnsupportedException;
/**
* Ruft elektronische Kontoauszuege als PDF von der Bank ab.
* Nutzt den HKEKP-Geschaeftsvorfall (Elektronischer Kontoauszug PDF).
*/
class GetBankStatement extends PaginateableAction
{
// Request-Parameter
/** @var SEPAAccount */
private $account;
/** @var string|null Auszugsnummer */
private $statementNumber;
/** @var string|null Jahr (JJJJ) */
private $year;
// Response: Gesammelte PDF-Daten
/** @var string[] Array von PDF-Binaerdaten (ein Eintrag pro Auszug) */
private $pdfStatements = [];
/**
* @param SEPAAccount $account Das Konto fuer das Auszuege abgerufen werden sollen.
* @param string|null $statementNumber Optionale Auszugsnummer.
* @param string|null $year Optionales Jahr (JJJJ).
* @return GetBankStatement
*/
public static function create(
SEPAAccount $account,
?string $statementNumber = null,
?string $year = null
): GetBankStatement {
$result = new GetBankStatement();
$result->account = $account;
$result->statementNumber = $statementNumber;
$result->year = $year;
return $result;
}
/**
* @deprecated
*/
public function serialize(): string
{
return serialize($this->__serialize());
}
public function __serialize(): array
{
return [
parent::__serialize(),
$this->account,
$this->statementNumber,
$this->year,
];
}
/**
* @deprecated
*/
public function unserialize($serialized)
{
self::__unserialize(unserialize($serialized));
}
public function __unserialize(array $serialized): void
{
list(
$parentSerialized,
$this->account,
$this->statementNumber,
$this->year
) = $serialized;
is_array($parentSerialized) ?
parent::__unserialize($parentSerialized) :
parent::unserialize($parentSerialized);
}
/**
* @return string[] Array von PDF-Binaerdaten (ein Eintrag pro Kontoauszug).
*/
public function getPdfStatements(): array
{
$this->ensureDone();
return $this->pdfStatements;
}
/** {@inheritdoc} */
protected function createRequest(BPD $bpd, ?UPD $upd)
{
error_log("[BankImport HKEKP] createRequest() aufgerufen");
/** @var HIEPS $hieps */
$hieps = $bpd->requireLatestSupportedParameters('HIEPS');
$version = $hieps->getVersion();
error_log("[BankImport HKEKP] HIEPS Version=".$version);
error_log("[BankImport HKEKP] Account IBAN=".$this->account->getIban().", BIC=".$this->account->getBic());
error_log("[BankImport HKEKP] Auszugsnummer=".($this->statementNumber ?: 'null').", Jahr=".($this->year ?: 'null'));
switch ($version) {
case 1:
error_log("[BankImport HKEKP] Erstelle HKEKPv1 (KtvV3-basiert, BLZ)");
return HKEKPv1::create(
KtvV3::fromAccount($this->account),
$this->statementNumber,
$this->year
);
case 2:
error_log("[BankImport HKEKP] Erstelle HKEKPv2 (Kti-basiert, IBAN/BIC)");
return HKEKPv2::create(
Kti::fromAccount($this->account),
$this->statementNumber,
$this->year
);
default:
error_log("[BankImport HKEKP] FEHLER: Nicht unterstuetzte Version ".$version);
throw new UnsupportedException('Nicht unterstuetzte HKEKP-Version: ' . $version);
}
}
/** {@inheritdoc} */
public function processResponse(Message $response)
{
error_log("[BankImport HKEKP] processResponse() aufgerufen");
parent::processResponse($response);
// Bank sendet 3010 wenn keine Auszuege verfuegbar
$isUnavailable = $response->findRueckmeldung(Rueckmeldungscode::NICHT_VERFUEGBAR) !== null;
$responseSegments = $response->findSegments(HIKEP::class);
error_log("[BankImport HKEKP] isUnavailable=".($isUnavailable ? 'JA' : 'NEIN')
.", HIKEP-Segmente=".count($responseSegments)
.", Request-Segment-Nummern=".implode(',', $this->getRequestSegmentNumbers()));
// Alle Rueckmeldungen loggen
try {
$rueckmeldungen = $response->findSegments(\Fhp\Segment\HIRMS\HIRMSv2::class);
foreach ($rueckmeldungen as $hirms) {
foreach ($hirms->rueckmeldung as $rm) {
error_log("[BankImport HKEKP] Rueckmeldung: Code=".$rm->rueckmeldungscode
." Ref=".$rm->bezugsdatenelement
." Text=".$rm->rueckmeldungstext);
}
}
} catch (\Throwable $e) {
error_log("[BankImport HKEKP] Rueckmeldungen konnten nicht gelesen werden: ".$e->getMessage());
}
if (!$isUnavailable && count($responseSegments) === 0 && count($this->getRequestSegmentNumbers()) > 0) {
error_log("[BankImport HKEKP] FEHLER: Keine HIKEP-Segmente in Antwort!");
throw new UnexpectedResponseException('Keine HIKEP-Antwort-Segmente erhalten!');
}
/** @var HIKEP $hikep */
foreach ($responseSegments as $segIdx => $hikep) {
error_log("[BankImport HKEKP] Verarbeite HIKEP-Segment ".($segIdx+1)."/".count($responseSegments));
$pdfData = $hikep->getKontoauszug()->getData();
$rawLen = strlen($pdfData);
$rawStart = substr($pdfData, 0, 20);
error_log("[BankImport HKEKP] Rohdaten: ".$rawLen." Bytes, Anfang='".$rawStart."', Hex=".bin2hex(substr($pdfData, 0, 10)));
// Pruefen ob Base64-kodiert (beginnt nicht mit %PDF-)
if (!str_starts_with($pdfData, '%PDF-')) {
error_log("[BankImport HKEKP] Daten beginnen NICHT mit %PDF-, pruefe Base64...");
$decoded = base64_decode($pdfData, true);
if ($decoded !== false && str_starts_with($decoded, '%PDF-')) {
error_log("[BankImport HKEKP] Base64-Dekodierung erfolgreich! ".strlen($decoded)." Bytes nach Dekodierung");
$pdfData = $decoded;
} else {
error_log("[BankImport HKEKP] WARNUNG: Weder PDF noch Base64-PDF erkannt! decoded=".($decoded !== false ? 'ja' : 'nein'));
}
} else {
error_log("[BankImport HKEKP] Daten sind direkt PDF (kein Base64)");
}
// Quittung pruefen
$quittung = $hikep->getQuittung();
if ($quittung !== null) {
error_log("[BankImport HKEKP] Quittung vorhanden: ".strlen($quittung->getData())." Bytes");
}
if (!empty($pdfData)) {
$this->pdfStatements[] = $pdfData;
error_log("[BankImport HKEKP] PDF hinzugefuegt (gesamt: ".count($this->pdfStatements).")");
} else {
error_log("[BankImport HKEKP] WARNUNG: Leere PDF-Daten, uebersprungen");
}
}
error_log("[BankImport HKEKP] processResponse() fertig, ".count($this->pdfStatements)." PDFs gesammelt");
}
}

6
vendor/nemiah/php-fints/lib/Fhp/Action/GetDepotAufstellung.php vendored Normal file → Executable file
View file

@ -24,7 +24,7 @@ use Fhp\UnsupportedException;
*/
class GetDepotAufstellung extends PaginateableAction
{
// Request (if you add a field here, update __serialize() and __unserialize() as well).
// Request (not available after serialization, i.e. not available in processResponse()).
/** @var SEPAAccount */
private $account;
@ -81,7 +81,7 @@ class GetDepotAufstellung extends PaginateableAction
{
list(
$parentSerialized,
$this->account,
$this->account
) = $serialized;
is_array($parentSerialized) ?
@ -111,6 +111,7 @@ class GetDepotAufstellung extends PaginateableAction
return $this->depotWert;
}
/** {@inheritdoc} */
protected function createRequest(BPD $bpd, ?UPD $upd)
{
/** @var HIWPDS $hiwpds */
@ -124,6 +125,7 @@ class GetDepotAufstellung extends PaginateableAction
}
}
/** {@inheritdoc} */
public function processResponse(Message $response)
{
parent::processResponse($response);

339
vendor/nemiah/php-fints/lib/Fhp/Action/GetElectronicStatement.php vendored Normal file → Executable file
View file

@ -6,173 +6,354 @@ use Fhp\Model\SEPAAccount;
use Fhp\PaginateableAction;
use Fhp\Protocol\BPD;
use Fhp\Protocol\Message;
use Fhp\Protocol\UnexpectedResponseException;
use Fhp\Protocol\UPD;
use Fhp\Segment\AnonymousSegment;
use Fhp\Segment\Common\Kti;
use Fhp\Segment\Common\KtvV3;
use Fhp\Segment\EKA\HIEKA;
use Fhp\Segment\EKA\HIEKASv5;
use Fhp\Segment\EKA\HIEKAS;
use Fhp\Segment\EKA\HKEKAv3;
use Fhp\Segment\EKA\HKEKAv4;
use Fhp\Segment\EKA\HKEKAv5;
use Fhp\Segment\HIRMS\Rueckmeldungscode;
use Fhp\UnsupportedException;
/**
* Retrieves electronic bank statements (Elektronischer Kontoauszug) via HKEKA.
* Ruft elektronische Kontoauszuege von der Bank ab (HKEKA).
*
* This supports both MT940 and PDF formats depending on what the bank offers.
* Im Gegensatz zu GetBankStatement (HKEKP, nur PDF) unterstuetzt HKEKA
* verschiedene Formate: 1=MT940, 2=ISO8583, 3=PDF.
* Das gewuenschte Format wird im Request mitgesendet.
*/
class GetElectronicStatement extends PaginateableAction
{
// Format codes
public const FORMAT_MT940 = 1;
public const FORMAT_PDF = 2;
// Kontoauszugsformate
const FORMAT_MT940 = '1';
const FORMAT_ISO8583 = '2';
const FORMAT_PDF = '3';
// Request-Parameter
/** @var SEPAAccount */
private $account;
/** @var int|null Format to request (1=MT940, 2=PDF, null=default) */
/** @var string|null Gewuenschtes Format (1=MT940, 2=ISO8583, 3=PDF) */
private $format;
/** @var string|null Auszugsnummer */
private $statementNumber;
/** @var string|null Jahr (JJJJ) */
private $year;
/** @var string|null Optional: from date YYYYMMDD */
private $fromDate;
/** @var string|null Optional: to date YYYYMMDD */
private $toDate;
// Response data
/** @var string Raw data (MT940 or PDF binary) */
private $data = '';
/** @var array Statement metadata from response */
private $statementInfo = [];
// Response: Gesammelte Daten
/** @var array Array von ['data' => string, 'format' => string|null] */
private $statements = [];
/**
* @param SEPAAccount $account The account to get statements for
* @param int|null $format Format code (1=MT940, 2=PDF, null=bank default)
* @param \DateTime|null $fromDate Optional: Start date for statement range
* @param \DateTime|null $toDate Optional: End date for statement range
* @param SEPAAccount $account Das Konto fuer das Auszuege abgerufen werden sollen.
* @param string|null $format Gewuenschtes Format (FORMAT_MT940/FORMAT_ISO8583/FORMAT_PDF), null=Bank-Standard.
* @param string|null $statementNumber Optionale Auszugsnummer.
* @param string|null $year Optionales Jahr (JJJJ).
* @return GetElectronicStatement
*/
public static function create(
SEPAAccount $account,
?int $format = null,
?\DateTime $fromDate = null,
?\DateTime $toDate = null
?string $format = null,
?string $statementNumber = null,
?string $year = null
): GetElectronicStatement {
$result = new GetElectronicStatement();
$result->account = $account;
$result->format = $format;
$result->fromDate = $fromDate ? $fromDate->format('Ymd') : null;
$result->toDate = $toDate ? $toDate->format('Ymd') : null;
$result->statementNumber = $statementNumber;
$result->year = $year;
return $result;
}
/**
* @deprecated
*/
public function serialize(): string
{
return serialize($this->__serialize());
}
public function __serialize(): array
{
return [
parent::__serialize(),
$this->account,
$this->format,
$this->fromDate,
$this->toDate,
$this->statementNumber,
$this->year,
];
}
/**
* @deprecated
*/
public function unserialize($serialized)
{
self::__unserialize(unserialize($serialized));
}
public function __unserialize(array $serialized): void
{
list(
$parentSerialized,
$this->account,
$this->format,
$this->fromDate,
$this->toDate
$this->statementNumber,
$this->year
) = $serialized;
is_array($parentSerialized)
? parent::__unserialize($parentSerialized)
: parent::unserialize($parentSerialized);
is_array($parentSerialized) ?
parent::__unserialize($parentSerialized) :
parent::unserialize($parentSerialized);
}
/**
* @return string The raw data (MT940 text or PDF binary)
* @return array Array von ['data' => string, 'format' => string|null] pro Kontoauszug.
*/
public function getData(): string
public function getStatements(): array
{
$this->ensureDone();
return $this->data;
return $this->statements;
}
/**
* @return array Statement metadata (number, year, iban, date, format)
* Hilfsfunktion: Gibt nur die PDF-Daten zurueck (filtert nach Format=3 oder erkennt %PDF-).
* @return string[] Array von PDF-Binaerdaten.
*/
public function getStatementInfo(): array
public function getPdfStatements(): array
{
$this->ensureDone();
return $this->statementInfo;
$pdfs = [];
foreach ($this->statements as $stmt) {
$data = $stmt['data'];
$format = $stmt['format'] ?? null;
// PDF wenn Format=3 oder Daten mit %PDF- beginnen
if ($format === self::FORMAT_PDF || str_starts_with($data, '%PDF-')) {
$pdfs[] = $data;
}
}
return $pdfs;
}
/**
* @return bool Whether receipt confirmation is needed
* Ermittelt die hoechste unterstuetzte HIEKAS-Version aus den BPD.
* Funktioniert auch mit AnonymousSegments (wenn unsere typisierten Klassen
* nicht zur Bank-Antwort passen).
*/
public function needsReceipt(): bool
private function resolveHiekasVersion(BPD $bpd): int
{
$this->ensureDone();
return !empty($this->statementInfo['receiptCode']);
// Erst typisierte Segmente versuchen
$hiekas = $bpd->getLatestSupportedParameters('HIEKAS');
if ($hiekas !== null) {
return $hiekas->getVersion();
}
// Fallback: Version aus anonymen BPD-Segmenten lesen
if (isset($bpd->parameters['HIEKAS'])) {
$versions = array_keys($bpd->parameters['HIEKAS']);
// Bereits absteigend sortiert (krsort in BPD::extractFromResponse)
$version = reset($versions);
error_log("[BankImport HKEKA] HIEKAS nur als AnonymousSegment verfuegbar, Version=" . $version);
return (int) $version;
}
throw new UnsupportedException('HIEKAS nicht in BPD gefunden');
}
/** {@inheritdoc} */
protected function createRequest(BPD $bpd, ?UPD $upd)
{
/** @var HIEKASv5|null $hiekas */
$hiekas = $bpd->getLatestSupportedParameters('HIEKAS');
error_log("[BankImport HKEKA] createRequest() aufgerufen");
if ($hiekas === null) {
throw new UnsupportedException('The bank does not support electronic statements (HKEKA).');
$version = $this->resolveHiekasVersion($bpd);
error_log("[BankImport HKEKA] HIEKAS Version=" . $version);
error_log("[BankImport HKEKA] Account IBAN=" . $this->account->getIban() . ", BIC=" . $this->account->getBic());
error_log("[BankImport HKEKA] Format=" . ($this->format ?: 'null (Bank-Standard)')
. ", Auszugsnummer=" . ($this->statementNumber ?: 'null')
. ", Jahr=" . ($this->year ?: 'null'));
switch ($version) {
case 3:
error_log("[BankImport HKEKA] Erstelle HKEKAv3 (KtvV3-basiert, BLZ)");
return HKEKAv3::create(
KtvV3::fromAccount($this->account),
$this->format,
$this->statementNumber,
$this->year
);
case 4:
error_log("[BankImport HKEKA] Erstelle HKEKAv4 (Kti-basiert, IBAN/BIC)");
return HKEKAv4::create(
Kti::fromAccount($this->account),
$this->format,
$this->statementNumber,
$this->year
);
case 5:
error_log("[BankImport HKEKA] Erstelle HKEKAv5 (Kti-basiert, IBAN/BIC)");
return HKEKAv5::create(
Kti::fromAccount($this->account),
$this->format,
$this->statementNumber,
$this->year
);
default:
error_log("[BankImport HKEKA] FEHLER: Nicht unterstuetzte Version " . $version);
throw new UnsupportedException('Nicht unterstuetzte HKEKA-Version: ' . $version);
}
}
$param = $hiekas->getParameter();
/**
* Loggt Diagnose-Informationen ueber anonyme HIEKA-Segmente in der Antwort.
* Wird aufgerufen wenn findSegments(HIEKA::class) leer ist, um die tatsaechliche
* Segment-Struktur der Bank zu analysieren.
*/
private function logAnonymousHiekaSegments(Message $response): void
{
foreach ($response->plainSegments as $seg) {
$name = $seg->getName();
if ($name !== 'HIEKA') {
continue;
}
$version = $seg->getVersion();
$class = get_class($seg);
error_log("[BankImport HKEKA] DIAGNOSE: Segment {$name}v{$version} class={$class}");
// Check if requested format is supported
if ($this->format === self::FORMAT_PDF && !$param->supportsPdf()) {
throw new UnsupportedException('The bank does not support PDF format for electronic statements.');
if ($seg instanceof AnonymousSegment) {
// Reflection um private 'elements' zu lesen
try {
$ref = new \ReflectionClass($seg);
$elProp = $ref->getProperty('elements');
$elProp->setAccessible(true);
$elements = $elProp->getValue($seg);
error_log("[BankImport HKEKA] DIAGNOSE: " . count($elements) . " Elemente im Segment");
foreach ($elements as $idx => $el) {
if ($el === null) {
error_log("[BankImport HKEKA] [{$idx}] NULL (leer)");
} elseif (is_array($el)) {
// DEG (Data Element Group)
$parts = array_map(function ($v) {
if ($v === null) return 'NULL';
$s = (string) $v;
return strlen($s) > 40 ? substr($s, 0, 40) . '...(' . strlen($s) . 'B)' : $s;
}, $el);
error_log("[BankImport HKEKA] [{$idx}] DEG: " . implode(' : ', $parts));
} else {
$val = (string) $el;
if (strlen($val) > 80) {
// Binaerdaten oder lange Strings kuerzen
$hex = bin2hex(substr($val, 0, 16));
error_log("[BankImport HKEKA] [{$idx}] BIN/LANG: " . strlen($val)
. " Bytes, Hex-Start=" . $hex
. ", Text-Start=" . substr($val, 0, 30));
} else {
error_log("[BankImport HKEKA] [{$idx}] " . $val);
}
}
}
} catch (\Throwable $e) {
error_log("[BankImport HKEKA] DIAGNOSE: Reflection fehlgeschlagen: " . $e->getMessage());
}
}
}
// Use Kti (IBAN/BIC) for version 5
$kti = Kti::fromAccount($this->account);
return HKEKAv5::create($kti, $this->format, $this->fromDate, $this->toDate);
}
/** {@inheritdoc} */
public function processResponse(Message $response)
{
error_log("[BankImport HKEKA] processResponse() aufgerufen");
parent::processResponse($response);
// Check if no statements available
if ($response->findRueckmeldung(Rueckmeldungscode::NICHT_VERFUEGBAR) !== null) {
return;
}
/** @var HIEKA[] $responseSegments */
// Bank sendet 3010 wenn keine Auszuege verfuegbar
$isUnavailable = $response->findRueckmeldung(Rueckmeldungscode::NICHT_VERFUEGBAR) !== null;
$responseSegments = $response->findSegments(HIEKA::class);
if (empty($responseSegments)) {
// No segments but also no error = empty response
return;
error_log("[BankImport HKEKA] isUnavailable=" . ($isUnavailable ? 'JA' : 'NEIN')
. ", HIEKA-Segmente=" . count($responseSegments)
. ", Request-Segment-Nummern=" . implode(',', $this->getRequestSegmentNumbers()));
// Alle Rueckmeldungen loggen
try {
$rueckmeldungen = $response->findSegments(\Fhp\Segment\HIRMS\HIRMSv2::class);
foreach ($rueckmeldungen as $hirms) {
foreach ($hirms->rueckmeldung as $rm) {
error_log("[BankImport HKEKA] Rueckmeldung: Code=" . $rm->rueckmeldungscode
. " Ref=" . $rm->bezugsdatenelement
. " Text=" . $rm->rueckmeldungstext);
}
}
} catch (\Throwable $e) {
error_log("[BankImport HKEKA] Rueckmeldungen konnten nicht gelesen werden: " . $e->getMessage());
}
foreach ($responseSegments as $hieka) {
// Append data (for pagination)
$this->data .= $hieka->getData();
// Wenn keine typisierten HIEKA-Segmente gefunden: Diagnose-Logging
if (count($responseSegments) === 0) {
error_log("[BankImport HKEKA] Keine typisierten HIEKA-Segmente, pruefe anonyme...");
$this->logAnonymousHiekaSegments($response);
}
// Store metadata from first segment
if (empty($this->statementInfo)) {
$this->statementInfo = [
'statementNumber' => $hieka->getStatementNumber(),
'statementYear' => $hieka->getStatementYear(),
'iban' => $hieka->getIban(),
'creationDate' => $hieka->getCreationDate(),
'format' => $hieka->getFormat(),
'receiptCode' => $hieka->needsReceipt() ? $hieka->getReceiptCode() : null,
if (!$isUnavailable && count($responseSegments) === 0 && count($this->getRequestSegmentNumbers()) > 0) {
error_log("[BankImport HKEKA] FEHLER: Keine HIEKA-Segmente in Antwort!");
throw new UnexpectedResponseException('Keine HIEKA-Antwort-Segmente erhalten!');
}
/** @var HIEKA $hieka */
foreach ($responseSegments as $segIdx => $hieka) {
error_log("[BankImport HKEKA] Verarbeite HIEKA-Segment " . ($segIdx + 1) . "/" . count($responseSegments));
$format = $hieka->getKontoauszugsformat();
error_log("[BankImport HKEKA] Format=" . ($format ?: 'null')
. " (1=MT940, 2=ISO8583, 3=PDF)");
$data = $hieka->getKontoauszug()->getData();
$rawLen = strlen($data);
$rawStart = substr($data, 0, 20);
error_log("[BankImport HKEKA] Rohdaten: " . $rawLen . " Bytes, Anfang='" . $rawStart
. "', Hex=" . bin2hex(substr($data, 0, 10)));
// Pruefen ob Base64-kodiert
if ($format === self::FORMAT_PDF && !str_starts_with($data, '%PDF-')) {
error_log("[BankImport HKEKA] PDF-Format aber beginnt NICHT mit %PDF-, pruefe Base64...");
$decoded = base64_decode($data, true);
if ($decoded !== false && str_starts_with($decoded, '%PDF-')) {
error_log("[BankImport HKEKA] Base64-Dekodierung erfolgreich! " . strlen($decoded) . " Bytes");
$data = $decoded;
} else {
error_log("[BankImport HKEKA] WARNUNG: Base64-Dekodierung fehlgeschlagen, verwende Rohdaten");
}
} elseif ($format === null && !str_starts_with($data, '%PDF-')) {
// Kein Format angegeben, trotzdem Base64 pruefen
$decoded = base64_decode($data, true);
if ($decoded !== false && str_starts_with($decoded, '%PDF-')) {
error_log("[BankImport HKEKA] Ohne Format-Angabe: Base64-PDF erkannt! " . strlen($decoded) . " Bytes");
$data = $decoded;
$format = self::FORMAT_PDF;
}
}
// Quittung pruefen
$quittung = $hieka->getQuittung();
if ($quittung !== null) {
error_log("[BankImport HKEKA] Quittung vorhanden: " . strlen($quittung->getData()) . " Bytes");
}
if (!empty($data)) {
$this->statements[] = [
'data' => $data,
'format' => $format,
];
error_log("[BankImport HKEKA] Statement hinzugefuegt (Format=" . ($format ?: 'unbekannt')
. ", gesamt: " . count($this->statements) . ")");
} else {
error_log("[BankImport HKEKA] WARNUNG: Leere Daten, uebersprungen");
}
}
error_log("[BankImport HKEKA] processResponse() fertig, " . count($this->statements) . " Statements gesammelt");
}
}

2
vendor/nemiah/php-fints/lib/Fhp/Action/GetSEPAAccounts.php vendored Normal file → Executable file
View file

@ -47,6 +47,7 @@ class GetSEPAAccounts extends PaginateableAction
return $this->accounts;
}
/** {@inheritdoc} */
protected function createRequest(BPD $bpd, ?UPD $upd)
{
/** @var BaseSegment $hispas */
@ -63,6 +64,7 @@ class GetSEPAAccounts extends PaginateableAction
}
}
/** {@inheritdoc} */
public function processResponse(Message $response)
{
parent::processResponse($response);

View file

@ -16,11 +16,12 @@ class GetSEPADirectDebitParameters extends BaseAction
public const SEQUENCE_TYPES = ['FRST', 'OOFF', 'FNAL', 'RCUR'];
public const DIRECT_DEBIT_TYPES = ['CORE', 'COR1', 'B2B'];
// Request (if you add a field here, update __serialize() and __unserialize() as well).
/** @var string */
private $directDebitType;
/** @var string */
private $seqType;
/** @var bool */
private $singleDirectDebit;
@ -42,45 +43,6 @@ class GetSEPADirectDebitParameters extends BaseAction
return $result;
}
/**
* @deprecated Beginning from PHP7.4 __unserialize is used for new generated strings, then this method is only used for previously generated strings - remove after May 2023
*/
public function serialize(): string
{
return serialize($this->__serialize());
}
public function __serialize(): array
{
return [
parent::__serialize(),
$this->directDebitType, $this->seqType, $this->singleDirectDebit,
];
}
/**
* @deprecated Beginning from PHP7.4 __unserialize is used for new generated strings, then this method is only used for previously generated strings - remove after May 2023
*
* @param string $serialized
* @return void
*/
public function unserialize($serialized)
{
self::__unserialize(unserialize($serialized));
}
public function __unserialize(array $serialized): void
{
list(
$parentSerialized,
$this->directDebitType, $this->seqType, $this->singleDirectDebit,
) = $serialized;
is_array($parentSerialized) ?
parent::__unserialize($parentSerialized) :
parent::unserialize($parentSerialized);
}
public static function getHixxesSegmentName(string $directDebitType, bool $singleDirectDebit): string
{
switch ($directDebitType) {
@ -94,6 +56,7 @@ class GetSEPADirectDebitParameters extends BaseAction
}
}
/** {@inheritdoc} */
protected function createRequest(BPD $bpd, ?UPD $upd)
{
$this->hidxes = $bpd->requireLatestSupportedParameters(static::getHixxesSegmentName($this->directDebitType, $this->singleDirectDebit));

View file

@ -1,194 +0,0 @@
<?php
namespace Fhp\Action;
use Fhp\Model\SEPAAccount;
use Fhp\PaginateableAction;
use Fhp\Protocol\BPD;
use Fhp\Protocol\Message;
use Fhp\Protocol\UPD;
use Fhp\Segment\Common\Kti;
use Fhp\Segment\KAA\HIKAA;
use Fhp\Segment\KAA\HIKAASv1;
use Fhp\Segment\KAA\HKKAAv1;
use Fhp\Segment\KAA\HKKAAv2;
use Fhp\Segment\HIRMS\Rueckmeldungscode;
use Fhp\UnsupportedException;
/**
* Retrieves PDF bank statements from bank archive (Kontoauszug aus Archiv) via HKKAA.
*
* This is an alternative to HKEKP for banks that store statements in an archive/mailbox
* instead of providing direct PDF generation.
*/
class GetStatementFromArchive extends PaginateableAction
{
// PDF Format code
public const FORMAT_PDF = 4;
public const FORMAT_MT940 = 1;
/** @var SEPAAccount */
private $account;
/** @var int Format to request (default: PDF = 4) */
private $format;
/** @var string|null Optional: from date YYYYMMDD */
private $fromDate;
/** @var string|null Optional: to date YYYYMMDD */
private $toDate;
// Response data
/** @var string Raw PDF data (may be from multiple pages) */
private $pdfData = '';
/** @var array Statement metadata from response */
private $statementInfo = [];
/**
* @param SEPAAccount $account The account to get statements for
* @param int $format Format code (4 = PDF)
* @param \DateTime|null $fromDate Optional: Start date for statement range
* @param \DateTime|null $toDate Optional: End date for statement range
* @return GetStatementFromArchive
*/
public static function create(
SEPAAccount $account,
int $format = self::FORMAT_PDF,
?\DateTime $fromDate = null,
?\DateTime $toDate = null
): GetStatementFromArchive {
$result = new GetStatementFromArchive();
$result->account = $account;
$result->format = $format;
$result->fromDate = $fromDate ? $fromDate->format('Ymd') : null;
$result->toDate = $toDate ? $toDate->format('Ymd') : null;
return $result;
}
public function __serialize(): array
{
return [
parent::__serialize(),
$this->account,
$this->format,
$this->fromDate,
$this->toDate,
];
}
public function __unserialize(array $serialized): void
{
list(
$parentSerialized,
$this->account,
$this->format,
$this->fromDate,
$this->toDate
) = $serialized;
is_array($parentSerialized)
? parent::__unserialize($parentSerialized)
: parent::unserialize($parentSerialized);
}
/**
* @return string The raw PDF data
*/
public function getPdfData(): string
{
$this->ensureDone();
return $this->pdfData;
}
/**
* @return array Statement metadata (number, year, iban, date, filename)
*/
public function getStatementInfo(): array
{
$this->ensureDone();
return $this->statementInfo;
}
/**
* @return bool Whether receipt confirmation is needed
*/
public function needsReceipt(): bool
{
$this->ensureDone();
return !empty($this->statementInfo['receiptCode']);
}
/** {@inheritdoc} */
protected function createRequest(BPD $bpd, ?UPD $upd)
{
/** @var HIKAASv1|null $hikaas */
$hikaas = $bpd->getLatestSupportedParameters('HIKAAS');
if ($hikaas === null) {
throw new UnsupportedException('The bank does not support archive statements (HKKAA).');
}
$param = $hikaas->getParameter();
// Check if PDF format is supported
if ($this->format === self::FORMAT_PDF && !$param->supportsPdf()) {
throw new UnsupportedException('The bank does not support PDF format for archive statements.');
}
// Check if date range queries are supported
if (($this->fromDate !== null || $this->toDate !== null) && !$param->canFetchByDateRange()) {
throw new UnsupportedException('The bank does not support date range queries for archive statements.');
}
// Use the correct HKKAA version based on HIKAAS version in BPD
$hikaasVersion = $hikaas->getVersion();
// Use Kti with IBAN/BIC only (not the full account details)
$kti = Kti::create($this->account->getIban(), $this->account->getBic());
if ($hikaasVersion >= 2) {
return HKKAAv2::create($kti, $this->format, $this->fromDate, $this->toDate);
} else {
return HKKAAv1::create($kti, $this->format, $this->fromDate, $this->toDate);
}
}
/** {@inheritdoc} */
public function processResponse(Message $response)
{
parent::processResponse($response);
// Check if no statements available
if ($response->findRueckmeldung(Rueckmeldungscode::NICHT_VERFUEGBAR) !== null) {
return;
}
/** @var HIKAA[] $responseSegments */
$responseSegments = $response->findSegments(HIKAA::class);
if (empty($responseSegments)) {
// No segments but also no error = empty response
return;
}
foreach ($responseSegments as $hikaa) {
// Append PDF data (for pagination)
$this->pdfData .= $hikaa->getPdfData();
// Store metadata from first segment
if (empty($this->statementInfo)) {
$this->statementInfo = [
'statementNumber' => $hikaa->getStatementNumber(),
'statementYear' => $hikaa->getStatementYear(),
'iban' => $hikaa->getIban(),
'creationDate' => $hikaa->getCreationDate(),
'filename' => $hikaa->getFilename(),
'format' => $hikaa->getFormat(),
'receiptCode' => $hikaa->needsReceipt() ? $hikaa->getReceiptCode() : null,
];
}
}
}
}

10
vendor/nemiah/php-fints/lib/Fhp/Action/GetStatementOfAccount.php vendored Normal file → Executable file
View file

@ -31,7 +31,7 @@ use Fhp\UnsupportedException;
*/
class GetStatementOfAccount extends PaginateableAction
{
// Request (if you add a field here, update __serialize() and __unserialize() as well).
// Request (not available after serialization, i.e. not available in processResponse()).
/** @var SEPAAccount */
private $account;
/** @var \DateTime */
@ -93,7 +93,7 @@ class GetStatementOfAccount extends PaginateableAction
{
return [
parent::__serialize(),
$this->account, $this->from, $this->to, $this->allAccounts, $this->includeUnbooked,
$this->account, $this->from, $this->to, $this->allAccounts,
$this->bankName,
];
}
@ -113,8 +113,8 @@ class GetStatementOfAccount extends PaginateableAction
{
list(
$parentSerialized,
$this->account, $this->from, $this->to, $this->allAccounts, $this->includeUnbooked,
$this->bankName,
$this->account, $this->from, $this->to, $this->allAccounts,
$this->bankName
) = $serialized;
is_array($parentSerialized) ?
@ -147,6 +147,7 @@ class GetStatementOfAccount extends PaginateableAction
return $this->statement;
}
/** {@inheritdoc} */
protected function createRequest(BPD $bpd, ?UPD $upd)
{
$this->bankName = $bpd->getBankName();
@ -170,6 +171,7 @@ class GetStatementOfAccount extends PaginateableAction
}
}
/** {@inheritdoc} */
public function processResponse(Message $response)
{
parent::processResponse($response);

6
vendor/nemiah/php-fints/lib/Fhp/Action/GetStatementOfAccountXML.php vendored Normal file → Executable file
View file

@ -24,7 +24,7 @@ use Fhp\UnsupportedException;
*/
class GetStatementOfAccountXML extends PaginateableAction
{
// Request (if you add a field here, update __serialize() and __unserialize() as well).
// Request (not available after serialization, i.e. not available in processResponse()).
/** @var SEPAAccount */
private $account;
/** @var \DateTime */
@ -98,7 +98,7 @@ class GetStatementOfAccountXML extends PaginateableAction
{
list(
$parentSerialized,
$this->account, $this->camtURN, $this->from, $this->to, $this->allAccounts,
$this->account, $this->camtURN, $this->from, $this->to, $this->allAccounts
) = $serialized;
is_array($parentSerialized) ?
@ -115,6 +115,7 @@ class GetStatementOfAccountXML extends PaginateableAction
return $this->xml;
}
/** {@inheritdoc} */
protected function createRequest(BPD $bpd, ?UPD $upd)
{
if ($upd === null) {
@ -148,6 +149,7 @@ class GetStatementOfAccountXML extends PaginateableAction
}
}
/** {@inheritdoc} */
public function processResponse(Message $response)
{
parent::processResponse($response);

View file

@ -13,11 +13,12 @@ use Fhp\Syntax\Bin;
class SendInternationalCreditTransfer extends BaseAction
{
// Request (if you add a field here, update __serialize() and __unserialize() as well).
/** @var SEPAAccount */
protected $account;
/** @var string */
protected $dtavzData;
/** @var string|null */
protected $dtavzVersion;
@ -35,45 +36,6 @@ class SendInternationalCreditTransfer extends BaseAction
return $result;
}
/**
* @deprecated Beginning from PHP7.4 __unserialize is used for new generated strings, then this method is only used for previously generated strings - remove after May 2023
*/
public function serialize(): string
{
return serialize($this->__serialize());
}
public function __serialize(): array
{
return [
parent::__serialize(),
$this->account, $this->dtavzData, $this->dtavzVersion,
];
}
/**
* @deprecated Beginning from PHP7.4 __unserialize is used for new generated strings, then this method is only used for previously generated strings - remove after May 2023
*
* @param string $serialized
* @return void
*/
public function unserialize($serialized)
{
self::__unserialize(unserialize($serialized));
}
public function __unserialize(array $serialized): void
{
list(
$parentSerialized,
$this->account, $this->dtavzData, $this->dtavzVersion,
) = $serialized;
is_array($parentSerialized) ?
parent::__unserialize($parentSerialized) :
parent::unserialize($parentSerialized);
}
protected function createRequest(BPD $bpd, ?UPD $upd)
{
/** @var HIAUBSv9 $hiaubs */

17
vendor/nemiah/php-fints/lib/Fhp/Action/SendSEPADirectDebit.php vendored Normal file → Executable file
View file

@ -11,10 +11,8 @@ use Fhp\Segment\Common\Btg;
use Fhp\Segment\Common\Kti;
use Fhp\Segment\DME\HIDMESv1;
use Fhp\Segment\DME\HIDMESv2;
use Fhp\Segment\DME\HKDMEv2;
use Fhp\Segment\DSE\HIDSESv2;
use Fhp\Segment\DSE\HIDXES;
use Fhp\Segment\DSE\HKDSEv2;
use Fhp\Segment\SPA\HISPAS;
use Fhp\Syntax\Bin;
use Fhp\UnsupportedException;
@ -24,24 +22,27 @@ use Fhp\UnsupportedException;
*/
class SendSEPADirectDebit extends BaseAction
{
// Request (if you add a field here, update __serialize() and __unserialize() as well).
/** @var SEPAAccount */
protected $account;
/** @var string */
protected $painMessage;
/** @var string */
protected $painNamespace;
/** @var float */
protected $ctrlSum;
/** @var bool */
protected $singleDirectDebit = false;
/** @var bool */
protected $tryToUseControlSumForSingleTransactions = false;
/** @var string */
private $coreType;
// There are no result fields. This action is simply marked as done to indicate that the transfer was executed.
public static function create(SEPAAccount $account, string $painMessage, bool $tryToUseControlSumForSingleTransactions = false): SendSEPADirectDebit
{
if (preg_match('/xmlns="(?<namespace>[^"]+)"/s', $painMessage, $matches) === 1) {
@ -113,7 +114,7 @@ class SendSEPADirectDebit extends BaseAction
{
list(
$parentSerialized,
$this->singleDirectDebit, $this->tryToUseControlSumForSingleTransactions, $this->ctrlSum, $this->coreType, $this->painMessage, $this->painNamespace, $this->account,
$this->singleDirectDebit, $this->tryToUseControlSumForSingleTransactions, $this->ctrlSum, $this->coreType, $this->painMessage, $this->painNamespace, $this->account
) = $serialized;
is_array($parentSerialized) ?
@ -150,7 +151,7 @@ class SendSEPADirectDebit extends BaseAction
// Sometimes the Bank reports supported schemas with a "_GBIC_X" postfix.
// GIBC_X stands for German Banking Industry Committee and a version counter.
$xmlSchema = $this->painNamespace;
$matchingSchemas = array_filter($supportedPainNamespaces, function ($value) use ($xmlSchema) {
$matchingSchemas = array_filter($supportedPainNamespaces, function($value) use ($xmlSchema) {
// For example urn:iso:std:iso:20022:tech:xsd:pain.008.001.08 from the xml matches
// urn:iso:std:iso:20022:tech:xsd:pain.008.001.08_GBIC_4
return str_starts_with($value, $xmlSchema);
@ -161,7 +162,7 @@ class SendSEPADirectDebit extends BaseAction
. implode(', ', $supportedPainNamespaces));
}
/** @var HKDMEv2|HKDSEv2|HIDXES $hkdxe */
/** @var mixed $hkdxe */ // TODO Put a new interface type here.
$hkdxe = $hidxes->createRequestSegment();
$hkdxe->kontoverbindungInternational = Kti::fromAccount($this->account);
$hkdxe->sepaDescriptor = $this->painNamespace;

51
vendor/nemiah/php-fints/lib/Fhp/Action/SendSEPARealtimeTransfer.php vendored Normal file → Executable file
View file

@ -23,16 +23,14 @@ use Fhp\UnsupportedException;
*/
class SendSEPARealtimeTransfer extends BaseAction
{
// Request (if you add a field here, update __serialize() and __unserialize() as well).
/** @var SEPAAccount */
private $account;
/** @var string */
private $painMessage;
/** @var string */
private $xmlSchema;
private bool $allowConversionToSEPATransfer = true;
// There are no result fields. This action is simply marked as done to indicate that the transfer was executed.
private bool $allowConversionToSEPATransfer = true;
/**
* @param SEPAAccount $account The account from which the transfer will be sent.
@ -54,45 +52,7 @@ class SendSEPARealtimeTransfer extends BaseAction
return $result;
}
/**
* @deprecated Beginning from PHP7.4 __unserialize is used for new generated strings, then this method is only used for previously generated strings - remove after May 2023
*/
public function serialize(): string
{
return serialize($this->__serialize());
}
public function __serialize(): array
{
return [
parent::__serialize(),
$this->account, $this->painMessage, $this->xmlSchema, $this->allowConversionToSEPATransfer,
];
}
/**
* @deprecated Beginning from PHP7.4 __unserialize is used for new generated strings, then this method is only used for previously generated strings - remove after May 2023
*
* @param string $serialized
* @return void
*/
public function unserialize($serialized)
{
self::__unserialize(unserialize($serialized));
}
public function __unserialize(array $serialized): void
{
list(
$parentSerialized,
$this->account, $this->painMessage, $this->xmlSchema, $this->allowConversionToSEPATransfer,
) = $serialized;
is_array($parentSerialized) ?
parent::__unserialize($parentSerialized) :
parent::unserialize($parentSerialized);
}
/** {@inheritdoc} */
protected function createRequest(BPD $bpd, ?UPD $upd)
{
/** @var HIIPZSv1|HIIPZSv2 $hiipzs */
@ -110,7 +70,7 @@ class SendSEPARealtimeTransfer extends BaseAction
// Sometimes the Bank reports supported schemas with a "_GBIC_X" postfix.
// GIBC_X stands for German Banking Industry Committee and a version counter.
$xmlSchema = $this->xmlSchema;
$matchingSchemas = array_filter($supportedSchemas, function ($value) use ($xmlSchema) {
$matchingSchemas = array_filter($supportedSchemas, function($value) use ($xmlSchema) {
// For example urn:iso:std:iso:20022:tech:xsd:pain.001.001.09 from the xml matches
// urn:iso:std:iso:20022:tech:xsd:pain.001.001.09_GBIC_4
return str_starts_with($value, $xmlSchema);
@ -132,6 +92,7 @@ class SendSEPARealtimeTransfer extends BaseAction
return $hkipz;
}
/** {@inheritdoc} */
public function processResponse(Message $response)
{
parent::processResponse($response);
@ -145,8 +106,8 @@ class SendSEPARealtimeTransfer extends BaseAction
return;
}
if ($response->findRueckmeldung(Rueckmeldungscode::ENTGEGENGENOMMEN) === null
&& $response->findRueckmeldung(Rueckmeldungscode::AUSGEFUEHRT) === null) {
if ($response->findRueckmeldung(Rueckmeldungscode::ENTGEGENGENOMMEN) === null &&
$response->findRueckmeldung(Rueckmeldungscode::AUSGEFUEHRT) === null) {
throw new UnexpectedResponseException('Bank did not confirm SEPATransfer execution');
}
}

83
vendor/nemiah/php-fints/lib/Fhp/Action/SendSEPATransfer.php vendored Normal file → Executable file
View file

@ -19,17 +19,12 @@ use Fhp\UnsupportedException;
*/
class SendSEPATransfer extends BaseAction
{
// Request (if you add a field here, update __serialize() and __unserialize() as well).
/** @var SEPAAccount */
private $account;
/** @var string */
private $painMessage;
/** @var string */
private $xmlSchema;
/** @var bool */
private $singleBookingRequested = false;
// There are no result fields. This action is simply marked as done to indicate that the transfer was executed.
/**
* @param SEPAAccount $account The account from which the transfer will be sent.
@ -49,62 +44,11 @@ class SendSEPATransfer extends BaseAction
return $result;
}
/**
* Request individual bookings instead of a batch booking on the bank statement.
* Only applicable for batch transfers (Sammelüberweisung).
*
* @param bool $singleBookingRequested If true, each transaction appears separately on the statement.
* @return $this
*/
public function setSingleBookingRequested(bool $singleBookingRequested): self
{
$this->singleBookingRequested = $singleBookingRequested;
return $this;
}
/**
* @deprecated Beginning from PHP7.4 __unserialize is used for new generated strings, then this method is only used for previously generated strings - remove after May 2023
*/
public function serialize(): string
{
return serialize($this->__serialize());
}
public function __serialize(): array
{
return [
parent::__serialize(),
$this->account, $this->painMessage, $this->xmlSchema, $this->singleBookingRequested,
];
}
/**
* @deprecated Beginning from PHP7.4 __unserialize is used for new generated strings, then this method is only used for previously generated strings - remove after May 2023
*
* @param string $serialized
* @return void
*/
public function unserialize($serialized)
{
self::__unserialize(unserialize($serialized));
}
public function __unserialize(array $serialized): void
{
list(
$parentSerialized,
$this->account, $this->painMessage, $this->xmlSchema, $this->singleBookingRequested,
) = $serialized;
is_array($parentSerialized) ?
parent::__unserialize($parentSerialized) :
parent::unserialize($parentSerialized);
}
/** {@inheritdoc} */
protected function createRequest(BPD $bpd, ?UPD $upd)
{
// ANALYSE XML FOR RECEIPTS AND PAYMENT DATE
$xmlAsObject = simplexml_load_string($this->painMessage, 'SimpleXMLElement', LIBXML_NOCDATA);
//ANALYSE XML FOR RECEIPTS AND PAYMENT DATE
$xmlAsObject = simplexml_load_string($this->painMessage, "SimpleXMLElement", LIBXML_NOCDATA);
$numberOfTransactions = $xmlAsObject->CstmrCdtTrfInitn->GrpHdr->NbOfTxs;
$hasReqdExDates = false;
foreach ($xmlAsObject->CstmrCdtTrfInitn?->PmtInf as $pmtInfo) {
@ -115,21 +59,25 @@ class SendSEPATransfer extends BaseAction
}
}
// NOW READ OUT, WICH SEGMENT SHOULD BE USED:
//NOW READ OUT, WICH SEGMENT SHOULD BE USED:
if ($numberOfTransactions > 1 && $hasReqdExDates) {
// Terminierte SEPA-Sammelüberweisung (Segment HKCME / Kennung HICMES)
$segmentID = 'HICMES';
$segment = \Fhp\Segment\CME\HKCMEv1::createEmpty();
} elseif ($numberOfTransactions == 1 && $hasReqdExDates) {
// Terminierte SEPA-Überweisung (Segment HKCSE / Kennung HICSES)
$segmentID = 'HICSES';
$segment = \Fhp\Segment\CSE\HKCSEv1::createEmpty();
} elseif ($numberOfTransactions > 1 && !$hasReqdExDates) {
// SEPA-Sammelüberweisungen (Segment HKCCM / Kennung HICSES)
$segmentID = 'HICSES';
$segment = \Fhp\Segment\CCM\HKCCMv1::createEmpty();
} else {
// SEPA Einzelüberweisung (Segment HKCCS / Kennung HICCSS).
//SEPA Einzelüberweisung (Segment HKCCS / Kennung HICCSS).
$segmentID = 'HICCSS';
$segment = \Fhp\Segment\CCS\HKCCSv1::createEmpty();
}
@ -145,7 +93,7 @@ class SendSEPATransfer extends BaseAction
// Sometimes the Bank reports supported schemas with a "_GBIC_X" postfix.
// GIBC_X stands for German Banking Industry Committee and a version counter.
$xmlSchema = $this->xmlSchema;
$matchingSchemas = array_filter($supportedSchemas, function ($value) use ($xmlSchema) {
$matchingSchemas = array_filter($supportedSchemas, function($value) use ($xmlSchema) {
// For example urn:iso:std:iso:20022:tech:xsd:pain.001.001.09 from the xml matches
// urn:iso:std:iso:20022:tech:xsd:pain.001.001.09_GBIC_4
return str_starts_with($value, $xmlSchema);
@ -159,19 +107,10 @@ class SendSEPATransfer extends BaseAction
$segment->kontoverbindungInternational = Kti::fromAccount($this->account);
$segment->sepaDescriptor = $this->xmlSchema;
$segment->sepaPainMessage = new Bin($this->painMessage);
// For batch transfers: set einzelbuchungGewuenscht if bank allows it
if ($numberOfTransactions > 1) {
$paramSegmentId = $hasReqdExDates ? 'HICMES' : 'HICCMS';
$paramSegment = $bpd->getLatestSupportedParameters($paramSegmentId);
if ($paramSegment !== null && $paramSegment->getParameter()->einzelbuchungErlaubt) {
$segment->einzelbuchungGewuenscht = $this->singleBookingRequested;
}
}
return $segment;
}
/** {@inheritdoc} */
public function processResponse(Message $response)
{
parent::processResponse($response);

107
vendor/nemiah/php-fints/lib/Fhp/BaseAction.php vendored Normal file → Executable file
View file

@ -4,17 +4,13 @@
namespace Fhp;
use Fhp\Model\PollingInfo;
use Fhp\Model\TanRequest;
use Fhp\Model\VopConfirmationRequest;
use Fhp\Protocol\ActionIncompleteException;
use Fhp\Protocol\ActionPendingException;
use Fhp\Protocol\BPD;
use Fhp\Protocol\Message;
use Fhp\Protocol\TanRequiredException;
use Fhp\Protocol\UnexpectedResponseException;
use Fhp\Protocol\UPD;
use Fhp\Protocol\VopConfirmationRequiredException;
use Fhp\Segment\BaseSegment;
use Fhp\Segment\HIRMS\Rueckmeldung;
use Fhp\Segment\HIRMS\Rueckmeldungscode;
@ -41,37 +37,37 @@ use Fhp\Segment\HIRMS\Rueckmeldungscode;
abstract class BaseAction implements \Serializable
{
/** @var int[] Stores segment numbers that were assigned to the segments returned from {@link createRequest()}. */
protected ?array $requestSegmentNumbers = null;
protected $requestSegmentNumbers;
/**
* Contains the name of the segment, that might need a tan, used by FinTs::execute to signal
* @var string|null Contains the name of the segment, that might need a tan, used by FinTs::execute to signal
* to the bank that supplying a tan is supported.
*/
protected ?string $needTanForSegment = null;
protected $needTanForSegment = null;
/** If set, the last response from the server regarding this action asked for a TAN from the user. */
protected ?TanRequest $tanRequest = null;
/**
* If set, the last response from the server regarding this action asked for a TAN from the user.
* @var TanRequest|null
*/
protected $tanRequest;
/** If set, this action is currently waiting for a long-running operation on the server to complete. */
protected ?PollingInfo $pollingInfo = null;
/** If set, this action needs the user's confirmation to be completed. */
protected ?VopConfirmationRequest $vopConfirmationRequest = null;
protected bool $isDone = false;
/** @var bool */
protected $isDone = false;
/**
* Will be populated with the message the bank sent along with the success indication, can be used to show to
* the user.
* @var string
*/
public ?string $successMessage = null;
public $successMessage;
/**
* @deprecated Beginning from PHP7.4 __unserialize is used for new generated strings, then this method is only used for previously generated strings - remove after May 2023
*
* NOTE: A common mistake is to call this function directly. Instead, you probably want `serialize($instance)`.
*
* An action can only be serialized before it was completed.
* An action can only be serialized *after* it has been executed in case it needs a TAN, i.e. when the result is not
* present yet.
* If a sub-class overrides this, it should call the parent function and include it in its result.
* @return string The serialized action, e.g. for storage in a database. This will not contain sensitive user data.
*/
@ -81,23 +77,21 @@ abstract class BaseAction implements \Serializable
}
/**
* An action can only be serialized before it was completed.
* An action can only be serialized *after* it has been executed in case it needs a TAN, i.e. when the result is not
* present yet.
* If a sub-class overrides this, it should call the parent function and include it in its result.
*
* @return array The serialized action, e.g. for storage in a database. This will not contain sensitive user data.
* Note that this is not necessarily valid UTF-8, so you should store it as a BLOB column or raw bytes.
*/
public function __serialize(): array
{
if ($this->isDone()) {
throw new \RuntimeException('Completed actions cannot be serialized.');
if (!$this->needsTan()) {
throw new \RuntimeException('Cannot serialize this action, because it is not waiting for a TAN.');
}
return [
$this->requestSegmentNumbers,
$this->tanRequest,
$this->needTanForSegment,
$this->pollingInfo,
$this->vopConfirmationRequest,
];
}
@ -117,10 +111,8 @@ abstract class BaseAction implements \Serializable
list(
$this->requestSegmentNumbers,
$this->tanRequest,
$this->needTanForSegment,
$this->pollingInfo,
$this->vopConfirmationRequest,
) = array_pad($serialized, 5, null);
$this->needTanForSegment
) = $serialized;
}
/**
@ -152,54 +144,25 @@ abstract class BaseAction implements \Serializable
return $this->tanRequest;
}
public function needsPollingWait(): bool
{
return !$this->isDone() && $this->pollingInfo !== null;
}
public function getPollingInfo(): ?PollingInfo
{
return $this->pollingInfo;
}
public function needsVopConfirmation(): bool
{
return !$this->isDone() && $this->vopConfirmationRequest !== null;
}
public function getVopConfirmationRequest(): ?VopConfirmationRequest
{
return $this->vopConfirmationRequest;
}
/**
* Throws an exception unless this action has been successfully executed, i.e. in the following cases:
* - the action has not been {@link FinTs::execute()}-d at all or the {@link FinTs::execute()} call for it threw an
* exception,
* - the action is awaiting a TAN/confirmation (as per {@link BaseAction::needsTan()},
* - the action is pending a long-running operation on the bank server ({@link BaseAction::needsPollingWait()}),
* - the action is awaiting the user's confirmation of the Verification of Payee result (as per
* {@link BaseAction::needsVopConfirmation()}).
* - the action is awaiting a TAN/confirmation (as per {@link BaseAction::needsTan()}.
*
* After executing an action, you can use this function to make sure that it succeeded. This is especially useful
* for actions that don't have any results (as each result getter would call {@link ensureDone()} internally).
* On the other hand, you do not need to call this function if you make sure that (1) you called
* {@link FinTs::execute()} and (2) you checked and resolved all other special outcome states documented there.
* Note that both exception types thrown from this method are sub-classes of {@link \RuntimeException}, so you
* shouldn't need a try-catch block at the call site for this.
* {@link FinTs::execute()} and (2) you checked {@link needsTan()} and, if it returned true, supplied a TAN by
* calling {@ink FinTs::submitTan()}. Note that both exception types thrown from this method are sub-classes of
* {@link \RuntimeException}, so you shouldn't need a try-catch block at the call site for this.
* @throws ActionIncompleteException If the action hasn't even been executed.
* @throws ActionPendingException If the action is pending a long-running server operation that needs polling.
* @throws VopConfirmationRequiredException If the action requires the user's confirmation for VOP.
* @throws TanRequiredException If the action needs a TAN.
*/
public function ensureDone(): void
public function ensureDone()
{
if ($this->tanRequest !== null) {
throw new TanRequiredException($this->tanRequest);
} elseif ($this->pollingInfo !== null) {
throw new ActionPendingException($this->pollingInfo);
} elseif ($this->vopConfirmationRequest !== null) {
throw new VopConfirmationRequiredException($this->vopConfirmationRequest);
} elseif (!$this->isDone()) {
throw new ActionIncompleteException();
}
@ -268,7 +231,7 @@ abstract class BaseAction implements \Serializable
/** @return int[] */
public function getRequestSegmentNumbers(): array
{
return $this->requestSegmentNumbers ?? [];
return $this->requestSegmentNumbers;
}
/**
@ -285,21 +248,11 @@ abstract class BaseAction implements \Serializable
$this->requestSegmentNumbers = $requestSegmentNumbers;
}
/** To be called only by the FinTs instance that executes this action. */
final public function setTanRequest(?TanRequest $tanRequest): void
/**
* To be called only by the FinTs instance that executes this action.
*/
final public function setTanRequest(?TanRequest $tanRequest)
{
$this->tanRequest = $tanRequest;
}
/** To be called only by the FinTs instance that executes this action. */
final public function setPollingInfo(?PollingInfo $pollingInfo): void
{
$this->pollingInfo = $pollingInfo;
}
/** To be called only by the FinTs instance that executes this action. */
final public function setVopConfirmationRequest(?VopConfirmationRequest $vopConfirmationRequest): void
{
$this->vopConfirmationRequest = $vopConfirmationRequest;
}
}

34
vendor/nemiah/php-fints/lib/Fhp/Connection.php vendored Normal file → Executable file
View file

@ -7,10 +7,25 @@ namespace Fhp;
*/
class Connection
{
protected string $url;
protected ?\CurlHandle $curlHandle = null;
protected int $timeoutConnect = 15;
protected int $timeoutResponse = 30;
/**
* @var string
*/
protected $url;
/**
* @var resource
*/
protected $curlHandle;
/**
* @var int
*/
protected $timeoutConnect = 15;
/**
* @var int
*/
protected $timeoutResponse = 30;
public function __construct(string $url, int $timeoutConnect = 15, int $timeoutResponse = 30)
{
@ -19,12 +34,9 @@ class Connection
$this->timeoutResponse = $timeoutResponse;
}
/**
* @throws CurlException When initializing cURL fails.
*/
private function connect(): void
private function connect()
{
$this->curlHandle = curl_init() ?: throw new CurlException('Failed initializing cURL.');
$this->curlHandle = curl_init();
curl_setopt($this->curlHandle, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($this->curlHandle, CURLOPT_SSL_VERIFYHOST, 2);
@ -40,7 +52,7 @@ class Connection
curl_setopt($this->curlHandle, CURLOPT_HTTPHEADER, ['cache-control: no-cache', 'Content-Type: text/plain']);
}
public function disconnect(): void
public function disconnect()
{
if ($this->curlHandle !== null) {
curl_close($this->curlHandle);
@ -64,7 +76,7 @@ class Connection
if (false === $response) {
throw new CurlException(
'Failed sending to ' . $this->url . ': ' . curl_error($this->curlHandle),
'Failed connection to ' . $this->url . ': ' . curl_error($this->curlHandle),
null,
curl_errno($this->curlHandle),
curl_getinfo($this->curlHandle),

0
vendor/nemiah/php-fints/lib/Fhp/CurlException.php vendored Normal file → Executable file
View file

261
vendor/nemiah/php-fints/lib/Fhp/FinTs.php vendored Normal file → Executable file
View file

@ -5,10 +5,6 @@ namespace Fhp;
use Fhp\Model\NoPsd2TanMode;
use Fhp\Model\TanMedium;
use Fhp\Model\TanMode;
use Fhp\Model\VopConfirmationRequest;
use Fhp\Model\VopConfirmationRequestImpl;
use Fhp\Model\VopPollingInfo;
use Fhp\Model\VopVerificationResult;
use Fhp\Options\Credentials;
use Fhp\Options\FinTsOptions;
use Fhp\Options\SanitizingLogger;
@ -30,8 +26,6 @@ use Fhp\Segment\TAN\HITAN;
use Fhp\Segment\TAN\HKTAN;
use Fhp\Segment\TAN\HKTANFactory;
use Fhp\Segment\TAN\HKTANv6;
use Fhp\Segment\VPP\HKVPPv1;
use Fhp\Segment\VPP\VopHelper;
use Fhp\Syntax\InvalidResponseException;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
@ -165,7 +159,6 @@ class FinTs
* carefully (not written to log files, only to a database or other storage system that would normally be used
* for user data). The returned string never contains highly sensitive information (not the user's password or
* PIN), so it probably does not need to be encrypted. Treat it like a session cookie of the same bank.
* Note that this is not necessarily valid UTF-8, so you should store it as a BLOB column or raw bytes.
*/
public function persist(bool $minimal = false): string
{
@ -208,7 +201,7 @@ class FinTs
*
* @throws \InvalidArgumentException
*/
public function loadPersistedInstance(string $persistedInstance): void
public function loadPersistedInstance(string $persistedInstance)
{
$unserialized = unserialize($persistedInstance);
if (!is_array($unserialized) || count($unserialized) === 0) {
@ -223,7 +216,7 @@ class FinTs
}
}
private function loadPersistedInstanceVersion2(array $data): void
private function loadPersistedInstanceVersion2(array $data)
{
list( // This should match persist().
$this->bpd,
@ -233,7 +226,7 @@ class FinTs
$this->selectedTanMedium,
$this->kundensystemId,
$this->dialogId,
$this->messageNumber,
$this->messageNumber
) = $data;
}
@ -261,7 +254,7 @@ class FinTs
* @param int $responseTimeout The number of seconds to wait before aborting a request to the bank server.
* @noinspection PhpUnused
*/
public function setTimeouts(int $connectTimeout, int $responseTimeout): void
public function setTimeouts(int $connectTimeout, int $responseTimeout)
{
$this->options->timeoutConnect = $connectTimeout;
$this->options->timeoutResponse = $responseTimeout;
@ -292,7 +285,7 @@ class FinTs
/**
* Executes an action. Be sure to {@link login()} first. See the `\Fhp\Action` package for actions that can be
* executed with this function. Note that, after this function returns, the action can be in the following states:
* executed with this function. Note that, after this function returns, the action can be in two possible states:
* 1. If {@link BaseAction::needsTan()} returns true, the action isn't completed yet because needs a TAN or other
* kind of two-factor authentication (2FA). In this case, use {@link BaseAction::getTanRequest()} to get more
* information about the TAN/2FA that is needed. Your application then needs to interact with the user to obtain
@ -300,30 +293,9 @@ class FinTs
* be verified with {@link checkDecoupledSubmission()}). Both of those functions require passing the same
* {@link BaseAction} argument as an argument, and once they succeed, the action will be in the same completed
* state as if it had been completed right away.
* 2. If {@link BaseAction::needsPollingWait()} returns true, the action isn't completed yet because the server is
* still running some slow operation. Importantly, the server has not necessarily accepted the action yet, so it
* is absolutely required that the client keeps polling if they don't want the action to be abandoned.
* In this case, use {@link BaseAction::getPollingInfo()} to get more information on how frequently to poll, and
* do the polling through {@link pollAction()}.
* 3. If {@link BaseAction::needsVopConfirmation()} returns true, the action isn't completed yet because the payee
* information couldn't be matched automatically, so an explicit confirmation from the user is required.
* In this case, use {@link BaseAction::getVopConfirmationRequest()} to get more information to display to the
* user, ask the user to confirm that they want to proceed with the action, and then call {@link confirmVop()}.
* 4. If none of the above return true, the action was completed right away.
* Use the respective getters on the action instance to retrieve the result. In case the action fails, the
* corresponding exception will be thrown from this function.
*
* Tip: In practice, polling (2.) and confirmation (3.) are needed only for Verification of Payee. So if your
* application only ever executes read-only actions like account statement fetching, but never executes any
* transfers, instead of handling these cases you could simply assert that {@link BaseAction::needsPollingWait()}
* and {@link BaseAction::needsVopConfirmation()} both return false.
*
* Note that all conditions above that leave the action in an incomplete state require some action from the client
* application. These actions then change the state of the action again, but they don't necessarily complete it.
* In practice, the typical sequence is: Maybe polling, maybe VOP confirmation, maybe TAN, done. That said, you
* should ideally implement your application to deal with any sequence of states. Just execute the action, check
* what's state it's in, resolve that state as appropriate, and then check again (using the same code as before). Do
* this repeatedly until none of the special conditions above happen anymore, at which point the action is done.
* 2. If {@link BaseAction::needsTan()} returns false, the action was completed right away. Use the respective
* getters on the action instance to retrieve the result. In case the action fails, the corresponding exception
* will be thrown from this function.
*
* @param BaseAction $action The action to be executed. Its {@link BaseAction::isDone()} status will be updated when
* this function returns successfully.
@ -332,36 +304,27 @@ class FinTs
* @throws ServerException When the server responds with a (FinTS-encoded) error message, which includes most things
* that can go wrong with the action itself, like wrong credentials, invalid IBANs, locked accounts, etc.
*/
public function execute(BaseAction $action): void
public function execute(BaseAction $action)
{
if ($this->dialogId === null && !($action instanceof DialogInitialization)) {
throw new \RuntimeException('Need to login (DialogInitialization) before executing other actions');
}
// Add the action's main request segments.
$requestSegments = $action->getNextRequest($this->bpd, $this->upd);
if (count($requestSegments) === 0) {
return; // No request needed.
}
$message = MessageBuilder::create()->add($requestSegments);
// Add HKTAN for authentication if necessary.
// Construct the full request message.
$message = MessageBuilder::create()->add($requestSegments); // This fills in the segment numbers.
if (!($this->getSelectedTanMode() instanceof NoPsd2TanMode)) {
if (($needTanForSegment = $action->getNeedTanForSegment()) !== null) {
$message->add(HKTANFactory::createProzessvariante2Step1(
$this->requireTanMode(), $this->selectedTanMedium, $needTanForSegment));
}
}
// Add HKVPP for VOP verification if necessary.
$hkvpp = null;
if ($this->bpd?->vopRequiredForRequest($requestSegments) !== null) {
$hkvpp = VopHelper::createHKVPPForInitialRequest($this->bpd);
$message->add($hkvpp);
}
// Construct the request message and tell the action about the segment numbers that were assigned.
$request = $this->buildMessage($message, $this->getSelectedTanMode()); // This fills in the segment numbers.
$request = $this->buildMessage($message, $this->getSelectedTanMode());
$action->setRequestSegmentNumbers(array_map(function ($segment) {
/* @var BaseSegment $segment */
return $segment->getSegmentNumber();
@ -369,28 +332,11 @@ class FinTs
// Execute the request.
$response = $this->sendMessage($request);
$this->processServerResponse($action, $response, $hkvpp);
}
/**
* Updates the state of this FinTs instance and of the `$action` based on the server's response.
* See {@link execute()} for more documentation on the possible outcomes.
* @param BaseAction $action The action for which the request was sent.
* @param Message $response The response we just got from the server.
* @param HKVPPv1|null $hkvpp The HKVPP segment, if any was present in the request.
* @throws CurlException When the connection fails in a layer below the FinTS protocol.
* @throws UnexpectedResponseException When the server responds with a valid but unexpected message.
* @throws ServerException When the server responds with a (FinTS-encoded) error message, which includes most things
* that can go wrong with the action itself, like wrong credentials, invalid IBANs, locked accounts, etc.
*/
private function processServerResponse(BaseAction $action, Message $response, ?HKVPPv1 $hkvpp = null): void
{
$this->readBPD($response);
// Detect if the bank wants a TAN.
/** @var HITAN $hitan */
$hitan = $response->findSegment(HITAN::class);
// Note: Instead of DUMMY_REFERENCE, it's officially the 3076 Rueckmeldungscode that tells we don't need a TAN.
if ($hitan !== null && $hitan->getAuftragsreferenz() !== HITAN::DUMMY_REFERENCE) {
if ($hitan->tanProzess !== HKTAN::TAN_PROZESS_4) {
throw new UnexpectedResponseException("Unsupported TAN request type $hitan->tanProzess");
@ -404,51 +350,14 @@ class FinTs
$action->setDialogId($response->header->dialogId);
$action->setMessageNumber($this->messageNumber);
}
return;
}
// Detect if the bank needs us to do something for Verification of Payee.
if ($hkvpp != null) {
if ($pollingInfo = VopHelper::checkPollingRequired($response, $hkvpp->getSegmentNumber())) {
$action->setPollingInfo($pollingInfo);
if ($action->needsTan()) {
throw new UnexpectedResponseException('Unexpected polling and TAN request in the same response.');
}
return;
}
if ($confirmationRequest = VopHelper::checkVopConfirmationRequired($response, $hkvpp->getSegmentNumber())) {
$action->setVopConfirmationRequest($confirmationRequest);
if ($action->needsTan()) {
if ($confirmationRequest->getVerificationResult() === VopVerificationResult::CompletedFullMatch) {
// If someone hits this branch in practice, we can implement it.
throw new UnsupportedException('Combined VOP match confirmation and TAN request');
} else {
throw new UnexpectedResponseException(
'Unexpected TAN request on VOP result: ' . $confirmationRequest->getVerificationResult()
);
}
}
}
}
if ($action->needsVopConfirmation() || $action->needsTan()) {
return; // The action isn't complete yet.
}
// If no TAN or VOP is needed, process the response normally, and maybe keep going for more pages.
// If no TAN is needed, process the response normally, and maybe keep going for more pages.
$this->processActionResponse($action, $response->filterByReferenceSegments($action->getRequestSegmentNumbers()));
if ($action instanceof PaginateableAction && $action->hasMorePages()) {
$this->execute($action);
}
// Check whether the server requested a Kundensystem-ID refresh.
if ($response->findRueckmeldung(Rueckmeldungscode::NEUE_KUNDENSYSTEM_ID_HOLEN) !== null) {
// TODO Properly implement the refresh here, see https://github.com/nemiah/phpFinTS/issues/458.
$this->logger->warning(
'The server asked us to refresh the Kundensystem-ID in response to a ' . gettype($action) .
' action, but that is not implemented yet. This could result in authentication errors or extraneous ' .
' re-authentication prompts from the bank.'
);
}
}
/**
@ -456,9 +365,9 @@ class FinTs
* `false`, this function sends the given $tan to the server to complete the action. By using {@link persist()},
* this can be done asynchronously, i.e., not in the same PHP process as the original {@link execute()} call.
*
* After this function returns, the `$action` is in any of the same states as after {@link execute()}, see there.
* In practice, the action is fully completed after completing the decoupled submission.
* In case the action fails, the corresponding exception will be thrown from this function.
* After this function returns, the `$action` is completed. That is, its result is available through its getters
* just as if it had been completed by the original call to {@link execute()} right away. In case the action fails,
* the corresponding exception will be thrown from this function.
*
* @link https://www.hbci-zka.de/dokumente/spezifikation_deutsch/fintsv3/FinTS_3.0_Security_Sicherheitsverfahren_PINTAN_2020-07-10_final_version.pdf
* Section B.4.2.1.1
@ -470,7 +379,7 @@ class FinTs
* @throws ServerException When the server responds with a (FinTS-encoded) error message, which includes most things
* that can go wrong with the action itself, like wrong credentials, invalid IBANs, locked accounts, etc.
*/
public function submitTan(BaseAction $action, string $tan): void
public function submitTan(BaseAction $action, string $tan)
{
// Check the action's state.
$tanRequest = $action->getTanRequest();
@ -524,9 +433,7 @@ class FinTs
* For an action where {@link BaseAction::needsTan()} returns `true` and {@link TanMode::isDecoupled()} returns
* `true`, this function checks with the server whether the second factor authentication has been completed yet on
* the secondary device of the user.
* - If so, this function returns `true` and the `$action` is then in any of the same states as after
* {@link execute()} (except {@link BaseAction::needsTan()} won't happen again). See there for documentation.
* In practice, the action is fully completed after completing the decoupled submission.
* - If so, this completes the given action and returns `true`.
* - In case the action fails, the corresponding exception will be thrown from this function.
* - If the authentication has not been completed yet, this returns `false` and the action remains in its
* previous, uncompleted state.
@ -542,10 +449,9 @@ class FinTs
* Section B.4.2.2
*
* @param BaseAction $action The action to be completed.
* @return bool True if the decoupled authentication is done and the $action was completed or entered one of the
* other states documented on {@link execute()}.
* If false, the {@link TanRequest} inside the action has been updated, which *may* provide new/more
* instructions to the user, though probably it rarely does in practice.
* @return bool True if the decoupled authentication is done and the $action was completed. If false, the
* {@link TanRequest} inside the action has been updated, which *may* provide new/more instructions to the user,
* though probably it rarely does in practice.
* @throws CurlException When the connection fails in a layer below the FinTS protocol.
* @throws UnexpectedResponseException When the server responds with a valid but unexpected message.
* @throws ServerException When the server responds with a (FinTS-encoded) error message, which includes most things
@ -624,99 +530,6 @@ class FinTs
return true;
}
/**
* For an action where {@link BaseAction::needsPollingWait()} returns `true`, this function polls the server.
* By using {@link persist()}, this can be done asynchronously, i.e., not in the same PHP process as the original
* {@link execute()} call or the previous {@link pollAction()} call.
*
* After this function returns, the `$action` is in any of the same states as after {@link execute()}, see there. In
* particular, it's possible that the long-running operation on the server has not completed yet and thus
* {@link BaseAction::needsPollingWait()} still returns `true`. In practice, actions often require VOP confirmation
* or a TAN after the polling is over, though they can also complete right away.
* In case the action fails, the corresponding exception will be thrown from this function.
*
* @param BaseAction $action The action to be completed.
* @throws CurlException When the connection fails in a layer below the FinTS protocol.
* @throws UnexpectedResponseException When the server responds with a valid but unexpected message.
* @throws ServerException When the server responds with a (FinTS-encoded) error message, which includes most things
* that can go wrong with the action itself, like wrong credentials, invalid IBANs, locked accounts, etc.
* @link FinTS_3.0_Messages_Geschaeftsvorfaelle_VOP_1.01_2025_06_27_FV.pdf
* Section C.10.7.1.1 a)
*/
public function pollAction(BaseAction $action): void
{
$pollingInfo = $action->getPollingInfo();
if ($pollingInfo === null) {
throw new \InvalidArgumentException('This action is not awaiting polling for a long-running operation');
} elseif ($pollingInfo instanceof VopPollingInfo) {
// Only send a new HKVPP.
$hkvpp = VopHelper::createHKVPPForPollingRequest($this->bpd, $pollingInfo);
$message = MessageBuilder::create()->add($hkvpp);
// Execute the request and process the response.
$response = $this->sendMessage($this->buildMessage($message, $this->getSelectedTanMode()));
$action->setPollingInfo(null);
$this->processServerResponse($action, $response, $hkvpp);
} else {
throw new \InvalidArgumentException('Unexpected PollingInfo type: ' . gettype($pollingInfo));
}
}
/**
* For an action where {@link BaseAction::needsVopConfirmation()} returns `true`, this function re-submits the
* action with the additional confirmation from the user that they want to execute the transfer(s) after having
* reviewed the information from the {@link VopConfirmationRequest}.
* By using {@link persist()}, this can be done asynchronously, i.e., not in the same PHP process as the original
* {@link execute()} call.
*
* After this function returns, the `$action` is in any of the same states as after {@link execute()}, see there. In
* practice, actions often require a TAN after VOP is confirmed, though they can also complete right away.
* In case the action fails, the corresponding exception will be thrown from this function.
*
* @param BaseAction $action The action to be completed.
* @throws CurlException When the connection fails in a layer below the FinTS protocol.
* @throws UnexpectedResponseException When the server responds with a valid but unexpected message.
* @throws ServerException When the server responds with a (FinTS-encoded) error message, which includes most things
* that can go wrong with the action itself, like wrong credentials, invalid IBANs, locked accounts, etc.
* @link FinTS_3.0_Messages_Geschaeftsvorfaelle_VOP_1.01_2025_06_27_FV.pdf
* Section C.10.7.1.2 a)
*/
public function confirmVop(BaseAction $action): void
{
$vopConfirmationRequest = $action->getVopConfirmationRequest();
if (!($vopConfirmationRequest instanceof VopConfirmationRequestImpl)) {
throw new \InvalidArgumentException('Unexpected type: ' . gettype($vopConfirmationRequest));
}
// We need to send the original request again, plus HKVPA as the confirmation.
$requestSegments = $action->getNextRequest($this->bpd, $this->upd);
if (count($requestSegments) === 0) {
throw new \AssertionError('Request unexpectedly became empty upon VOP confirmation');
}
$message = MessageBuilder::create()
->add($requestSegments)
->add(VopHelper::createHKVPAForConfirmation($vopConfirmationRequest));
// Add HKTAN for authentication if necessary.
if (!($this->getSelectedTanMode() instanceof NoPsd2TanMode)) {
if (($needTanForSegment = $action->getNeedTanForSegment()) !== null) {
$message->add(HKTANFactory::createProzessvariante2Step1(
$this->requireTanMode(), $this->selectedTanMedium, $needTanForSegment));
}
}
// Construct the request message and tell the action about the segment numbers that were assigned.
$request = $this->buildMessage($message, $this->getSelectedTanMode()); // This fills in the segment numbers.
$action->setRequestSegmentNumbers(array_map(function ($segment) {
/* @var BaseSegment $segment */
return $segment->getSegmentNumber();
}, $requestSegments));
// Execute the request and process the response.
$response = $this->sendMessage($this->buildMessage($message, $this->getSelectedTanMode()));
$action->setVopConfirmationRequest(null);
$this->processServerResponse($action, $response);
}
/**
* Closes the session/dialog/connection, if open. This is equivalent to logging out. You should call this function
* when you're done with all the actions, but NOT when you're persisting the instance to fulfill the TAN request of
@ -725,7 +538,7 @@ class FinTs
* from cached BPD/UPD upon the next {@link login()}, for instance.
* @throws ServerException When closing the dialog fails.
*/
public function close(): void
public function close()
{
if ($this->dialogId !== null) {
$this->endDialog();
@ -739,7 +552,7 @@ class FinTs
* This can be called by the application using this library when it just restored this FinTs instance from the
* persisted format after a long time, during which the session/dialog has most likely expired on the server side.
*/
public function forgetDialog(): void
public function forgetDialog()
{
$this->dialogId = null;
}
@ -758,11 +571,9 @@ class FinTs
public function getTanModes(): array
{
$this->ensureTanModesAvailable();
$result = [];
$result = array();
foreach ($this->allowedTanModes as $tanModeId) {
if (!array_key_exists($tanModeId, $this->bpd->allTanModes)) {
continue;
}
if (!array_key_exists($tanModeId, $this->bpd->allTanModes)) continue;
$result[$tanModeId] = $this->bpd->allTanModes[$tanModeId];
}
return $result;
@ -813,7 +624,7 @@ class FinTs
* must be the value returned from {@link TanMedium::getName()} for one of the TAN media supported with that TAN
* mode. Use {@link getTanMedia()} to obtain a list of possible TAN media options.
*/
public function selectTanMode($tanMode, $tanMedium = null): void
public function selectTanMode($tanMode, $tanMedium = null)
{
if (!is_int($tanMode) && !($tanMode instanceof TanMode)) {
throw new \InvalidArgumentException('tanMode must be an int or a TanMode');
@ -853,7 +664,7 @@ class FinTs
* @throws UnexpectedResponseException When the server does not send the BPD or close the dialog properly.
* @throws ServerException When the server resopnds with an error.
*/
private function ensureBpdAvailable(): void
private function ensureBpdAvailable()
{
if ($this->bpd !== null) {
return; // Nothing to do.
@ -900,7 +711,7 @@ class FinTs
* like it should according to the protocol, or when the dialog is not closed properly.
* @throws ServerException When the server responds with an error.
*/
private function ensureTanModesAvailable(): void
private function ensureTanModesAvailable()
{
if ($this->allowedTanModes === null) {
$this->ensureBpdAvailable();
@ -919,7 +730,7 @@ class FinTs
* dialog is not closed properly.
* @throws ServerException When the server responds with an error.
*/
private function ensureSynchronized(): void
private function ensureSynchronized()
{
if ($this->kundensystemId === null) {
$this->ensureBpdAvailable();
@ -1009,7 +820,7 @@ class FinTs
/**
* Closes the physical connection, if necessary.
*/
private function disconnect(): void
private function disconnect()
{
if ($this->connection !== null) {
$this->connection->disconnect();
@ -1023,7 +834,7 @@ class FinTs
* @param Message $fakeResponseMessage A messsage that contains the response segments for this action.
* @throws UnexpectedResponseException When the server responded with a valid but unexpected message.
*/
private function processActionResponse(BaseAction $action, Message $fakeResponseMessage): void
private function processActionResponse(BaseAction $action, Message $fakeResponseMessage)
{
$action->processResponse($fakeResponseMessage);
if ($action instanceof DialogInitialization) {
@ -1053,7 +864,7 @@ class FinTs
* properly.
* @throws ServerException When the server responds with an error.
*/
private function executeWeakDialogInitialization(?string $hktanRef): void
private function executeWeakDialogInitialization(?string $hktanRef)
{
if ($this->dialogId !== null) {
throw new \RuntimeException('Cannot init another dialog.');
@ -1094,7 +905,7 @@ class FinTs
* @throws ServerException When the server responds with an error instead of closing the dialog. This means that
* the connection is tainted and can probably not be used for another dialog.
*/
protected function endDialog(bool $isAnonymous = false): void
protected function endDialog(bool $isAnonymous = false)
{
if ($this->connection === null) {
$this->dialogId = null;
@ -1132,7 +943,7 @@ class FinTs
* @param MessageBuilder $message The message to be built.
* @param TanMode|null $tanMode Optionally a TAN mode that will be used when sending this message, defaults to 999
* (single step).
* @param string|null $tan Optionally a TAN to sign this message with.
* @param string|null Optionally a TAN to sign this message with.
* @return Message The built message.
*/
private function buildMessage(MessageBuilder $message, ?TanMode $tanMode = null, ?string $tan = null): Message

0
vendor/nemiah/php-fints/lib/Fhp/MT535/MT535.php vendored Normal file → Executable file
View file

1
vendor/nemiah/php-fints/lib/Fhp/MT940/Dialect/PostbankMT940.php vendored Normal file → Executable file
View file

@ -8,6 +8,7 @@ class PostbankMT940 extends MT940
{
public const DIALECT_ID = 'https://hbci.postbank.de/banking/hbci.do';
/** {@inheritdoc} */
public function extractStructuredDataFromRemittanceLines($descriptionLines, string &$gvc, array &$rawLines, array $transaction): array
{
// z.B bei Zinsen o.ä. ist alles leer

1
vendor/nemiah/php-fints/lib/Fhp/MT940/Dialect/SpardaMT940.php vendored Normal file → Executable file
View file

@ -8,6 +8,7 @@ class SpardaMT940 extends MT940
{
public const DIALECT_ID = 'https://fints.bankingonline.de/fints/FinTs30PinTanHttpGate';
/** {@inheritdoc} */
public function extractStructuredDataFromRemittanceLines($descriptionLines, string &$gvc, array &$rawLines, array $transaction): array
{
$otherInfo = [];

4
vendor/nemiah/php-fints/lib/Fhp/MT940/MT940.php vendored Normal file → Executable file
View file

@ -142,8 +142,8 @@ class MT940
$soaDate = $this->getDate(substr($day[$i], 1, 6));
if (isset($result[$soaDate])) {
// $result[$soaDate] = ['end_balance' => []];
#$result[$soaDate] = ['end_balance' => []];
$amount = str_replace(',', '.', substr($day[$i], 10, -1));
$cdMark = substr($day[$i], 0, 1);
if ($cdMark == 'C') {

0
vendor/nemiah/php-fints/lib/Fhp/MT940/MT940Exception.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/Model/Account.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/Model/FlickerTan/DataElement.php vendored Normal file → Executable file
View file

9
vendor/nemiah/php-fints/lib/Fhp/Model/FlickerTan/StartCode.php vendored Normal file → Executable file
View file

@ -68,11 +68,17 @@ class StartCode extends DataElement
$this->headerHighBit = '1';
}
/**
* {@inheritDoc}
*/
public function toHex(): string
{
return $this->getHeaderHex() . implode('', $this->controlBytes) . $this->getDataHex();
}
/**
* {@inheritDoc}
*/
public function getLuhnChecksum(): int
{
$luhn = 0;
@ -83,6 +89,9 @@ class StartCode extends DataElement
return $luhn;
}
/**
* {@inheritDoc}
*/
public function __debugInfo(): ?array
{
return [

0
vendor/nemiah/php-fints/lib/Fhp/Model/FlickerTan/SvgRenderer.php vendored Normal file → Executable file
View file

View file

17
vendor/nemiah/php-fints/lib/Fhp/Model/NoPsd2TanMode.php vendored Normal file → Executable file
View file

@ -16,11 +16,13 @@ final class NoPsd2TanMode implements TanMode
{
public const ID = -1;
/** {@inheritdoc} */
public function getId(): int
{
return self::ID;
}
/** {@inheritdoc} */
public function getName(): string
{
return 'No PSD2/TANs supported';
@ -31,76 +33,91 @@ final class NoPsd2TanMode implements TanMode
return false;
}
/** {@inheritdoc} */
public function isDecoupled(): bool
{
return false;
}
/** {@inheritdoc} */
public function getChallengeLabel(): string
{
return '';
}
/** {@inheritdoc} */
public function getMaxChallengeLength(): int
{
return 0;
}
/** {@inheritdoc} */
public function getMaxTanLength(): int
{
return 0;
}
/** {@inheritdoc} */
public function getTanFormat(): int
{
return 0;
}
/** {@inheritdoc} */
public function needsTanMedium(): bool
{
return false;
}
/** {@inheritdoc} */
public function getSmsAbbuchungskontoErforderlich(): bool
{
return false;
}
/** {@inheritdoc} */
public function getAuftraggeberkontoErforderlich(): bool
{
return false;
}
/** {@inheritdoc} */
public function getChallengeKlasseErforderlich(): bool
{
return false;
}
/** {@inheritdoc} */
public function getAntwortHhdUcErforderlich(): bool
{
return false;
}
/** {@inheritdoc} */
public function getMaxDecoupledChecks(): int
{
throw new \RuntimeException('Only allowed for decoupled TAN modes');
}
/** {@inheritdoc} */
public function getFirstDecoupledCheckDelaySeconds(): int
{
throw new \RuntimeException('Only allowed for decoupled TAN modes');
}
/** {@inheritdoc} */
public function getPeriodicDecoupledCheckDelaySeconds(): int
{
throw new \RuntimeException('Only allowed for decoupled TAN modes');
}
/** {@inheritdoc} */
public function allowsManualConfirmation(): bool
{
throw new \RuntimeException('Only allowed for decoupled TAN modes');
}
/** {@inheritdoc} */
public function allowsAutomatedPolling(): bool
{
throw new \RuntimeException('Only allowed for decoupled TAN modes');

View file

@ -1,23 +0,0 @@
<?php
namespace Fhp\Model;
/**
* Provides information that the client application should use to poll for the completion of a long-running operation on
* the server.
*/
interface PollingInfo
{
/**
* @return ?int The number of seconds (measured from the time when the client received this {@link PollingInfo})
* after which the client is allowed to contact the server again regarding this action. If this returns null,
* there is no restriction.
*/
public function getNextAttemptInSeconds(): ?int;
/**
* @return ?string An HTML-formatted text (either in the bank's language or in English!) that the application may
* display to the user to inform them (on a very high level) about why they have to wait.
*/
public function getInformationForUser(): ?string;
}

0
vendor/nemiah/php-fints/lib/Fhp/Model/SEPAAccount.php vendored Normal file → Executable file
View file

View file

@ -7,12 +7,30 @@ class Statement
public const CD_CREDIT = 'credit';
public const CD_DEBIT = 'debit';
/** @var Transaction[] */
protected array $transactions = [];
protected float $startBalance = 0.0;
protected ?float $endBalance = null;
protected ?string $creditDebit = null;
protected ?\DateTime $date = null;
/**
* @var array of Transaction
*/
protected $transactions = [];
/**
* @var float
*/
protected $startBalance = 0.0;
/**
* @var float|null
*/
protected $endBalance = null;
/**
* @var string|null
*/
protected $creditDebit = null;
/**
* @var \DateTime|null
*/
protected $date;
/**
* Get transactions

View file

@ -6,8 +6,10 @@ use Fhp\MT940\MT940;
class StatementOfAccount
{
/** @var Statement[] */
protected array $statements = [];
/**
* @var Statement[]
*/
protected $statements = [];
/**
* Get statements
@ -75,7 +77,7 @@ class StatementOfAccount
$statementModel->setStartBalance((float) $statement['start_balance']['amount']);
}
if (isset($statement['end_balance'])) {
$statementModel->setEndBalance((float) $statement['end_balance']['amount'] * ($statement['end_balance']['credit_debit'] == MT940::CD_CREDIT ? 1 : -1));
$statementModel->setEndBalance((float) $statement['end_balance']['amount'] * ($statement["end_balance"]['credit_debit'] == MT940::CD_CREDIT ? 1 : -1));
}
if (isset($statement['start_balance']['credit_debit'])) {
$statementModel->setCreditDebit($statement['start_balance']['credit_debit']);

View file

@ -8,28 +8,86 @@ class Transaction
public const CD_CREDIT = 'credit';
public const CD_DEBIT = 'debit';
protected ?\DateTime $bookingDate = null;
protected ?\DateTime $valutaDate = null;
protected float $amount;
protected string $creditDebit;
protected bool $isStorno;
protected string $bookingCode;
protected string $bookingText;
protected string $description1;
protected string $description2;
/**
* @var \DateTime|null
*/
protected $bookingDate;
/**
* @var \DateTime|null
*/
protected $valutaDate;
/**
* @var float
*/
protected $amount;
/**
* @var string
*/
protected $creditDebit;
/**
* @var bool
*/
protected $isStorno;
/**
* @var string
*/
protected $bookingCode;
/**
* @var string
*/
protected $bookingText;
/**
* @var string
*/
protected $description1;
/**
* @var string
*/
protected $description2;
/**
* Array keys are identifiers like "SVWZ" for the main description.
* @var string[]
*/
protected array $structuredDescription;
protected $structuredDescription;
protected string $bankCode;
protected string $accountNumber;
protected string $name;
protected bool $booked;
protected int $pn;
protected int $textKeyAddition;
/**
* @var string
*/
protected $bankCode;
/**
* @var string
*/
protected $accountNumber;
/**
* @var string
*/
protected $name;
/**
* @var bool
*/
protected $booked;
/**
* @var int
*/
protected $pn;
/**
* @var int
*/
protected $textKeyAddition;
/**
* Get booking date.
@ -63,9 +121,10 @@ class Transaction
*
* @return $this
*/
public function setBookingDate(?\DateTime $date = null): static
public function setBookingDate(?\DateTime $date = null)
{
$this->bookingDate = $date;
return $this;
}
@ -74,9 +133,10 @@ class Transaction
*
* @return $this
*/
public function setValutaDate(?\DateTime $date = null): static
public function setValutaDate(?\DateTime $date = null)
{
$this->valutaDate = $date;
return $this;
}
@ -93,9 +153,10 @@ class Transaction
*
* @return $this
*/
public function setBooked(bool $booked): static
public function setBooked(bool $booked)
{
$this->booked = $booked;
return $this;
}
@ -104,9 +165,10 @@ class Transaction
*
* @return $this
*/
public function setAmount(float $amount): static
public function setAmount(float $amount)
{
$this->amount = $amount;
$this->amount = (float) $amount;
return $this;
}
@ -123,9 +185,10 @@ class Transaction
*
* @return $this
*/
public function setCreditDebit(string $creditDebit): static
public function setCreditDebit(string $creditDebit)
{
$this->creditDebit = $creditDebit;
return $this;
}
@ -142,9 +205,10 @@ class Transaction
*
* @return $this
*/
public function setIsStorno(bool $isStorno): static
public function setIsStorno(bool $isStorno)
{
$this->isStorno = $isStorno;
return $this;
}
@ -161,9 +225,10 @@ class Transaction
*
* @return $this
*/
public function setBookingCode(string $bookingCode): static
public function setBookingCode(string $bookingCode)
{
$this->bookingCode = $bookingCode;
$this->bookingCode = (string) $bookingCode;
return $this;
}
@ -180,9 +245,10 @@ class Transaction
*
* @return $this
*/
public function setBookingText(string $bookingText): static
public function setBookingText(string $bookingText)
{
$this->bookingText = $bookingText;
$this->bookingText = (string) $bookingText;
return $this;
}
@ -199,9 +265,10 @@ class Transaction
*
* @return $this
*/
public function setDescription1(string $description1): static
public function setDescription1(string $description1)
{
$this->description1 = $description1;
$this->description1 = (string) $description1;
return $this;
}
@ -218,9 +285,10 @@ class Transaction
*
* @return $this
*/
public function setDescription2(string $description2): static
public function setDescription2(string $description2)
{
$this->description2 = $description2;
$this->description2 = (string) $description2;
return $this;
}
@ -238,9 +306,10 @@ class Transaction
* Set structuredDescription
*
* @param string[] $structuredDescription
*
* @return $this
*/
public function setStructuredDescription(array $structuredDescription): static
public function setStructuredDescription(array $structuredDescription)
{
$this->structuredDescription = $structuredDescription;
@ -284,9 +353,10 @@ class Transaction
*
* @return $this
*/
public function setBankCode(string $bankCode): static
public function setBankCode(string $bankCode)
{
$this->bankCode = $bankCode;
$this->bankCode = (string) $bankCode;
return $this;
}
@ -303,9 +373,10 @@ class Transaction
*
* @return $this
*/
public function setAccountNumber(string $accountNumber): static
public function setAccountNumber(string $accountNumber)
{
$this->accountNumber = $accountNumber;
$this->accountNumber = (string) $accountNumber;
return $this;
}
@ -330,9 +401,10 @@ class Transaction
*
* @return $this
*/
public function setName(string $name): static
public function setName(string $name)
{
$this->name = $name;
$this->name = (string) $name;
return $this;
}
@ -350,7 +422,7 @@ class Transaction
* @param int|mixed $nr Will be parsed to an int.
* @return $this
*/
public function setPN($nr): static
public function setPN($nr)
{
$this->pn = intval($nr);
return $this;
@ -370,7 +442,7 @@ class Transaction
* @param int|mixed $textKeyAddition Will be parsed to an int.
* @return $this
*/
public function setTextKeyAddition($textKeyAddition): static
public function setTextKeyAddition($textKeyAddition)
{
$this->textKeyAddition = intval($textKeyAddition);
return $this;

View file

View file

0
vendor/nemiah/php-fints/lib/Fhp/Model/TanMedium.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/Model/TanMode.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/Model/TanRequest.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/Model/TanRequestChallengeImage.php vendored Normal file → Executable file
View file

View file

@ -1,25 +0,0 @@
<?php
namespace Fhp\Model;
/**
* Provides information (about the payee) that the client application should present to the user and then ask for their
* confirmation that the transfer (to this payee) should be executed.
*/
interface VopConfirmationRequest
{
/** An HTML-formatted text that (if present) the application must show to the user when asking for confirmation. */
public function getInformationForUser(): ?string;
/** If this returns a non-null value, the confirmation request is only valid up to that time. */
public function getExpiration(): ?\DateTime;
/** The main outcome of the payee verification. See {@link VopVerificationResult} for possible values. */
public function getVerificationResult(): ?string;
/**
* If {@link getVerificationResult()} returns {@link VopVerificationResult::NotApplicable}, then this function MAY
* return an additional explanation (in the user's language or in English), but it may also return null.
*/
public function getVerificationNotApplicableReason(): ?string;
}

View file

@ -1,54 +0,0 @@
<?php
namespace Fhp\Model;
use Fhp\Syntax\Bin;
/** Application code should not interact directly with this type, see {@link VopConfirmationRequest instead}. */
class VopConfirmationRequestImpl implements VopConfirmationRequest
{
private Bin $vopId;
private ?\DateTime $expiration;
private ?string $informationForUser;
private ?string $verificationResult;
private ?string $verificationNotApplicableReason;
public function __construct(
Bin $vopId,
?\DateTime $expiration,
?string $informationForUser,
?string $verificationResult,
?string $verificationNotApplicableReason,
) {
$this->vopId = $vopId;
$this->expiration = $expiration;
$this->informationForUser = $informationForUser;
$this->verificationResult = $verificationResult;
$this->verificationNotApplicableReason = $verificationNotApplicableReason;
}
public function getVopId(): Bin
{
return $this->vopId;
}
public function getExpiration(): ?\DateTime
{
return $this->expiration;
}
public function getInformationForUser(): ?string
{
return $this->informationForUser;
}
public function getVerificationResult(): ?string
{
return $this->verificationResult;
}
public function getVerificationNotApplicableReason(): ?string
{
return $this->verificationNotApplicableReason;
}
}

View file

@ -1,50 +0,0 @@
<?php
namespace Fhp\Model;
use Fhp\Syntax\Bin;
/**
* Application code should not interact directly with this type, see {@link PollingInfo instead}.
*
* When we send a request to the bank that requires a Verification of Payee, this means that the bank server has to
* contact another bank's server and compare payee names. Especially for larger requests (e.g. bulk transfers), this can
* take some time. During this time, the server asks the client to poll regularly in order to find out when the process
* is done. This class contains the state that the client needs to do this polling.
*/
class VopPollingInfo implements PollingInfo
{
// Both of these are effectively opaque tokens that only the server understands. Our job is to relay them back to
// the server when polling. And for some reason there's two of them.
private string $aufsetzpunkt;
private ?Bin $pollingId;
private ?int $nextAttemptInSeconds = null;
public function __construct(string $aufsetzpunkt, ?Bin $pollingId, ?int $nextAttemptInSeconds)
{
$this->aufsetzpunkt = $aufsetzpunkt;
$this->pollingId = $pollingId;
$this->nextAttemptInSeconds = $nextAttemptInSeconds;
}
public function getAufsetzpunkt(): string
{
return $this->aufsetzpunkt;
}
public function getPollingId(): ?Bin
{
return $this->pollingId;
}
public function getNextAttemptInSeconds(): ?int
{
return $this->nextAttemptInSeconds;
}
public function getInformationForUser(): string
{
return 'The bank is verifying payee information...';
}
}

View file

@ -1,51 +0,0 @@
<?php
namespace Fhp\Model;
use Fhp\Protocol\UnexpectedResponseException;
/**
* Possible outcomes of the Verification of Payee check that the bank did on a transfer we want to execute.
* TODO Once we have PHP8.1, turn this into an enum. That's why we use UpperCamelCase below (Symfony style for enums).
* @see FinTS_3.0_Messages_Geschaeftsvorfaelle_VOP_1.01_2025_06_27_FV.pdf (chapter D under "VOP-Prüfergebnis")
* @see https://febelfin.be/media/pages/publicaties/2023/febelfin-standaarden-voor-online-bankieren/971728b297-1746523070/febelfin-standard-payment-status-report-xml-2025-v1.0-en_final.pdf
*/
class VopVerificationResult
{
/** The verification completed and successfully matched the payee information. */
public const CompletedFullMatch = 'CompletedFullMatch';
/** The verification completed and only partially matched the payee information. */
public const CompletedCloseMatch = 'CompletedCloseMatch';
/** The verification completed but could not match the payee information. */
public const CompletedNoMatch = 'CompletedNoMatch';
/** The verification completed but not all included transfers were successfully matched. */
public const CompletedPartialMatch = 'CompletedPartialMatch';
/**
* The verification was attempted but could not be completed. More information MAY be available from
* {@link VopConfirmationRequest::getVerificationNotApplicableReason()}.
*/
public const NotApplicable = 'NotApplicable';
public function __construct()
{
// Disallow instantiation, because we'll turn this into an enum.
throw new \AssertionError('There should be no instances of VopVerificationResult');
}
/**
* @param ?string $codeFromBank The verification status code received from the bank.
* @return ?string One of the constants defined above, or null if the code could not be recognized.
*/
public static function parse(?string $codeFromBank): ?string
{
return match ($codeFromBank) {
null => null,
'RCVC' => self::CompletedFullMatch,
'RVMC' => self::CompletedCloseMatch,
'RVNM' => self::CompletedNoMatch,
'RVCM' => self::CompletedPartialMatch,
'RVNA' => self::NotApplicable,
default => throw new UnexpectedResponseException("Unexpected VOP result code: $codeFromBank"),
};
}
}

0
vendor/nemiah/php-fints/lib/Fhp/Options/Credentials.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/Options/FinTsOptions.php vendored Normal file → Executable file
View file

0
vendor/nemiah/php-fints/lib/Fhp/Options/SanitizingLogger.php vendored Normal file → Executable file
View file

16
vendor/nemiah/php-fints/lib/Fhp/PaginateableAction.php vendored Normal file → Executable file
View file

@ -6,6 +6,7 @@ use Fhp\Protocol\BPD;
use Fhp\Protocol\Message;
use Fhp\Protocol\UnexpectedResponseException;
use Fhp\Protocol\UPD;
use Fhp\Segment\BaseSegment;
use Fhp\Segment\HIRMS\Rueckmeldungscode;
use Fhp\Segment\Paginateable;
@ -18,20 +19,23 @@ use Fhp\Segment\Paginateable;
abstract class PaginateableAction extends BaseAction
{
/**
* Stores the request created by BaseAction::getNextRequest to be reused in case the bank wants
* @var BaseSegment[] Stores the request created by BaseAction::getNextRequest to be reused in case the bank wants
* to split the result over multiple pages e.g. request/response pairs. This avoids the need for {@link BPD} to be
* available for paginated requests.
*/
protected ?array $requestSegments = null;
protected $requestSegments;
/**
* If set, the last response from the server regarding this action indicated that there are more results to be
* fetched using this pagination token. This is called "Aufsetzpunkt" in the specification.
* @var string|null
*/
protected ?string $paginationToken = null;
protected $paginationToken;
/**
* @deprecated Beginning from PHP7.4 __unserialize is used for new generated strings, then this method is only used for previously generated strings - remove after May 2023
*
* {@inheritdoc}
*/
public function serialize(): string
{
@ -49,6 +53,8 @@ abstract class PaginateableAction extends BaseAction
/**
* @deprecated Beginning from PHP7.4 __unserialize is used for new generated strings, then this method is only used for previously generated strings - remove after May 2023
*
* {@inheritdoc}
*/
public function unserialize($serialized)
{
@ -76,9 +82,10 @@ abstract class PaginateableAction extends BaseAction
return !$this->isDone() && $this->paginationToken !== null;
}
/** {@inheritdoc} */
public function processResponse(Message $response)
{
if (($pagination = $response->findRueckmeldung(Rueckmeldungscode::AUFSETZPUNKT)) !== null) {
if (($pagination = $response->findRueckmeldung(Rueckmeldungscode::PAGINATION)) !== null) {
if (count($pagination->rueckmeldungsparameter) !== 1) {
throw new UnexpectedResponseException("Unexpected pagination request: $pagination");
}
@ -90,6 +97,7 @@ abstract class PaginateableAction extends BaseAction
}
}
/** {@inheritdoc} */
public function getNextRequest(?BPD $bpd, ?UPD $upd)
{
if ($this->requestSegments === null) {

View file

View file

@ -1,26 +0,0 @@
<?php
/** @noinspection PhpUnused */
namespace Fhp\Protocol;
use Fhp\Model\PollingInfo;
/**
* Thrown when an action result is read, but the action is still pending a long-running operation on the server and
* requires polling to find out when it's completed.
*/
class ActionPendingException extends \RuntimeException
{
private PollingInfo $pollingInfo;
public function __construct(PollingInfo $pollingInfo)
{
parent::__construct('This action needs polling to await finishing a server-side operation.');
$this->pollingInfo = $pollingInfo;
}
public function getPollingInfo(): PollingInfo
{
return $this->pollingInfo;
}
}

23
vendor/nemiah/php-fints/lib/Fhp/Protocol/BPD.php vendored Normal file → Executable file
View file

@ -10,7 +10,6 @@ use Fhp\Segment\HIBPA\HIBPAv3;
use Fhp\Segment\HIPINS\HIPINSv1;
use Fhp\Segment\SegmentInterface;
use Fhp\Segment\TAN\HITANS;
use Fhp\Segment\VPP\HIVPPSv1;
/**
* Segmentfolge: Bankparameterdaten (Version 3)
@ -153,28 +152,6 @@ class BPD
return null;
}
/**
* @param SegmentInterface[] $requestSegments The segments that shall be sent to the bank.
* @return string|null Identifier of the (first) segment that requires Verification of Payee according to HIPINS, or
* null if none of the segments require verification.
*/
public function vopRequiredForRequest(array $requestSegments): ?string
{
/** @var HIVPPSv1 $hivpps */
$hivpps = $this->getLatestSupportedParameters('HIVPPS');
$vopRequiredTypes = $hivpps?->parameter?->vopPflichtigerZahlungsverkehrsauftrag;
if ($vopRequiredTypes === null) {
return null;
}
foreach ($requestSegments as $segment) {
if (in_array($segment->getName(), $vopRequiredTypes)) {
return $segment->getName();
}
}
return null;
}
/**
* @return bool Whether the BPD indicates that the bank supports PSD2.
*/

Some files were not shown because too many files have changed in this diff Show more