feat: php-fints 4.0 Update + HKEKA/HKKAA Segmente (WIP)

- php-fints Bibliothek von 3.7.0 auf 4.0.0 aktualisiert
- Parser-Fix: Ignoriert zusätzliche Bank-Felder statt Exception
- HKEKA Segmente implementiert (HIEKASv5, HKEKAv5, HIEKAv5)
- HKKAA Segmente implementiert (HIKAASv1, HKKAAv1)
- GetStatementFromArchive und GetElectronicStatement Actions

HINWEIS: HKKAA/HKEKA funktionieren noch nicht mit VR Bank
(Fehler "unerwarteter Aufbau wrt DE 2" - Kontoverbindungsformat)
Normale Funktionalität (Transaktionsimport) ist nicht betroffen.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-03-05 15:47:27 +01:00
parent fc380892f0
commit 8b64fd24d3
393 changed files with 3668 additions and 1031 deletions

View file

@ -5,23 +5,29 @@ Alle wesentlichen Änderungen an diesem Projekt werden in dieser Datei dokumenti
## [3.5] - 2026-03-05 ## [3.5] - 2026-03-05
### Hinzugefügt ### Hinzugefügt
- **PDF-Kontoauszüge per FinTS (HKEKP)**: Elektronische Kontoauszüge direkt von der Bank abrufen - **PDF-Kontoauszüge per FinTS**: Elektronische Kontoauszüge direkt von der Bank abrufen
- Neue Segmente für php-fints: HKEKPv2, HIEKPv2, HIEKPSv2, ParameterKontoauszugPdf - **HKEKP**: Direkt-Abruf (für Banken die dies unterstützen)
- Neue Action-Klasse: GetStatementPDF für PDF-Abruf - **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 - Integration in bestehende PDF-Kontoauszüge-Seite
- Support für Base64-kodierte PDFs (automatische Erkennung aus BPD)
- **Hinweis**: Nicht alle Banken unterstützen HKEKP - prüfbar via BPD-Parameter HIEKPS
- **Cronjob für automatischen PDF-Abruf**: Neue geplante Aufgabe `doAutoFetchPdf` - **Cronjob für automatischen PDF-Abruf**: Neue geplante Aufgabe `doAutoFetchPdf`
- Aktivierbar über Konstante `BANKIMPORT_PDF_AUTO_ENABLED` - Aktivierbar über Konstante `BANKIMPORT_PDF_AUTO_ENABLED`
- Ruft automatisch neue PDF-Kontoauszüge ab und speichert sie - Ruft automatisch neue PDF-Kontoauszüge ab und speichert sie
### Geändert ### Geändert
- PDF-Kontoauszüge-Seite: Neues Layout mit zwei Spalten (FinTS-Abruf links, Upload rechts) - PDF-Kontoauszüge-Seite: Neues Layout mit zwei Spalten (FinTS-Abruf links, Upload rechts)
- fints.class.php: Neue Methoden `getStatementPDF()` und `supportsPdfStatements()` - fints.class.php: Neue Methoden für PDF-Abruf mit automatischer Methodenwahl
### Technisch ### Technisch
- Erweiterung der php-fints Bibliothek um HKEKP-Unterstützung (Segment/EKP/*) - Erweiterung der php-fints Bibliothek:
- Neue Action-Klasse mit Pagination-Support für große PDF-Auszüge - 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 ## [3.1] - 2026-03-05

View file

@ -17,35 +17,54 @@
| `cron/bankimport.cron.php` | Cronjob für automatischen Import | | `cron/bankimport.cron.php` | Cronjob für automatischen Import |
| `admin/cronmonitor.php` | Cron-Monitoring und Pause/Resume | | `admin/cronmonitor.php` | Cron-Monitoring und Pause/Resume |
| `pdfstatements.php` | PDF-Kontoauszüge hochladen und per FinTS abrufen | | `pdfstatements.php` | PDF-Kontoauszüge hochladen und per FinTS abrufen |
| `vendor/.../Segment/EKP/*` | HKEKP-Segmente für PDF-Abruf | | `vendor/.../Segment/EKP/*` | HKEKP-Segmente für PDF-Abruf (direkt) |
| `vendor/.../Action/GetStatementPDF.php` | Action-Klasse für PDF-Abruf | | `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 (HKEKP) ## PDF-Kontoauszüge per FinTS
### Übersicht ### Übersicht
Seit Version 3.5 können PDF-Kontoauszüge direkt von der Bank abgerufen werden (HKEKP = Elektronischer Kontoauszug PDF). 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 ### Neue Dateien in php-fints
``` ```
vendor/nemiah/php-fints/lib/Fhp/ vendor/nemiah/php-fints/lib/Fhp/
├── Action/GetStatementPDF.php # Haupt-Action-Klasse ├── Action/
└── Segment/EKP/ │ ├── GetStatementPDF.php # HKEKP Action
├── HKEKPv2.php # Request-Segment │ └── GetStatementFromArchive.php # HKKAA Action
├── HIEKPv2.php # Response-Segment └── Segment/
├── HIEKP.php # Response-Interface ├── EKP/ # HKEKP Segmente
├── HIEKPSv2.php # Parameter-Segment │ ├── HKEKPv2.php
├── HIEKPS.php # Parameter-Interface │ ├── HIEKPv2.php
└── ParameterKontoauszugPdf.php # Parameter-Model │ └── ...
└── KAA/ # HKKAA Segmente
├── HKKAAv2.php
├── HIKAAv2.php
├── HIKAASv1.php
└── ParameterKontoauszugArchiv.php
``` ```
### Verwendung ### Verwendung (empfohlen: Auto-Modus)
```php ```php
$fints = new BankImportFinTS(); $fints = new BankImportFinTS($db);
if ($fints->supportsPdfStatements()) { $fints->login();
$result = $fints->getStatementPDF(0); // Account-Index, optional Nr+Jahr
if ($result['success']) { // Automatische Methodenwahl
$pdfData = $result['data']['pdf']; $method = $fints->getPdfStatementMethod(); // 'HKEKP', 'HKKAA' oder false
$info = $result['data']['info']; // statementNumber, statementYear, etc. 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'
} }
} }
``` ```

View file

@ -39,6 +39,7 @@ use Fhp\Action\GetSEPAAccounts;
use Fhp\Action\GetStatementOfAccount; use Fhp\Action\GetStatementOfAccount;
use Fhp\Action\GetStatementOfAccountXML; use Fhp\Action\GetStatementOfAccountXML;
use Fhp\Action\GetStatementPDF; use Fhp\Action\GetStatementPDF;
use Fhp\Action\GetStatementFromArchive;
use Fhp\Model\StatementOfAccount\Statement; use Fhp\Model\StatementOfAccount\Statement;
use Fhp\Model\StatementOfAccount\Transaction; use Fhp\Model\StatementOfAccount\Transaction;
@ -1139,4 +1140,180 @@ class BankImportFinTS
return false; return false;
} }
} }
/**
* Check if bank supports archive statements (HKKAA) with PDF format
*
* @return bool
*/
public function supportsArchiveStatements()
{
if (!$this->fints) {
return false;
}
try {
$reflection = new ReflectionClass($this->fints);
$bpdProperty = $reflection->getProperty('bpd');
$bpdProperty->setAccessible(true);
$bpd = $bpdProperty->getValue($this->fints);
if ($bpd === null) {
return false;
}
$hikaas = $bpd->getLatestSupportedParameters('HIKAAS');
if ($hikaas === null) {
return false;
}
// Check if PDF format is supported
$param = $hikaas->getParameter();
return $param->supportsPdf();
} catch (Exception $e) {
dol_syslog("BankImport: supportsArchiveStatements exception: ".$e->getMessage(), LOG_DEBUG);
return false;
}
}
/**
* Check if bank supports any PDF statement method (HKEKP or HKKAA)
*
* @return string|false Method name ('HKEKP', 'HKKAA') or false if none supported
*/
public function getPdfStatementMethod()
{
// First try HKEKP (direct PDF)
if ($this->supportsPdfStatements()) {
return 'HKEKP';
}
// Fallback to HKKAA (archive)
if ($this->supportsArchiveStatements()) {
return 'HKKAA';
}
return false;
}
/**
* Get PDF bank statement from archive via HKKAA
*
* @param int $accountIndex Index of account to use (default 0)
* @param \DateTime|null $fromDate Optional: start date
* @param \DateTime|null $toDate Optional: end date
* @return array|int Array with 'pdfData' and 'info', or 0 if TAN required, or -1 on error
*/
public function getStatementFromArchive($accountIndex = 0, $fromDate = null, $toDate = null)
{
global $conf;
$this->error = '';
if (!$this->fints) {
$this->error = 'Not connected';
return -1;
}
try {
// Get accounts if not cached
if (empty($this->accounts)) {
$getAccounts = GetSEPAAccounts::create();
$this->fints->execute($getAccounts);
if ($getAccounts->needsTan()) {
$this->pendingAction = $getAccounts;
$tanRequest = $getAccounts->getTanRequest();
$this->tanChallenge = $tanRequest->getChallenge();
return 0;
}
$this->accounts = $getAccounts->getAccounts();
}
if (empty($this->accounts) || !isset($this->accounts[$accountIndex])) {
$this->error = 'No accounts available or invalid account index';
return -1;
}
$selectedAccount = $this->accounts[$accountIndex];
dol_syslog("BankImport: Fetching PDF statement from archive via HKKAA", LOG_DEBUG);
$getArchive = GetStatementFromArchive::create(
$selectedAccount,
GetStatementFromArchive::FORMAT_PDF,
$fromDate,
$toDate
);
$this->fints->execute($getArchive);
if ($getArchive->needsTan()) {
$this->pendingAction = $getArchive;
$tanRequest = $getArchive->getTanRequest();
$this->tanChallenge = $tanRequest->getChallenge();
return 0;
}
$pdfData = $getArchive->getPdfData();
$info = $getArchive->getStatementInfo();
if (empty($pdfData)) {
dol_syslog("BankImport: No PDF data received from archive", LOG_DEBUG);
return array(
'pdfData' => '',
'info' => array(),
'message' => 'No statements available in archive'
);
}
dol_syslog("BankImport: Received PDF statement from archive #".($info['statementNumber'] ?? '?').'/'.($info['statementYear'] ?? '?'), LOG_DEBUG);
return array(
'pdfData' => $pdfData,
'info' => $info
);
} catch (Exception $e) {
$this->error = $e->getMessage();
dol_syslog("BankImport: HKKAA failed: ".$this->error, LOG_ERR);
dol_syslog("BankImport: HKKAA Exception class: ".get_class($e), LOG_DEBUG);
// Keep original error message for better debugging
return -1;
}
}
/**
* Get PDF statement using best available method (HKEKP or HKKAA)
*
* @param int $accountIndex Index of account to use (default 0)
* @param int|null $statementNumber Optional: specific statement number (only for HKEKP)
* @param int|null $statementYear Optional: statement year (only for HKEKP)
* @return array|int Array with 'pdfData', 'info' and 'method', or 0 if TAN required, or -1 on error
*/
public function getStatementPDFAuto($accountIndex = 0, $statementNumber = null, $statementYear = null)
{
$method = $this->getPdfStatementMethod();
if ($method === false) {
$this->error = 'Bank does not support PDF statements (neither HKEKP nor HKKAA)';
return -1;
}
if ($method === 'HKEKP') {
$result = $this->getStatementPDF($accountIndex, $statementNumber, $statementYear);
} else {
// HKKAA doesn't support statement number, use date range instead
$result = $this->getStatementFromArchive($accountIndex);
}
if (is_array($result)) {
$result['method'] = $method;
}
return $result;
}
} }

View file

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

16
composer.lock generated
View file

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

View file

@ -101,13 +101,16 @@ if ($action == 'fetchpdf' || $action == 'fetchpdf_single') {
setEventMessages($langs->trans("TANRequired").($fints->tanChallenge ? ': '.$fints->tanChallenge : ''), null, 'warnings'); setEventMessages($langs->trans("TANRequired").($fints->tanChallenge ? ': '.$fints->tanChallenge : ''), null, 'warnings');
$fints->close(); $fints->close();
} else { } else {
// Check if bank supports HKEKP // Check if bank supports any PDF statement method (HKEKP or HKKAA)
if (!$fints->supportsPdfStatements()) { $pdfMethod = $fints->getPdfStatementMethod();
if ($pdfMethod === false) {
setEventMessages($langs->trans("ErrorBankDoesNotSupportPdfStatements"), null, 'errors'); setEventMessages($langs->trans("ErrorBankDoesNotSupportPdfStatements"), null, 'errors');
$fints->close(); $fints->close();
} else { } else {
// Fetch PDF dol_syslog("BankImport: Using PDF method: ".$pdfMethod, LOG_DEBUG);
$pdfResult = $fints->getStatementPDF(0, $fetchNumber, $fetchYear);
// Fetch PDF using auto method (tries HKEKP first, falls back to HKKAA)
$pdfResult = $fints->getStatementPDFAuto(0, $fetchNumber, $fetchYear);
if ($pdfResult === 0) { if ($pdfResult === 0) {
// TAN required - save to session and show TAN form // TAN required - save to session and show TAN form

View file

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

View file

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

View file

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

View file

@ -0,0 +1,50 @@
# .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 Normal file
View file

@ -0,0 +1,22 @@
.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 Executable file → Normal file
View file

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

View file

@ -1,12 +0,0 @@
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 Executable file → Normal file
View file

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

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

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

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

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

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

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

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

@ -46,6 +46,9 @@ $xml = $directDebitFile->generateOutput(['zipToOneFile' => false])[0]['data'];
$sendSEPADirectDebit = \Fhp\Action\SendSEPADirectDebit::create($oneAccount, $xml); $sendSEPADirectDebit = \Fhp\Action\SendSEPADirectDebit::create($oneAccount, $xml);
$fints->execute($sendSEPADirectDebit); $fints->execute($sendSEPADirectDebit);
if ($sendSEPADirectDebit->needsTan()) {
handleStrongAuthentication($sendSEPADirectDebit); // See login.php for the implementation. 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();

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

@ -62,6 +62,9 @@ $oneAccount = $getSepaAccounts->getAccounts()[0];
$sendSEPADirectDebit = \Fhp\Action\SendSEPADirectDebit::create($oneAccount, $sepaDD->toXML('pain.008.001.02')); $sendSEPADirectDebit = \Fhp\Action\SendSEPADirectDebit::create($oneAccount, $sepaDD->toXML('pain.008.001.02'));
$fints->execute($sendSEPADirectDebit); $fints->execute($sendSEPADirectDebit);
if ($sendSEPADirectDebit->needsTan()) {
handleStrongAuthentication($sendSEPADirectDebit); // See login.php for the implementation. 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();

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

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

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

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

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

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

@ -21,6 +21,15 @@ use nemiah\phpSepaXml\SEPATransfer;
/** @var \Fhp\FinTs $fints */ /** @var \Fhp\FinTs $fints */
$fints = require_once 'login.php'; $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 = new \DateTime();
$dt->add(new \DateInterval('P1D')); $dt->add(new \DateInterval('P1D'));
@ -49,6 +58,9 @@ $sepaDD->addCreditor(new SEPACreditor([ //this is who you want to send money to
$sendSEPATransfer = \Fhp\Action\SendSEPATransfer::create($oneAccount, $sepaDD->toXML()); $sendSEPATransfer = \Fhp\Action\SendSEPATransfer::create($oneAccount, $sepaDD->toXML());
$fints->execute($sendSEPATransfer); $fints->execute($sendSEPATransfer);
if ($sendSEPATransfer->needsTan()) {
handleStrongAuthentication($sendSEPATransfer); // See login.php for the implementation. 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();

138
vendor/nemiah/php-fints/Samples/vop.php vendored Normal file
View file

@ -0,0 +1,138 @@
<?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 Executable file → Normal file
View file

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

View file

@ -1,36 +0,0 @@
#!/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)

View file

@ -1,37 +0,0 @@
#!/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 Executable file → Normal file
View file

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

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

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

View file

@ -0,0 +1,178 @@
<?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\EKA\HIEKA;
use Fhp\Segment\EKA\HIEKASv5;
use Fhp\Segment\EKA\HKEKAv5;
use Fhp\Segment\HIRMS\Rueckmeldungscode;
use Fhp\UnsupportedException;
/**
* Retrieves electronic bank statements (Elektronischer Kontoauszug) via HKEKA.
*
* This supports both MT940 and PDF formats depending on what the bank offers.
*/
class GetElectronicStatement extends PaginateableAction
{
// Format codes
public const FORMAT_MT940 = 1;
public const FORMAT_PDF = 2;
/** @var SEPAAccount */
private $account;
/** @var int|null Format to request (1=MT940, 2=PDF, null=default) */
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 data (MT940 or PDF binary) */
private $data = '';
/** @var array Statement metadata from response */
private $statementInfo = [];
/**
* @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
* @return GetElectronicStatement
*/
public static function create(
SEPAAccount $account,
?int $format = null,
?\DateTime $fromDate = null,
?\DateTime $toDate = 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;
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 data (MT940 text or PDF binary)
*/
public function getData(): string
{
$this->ensureDone();
return $this->data;
}
/**
* @return array Statement metadata (number, year, iban, date, format)
*/
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 HIEKASv5|null $hiekas */
$hiekas = $bpd->getLatestSupportedParameters('HIEKAS');
if ($hiekas === null) {
throw new UnsupportedException('The bank does not support electronic statements (HKEKA).');
}
$param = $hiekas->getParameter();
// 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.');
}
// 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)
{
parent::processResponse($response);
// Check if no statements available
if ($response->findRueckmeldung(Rueckmeldungscode::NICHT_VERFUEGBAR) !== null) {
return;
}
/** @var HIEKA[] $responseSegments */
$responseSegments = $response->findSegments(HIEKA::class);
if (empty($responseSegments)) {
// No segments but also no error = empty response
return;
}
foreach ($responseSegments as $hieka) {
// Append data (for pagination)
$this->data .= $hieka->getData();
// 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,
];
}
}
}
}

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

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

View file

@ -16,12 +16,11 @@ class GetSEPADirectDebitParameters extends BaseAction
public const SEQUENCE_TYPES = ['FRST', 'OOFF', 'FNAL', 'RCUR']; public const SEQUENCE_TYPES = ['FRST', 'OOFF', 'FNAL', 'RCUR'];
public const DIRECT_DEBIT_TYPES = ['CORE', 'COR1', 'B2B']; public const DIRECT_DEBIT_TYPES = ['CORE', 'COR1', 'B2B'];
// Request (if you add a field here, update __serialize() and __unserialize() as well).
/** @var string */ /** @var string */
private $directDebitType; private $directDebitType;
/** @var string */ /** @var string */
private $seqType; private $seqType;
/** @var bool */ /** @var bool */
private $singleDirectDebit; private $singleDirectDebit;
@ -43,6 +42,45 @@ class GetSEPADirectDebitParameters extends BaseAction
return $result; 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 public static function getHixxesSegmentName(string $directDebitType, bool $singleDirectDebit): string
{ {
switch ($directDebitType) { switch ($directDebitType) {
@ -56,7 +94,6 @@ class GetSEPADirectDebitParameters extends BaseAction
} }
} }
/** {@inheritdoc} */
protected function createRequest(BPD $bpd, ?UPD $upd) protected function createRequest(BPD $bpd, ?UPD $upd)
{ {
$this->hidxes = $bpd->requireLatestSupportedParameters(static::getHixxesSegmentName($this->directDebitType, $this->singleDirectDebit)); $this->hidxes = $bpd->requireLatestSupportedParameters(static::getHixxesSegmentName($this->directDebitType, $this->singleDirectDebit));

View file

@ -0,0 +1,194 @@
<?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 Executable file → Normal file
View file

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

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

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

View file

@ -1,182 +0,0 @@
<?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\EKP\HIEKP;
use Fhp\Segment\EKP\HIEKPSv2;
use Fhp\Segment\EKP\HKEKPv2;
use Fhp\Segment\HIRMS\Rueckmeldungscode;
use Fhp\UnsupportedException;
/**
* Retrieves PDF bank statements (Elektronischer Kontoauszug) via HKEKP.
*/
class GetStatementPDF extends PaginateableAction
{
/** @var SEPAAccount */
private $account;
/** @var int|null Optional: specific statement number */
private $statementNumber;
/** @var int|null Optional: statement year */
private $statementYear;
/** @var bool Whether PDF is base64 encoded (from BPD) */
private $isBase64 = false;
// 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|null $statementNumber Optional: Request specific statement by number
* @param int|null $statementYear Optional: Year of the statement (required if number given)
* @return GetStatementPDF
*/
public static function create(
SEPAAccount $account,
?int $statementNumber = null,
?int $statementYear = null
): GetStatementPDF {
$result = new GetStatementPDF();
$result->account = $account;
$result->statementNumber = $statementNumber;
$result->statementYear = $statementYear;
return $result;
}
public function __serialize(): array
{
return [
parent::__serialize(),
$this->account,
$this->statementNumber,
$this->statementYear,
$this->isBase64,
];
}
public function __unserialize(array $serialized): void
{
list(
$parentSerialized,
$this->account,
$this->statementNumber,
$this->statementYear,
$this->isBase64
) = $serialized;
is_array($parentSerialized)
? parent::__unserialize($parentSerialized)
: parent::unserialize($parentSerialized);
}
/**
* @return string The raw PDF data
*/
public function getPdfData(): string
{
$this->ensureDone();
// Decode base64 if needed
if ($this->isBase64 && !empty($this->pdfData)) {
$decoded = base64_decode($this->pdfData, true);
if ($decoded !== false) {
return $decoded;
}
}
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 HIEKPSv2|null $hiekps */
$hiekps = $bpd->getLatestSupportedParameters('HIEKPS');
if ($hiekps === null) {
throw new UnsupportedException('The bank does not support PDF statements (HKEKP).');
}
$param = $hiekps->getParameter();
$this->isBase64 = $param->isBase64Encoded();
// Check if historical statements are allowed
if ($this->statementNumber !== null && !$param->isHistoricalStatementsAllowed()) {
throw new UnsupportedException('The bank does not allow requesting historical statements by number.');
}
return HKEKPv2::create(
Kti::fromAccount($this->account),
$this->statementNumber,
$this->statementYear
);
}
/** {@inheritdoc} */
public function processResponse(Message $response)
{
parent::processResponse($response);
// Check if no statements available
if ($response->findRueckmeldung(Rueckmeldungscode::NICHT_VERFUEGBAR) !== null) {
return;
}
/** @var HIEKP[] $responseSegments */
$responseSegments = $response->findSegments(HIEKP::class);
if (empty($responseSegments)) {
// No segments but also no error = empty response
return;
}
foreach ($responseSegments as $hiekp) {
// Append PDF data (for pagination)
$this->pdfData .= $hiekp->getPdfData();
// Store metadata from first segment
if (empty($this->statementInfo)) {
$this->statementInfo = [
'statementNumber' => $hiekp->getStatementNumber(),
'statementYear' => $hiekp->getStatementYear(),
'iban' => $hiekp->getIban(),
'creationDate' => $hiekp->getCreationDate(),
'filename' => $hiekp->getFilename(),
'receiptCode' => $hiekp->needsReceipt() ? $hiekp->getReceiptCode() : null,
];
}
}
}
}

View file

@ -13,12 +13,11 @@ use Fhp\Syntax\Bin;
class SendInternationalCreditTransfer extends BaseAction class SendInternationalCreditTransfer extends BaseAction
{ {
// Request (if you add a field here, update __serialize() and __unserialize() as well).
/** @var SEPAAccount */ /** @var SEPAAccount */
protected $account; protected $account;
/** @var string */ /** @var string */
protected $dtavzData; protected $dtavzData;
/** @var string|null */ /** @var string|null */
protected $dtavzVersion; protected $dtavzVersion;
@ -36,6 +35,45 @@ class SendInternationalCreditTransfer extends BaseAction
return $result; 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) protected function createRequest(BPD $bpd, ?UPD $upd)
{ {
/** @var HIAUBSv9 $hiaubs */ /** @var HIAUBSv9 $hiaubs */

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

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

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

@ -23,15 +23,17 @@ use Fhp\UnsupportedException;
*/ */
class SendSEPARealtimeTransfer extends BaseAction class SendSEPARealtimeTransfer extends BaseAction
{ {
// Request (if you add a field here, update __serialize() and __unserialize() as well).
/** @var SEPAAccount */ /** @var SEPAAccount */
private $account; private $account;
/** @var string */ /** @var string */
private $painMessage; private $painMessage;
/** @var string */ /** @var string */
private $xmlSchema; private $xmlSchema;
private bool $allowConversionToSEPATransfer = true; private bool $allowConversionToSEPATransfer = true;
// 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. * @param SEPAAccount $account The account from which the transfer will be sent.
* @param string $painMessage An XML-formatted ISO 20022 message. You may want to use github.com/nemiah/phpSepaXml * @param string $painMessage An XML-formatted ISO 20022 message. You may want to use github.com/nemiah/phpSepaXml
@ -52,7 +54,45 @@ class SendSEPARealtimeTransfer extends BaseAction
return $result; return $result;
} }
/** {@inheritdoc} */ /**
* @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);
}
protected function createRequest(BPD $bpd, ?UPD $upd) protected function createRequest(BPD $bpd, ?UPD $upd)
{ {
/** @var HIIPZSv1|HIIPZSv2 $hiipzs */ /** @var HIIPZSv1|HIIPZSv2 $hiipzs */
@ -70,7 +110,7 @@ class SendSEPARealtimeTransfer extends BaseAction
// Sometimes the Bank reports supported schemas with a "_GBIC_X" postfix. // Sometimes the Bank reports supported schemas with a "_GBIC_X" postfix.
// GIBC_X stands for German Banking Industry Committee and a version counter. // GIBC_X stands for German Banking Industry Committee and a version counter.
$xmlSchema = $this->xmlSchema; $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 // 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 // urn:iso:std:iso:20022:tech:xsd:pain.001.001.09_GBIC_4
return str_starts_with($value, $xmlSchema); return str_starts_with($value, $xmlSchema);
@ -92,7 +132,6 @@ class SendSEPARealtimeTransfer extends BaseAction
return $hkipz; return $hkipz;
} }
/** {@inheritdoc} */
public function processResponse(Message $response) public function processResponse(Message $response)
{ {
parent::processResponse($response); parent::processResponse($response);
@ -106,8 +145,8 @@ class SendSEPARealtimeTransfer extends BaseAction
return; return;
} }
if ($response->findRueckmeldung(Rueckmeldungscode::ENTGEGENGENOMMEN) === null && if ($response->findRueckmeldung(Rueckmeldungscode::ENTGEGENGENOMMEN) === null
$response->findRueckmeldung(Rueckmeldungscode::AUSGEFUEHRT) === null) { && $response->findRueckmeldung(Rueckmeldungscode::AUSGEFUEHRT) === null) {
throw new UnexpectedResponseException('Bank did not confirm SEPATransfer execution'); throw new UnexpectedResponseException('Bank did not confirm SEPATransfer execution');
} }
} }

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

@ -19,12 +19,17 @@ use Fhp\UnsupportedException;
*/ */
class SendSEPATransfer extends BaseAction class SendSEPATransfer extends BaseAction
{ {
// Request (if you add a field here, update __serialize() and __unserialize() as well).
/** @var SEPAAccount */ /** @var SEPAAccount */
private $account; private $account;
/** @var string */ /** @var string */
private $painMessage; private $painMessage;
/** @var string */ /** @var string */
private $xmlSchema; 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. * @param SEPAAccount $account The account from which the transfer will be sent.
@ -44,11 +49,62 @@ class SendSEPATransfer extends BaseAction
return $result; return $result;
} }
/** {@inheritdoc} */ /**
* 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);
}
protected function createRequest(BPD $bpd, ?UPD $upd) protected function createRequest(BPD $bpd, ?UPD $upd)
{ {
//ANALYSE XML FOR RECEIPTS AND PAYMENT DATE // ANALYSE XML FOR RECEIPTS AND PAYMENT DATE
$xmlAsObject = simplexml_load_string($this->painMessage, "SimpleXMLElement", LIBXML_NOCDATA); $xmlAsObject = simplexml_load_string($this->painMessage, 'SimpleXMLElement', LIBXML_NOCDATA);
$numberOfTransactions = $xmlAsObject->CstmrCdtTrfInitn->GrpHdr->NbOfTxs; $numberOfTransactions = $xmlAsObject->CstmrCdtTrfInitn->GrpHdr->NbOfTxs;
$hasReqdExDates = false; $hasReqdExDates = false;
foreach ($xmlAsObject->CstmrCdtTrfInitn?->PmtInf as $pmtInfo) { foreach ($xmlAsObject->CstmrCdtTrfInitn?->PmtInf as $pmtInfo) {
@ -59,25 +115,21 @@ class SendSEPATransfer extends BaseAction
} }
} }
//NOW READ OUT, WICH SEGMENT SHOULD BE USED: // NOW READ OUT, WICH SEGMENT SHOULD BE USED:
if ($numberOfTransactions > 1 && $hasReqdExDates) { if ($numberOfTransactions > 1 && $hasReqdExDates) {
// Terminierte SEPA-Sammelüberweisung (Segment HKCME / Kennung HICMES) // Terminierte SEPA-Sammelüberweisung (Segment HKCME / Kennung HICMES)
$segmentID = 'HICMES'; $segmentID = 'HICMES';
$segment = \Fhp\Segment\CME\HKCMEv1::createEmpty(); $segment = \Fhp\Segment\CME\HKCMEv1::createEmpty();
} elseif ($numberOfTransactions == 1 && $hasReqdExDates) { } elseif ($numberOfTransactions == 1 && $hasReqdExDates) {
// Terminierte SEPA-Überweisung (Segment HKCSE / Kennung HICSES) // Terminierte SEPA-Überweisung (Segment HKCSE / Kennung HICSES)
$segmentID = 'HICSES'; $segmentID = 'HICSES';
$segment = \Fhp\Segment\CSE\HKCSEv1::createEmpty(); $segment = \Fhp\Segment\CSE\HKCSEv1::createEmpty();
} elseif ($numberOfTransactions > 1 && !$hasReqdExDates) { } elseif ($numberOfTransactions > 1 && !$hasReqdExDates) {
// SEPA-Sammelüberweisungen (Segment HKCCM / Kennung HICSES) // SEPA-Sammelüberweisungen (Segment HKCCM / Kennung HICSES)
$segmentID = 'HICSES'; $segmentID = 'HICSES';
$segment = \Fhp\Segment\CCM\HKCCMv1::createEmpty(); $segment = \Fhp\Segment\CCM\HKCCMv1::createEmpty();
} else { } else {
// SEPA Einzelüberweisung (Segment HKCCS / Kennung HICCSS).
//SEPA Einzelüberweisung (Segment HKCCS / Kennung HICCSS).
$segmentID = 'HICCSS'; $segmentID = 'HICCSS';
$segment = \Fhp\Segment\CCS\HKCCSv1::createEmpty(); $segment = \Fhp\Segment\CCS\HKCCSv1::createEmpty();
} }
@ -93,7 +145,7 @@ class SendSEPATransfer extends BaseAction
// Sometimes the Bank reports supported schemas with a "_GBIC_X" postfix. // Sometimes the Bank reports supported schemas with a "_GBIC_X" postfix.
// GIBC_X stands for German Banking Industry Committee and a version counter. // GIBC_X stands for German Banking Industry Committee and a version counter.
$xmlSchema = $this->xmlSchema; $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 // 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 // urn:iso:std:iso:20022:tech:xsd:pain.001.001.09_GBIC_4
return str_starts_with($value, $xmlSchema); return str_starts_with($value, $xmlSchema);
@ -107,10 +159,19 @@ class SendSEPATransfer extends BaseAction
$segment->kontoverbindungInternational = Kti::fromAccount($this->account); $segment->kontoverbindungInternational = Kti::fromAccount($this->account);
$segment->sepaDescriptor = $this->xmlSchema; $segment->sepaDescriptor = $this->xmlSchema;
$segment->sepaPainMessage = new Bin($this->painMessage); $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; return $segment;
} }
/** {@inheritdoc} */
public function processResponse(Message $response) public function processResponse(Message $response)
{ {
parent::processResponse($response); parent::processResponse($response);

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

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

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

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

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

@ -5,6 +5,10 @@ namespace Fhp;
use Fhp\Model\NoPsd2TanMode; use Fhp\Model\NoPsd2TanMode;
use Fhp\Model\TanMedium; use Fhp\Model\TanMedium;
use Fhp\Model\TanMode; 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\Credentials;
use Fhp\Options\FinTsOptions; use Fhp\Options\FinTsOptions;
use Fhp\Options\SanitizingLogger; use Fhp\Options\SanitizingLogger;
@ -26,6 +30,8 @@ use Fhp\Segment\TAN\HITAN;
use Fhp\Segment\TAN\HKTAN; use Fhp\Segment\TAN\HKTAN;
use Fhp\Segment\TAN\HKTANFactory; use Fhp\Segment\TAN\HKTANFactory;
use Fhp\Segment\TAN\HKTANv6; use Fhp\Segment\TAN\HKTANv6;
use Fhp\Segment\VPP\HKVPPv1;
use Fhp\Segment\VPP\VopHelper;
use Fhp\Syntax\InvalidResponseException; use Fhp\Syntax\InvalidResponseException;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger; use Psr\Log\NullLogger;
@ -159,6 +165,7 @@ class FinTs
* carefully (not written to log files, only to a database or other storage system that would normally be used * 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 * 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. * 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 public function persist(bool $minimal = false): string
{ {
@ -201,7 +208,7 @@ class FinTs
* *
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
*/ */
public function loadPersistedInstance(string $persistedInstance) public function loadPersistedInstance(string $persistedInstance): void
{ {
$unserialized = unserialize($persistedInstance); $unserialized = unserialize($persistedInstance);
if (!is_array($unserialized) || count($unserialized) === 0) { if (!is_array($unserialized) || count($unserialized) === 0) {
@ -216,7 +223,7 @@ class FinTs
} }
} }
private function loadPersistedInstanceVersion2(array $data) private function loadPersistedInstanceVersion2(array $data): void
{ {
list( // This should match persist(). list( // This should match persist().
$this->bpd, $this->bpd,
@ -226,7 +233,7 @@ class FinTs
$this->selectedTanMedium, $this->selectedTanMedium,
$this->kundensystemId, $this->kundensystemId,
$this->dialogId, $this->dialogId,
$this->messageNumber $this->messageNumber,
) = $data; ) = $data;
} }
@ -254,7 +261,7 @@ class FinTs
* @param int $responseTimeout The number of seconds to wait before aborting a request to the bank server. * @param int $responseTimeout The number of seconds to wait before aborting a request to the bank server.
* @noinspection PhpUnused * @noinspection PhpUnused
*/ */
public function setTimeouts(int $connectTimeout, int $responseTimeout) public function setTimeouts(int $connectTimeout, int $responseTimeout): void
{ {
$this->options->timeoutConnect = $connectTimeout; $this->options->timeoutConnect = $connectTimeout;
$this->options->timeoutResponse = $responseTimeout; $this->options->timeoutResponse = $responseTimeout;
@ -285,7 +292,7 @@ class FinTs
/** /**
* Executes an action. Be sure to {@link login()} first. See the `\Fhp\Action` package for actions that can be * 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 two possible states: * executed with this function. Note that, after this function returns, the action can be in the following states:
* 1. If {@link BaseAction::needsTan()} returns true, the action isn't completed yet because needs a TAN or other * 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 * 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 * information about the TAN/2FA that is needed. Your application then needs to interact with the user to obtain
@ -293,9 +300,30 @@ class FinTs
* be verified with {@link checkDecoupledSubmission()}). Both of those functions require passing the same * 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 * {@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. * state as if it had been completed right away.
* 2. If {@link BaseAction::needsTan()} returns false, the action was completed right away. Use the respective * 2. If {@link BaseAction::needsPollingWait()} returns true, the action isn't completed yet because the server is
* getters on the action instance to retrieve the result. In case the action fails, the corresponding exception * still running some slow operation. Importantly, the server has not necessarily accepted the action yet, so it
* will be thrown from this function. * 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.
* *
* @param BaseAction $action The action to be executed. Its {@link BaseAction::isDone()} status will be updated when * @param BaseAction $action The action to be executed. Its {@link BaseAction::isDone()} status will be updated when
* this function returns successfully. * this function returns successfully.
@ -304,27 +332,36 @@ class FinTs
* @throws ServerException When the server responds with a (FinTS-encoded) error message, which includes most things * @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. * that can go wrong with the action itself, like wrong credentials, invalid IBANs, locked accounts, etc.
*/ */
public function execute(BaseAction $action) public function execute(BaseAction $action): void
{ {
if ($this->dialogId === null && !($action instanceof DialogInitialization)) { if ($this->dialogId === null && !($action instanceof DialogInitialization)) {
throw new \RuntimeException('Need to login (DialogInitialization) before executing other actions'); 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); $requestSegments = $action->getNextRequest($this->bpd, $this->upd);
if (count($requestSegments) === 0) { if (count($requestSegments) === 0) {
return; // No request needed. return; // No request needed.
} }
$message = MessageBuilder::create()->add($requestSegments);
// Construct the full request message. // Add HKTAN for authentication if necessary.
$message = MessageBuilder::create()->add($requestSegments); // This fills in the segment numbers.
if (!($this->getSelectedTanMode() instanceof NoPsd2TanMode)) { if (!($this->getSelectedTanMode() instanceof NoPsd2TanMode)) {
if (($needTanForSegment = $action->getNeedTanForSegment()) !== null) { if (($needTanForSegment = $action->getNeedTanForSegment()) !== null) {
$message->add(HKTANFactory::createProzessvariante2Step1( $message->add(HKTANFactory::createProzessvariante2Step1(
$this->requireTanMode(), $this->selectedTanMedium, $needTanForSegment)); $this->requireTanMode(), $this->selectedTanMedium, $needTanForSegment));
} }
} }
$request = $this->buildMessage($message, $this->getSelectedTanMode());
// 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.
$action->setRequestSegmentNumbers(array_map(function ($segment) { $action->setRequestSegmentNumbers(array_map(function ($segment) {
/* @var BaseSegment $segment */ /* @var BaseSegment $segment */
return $segment->getSegmentNumber(); return $segment->getSegmentNumber();
@ -332,11 +369,28 @@ class FinTs
// Execute the request. // Execute the request.
$response = $this->sendMessage($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); $this->readBPD($response);
// Detect if the bank wants a TAN. // Detect if the bank wants a TAN.
/** @var HITAN $hitan */ /** @var HITAN $hitan */
$hitan = $response->findSegment(HITAN::class); $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 !== null && $hitan->getAuftragsreferenz() !== HITAN::DUMMY_REFERENCE) {
if ($hitan->tanProzess !== HKTAN::TAN_PROZESS_4) { if ($hitan->tanProzess !== HKTAN::TAN_PROZESS_4) {
throw new UnexpectedResponseException("Unsupported TAN request type $hitan->tanProzess"); throw new UnexpectedResponseException("Unsupported TAN request type $hitan->tanProzess");
@ -350,14 +404,51 @@ class FinTs
$action->setDialogId($response->header->dialogId); $action->setDialogId($response->header->dialogId);
$action->setMessageNumber($this->messageNumber); $action->setMessageNumber($this->messageNumber);
} }
return;
} }
// If no TAN is needed, process the response normally, and maybe keep going for more pages. // 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.
$this->processActionResponse($action, $response->filterByReferenceSegments($action->getRequestSegmentNumbers())); $this->processActionResponse($action, $response->filterByReferenceSegments($action->getRequestSegmentNumbers()));
if ($action instanceof PaginateableAction && $action->hasMorePages()) { if ($action instanceof PaginateableAction && $action->hasMorePages()) {
$this->execute($action); $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.'
);
}
} }
/** /**
@ -365,9 +456,9 @@ class FinTs
* `false`, this function sends the given $tan to the server to complete the action. By using {@link persist()}, * `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. * 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 completed. That is, its result is available through its getters * After this function returns, the `$action` is in any of the same states as after {@link execute()}, see there.
* just as if it had been completed by the original call to {@link execute()} right away. In case the action fails, * In practice, the action is fully completed after completing the decoupled submission.
* the corresponding exception will be thrown from this function. * 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 * @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 * Section B.4.2.1.1
@ -379,7 +470,7 @@ class FinTs
* @throws ServerException When the server responds with a (FinTS-encoded) error message, which includes most things * @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. * that can go wrong with the action itself, like wrong credentials, invalid IBANs, locked accounts, etc.
*/ */
public function submitTan(BaseAction $action, string $tan) public function submitTan(BaseAction $action, string $tan): void
{ {
// Check the action's state. // Check the action's state.
$tanRequest = $action->getTanRequest(); $tanRequest = $action->getTanRequest();
@ -433,7 +524,9 @@ class FinTs
* For an action where {@link BaseAction::needsTan()} returns `true` and {@link TanMode::isDecoupled()} returns * 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 * `true`, this function checks with the server whether the second factor authentication has been completed yet on
* the secondary device of the user. * the secondary device of the user.
* - If so, this completes the given action and returns `true`. * - 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.
* - In case the action fails, the corresponding exception will be thrown from this function. * - 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 * - If the authentication has not been completed yet, this returns `false` and the action remains in its
* previous, uncompleted state. * previous, uncompleted state.
@ -449,9 +542,10 @@ class FinTs
* Section B.4.2.2 * Section B.4.2.2
* *
* @param BaseAction $action The action to be completed. * @param BaseAction $action The action to be completed.
* @return bool True if the decoupled authentication is done and the $action was completed. If false, the * @return bool True if the decoupled authentication is done and the $action was completed or entered one of the
* {@link TanRequest} inside the action has been updated, which *may* provide new/more instructions to the user, * other states documented on {@link execute()}.
* though probably it rarely does in practice. * 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 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 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 * @throws ServerException When the server responds with a (FinTS-encoded) error message, which includes most things
@ -530,6 +624,99 @@ class FinTs
return true; 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 * 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 * when you're done with all the actions, but NOT when you're persisting the instance to fulfill the TAN request of
@ -538,7 +725,7 @@ class FinTs
* from cached BPD/UPD upon the next {@link login()}, for instance. * from cached BPD/UPD upon the next {@link login()}, for instance.
* @throws ServerException When closing the dialog fails. * @throws ServerException When closing the dialog fails.
*/ */
public function close() public function close(): void
{ {
if ($this->dialogId !== null) { if ($this->dialogId !== null) {
$this->endDialog(); $this->endDialog();
@ -552,7 +739,7 @@ class FinTs
* This can be called by the application using this library when it just restored this FinTs instance from the * 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. * persisted format after a long time, during which the session/dialog has most likely expired on the server side.
*/ */
public function forgetDialog() public function forgetDialog(): void
{ {
$this->dialogId = null; $this->dialogId = null;
} }
@ -571,9 +758,11 @@ class FinTs
public function getTanModes(): array public function getTanModes(): array
{ {
$this->ensureTanModesAvailable(); $this->ensureTanModesAvailable();
$result = array(); $result = [];
foreach ($this->allowedTanModes as $tanModeId) { 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]; $result[$tanModeId] = $this->bpd->allTanModes[$tanModeId];
} }
return $result; return $result;
@ -624,7 +813,7 @@ class FinTs
* must be the value returned from {@link TanMedium::getName()} for one of the TAN media supported with that TAN * 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. * mode. Use {@link getTanMedia()} to obtain a list of possible TAN media options.
*/ */
public function selectTanMode($tanMode, $tanMedium = null) public function selectTanMode($tanMode, $tanMedium = null): void
{ {
if (!is_int($tanMode) && !($tanMode instanceof TanMode)) { if (!is_int($tanMode) && !($tanMode instanceof TanMode)) {
throw new \InvalidArgumentException('tanMode must be an int or a TanMode'); throw new \InvalidArgumentException('tanMode must be an int or a TanMode');
@ -664,7 +853,7 @@ class FinTs
* @throws UnexpectedResponseException When the server does not send the BPD or close the dialog properly. * @throws UnexpectedResponseException When the server does not send the BPD or close the dialog properly.
* @throws ServerException When the server resopnds with an error. * @throws ServerException When the server resopnds with an error.
*/ */
private function ensureBpdAvailable() private function ensureBpdAvailable(): void
{ {
if ($this->bpd !== null) { if ($this->bpd !== null) {
return; // Nothing to do. return; // Nothing to do.
@ -711,7 +900,7 @@ class FinTs
* like it should according to the protocol, or when the dialog is not closed properly. * like it should according to the protocol, or when the dialog is not closed properly.
* @throws ServerException When the server responds with an error. * @throws ServerException When the server responds with an error.
*/ */
private function ensureTanModesAvailable() private function ensureTanModesAvailable(): void
{ {
if ($this->allowedTanModes === null) { if ($this->allowedTanModes === null) {
$this->ensureBpdAvailable(); $this->ensureBpdAvailable();
@ -730,7 +919,7 @@ class FinTs
* dialog is not closed properly. * dialog is not closed properly.
* @throws ServerException When the server responds with an error. * @throws ServerException When the server responds with an error.
*/ */
private function ensureSynchronized() private function ensureSynchronized(): void
{ {
if ($this->kundensystemId === null) { if ($this->kundensystemId === null) {
$this->ensureBpdAvailable(); $this->ensureBpdAvailable();
@ -820,7 +1009,7 @@ class FinTs
/** /**
* Closes the physical connection, if necessary. * Closes the physical connection, if necessary.
*/ */
private function disconnect() private function disconnect(): void
{ {
if ($this->connection !== null) { if ($this->connection !== null) {
$this->connection->disconnect(); $this->connection->disconnect();
@ -834,7 +1023,7 @@ class FinTs
* @param Message $fakeResponseMessage A messsage that contains the response segments for this action. * @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. * @throws UnexpectedResponseException When the server responded with a valid but unexpected message.
*/ */
private function processActionResponse(BaseAction $action, Message $fakeResponseMessage) private function processActionResponse(BaseAction $action, Message $fakeResponseMessage): void
{ {
$action->processResponse($fakeResponseMessage); $action->processResponse($fakeResponseMessage);
if ($action instanceof DialogInitialization) { if ($action instanceof DialogInitialization) {
@ -864,7 +1053,7 @@ class FinTs
* properly. * properly.
* @throws ServerException When the server responds with an error. * @throws ServerException When the server responds with an error.
*/ */
private function executeWeakDialogInitialization(?string $hktanRef) private function executeWeakDialogInitialization(?string $hktanRef): void
{ {
if ($this->dialogId !== null) { if ($this->dialogId !== null) {
throw new \RuntimeException('Cannot init another dialog.'); throw new \RuntimeException('Cannot init another dialog.');
@ -905,7 +1094,7 @@ class FinTs
* @throws ServerException When the server responds with an error instead of closing the dialog. This means that * @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. * the connection is tainted and can probably not be used for another dialog.
*/ */
protected function endDialog(bool $isAnonymous = false) protected function endDialog(bool $isAnonymous = false): void
{ {
if ($this->connection === null) { if ($this->connection === null) {
$this->dialogId = null; $this->dialogId = null;
@ -943,7 +1132,7 @@ class FinTs
* @param MessageBuilder $message The message to be built. * @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 * @param TanMode|null $tanMode Optionally a TAN mode that will be used when sending this message, defaults to 999
* (single step). * (single step).
* @param string|null Optionally a TAN to sign this message with. * @param string|null $tan Optionally a TAN to sign this message with.
* @return Message The built message. * @return Message The built message.
*/ */
private function buildMessage(MessageBuilder $message, ?TanMode $tanMode = null, ?string $tan = null): Message private function buildMessage(MessageBuilder $message, ?TanMode $tanMode = null, ?string $tan = null): Message

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

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

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

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

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

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

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

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

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

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

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

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

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

View file

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

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

View file

@ -0,0 +1,23 @@
<?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 Executable file → Normal file
View file

View file

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

View file

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

View file

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

View file

View file

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

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

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

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,51 @@
<?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 Executable file → Normal file
View file

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

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

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

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

View file

View file

@ -0,0 +1,26 @@
<?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 Executable file → Normal file
View file

@ -10,6 +10,7 @@ use Fhp\Segment\HIBPA\HIBPAv3;
use Fhp\Segment\HIPINS\HIPINSv1; use Fhp\Segment\HIPINS\HIPINSv1;
use Fhp\Segment\SegmentInterface; use Fhp\Segment\SegmentInterface;
use Fhp\Segment\TAN\HITANS; use Fhp\Segment\TAN\HITANS;
use Fhp\Segment\VPP\HIVPPSv1;
/** /**
* Segmentfolge: Bankparameterdaten (Version 3) * Segmentfolge: Bankparameterdaten (Version 3)
@ -152,6 +153,28 @@ class BPD
return null; 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. * @return bool Whether the BPD indicates that the bank supports PSD2.
*/ */

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

@ -143,7 +143,7 @@ class DialogInitialization extends BaseAction
$this->hktanRef, $this->hktanRef,
$this->kundensystemId, $this->kundensystemId,
$this->messageNumber, $this->messageNumber,
$this->dialogId $this->dialogId,
) = $serialized; ) = $serialized;
is_array($parentSerialized) ? is_array($parentSerialized) ?
@ -151,7 +151,6 @@ class DialogInitialization extends BaseAction
parent::unserialize($parentSerialized); parent::unserialize($parentSerialized);
} }
/** {@inheritdoc} */
protected function createRequest(BPD $bpd, ?UPD $upd) protected function createRequest(BPD $bpd, ?UPD $upd)
{ {
throw new \AssertionError('DialogInitialization::createRequest should not be used.'); throw new \AssertionError('DialogInitialization::createRequest should not be used.');

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

@ -17,7 +17,6 @@ class GetTanMedia extends BaseAction
/** @var TanMediumListe[]|null */ /** @var TanMediumListe[]|null */
private $tanMedia; private $tanMedia;
/** {@inheritdoc} */
protected function createRequest(BPD $bpd, ?UPD $upd) protected function createRequest(BPD $bpd, ?UPD $upd)
{ {
// Prepare the HKTAB request. // Prepare the HKTAB request.
@ -32,7 +31,6 @@ class GetTanMedia extends BaseAction
} }
} }
/** {@inheritdoc} */
public function processResponse(Message $response) public function processResponse(Message $response)
{ {
parent::processResponse($response); parent::processResponse($response);

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

@ -190,12 +190,17 @@ class Message
/** /**
* @param int $code The response code to search for. * @param int $code The response code to search for.
* @param ?int $requestSegmentNumber If set, only consider Rueckmeldungen that pertain to this request segment.
* @return Rueckmeldung|null The corresponding Rueckmeldung instance, or null if not found. * @return Rueckmeldung|null The corresponding Rueckmeldung instance, or null if not found.
*/ */
public function findRueckmeldung(int $code): ?Rueckmeldung public function findRueckmeldung(int $code, ?int $requestSegmentNumber = null): ?Rueckmeldung
{ {
foreach ($this->plainSegments as $segment) { foreach ($this->plainSegments as $segment) {
if ($segment instanceof RueckmeldungContainer) { if (
$segment instanceof RueckmeldungContainer && (
$requestSegmentNumber === null || $segment->segmentkopf->bezugselement === $requestSegmentNumber
)
) {
$rueckmeldung = $segment->findRueckmeldung($code); $rueckmeldung = $segment->findRueckmeldung($code);
if ($rueckmeldung !== null) { if ($rueckmeldung !== null) {
return $rueckmeldung; return $rueckmeldung;
@ -217,16 +222,29 @@ class Message
return $rueckmeldungen; return $rueckmeldungen;
} }
/**
* @param int $requestSegmentNumber Only consider Rueckmeldungen that pertain to this request segment.
* @return int[] The codes of all the Rueckmeldung instances matching the request segment.
*/
public function findRueckmeldungscodesForReferenceSegment(int $requestSegmentNumber): array
{
$codes = [];
foreach ($this->plainSegments as $segment) {
if ($segment instanceof RueckmeldungContainer && $segment->segmentkopf->bezugselement === $requestSegmentNumber) {
foreach ($segment->getAllRueckmeldungen() as $rueckmeldung) {
$codes[] = $rueckmeldung->rueckmeldungscode;
}
}
}
return $codes;
}
/** /**
* @return string The HBCI/FinTS wire format for this message, ISO-8859-1 encoded. * @return string The HBCI/FinTS wire format for this message, ISO-8859-1 encoded.
*/ */
public function serialize(): string public function serialize(): string
{ {
$result = ''; return Serializer::serializeSegments($this->wrapperSegments);
foreach ($this->wrapperSegments as $segment) {
$result .= Serializer::serializeSegment($segment);
}
return $result;
} }
/** /**
@ -300,14 +318,16 @@ class Message
$segments = Parser::parseSegments($rawMessage); $segments = Parser::parseSegments($rawMessage);
// Message header and footer must always be there, or something went badly wrong. // Message header and footer must always be there, or something went badly wrong.
if (!($segments[0] instanceof HNHBKv3)) {
throw new \InvalidArgumentException("Expected first segment to be HNHBK: $rawMessage");
}
if (!($segments[count($segments) - 1] instanceof HNHBSv1)) {
throw new \InvalidArgumentException("Expected last segment to be HNHBS: $rawMessage");
}
$result->header = $segments[0]; $result->header = $segments[0];
$result->footer = $segments[count($segments) - 1]; $result->footer = $segments[count($segments) - 1];
if (!($result->header instanceof HNHBKv3)) {
$actual = $result->header->getName();
throw new \InvalidArgumentException("Expected first segment to be HNHBK, but got $actual: $rawMessage");
}
if (!($result->footer instanceof HNHBSv1)) {
$actual = $result->footer->getName();
throw new \InvalidArgumentException("Expected last segment to be HNHBS, but got $actual: $rawMessage");
}
// Check if there's an encryption header and "encrypted" data. // Check if there's an encryption header and "encrypted" data.
// Section B.8 specifies that there are exactly 4 segments: HNHBK, HNVSK, HNVSD, HNHBS. // Section B.8 specifies that there are exactly 4 segments: HNHBK, HNVSK, HNVSD, HNHBS.
@ -351,7 +371,7 @@ class Message
* @param int $segmentNumber The number for the *first* segment, subsequent segment get the subsequent integers. * @param int $segmentNumber The number for the *first* segment, subsequent segment get the subsequent integers.
* @return BaseSegment[] The same array, for chaining. * @return BaseSegment[] The same array, for chaining.
*/ */
private static function setSegmentNumbers(array $segments, int $segmentNumber): array public static function setSegmentNumbers(array $segments, int $segmentNumber): array
{ {
foreach ($segments as $segment) { foreach ($segments as $segment) {
$segment->segmentkopf->segmentnummer = $segmentNumber; $segment->segmentkopf->segmentnummer = $segmentNumber;

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

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

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

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

View file

@ -8,7 +8,7 @@ namespace Fhp\Protocol;
*/ */
class UnexpectedResponseException extends \RuntimeException class UnexpectedResponseException extends \RuntimeException
{ {
public function __construct(string $message, int $code = 0, \Exception $previous = null) public function __construct(string $message, int $code = 0, ?\Exception $previous = null)
{ {
parent::__construct($message, $code, $previous); parent::__construct($message, $code, $previous);
} }

View file

@ -0,0 +1,26 @@
<?php
/** @noinspection PhpUnused */
namespace Fhp\Protocol;
use Fhp\Model\VopConfirmationRequest;
/**
* Thrown when an action result is read, but the action is still pending the user's confirmation of the Verification of
* Payee result.
*/
class VopConfirmationRequiredException extends \RuntimeException
{
private VopConfirmationRequest $vopConfirmationRequest;
public function __construct(VopConfirmationRequest $vopConfirmationRequest)
{
parent::__construct('This action needs VOP confirmation before it will be executed.');
$this->vopConfirmationRequest = $vopConfirmationRequest;
}
public function getVopConfirmationRequest(): VopConfirmationRequest
{
return $this->vopConfirmationRequest;
}
}

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

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

View file

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

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

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

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

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

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