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:
parent
8b64fd24d3
commit
014a943f78
425 changed files with 2359 additions and 4223 deletions
60
CHANGELOG.md
60
CHANGELOG.md
|
|
@ -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
175
CLAUDE.md
|
|
@ -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.
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
16
composer.lock
generated
|
|
@ -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": [],
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>';
|
||||
|
|
|
|||
3
vendor/composer/autoload_classmap.php
vendored
3
vendor/composer/autoload_classmap.php
vendored
|
|
@ -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',
|
||||
);
|
||||
|
|
|
|||
3
vendor/composer/autoload_static.php
vendored
3
vendor/composer/autoload_static.php
vendored
|
|
@ -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',
|
||||
);
|
||||
|
||||
|
|
|
|||
16
vendor/composer/installed.json
vendored
16
vendor/composer/installed.json
vendored
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
18
vendor/composer/installed.php
vendored
18
vendor/composer/installed.php
vendored
|
|
@ -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(),
|
||||
|
|
|
|||
4
vendor/composer/platform_check.php
vendored
4
vendor/composer/platform_check.php
vendored
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
22
vendor/nemiah/php-fints/.gitignore
vendored
22
vendor/nemiah/php-fints/.gitignore
vendored
|
|
@ -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
20
vendor/nemiah/php-fints/.php-cs-fixer.php
vendored
Normal file → Executable 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
12
vendor/nemiah/php-fints/.travis.yml
vendored
Executable 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
0
vendor/nemiah/php-fints/DEVELOPER-GUIDE.md
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/LICENSE
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/LICENSE
vendored
Normal file → Executable file
2
vendor/nemiah/php-fints/README.md
vendored
Normal file → Executable file
2
vendor/nemiah/php-fints/README.md
vendored
Normal file → Executable file
|
|
@ -1,6 +1,6 @@
|
|||
# PHP FinTS/HBCI library
|
||||
|
||||
[](https://github.com/nemiah/phpFinTS/actions/workflows/tests.yml)
|
||||
[](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
0
vendor/nemiah/php-fints/Samples/accounts.php
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/Samples/balance.php
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/Samples/balance.php
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/Samples/bpd.php
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/Samples/bpd.php
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/Samples/browser.php
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/Samples/browser.php
vendored
Normal file → Executable file
9
vendor/nemiah/php-fints/Samples/directDebit_Sephpa.php
vendored
Normal file → Executable file
9
vendor/nemiah/php-fints/Samples/directDebit_Sephpa.php
vendored
Normal file → Executable 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
9
vendor/nemiah/php-fints/Samples/directDebit_phpSepaXml.php
vendored
Normal file → Executable 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
0
vendor/nemiah/php-fints/Samples/init.php
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/Samples/login.php
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/Samples/login.php
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/Samples/statementOfAccount.php
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/Samples/statementOfAccount.php
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/Samples/statementOfHoldings.php
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/Samples/statementOfHoldings.php
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/Samples/tanModesAndMedia.php
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/Samples/tanModesAndMedia.php
vendored
Normal file → Executable file
18
vendor/nemiah/php-fints/Samples/transfer.php
vendored
Normal file → Executable file
18
vendor/nemiah/php-fints/Samples/transfer.php
vendored
Normal file → Executable 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.
|
||||
}
|
||||
|
|
|
|||
138
vendor/nemiah/php-fints/Samples/vop.php
vendored
138
vendor/nemiah/php-fints/Samples/vop.php
vendored
|
|
@ -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
4
vendor/nemiah/php-fints/composer.json
vendored
Normal file → Executable 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
36
vendor/nemiah/php-fints/csfixer-check.sh
vendored
Executable 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
37
vendor/nemiah/php-fints/disallowtabs.sh
vendored
Executable 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
6
vendor/nemiah/php-fints/lib/Fhp/Action/GetBalance.php
vendored
Normal file → Executable 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);
|
||||
|
|
|
|||
211
vendor/nemiah/php-fints/lib/Fhp/Action/GetBankStatement.php
vendored
Executable file
211
vendor/nemiah/php-fints/lib/Fhp/Action/GetBankStatement.php
vendored
Executable 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
6
vendor/nemiah/php-fints/lib/Fhp/Action/GetDepotAufstellung.php
vendored
Normal file → Executable 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
339
vendor/nemiah/php-fints/lib/Fhp/Action/GetElectronicStatement.php
vendored
Normal file → Executable 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
2
vendor/nemiah/php-fints/lib/Fhp/Action/GetSEPAAccounts.php
vendored
Normal file → Executable 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);
|
||||
|
|
|
|||
43
vendor/nemiah/php-fints/lib/Fhp/Action/GetSEPADirectDebitParameters.php
vendored
Normal file → Executable file
43
vendor/nemiah/php-fints/lib/Fhp/Action/GetSEPADirectDebitParameters.php
vendored
Normal file → Executable 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));
|
||||
|
|
|
|||
|
|
@ -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
10
vendor/nemiah/php-fints/lib/Fhp/Action/GetStatementOfAccount.php
vendored
Normal file → Executable 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
6
vendor/nemiah/php-fints/lib/Fhp/Action/GetStatementOfAccountXML.php
vendored
Normal file → Executable 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);
|
||||
|
|
|
|||
42
vendor/nemiah/php-fints/lib/Fhp/Action/SendInternationalCreditTransfer.php
vendored
Normal file → Executable file
42
vendor/nemiah/php-fints/lib/Fhp/Action/SendInternationalCreditTransfer.php
vendored
Normal file → Executable 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
17
vendor/nemiah/php-fints/lib/Fhp/Action/SendSEPADirectDebit.php
vendored
Normal file → Executable 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
51
vendor/nemiah/php-fints/lib/Fhp/Action/SendSEPARealtimeTransfer.php
vendored
Normal file → Executable 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
83
vendor/nemiah/php-fints/lib/Fhp/Action/SendSEPATransfer.php
vendored
Normal file → Executable 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
107
vendor/nemiah/php-fints/lib/Fhp/BaseAction.php
vendored
Normal file → Executable 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
34
vendor/nemiah/php-fints/lib/Fhp/Connection.php
vendored
Normal file → Executable 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
0
vendor/nemiah/php-fints/lib/Fhp/CurlException.php
vendored
Normal file → Executable file
261
vendor/nemiah/php-fints/lib/Fhp/FinTs.php
vendored
Normal file → Executable file
261
vendor/nemiah/php-fints/lib/Fhp/FinTs.php
vendored
Normal file → Executable 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
0
vendor/nemiah/php-fints/lib/Fhp/MT535/MT535.php
vendored
Normal file → Executable file
1
vendor/nemiah/php-fints/lib/Fhp/MT940/Dialect/PostbankMT940.php
vendored
Normal file → Executable file
1
vendor/nemiah/php-fints/lib/Fhp/MT940/Dialect/PostbankMT940.php
vendored
Normal file → Executable 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
1
vendor/nemiah/php-fints/lib/Fhp/MT940/Dialect/SpardaMT940.php
vendored
Normal file → Executable 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
4
vendor/nemiah/php-fints/lib/Fhp/MT940/MT940.php
vendored
Normal file → Executable 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
0
vendor/nemiah/php-fints/lib/Fhp/MT940/MT940Exception.php
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/lib/Fhp/Model/Account.php
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/lib/Fhp/Model/Account.php
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/lib/Fhp/Model/FlickerTan/DataElement.php
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/lib/Fhp/Model/FlickerTan/DataElement.php
vendored
Normal file → Executable file
9
vendor/nemiah/php-fints/lib/Fhp/Model/FlickerTan/StartCode.php
vendored
Normal file → Executable file
9
vendor/nemiah/php-fints/lib/Fhp/Model/FlickerTan/StartCode.php
vendored
Normal file → Executable 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
0
vendor/nemiah/php-fints/lib/Fhp/Model/FlickerTan/SvgRenderer.php
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/lib/Fhp/Model/FlickerTan/TanRequestChallengeFlicker.php
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/lib/Fhp/Model/FlickerTan/TanRequestChallengeFlicker.php
vendored
Normal file → Executable file
17
vendor/nemiah/php-fints/lib/Fhp/Model/NoPsd2TanMode.php
vendored
Normal file → Executable file
17
vendor/nemiah/php-fints/lib/Fhp/Model/NoPsd2TanMode.php
vendored
Normal file → Executable 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');
|
||||
|
|
|
|||
|
|
@ -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
0
vendor/nemiah/php-fints/lib/Fhp/Model/SEPAAccount.php
vendored
Normal file → Executable file
30
vendor/nemiah/php-fints/lib/Fhp/Model/StatementOfAccount/Statement.php
vendored
Normal file → Executable file
30
vendor/nemiah/php-fints/lib/Fhp/Model/StatementOfAccount/Statement.php
vendored
Normal file → Executable 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
|
||||
|
|
|
|||
8
vendor/nemiah/php-fints/lib/Fhp/Model/StatementOfAccount/StatementOfAccount.php
vendored
Normal file → Executable file
8
vendor/nemiah/php-fints/lib/Fhp/Model/StatementOfAccount/StatementOfAccount.php
vendored
Normal file → Executable 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']);
|
||||
|
|
|
|||
152
vendor/nemiah/php-fints/lib/Fhp/Model/StatementOfAccount/Transaction.php
vendored
Normal file → Executable file
152
vendor/nemiah/php-fints/lib/Fhp/Model/StatementOfAccount/Transaction.php
vendored
Normal file → Executable 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;
|
||||
|
|
|
|||
0
vendor/nemiah/php-fints/lib/Fhp/Model/StatementOfHoldings/Holding.php
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/lib/Fhp/Model/StatementOfHoldings/Holding.php
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/lib/Fhp/Model/StatementOfHoldings/StatementOfHoldings.php
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/lib/Fhp/Model/StatementOfHoldings/StatementOfHoldings.php
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/lib/Fhp/Model/TanMedium.php
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/lib/Fhp/Model/TanMedium.php
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/lib/Fhp/Model/TanMode.php
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/lib/Fhp/Model/TanMode.php
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/lib/Fhp/Model/TanRequest.php
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/lib/Fhp/Model/TanRequest.php
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/lib/Fhp/Model/TanRequestChallengeImage.php
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/lib/Fhp/Model/TanRequestChallengeImage.php
vendored
Normal file → Executable 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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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...';
|
||||
}
|
||||
}
|
||||
|
|
@ -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
0
vendor/nemiah/php-fints/lib/Fhp/Options/Credentials.php
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/lib/Fhp/Options/FinTsOptions.php
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/lib/Fhp/Options/FinTsOptions.php
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/lib/Fhp/Options/SanitizingLogger.php
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/lib/Fhp/Options/SanitizingLogger.php
vendored
Normal file → Executable file
16
vendor/nemiah/php-fints/lib/Fhp/PaginateableAction.php
vendored
Normal file → Executable file
16
vendor/nemiah/php-fints/lib/Fhp/PaginateableAction.php
vendored
Normal file → Executable 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) {
|
||||
|
|
|
|||
0
vendor/nemiah/php-fints/lib/Fhp/Protocol/ActionIncompleteException.php
vendored
Normal file → Executable file
0
vendor/nemiah/php-fints/lib/Fhp/Protocol/ActionIncompleteException.php
vendored
Normal file → Executable 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
23
vendor/nemiah/php-fints/lib/Fhp/Protocol/BPD.php
vendored
Normal file → Executable 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
Loading…
Reference in a new issue