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
### Hinzugefügt
- **PDF-Kontoauszüge per FinTS (HKEKP)**: Elektronische Kontoauszüge direkt von der Bank abrufen
- Neue Segmente für php-fints: HKEKPv2, HIEKPv2, HIEKPSv2, ParameterKontoauszugPdf
- Neue Action-Klasse: GetStatementPDF für PDF-Abruf
- **PDF-Kontoauszüge per FinTS**: Elektronische Kontoauszüge direkt von der Bank abrufen
- **HKEKP**: Direkt-Abruf (für Banken die dies unterstützen)
- **HKKAA**: Fallback über Bank-Archiv/Postfach (VR Banken, etc.)
- Automatische Methodenwahl: System prüft BPD und wählt beste verfügbare Methode
- Neue Segmente für php-fints: EKP/* (HKEKP) und KAA/* (HKKAA)
- Integration in bestehende PDF-Kontoauszüge-Seite
- 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`
- Aktivierbar über Konstante `BANKIMPORT_PDF_AUTO_ENABLED`
- Ruft automatisch neue PDF-Kontoauszüge ab und speichert sie
### Geändert
- PDF-Kontoauszüge-Seite: Neues Layout mit zwei Spalten (FinTS-Abruf links, Upload rechts)
- fints.class.php: Neue Methoden `getStatementPDF()` und `supportsPdfStatements()`
- fints.class.php: Neue Methoden für PDF-Abruf mit automatischer Methodenwahl
### Technisch
- Erweiterung der php-fints Bibliothek um HKEKP-Unterstützung (Segment/EKP/*)
- Neue Action-Klasse mit Pagination-Support für große PDF-Auszüge
- Erweiterung der php-fints Bibliothek:
- HKEKP-Unterstützung (Segment/EKP/*): HKEKPv2, HIEKPv2, HIEKPSv2
- HKKAA-Unterstützung (Segment/KAA/*): HKKAAv2, HIKAAv2, HIKAASv1
- Action-Klassen: GetStatementPDF, GetStatementFromArchive
- Neue Methoden in BankImportFinTS:
- `getPdfStatementMethod()`: Prüft welche Methode die Bank unterstützt
- `getStatementPDFAuto()`: Automatische Methodenwahl
- `supportsArchiveStatements()`: Prüft HKKAA-Support
## [3.1] - 2026-03-05

View file

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

View file

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

View file

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

16
composer.lock generated
View file

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

View file

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

View file

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

View file

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

View file

@ -4,8 +4,8 @@
$issues = array();
if (!(PHP_VERSION_ID >= 80000)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.0.0". You are running ' . PHP_VERSION . '.';
if (!(PHP_VERSION_ID >= 80300)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.3.0". You are running ' . PHP_VERSION . '.';
}
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:
// Logic
'yoda_style' => FALSE, // Allow both Yoda-style and regular comparisons.
'yoda_style' => false, // Allow both Yoda-style and regular comparisons.
// Whitespace
'blank_line_before_statement' => FALSE, // Don't put blank lines before `return` statements.
'concat_space' => FALSE, // Allow spaces around string concatenation operator.
'blank_line_after_opening_tag' => FALSE, // Allow file-level @noinspection suppressions to live on the `<?php` line.
'single_line_throw' => FALSE, // Allow `throw` statements to span multiple lines.
'blank_line_before_statement' => false, // Don't put blank lines before `return` statements.
'concat_space' => false, // Allow spaces around string concatenation operator.
'blank_line_after_opening_tag' => false, // Allow file-level @noinspection suppressions to live on the `<?php` line.
'single_line_throw' => false, // Allow `throw` statements to span multiple lines.
// phpDoc
'phpdoc_align' => FALSE, // Don't add spaces within phpDoc just to make parameter names / descriptions align.
'phpdoc_annotation_without_dot' => FALSE, // Allow terminating dot on @param and such.
'phpdoc_no_alias_tag' => FALSE, // Allow @link in addition to @see.
'phpdoc_separation' => FALSE, // Don't put blank line between @params, @throws and @return.
'phpdoc_summary' => FALSE, // Don't force terminating dot on the first line.
'phpdoc_align' => false, // Don't add spaces within phpDoc just to make parameter names / descriptions align.
'phpdoc_annotation_without_dot' => false, // Allow terminating dot on @param and such.
'phpdoc_no_alias_tag' => false, // Allow @link in addition to @see.
'phpdoc_separation' => false, // Don't put blank line between @params, @throws and @return.
'phpdoc_summary' => false, // Don't force terminating dot on the first line.
])
->setFinder($finder);

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
[![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:

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);
$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'));
$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 */
$fints = require_once 'login.php';
// Just pick the first account, for demonstration purposes. You could also have the user choose, or have SEPAAccount
// hard-coded and not call getSEPAAccounts() at all.
$getSepaAccounts = \Fhp\Action\GetSEPAAccounts::create();
$fints->execute($getSepaAccounts);
if ($getSepaAccounts->needsTan()) {
handleStrongAuthentication($getSepaAccounts); // See login.php for the implementation.
}
$oneAccount = $getSepaAccounts->getAccounts()[0];
$dt = new \DateTime();
$dt->add(new \DateInterval('P1D'));
@ -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());
$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",
"description": "PHP Library for the protocols fints and hbci",
"homepage": "https://github.com/nemiah/phpFinTS",
"version": "3.7.0",
"version": "4.0.0",
"license": "MIT",
"autoload": {
"psr-0": {
@ -11,7 +11,7 @@
}
},
"require": {
"php": ">=8.0",
"php": ">=8.3",
"psr/log": "^1|^2|^3",
"ext-curl": "*",
"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
{
// 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 */
private $account;
/** @var bool */
@ -79,7 +79,7 @@ class GetBalance extends PaginateableAction
{
list(
$parentSerialized,
$this->account, $this->allAccounts
$this->account, $this->allAccounts,
) = $serialized;
is_array($parentSerialized) ?
@ -96,7 +96,6 @@ class GetBalance extends PaginateableAction
return $this->response;
}
/** {@inheritdoc} */
protected function createRequest(BPD $bpd, ?UPD $upd)
{
/** @var BaseSegment $hisals */
@ -115,7 +114,6 @@ class GetBalance extends PaginateableAction
}
}
/** {@inheritdoc} */
public function processResponse(Message $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
{
// 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 */
private $account;
@ -81,7 +81,7 @@ class GetDepotAufstellung extends PaginateableAction
{
list(
$parentSerialized,
$this->account
$this->account,
) = $serialized;
is_array($parentSerialized) ?
@ -111,7 +111,6 @@ class GetDepotAufstellung extends PaginateableAction
return $this->depotWert;
}
/** {@inheritdoc} */
protected function createRequest(BPD $bpd, ?UPD $upd)
{
/** @var HIWPDS $hiwpds */
@ -125,7 +124,6 @@ class GetDepotAufstellung extends PaginateableAction
}
}
/** {@inheritdoc} */
public function processResponse(Message $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;
}
/** {@inheritdoc} */
protected function createRequest(BPD $bpd, ?UPD $upd)
{
/** @var BaseSegment $hispas */
@ -64,7 +63,6 @@ class GetSEPAAccounts extends PaginateableAction
}
}
/** {@inheritdoc} */
public function processResponse(Message $response)
{
parent::processResponse($response);

View file

@ -16,12 +16,11 @@ class GetSEPADirectDebitParameters extends BaseAction
public const SEQUENCE_TYPES = ['FRST', 'OOFF', 'FNAL', 'RCUR'];
public const DIRECT_DEBIT_TYPES = ['CORE', 'COR1', 'B2B'];
// Request (if you add a field here, update __serialize() and __unserialize() as well).
/** @var string */
private $directDebitType;
/** @var string */
private $seqType;
/** @var bool */
private $singleDirectDebit;
@ -43,6 +42,45 @@ class GetSEPADirectDebitParameters extends BaseAction
return $result;
}
/**
* @deprecated Beginning from PHP7.4 __unserialize is used for new generated strings, then this method is only used for previously generated strings - remove after May 2023
*/
public function serialize(): string
{
return serialize($this->__serialize());
}
public function __serialize(): array
{
return [
parent::__serialize(),
$this->directDebitType, $this->seqType, $this->singleDirectDebit,
];
}
/**
* @deprecated Beginning from PHP7.4 __unserialize is used for new generated strings, then this method is only used for previously generated strings - remove after May 2023
*
* @param string $serialized
* @return void
*/
public function unserialize($serialized)
{
self::__unserialize(unserialize($serialized));
}
public function __unserialize(array $serialized): void
{
list(
$parentSerialized,
$this->directDebitType, $this->seqType, $this->singleDirectDebit,
) = $serialized;
is_array($parentSerialized) ?
parent::__unserialize($parentSerialized) :
parent::unserialize($parentSerialized);
}
public static function getHixxesSegmentName(string $directDebitType, bool $singleDirectDebit): string
{
switch ($directDebitType) {
@ -56,7 +94,6 @@ class GetSEPADirectDebitParameters extends BaseAction
}
}
/** {@inheritdoc} */
protected function createRequest(BPD $bpd, ?UPD $upd)
{
$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
{
// 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 */
private $account;
/** @var \DateTime */
@ -93,7 +93,7 @@ class GetStatementOfAccount extends PaginateableAction
{
return [
parent::__serialize(),
$this->account, $this->from, $this->to, $this->allAccounts,
$this->account, $this->from, $this->to, $this->allAccounts, $this->includeUnbooked,
$this->bankName,
];
}
@ -113,8 +113,8 @@ class GetStatementOfAccount extends PaginateableAction
{
list(
$parentSerialized,
$this->account, $this->from, $this->to, $this->allAccounts,
$this->bankName
$this->account, $this->from, $this->to, $this->allAccounts, $this->includeUnbooked,
$this->bankName,
) = $serialized;
is_array($parentSerialized) ?
@ -147,7 +147,6 @@ class GetStatementOfAccount extends PaginateableAction
return $this->statement;
}
/** {@inheritdoc} */
protected function createRequest(BPD $bpd, ?UPD $upd)
{
$this->bankName = $bpd->getBankName();
@ -171,7 +170,6 @@ class GetStatementOfAccount extends PaginateableAction
}
}
/** {@inheritdoc} */
public function processResponse(Message $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
{
// 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 */
private $account;
/** @var \DateTime */
@ -98,7 +98,7 @@ class GetStatementOfAccountXML extends PaginateableAction
{
list(
$parentSerialized,
$this->account, $this->camtURN, $this->from, $this->to, $this->allAccounts
$this->account, $this->camtURN, $this->from, $this->to, $this->allAccounts,
) = $serialized;
is_array($parentSerialized) ?
@ -115,7 +115,6 @@ class GetStatementOfAccountXML extends PaginateableAction
return $this->xml;
}
/** {@inheritdoc} */
protected function createRequest(BPD $bpd, ?UPD $upd)
{
if ($upd === null) {
@ -149,7 +148,6 @@ class GetStatementOfAccountXML extends PaginateableAction
}
}
/** {@inheritdoc} */
public function processResponse(Message $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
{
// Request (if you add a field here, update __serialize() and __unserialize() as well).
/** @var SEPAAccount */
protected $account;
/** @var string */
protected $dtavzData;
/** @var string|null */
protected $dtavzVersion;
@ -36,6 +35,45 @@ class SendInternationalCreditTransfer extends BaseAction
return $result;
}
/**
* @deprecated Beginning from PHP7.4 __unserialize is used for new generated strings, then this method is only used for previously generated strings - remove after May 2023
*/
public function serialize(): string
{
return serialize($this->__serialize());
}
public function __serialize(): array
{
return [
parent::__serialize(),
$this->account, $this->dtavzData, $this->dtavzVersion,
];
}
/**
* @deprecated Beginning from PHP7.4 __unserialize is used for new generated strings, then this method is only used for previously generated strings - remove after May 2023
*
* @param string $serialized
* @return void
*/
public function unserialize($serialized)
{
self::__unserialize(unserialize($serialized));
}
public function __unserialize(array $serialized): void
{
list(
$parentSerialized,
$this->account, $this->dtavzData, $this->dtavzVersion,
) = $serialized;
is_array($parentSerialized) ?
parent::__unserialize($parentSerialized) :
parent::unserialize($parentSerialized);
}
protected function createRequest(BPD $bpd, ?UPD $upd)
{
/** @var HIAUBSv9 $hiaubs */

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

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

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

@ -19,12 +19,17 @@ use Fhp\UnsupportedException;
*/
class SendSEPATransfer extends BaseAction
{
// Request (if you add a field here, update __serialize() and __unserialize() as well).
/** @var SEPAAccount */
private $account;
/** @var string */
private $painMessage;
/** @var string */
private $xmlSchema;
/** @var bool */
private $singleBookingRequested = false;
// There are no result fields. This action is simply marked as done to indicate that the transfer was executed.
/**
* @param SEPAAccount $account The account from which the transfer will be sent.
@ -44,11 +49,62 @@ class SendSEPATransfer extends BaseAction
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)
{
//ANALYSE XML FOR RECEIPTS AND PAYMENT DATE
$xmlAsObject = simplexml_load_string($this->painMessage, "SimpleXMLElement", LIBXML_NOCDATA);
// ANALYSE XML FOR RECEIPTS AND PAYMENT DATE
$xmlAsObject = simplexml_load_string($this->painMessage, 'SimpleXMLElement', LIBXML_NOCDATA);
$numberOfTransactions = $xmlAsObject->CstmrCdtTrfInitn->GrpHdr->NbOfTxs;
$hasReqdExDates = false;
foreach ($xmlAsObject->CstmrCdtTrfInitn?->PmtInf as $pmtInfo) {
@ -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) {
// Terminierte SEPA-Sammelüberweisung (Segment HKCME / Kennung HICMES)
$segmentID = 'HICMES';
$segment = \Fhp\Segment\CME\HKCMEv1::createEmpty();
} elseif ($numberOfTransactions == 1 && $hasReqdExDates) {
// Terminierte SEPA-Überweisung (Segment HKCSE / Kennung HICSES)
$segmentID = 'HICSES';
$segment = \Fhp\Segment\CSE\HKCSEv1::createEmpty();
} elseif ($numberOfTransactions > 1 && !$hasReqdExDates) {
// SEPA-Sammelüberweisungen (Segment HKCCM / Kennung HICSES)
$segmentID = 'HICSES';
$segment = \Fhp\Segment\CCM\HKCCMv1::createEmpty();
} else {
//SEPA Einzelüberweisung (Segment HKCCS / Kennung HICCSS).
// SEPA Einzelüberweisung (Segment HKCCS / Kennung HICCSS).
$segmentID = 'HICCSS';
$segment = \Fhp\Segment\CCS\HKCCSv1::createEmpty();
}
@ -93,7 +145,7 @@ class SendSEPATransfer extends BaseAction
// Sometimes the Bank reports supported schemas with a "_GBIC_X" postfix.
// GIBC_X stands for German Banking Industry Committee and a version counter.
$xmlSchema = $this->xmlSchema;
$matchingSchemas = array_filter($supportedSchemas, function($value) use ($xmlSchema) {
$matchingSchemas = array_filter($supportedSchemas, function ($value) use ($xmlSchema) {
// For example urn:iso:std:iso:20022:tech:xsd:pain.001.001.09 from the xml matches
// urn:iso:std:iso:20022:tech:xsd:pain.001.001.09_GBIC_4
return str_starts_with($value, $xmlSchema);
@ -107,10 +159,19 @@ class SendSEPATransfer extends BaseAction
$segment->kontoverbindungInternational = Kti::fromAccount($this->account);
$segment->sepaDescriptor = $this->xmlSchema;
$segment->sepaPainMessage = new Bin($this->painMessage);
// For batch transfers: set einzelbuchungGewuenscht if bank allows it
if ($numberOfTransactions > 1) {
$paramSegmentId = $hasReqdExDates ? 'HICMES' : 'HICCMS';
$paramSegment = $bpd->getLatestSupportedParameters($paramSegmentId);
if ($paramSegment !== null && $paramSegment->getParameter()->einzelbuchungErlaubt) {
$segment->einzelbuchungGewuenscht = $this->singleBookingRequested;
}
}
return $segment;
}
/** {@inheritdoc} */
public function processResponse(Message $response)
{
parent::processResponse($response);

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

@ -4,13 +4,17 @@
namespace Fhp;
use Fhp\Model\PollingInfo;
use Fhp\Model\TanRequest;
use Fhp\Model\VopConfirmationRequest;
use Fhp\Protocol\ActionIncompleteException;
use Fhp\Protocol\ActionPendingException;
use Fhp\Protocol\BPD;
use Fhp\Protocol\Message;
use Fhp\Protocol\TanRequiredException;
use Fhp\Protocol\UnexpectedResponseException;
use Fhp\Protocol\UPD;
use Fhp\Protocol\VopConfirmationRequiredException;
use Fhp\Segment\BaseSegment;
use Fhp\Segment\HIRMS\Rueckmeldung;
use Fhp\Segment\HIRMS\Rueckmeldungscode;
@ -37,37 +41,37 @@ use Fhp\Segment\HIRMS\Rueckmeldungscode;
abstract class BaseAction implements \Serializable
{
/** @var int[] Stores segment numbers that were assigned to the segments returned from {@link createRequest()}. */
protected $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.
*/
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.
* @var TanRequest|null
*/
protected $tanRequest;
/** If set, the last response from the server regarding this action asked for a TAN from the user. */
protected ?TanRequest $tanRequest = null;
/** @var bool */
protected $isDone = false;
/** If set, this action is currently waiting for a long-running operation on the server to complete. */
protected ?PollingInfo $pollingInfo = null;
/** If set, this action needs the user's confirmation to be completed. */
protected ?VopConfirmationRequest $vopConfirmationRequest = null;
protected bool $isDone = false;
/**
* Will be populated with the message the bank sent along with the success indication, can be used to show to
* the user.
* @var string
*/
public $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
*
* 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
* present yet.
* An action can only be serialized before it was completed.
* 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.
*/
@ -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
* present yet.
* An action can only be serialized before it was completed.
* If a sub-class overrides this, it should call the parent function and include it in its result.
*
* @return array The serialized action, e.g. for storage in a database. This will not contain sensitive user data.
* Note that this is not necessarily valid UTF-8, so you should store it as a BLOB column or raw bytes.
*/
public function __serialize(): array
{
if (!$this->needsTan()) {
throw new \RuntimeException('Cannot serialize this action, because it is not waiting for a TAN.');
if ($this->isDone()) {
throw new \RuntimeException('Completed actions cannot be serialized.');
}
return [
$this->requestSegmentNumbers,
$this->tanRequest,
$this->needTanForSegment,
$this->pollingInfo,
$this->vopConfirmationRequest,
];
}
@ -111,8 +117,10 @@ abstract class BaseAction implements \Serializable
list(
$this->requestSegmentNumbers,
$this->tanRequest,
$this->needTanForSegment
) = $serialized;
$this->needTanForSegment,
$this->pollingInfo,
$this->vopConfirmationRequest,
) = array_pad($serialized, 5, null);
}
/**
@ -144,25 +152,54 @@ abstract class BaseAction implements \Serializable
return $this->tanRequest;
}
public function needsPollingWait(): bool
{
return !$this->isDone() && $this->pollingInfo !== null;
}
public function getPollingInfo(): ?PollingInfo
{
return $this->pollingInfo;
}
public function needsVopConfirmation(): bool
{
return !$this->isDone() && $this->vopConfirmationRequest !== null;
}
public function getVopConfirmationRequest(): ?VopConfirmationRequest
{
return $this->vopConfirmationRequest;
}
/**
* Throws an exception unless this action has been successfully executed, i.e. in the following cases:
* - the action has not been {@link FinTs::execute()}-d at all or the {@link FinTs::execute()} call for it threw an
* exception,
* - the action is awaiting a TAN/confirmation (as per {@link BaseAction::needsTan()}.
* - the action is 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
* for actions that don't have any results (as each result getter would call {@link ensureDone()} internally).
* On the other hand, you do not need to call this function if you make sure that (1) you called
* {@link FinTs::execute()} and (2) you checked {@link needsTan()} and, if it returned true, supplied a TAN by
* calling {@ink FinTs::submitTan()}. Note that both exception types thrown from this method are sub-classes of
* {@link \RuntimeException}, so you shouldn't need a try-catch block at the call site for this.
* {@link FinTs::execute()} and (2) you checked and resolved all other special outcome states documented there.
* Note that both exception types thrown from this method are sub-classes of {@link \RuntimeException}, so you
* shouldn't need a try-catch block at the call site for this.
* @throws ActionIncompleteException If the action hasn't even been executed.
* @throws ActionPendingException If the action is pending a long-running server operation that needs polling.
* @throws VopConfirmationRequiredException If the action requires the user's confirmation for VOP.
* @throws TanRequiredException If the action needs a TAN.
*/
public function ensureDone()
public function ensureDone(): void
{
if ($this->tanRequest !== null) {
throw new TanRequiredException($this->tanRequest);
} elseif ($this->pollingInfo !== null) {
throw new ActionPendingException($this->pollingInfo);
} elseif ($this->vopConfirmationRequest !== null) {
throw new VopConfirmationRequiredException($this->vopConfirmationRequest);
} elseif (!$this->isDone()) {
throw new ActionIncompleteException();
}
@ -231,7 +268,7 @@ abstract class BaseAction implements \Serializable
/** @return int[] */
public function getRequestSegmentNumbers(): array
{
return $this->requestSegmentNumbers;
return $this->requestSegmentNumbers ?? [];
}
/**
@ -248,11 +285,21 @@ abstract class BaseAction implements \Serializable
$this->requestSegmentNumbers = $requestSegmentNumbers;
}
/**
* To be called only by the FinTs instance that executes this action.
*/
final public function setTanRequest(?TanRequest $tanRequest)
/** To be called only by the FinTs instance that executes this action. */
final public function setTanRequest(?TanRequest $tanRequest): void
{
$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
{
/**
* @var string
*/
protected $url;
/**
* @var resource
*/
protected $curlHandle;
/**
* @var int
*/
protected $timeoutConnect = 15;
/**
* @var int
*/
protected $timeoutResponse = 30;
protected string $url;
protected ?\CurlHandle $curlHandle = null;
protected int $timeoutConnect = 15;
protected int $timeoutResponse = 30;
public function __construct(string $url, int $timeoutConnect = 15, int $timeoutResponse = 30)
{
@ -34,9 +19,12 @@ class Connection
$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_VERIFYHOST, 2);
@ -52,7 +40,7 @@ class Connection
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) {
curl_close($this->curlHandle);
@ -76,7 +64,7 @@ class Connection
if (false === $response) {
throw new CurlException(
'Failed connection to ' . $this->url . ': ' . curl_error($this->curlHandle),
'Failed sending to ' . $this->url . ': ' . curl_error($this->curlHandle),
null,
curl_errno($this->curlHandle),
curl_getinfo($this->curlHandle),

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

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

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

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));
if (isset($result[$soaDate])) {
#$result[$soaDate] = ['end_balance' => []];
// $result[$soaDate] = ['end_balance' => []];
$amount = str_replace(',', '.', substr($day[$i], 10, -1));
$cdMark = substr($day[$i], 0, 1);

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';
}
/**
* {@inheritDoc}
*/
public function toHex(): string
{
return $this->getHeaderHex() . implode('', $this->controlBytes) . $this->getDataHex();
}
/**
* {@inheritDoc}
*/
public function getLuhnChecksum(): int
{
$luhn = 0;
@ -89,9 +83,6 @@ class StartCode extends DataElement
return $luhn;
}
/**
* {@inheritDoc}
*/
public function __debugInfo(): ?array
{
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;
/** {@inheritdoc} */
public function getId(): int
{
return self::ID;
}
/** {@inheritdoc} */
public function getName(): string
{
return 'No PSD2/TANs supported';
@ -33,91 +31,76 @@ final class NoPsd2TanMode implements TanMode
return false;
}
/** {@inheritdoc} */
public function isDecoupled(): bool
{
return false;
}
/** {@inheritdoc} */
public function getChallengeLabel(): string
{
return '';
}
/** {@inheritdoc} */
public function getMaxChallengeLength(): int
{
return 0;
}
/** {@inheritdoc} */
public function getMaxTanLength(): int
{
return 0;
}
/** {@inheritdoc} */
public function getTanFormat(): int
{
return 0;
}
/** {@inheritdoc} */
public function needsTanMedium(): bool
{
return false;
}
/** {@inheritdoc} */
public function getSmsAbbuchungskontoErforderlich(): bool
{
return false;
}
/** {@inheritdoc} */
public function getAuftraggeberkontoErforderlich(): bool
{
return false;
}
/** {@inheritdoc} */
public function getChallengeKlasseErforderlich(): bool
{
return false;
}
/** {@inheritdoc} */
public function getAntwortHhdUcErforderlich(): bool
{
return false;
}
/** {@inheritdoc} */
public function getMaxDecoupledChecks(): int
{
throw new \RuntimeException('Only allowed for decoupled TAN modes');
}
/** {@inheritdoc} */
public function getFirstDecoupledCheckDelaySeconds(): int
{
throw new \RuntimeException('Only allowed for decoupled TAN modes');
}
/** {@inheritdoc} */
public function getPeriodicDecoupledCheckDelaySeconds(): int
{
throw new \RuntimeException('Only allowed for decoupled TAN modes');
}
/** {@inheritdoc} */
public function allowsManualConfirmation(): bool
{
throw new \RuntimeException('Only allowed for decoupled TAN modes');
}
/** {@inheritdoc} */
public function allowsAutomatedPolling(): bool
{
throw new \RuntimeException('Only allowed for decoupled TAN modes');

View file

@ -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_DEBIT = 'debit';
/**
* @var array of Transaction
*/
protected $transactions = [];
/**
* @var float
*/
protected $startBalance = 0.0;
/**
* @var float|null
*/
protected $endBalance = null;
/**
* @var string|null
*/
protected $creditDebit = null;
/**
* @var \DateTime|null
*/
protected $date;
/** @var Transaction[] */
protected array $transactions = [];
protected float $startBalance = 0.0;
protected ?float $endBalance = null;
protected ?string $creditDebit = null;
protected ?\DateTime $date = null;
/**
* Get transactions

View file

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

View file

@ -8,86 +8,28 @@ class Transaction
public const CD_CREDIT = 'credit';
public const CD_DEBIT = 'debit';
/**
* @var \DateTime|null
*/
protected $bookingDate;
/**
* @var \DateTime|null
*/
protected $valutaDate;
/**
* @var float
*/
protected $amount;
/**
* @var string
*/
protected $creditDebit;
/**
* @var bool
*/
protected $isStorno;
/**
* @var string
*/
protected $bookingCode;
/**
* @var string
*/
protected $bookingText;
/**
* @var string
*/
protected $description1;
/**
* @var string
*/
protected $description2;
protected ?\DateTime $bookingDate = null;
protected ?\DateTime $valutaDate = null;
protected float $amount;
protected string $creditDebit;
protected bool $isStorno;
protected string $bookingCode;
protected string $bookingText;
protected string $description1;
protected string $description2;
/**
* Array keys are identifiers like "SVWZ" for the main description.
* @var string[]
*/
protected $structuredDescription;
protected array $structuredDescription;
/**
* @var string
*/
protected $bankCode;
/**
* @var string
*/
protected $accountNumber;
/**
* @var string
*/
protected $name;
/**
* @var bool
*/
protected $booked;
/**
* @var int
*/
protected $pn;
/**
* @var int
*/
protected $textKeyAddition;
protected string $bankCode;
protected string $accountNumber;
protected string $name;
protected bool $booked;
protected int $pn;
protected int $textKeyAddition;
/**
* Get booking date.
@ -121,10 +63,9 @@ class Transaction
*
* @return $this
*/
public function setBookingDate(?\DateTime $date = null)
public function setBookingDate(?\DateTime $date = null): static
{
$this->bookingDate = $date;
return $this;
}
@ -133,10 +74,9 @@ class Transaction
*
* @return $this
*/
public function setValutaDate(?\DateTime $date = null)
public function setValutaDate(?\DateTime $date = null): static
{
$this->valutaDate = $date;
return $this;
}
@ -153,10 +93,9 @@ class Transaction
*
* @return $this
*/
public function setBooked(bool $booked)
public function setBooked(bool $booked): static
{
$this->booked = $booked;
return $this;
}
@ -165,10 +104,9 @@ class Transaction
*
* @return $this
*/
public function setAmount(float $amount)
public function setAmount(float $amount): static
{
$this->amount = (float) $amount;
$this->amount = $amount;
return $this;
}
@ -185,10 +123,9 @@ class Transaction
*
* @return $this
*/
public function setCreditDebit(string $creditDebit)
public function setCreditDebit(string $creditDebit): static
{
$this->creditDebit = $creditDebit;
return $this;
}
@ -205,10 +142,9 @@ class Transaction
*
* @return $this
*/
public function setIsStorno(bool $isStorno)
public function setIsStorno(bool $isStorno): static
{
$this->isStorno = $isStorno;
return $this;
}
@ -225,10 +161,9 @@ class Transaction
*
* @return $this
*/
public function setBookingCode(string $bookingCode)
public function setBookingCode(string $bookingCode): static
{
$this->bookingCode = (string) $bookingCode;
$this->bookingCode = $bookingCode;
return $this;
}
@ -245,10 +180,9 @@ class Transaction
*
* @return $this
*/
public function setBookingText(string $bookingText)
public function setBookingText(string $bookingText): static
{
$this->bookingText = (string) $bookingText;
$this->bookingText = $bookingText;
return $this;
}
@ -265,10 +199,9 @@ class Transaction
*
* @return $this
*/
public function setDescription1(string $description1)
public function setDescription1(string $description1): static
{
$this->description1 = (string) $description1;
$this->description1 = $description1;
return $this;
}
@ -285,10 +218,9 @@ class Transaction
*
* @return $this
*/
public function setDescription2(string $description2)
public function setDescription2(string $description2): static
{
$this->description2 = (string) $description2;
$this->description2 = $description2;
return $this;
}
@ -306,10 +238,9 @@ class Transaction
* Set structuredDescription
*
* @param string[] $structuredDescription
*
* @return $this
*/
public function setStructuredDescription(array $structuredDescription)
public function setStructuredDescription(array $structuredDescription): static
{
$this->structuredDescription = $structuredDescription;
@ -353,10 +284,9 @@ class Transaction
*
* @return $this
*/
public function setBankCode(string $bankCode)
public function setBankCode(string $bankCode): static
{
$this->bankCode = (string) $bankCode;
$this->bankCode = $bankCode;
return $this;
}
@ -373,10 +303,9 @@ class Transaction
*
* @return $this
*/
public function setAccountNumber(string $accountNumber)
public function setAccountNumber(string $accountNumber): static
{
$this->accountNumber = (string) $accountNumber;
$this->accountNumber = $accountNumber;
return $this;
}
@ -401,10 +330,9 @@ class Transaction
*
* @return $this
*/
public function setName(string $name)
public function setName(string $name): static
{
$this->name = (string) $name;
$this->name = $name;
return $this;
}
@ -422,7 +350,7 @@ class Transaction
* @param int|mixed $nr Will be parsed to an int.
* @return $this
*/
public function setPN($nr)
public function setPN($nr): static
{
$this->pn = intval($nr);
return $this;
@ -442,7 +370,7 @@ class Transaction
* @param int|mixed $textKeyAddition Will be parsed to an int.
* @return $this
*/
public function setTextKeyAddition($textKeyAddition)
public function setTextKeyAddition($textKeyAddition): static
{
$this->textKeyAddition = intval($textKeyAddition);
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\UnexpectedResponseException;
use Fhp\Protocol\UPD;
use Fhp\Segment\BaseSegment;
use Fhp\Segment\HIRMS\Rueckmeldungscode;
use Fhp\Segment\Paginateable;
@ -19,23 +18,20 @@ use Fhp\Segment\Paginateable;
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
* 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
* 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
*
* {@inheritdoc}
*/
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
*
* {@inheritdoc}
*/
public function unserialize($serialized)
{
@ -82,10 +76,9 @@ abstract class PaginateableAction extends BaseAction
return !$this->isDone() && $this->paginationToken !== null;
}
/** {@inheritdoc} */
public function processResponse(Message $response)
{
if (($pagination = $response->findRueckmeldung(Rueckmeldungscode::PAGINATION)) !== null) {
if (($pagination = $response->findRueckmeldung(Rueckmeldungscode::AUFSETZPUNKT)) !== null) {
if (count($pagination->rueckmeldungsparameter) !== 1) {
throw new UnexpectedResponseException("Unexpected pagination request: $pagination");
}
@ -97,7 +90,6 @@ abstract class PaginateableAction extends BaseAction
}
}
/** {@inheritdoc} */
public function getNextRequest(?BPD $bpd, ?UPD $upd)
{
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\SegmentInterface;
use Fhp\Segment\TAN\HITANS;
use Fhp\Segment\VPP\HIVPPSv1;
/**
* Segmentfolge: Bankparameterdaten (Version 3)
@ -152,6 +153,28 @@ class BPD
return null;
}
/**
* @param SegmentInterface[] $requestSegments The segments that shall be sent to the bank.
* @return string|null Identifier of the (first) segment that requires Verification of Payee according to HIPINS, or
* null if none of the segments require verification.
*/
public function vopRequiredForRequest(array $requestSegments): ?string
{
/** @var HIVPPSv1 $hivpps */
$hivpps = $this->getLatestSupportedParameters('HIVPPS');
$vopRequiredTypes = $hivpps?->parameter?->vopPflichtigerZahlungsverkehrsauftrag;
if ($vopRequiredTypes === null) {
return null;
}
foreach ($requestSegments as $segment) {
if (in_array($segment->getName(), $vopRequiredTypes)) {
return $segment->getName();
}
}
return null;
}
/**
* @return bool Whether the BPD indicates that the bank supports PSD2.
*/

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->kundensystemId,
$this->messageNumber,
$this->dialogId
$this->dialogId,
) = $serialized;
is_array($parentSerialized) ?
@ -151,7 +151,6 @@ class DialogInitialization extends BaseAction
parent::unserialize($parentSerialized);
}
/** {@inheritdoc} */
protected function createRequest(BPD $bpd, ?UPD $upd)
{
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 */
private $tanMedia;
/** {@inheritdoc} */
protected function createRequest(BPD $bpd, ?UPD $upd)
{
// Prepare the HKTAB request.
@ -32,7 +31,6 @@ class GetTanMedia extends BaseAction
}
}
/** {@inheritdoc} */
public function processResponse(Message $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 $requestSegmentNumber If set, only consider Rueckmeldungen that pertain to this request segment.
* @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) {
if ($segment instanceof RueckmeldungContainer) {
if (
$segment instanceof RueckmeldungContainer && (
$requestSegmentNumber === null || $segment->segmentkopf->bezugselement === $requestSegmentNumber
)
) {
$rueckmeldung = $segment->findRueckmeldung($code);
if ($rueckmeldung !== null) {
return $rueckmeldung;
@ -217,16 +222,29 @@ class Message
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.
*/
public function serialize(): string
{
$result = '';
foreach ($this->wrapperSegments as $segment) {
$result .= Serializer::serializeSegment($segment);
}
return $result;
return Serializer::serializeSegments($this->wrapperSegments);
}
/**
@ -300,14 +318,16 @@ class Message
$segments = Parser::parseSegments($rawMessage);
// 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->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.
// 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.
* @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) {
$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
{
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);
}

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