- HKEKA v3/v4/v5 Segmente fuer phpFinTS implementiert (VR Bank unterstuetzt kein HKEKP) - GetElectronicStatement Action mit Base64-Erkennung und Quittungscode - PDF-Deduplizierung per MD5 (Bank sendet identische Saldenmitteilungen) - Saldenmitteilungen ohne Auszugsnummer werden uebersprungen - Datums-Validierung: 30.02. (Bank-Konvention) wird auf 28.02. korrigiert - Numerische Sortierung fuer statement_number (CAST statt String-Sort) - Jahr-Filter: statement_year=0 ausgeschlossen - Menue/Button: "Kontoauszuege" -> "Umsaetze" (statements.php zeigt MT940, nicht PDFs) - Redirect nach FinTS-Abruf auf aktuelles Jahr statt year=0 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1273 lines
38 KiB
PHP
Executable file
1273 lines
38 KiB
PHP
Executable file
<?php
|
|
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*/
|
|
|
|
/**
|
|
* \file bankimport/class/fints.class.php
|
|
* \ingroup bankimport
|
|
* \brief Class for FinTS/HBCI bank connection
|
|
*/
|
|
|
|
// Load Dolibarr's PSR classes first to avoid version conflicts
|
|
// Dolibarr includes psr/log 1.x which must be loaded before phpFinTS
|
|
if (defined('DOL_DOCUMENT_ROOT')) {
|
|
$dolibarrPsrLog = DOL_DOCUMENT_ROOT.'/includes/sabre/psr/log/Psr/Log/LoggerInterface.php';
|
|
if (file_exists($dolibarrPsrLog) && !interface_exists('Psr\Log\LoggerInterface', false)) {
|
|
require_once DOL_DOCUMENT_ROOT.'/includes/sabre/psr/log/Psr/Log/LoggerInterface.php';
|
|
require_once DOL_DOCUMENT_ROOT.'/includes/sabre/psr/log/Psr/Log/LoggerTrait.php';
|
|
require_once DOL_DOCUMENT_ROOT.'/includes/sabre/psr/log/Psr/Log/AbstractLogger.php';
|
|
require_once DOL_DOCUMENT_ROOT.'/includes/sabre/psr/log/Psr/Log/NullLogger.php';
|
|
require_once DOL_DOCUMENT_ROOT.'/includes/sabre/psr/log/Psr/Log/LogLevel.php';
|
|
}
|
|
}
|
|
|
|
// Autoload phpFinTS library if available
|
|
$composerAutoload = dirname(__DIR__).'/vendor/autoload.php';
|
|
if (file_exists($composerAutoload)) {
|
|
require_once $composerAutoload;
|
|
}
|
|
|
|
use Fhp\FinTs;
|
|
use Fhp\Options\FinTsOptions;
|
|
use Fhp\Options\Credentials;
|
|
use Fhp\Action\GetSEPAAccounts;
|
|
use Fhp\Action\GetStatementOfAccount;
|
|
use Fhp\Action\GetStatementOfAccountXML;
|
|
use Fhp\Action\GetBankStatement;
|
|
use Fhp\Action\GetElectronicStatement;
|
|
use Fhp\Model\StatementOfAccount\Statement;
|
|
use Fhp\Model\StatementOfAccount\Transaction;
|
|
|
|
/**
|
|
* Class BankImportFinTS
|
|
* Handles FinTS/HBCI bank connections for retrieving account statements
|
|
*/
|
|
class BankImportFinTS
|
|
{
|
|
/**
|
|
* @var DoliDB Database handler
|
|
*/
|
|
public $db;
|
|
|
|
/**
|
|
* @var string Error message
|
|
*/
|
|
public $error = '';
|
|
|
|
/**
|
|
* @var array Error messages
|
|
*/
|
|
public $errors = array();
|
|
|
|
/**
|
|
* @var string FinTS Server URL
|
|
*/
|
|
private $fintsUrl;
|
|
|
|
/**
|
|
* @var string BLZ (Bankleitzahl)
|
|
*/
|
|
private $blz;
|
|
|
|
/**
|
|
* @var string Username
|
|
*/
|
|
private $username;
|
|
|
|
/**
|
|
* @var string PIN (decrypted)
|
|
*/
|
|
private $pin;
|
|
|
|
/**
|
|
* @var string IBAN
|
|
*/
|
|
private $iban;
|
|
|
|
/**
|
|
* @var string FinTS Product ID
|
|
*/
|
|
private $productId;
|
|
|
|
/**
|
|
* @var FinTs|null FinTS instance
|
|
*/
|
|
private $fints = null;
|
|
|
|
/**
|
|
* @var array Available TAN modes
|
|
*/
|
|
public $tanModes = array();
|
|
|
|
/**
|
|
* @var mixed Selected TAN mode
|
|
*/
|
|
public $selectedTanMode = null;
|
|
|
|
/**
|
|
* @var string TAN challenge (for display to user)
|
|
*/
|
|
public $tanChallenge = '';
|
|
|
|
/**
|
|
* @var mixed Current action requiring TAN
|
|
*/
|
|
private $pendingAction = null;
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param DoliDB $db Database handler
|
|
*/
|
|
public function __construct($db)
|
|
{
|
|
global $conf;
|
|
|
|
$this->db = $db;
|
|
|
|
// Load configuration
|
|
$this->fintsUrl = getDolGlobalString('BANKIMPORT_FINTS_URL');
|
|
$this->blz = getDolGlobalString('BANKIMPORT_FINTS_BLZ');
|
|
$this->username = getDolGlobalString('BANKIMPORT_FINTS_USERNAME');
|
|
|
|
// Decrypt PIN
|
|
$encryptedPin = getDolGlobalString('BANKIMPORT_FINTS_PIN');
|
|
if (!empty($encryptedPin)) {
|
|
$this->pin = dolDecrypt($encryptedPin);
|
|
}
|
|
|
|
$this->iban = getDolGlobalString('BANKIMPORT_FINTS_IBAN');
|
|
$this->productId = getDolGlobalString('BANKIMPORT_FINTS_PRODUCT_ID');
|
|
|
|
// Default product ID if not set
|
|
// Use Hibiscus/Jameica registered product ID
|
|
if (empty($this->productId)) {
|
|
// Official Hibiscus product ID (registered with Deutsche Kreditwirtschaft)
|
|
$this->productId = '36792786FA12F235F04647689';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if configuration is complete
|
|
*
|
|
* @return bool True if all required fields are set
|
|
*/
|
|
public function isConfigured()
|
|
{
|
|
return !empty($this->fintsUrl)
|
|
&& !empty($this->blz)
|
|
&& !empty($this->username)
|
|
&& !empty($this->pin)
|
|
&& !empty($this->iban);
|
|
}
|
|
|
|
/**
|
|
* Check if phpFinTS library is available
|
|
*
|
|
* @return bool True if library is loaded
|
|
*/
|
|
public function isLibraryAvailable()
|
|
{
|
|
return class_exists('Fhp\FinTs');
|
|
}
|
|
|
|
/**
|
|
* Initialize FinTS connection
|
|
*
|
|
* @return int 1 if success, -1 if error
|
|
*/
|
|
public function initConnection()
|
|
{
|
|
if (!$this->isConfigured()) {
|
|
$this->error = 'Configuration incomplete';
|
|
return -1;
|
|
}
|
|
|
|
if (!$this->isLibraryAvailable()) {
|
|
$this->error = 'phpFinTS library not found. Run: composer install';
|
|
return -1;
|
|
}
|
|
|
|
try {
|
|
$options = new FinTsOptions();
|
|
$options->url = $this->fintsUrl;
|
|
$options->bankCode = $this->blz;
|
|
$options->productName = $this->productId;
|
|
$options->productVersion = '1.0';
|
|
|
|
$credentials = Credentials::create($this->username, $this->pin);
|
|
|
|
$this->fints = FinTs::new($options, $credentials);
|
|
|
|
return 1;
|
|
} catch (Exception $e) {
|
|
$this->error = $e->getMessage();
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test the FinTS connection
|
|
*
|
|
* @return int 1 if success, -1 if error
|
|
*/
|
|
public function testConnection()
|
|
{
|
|
if (!$this->isConfigured()) {
|
|
$this->error = 'Configuration incomplete';
|
|
return -1;
|
|
}
|
|
|
|
// Check URL format
|
|
if (!filter_var($this->fintsUrl, FILTER_VALIDATE_URL)) {
|
|
$this->error = 'Invalid FinTS URL format';
|
|
return -1;
|
|
}
|
|
|
|
// Check BLZ format (8 digits)
|
|
if (!preg_match('/^\d{8}$/', $this->blz)) {
|
|
$this->error = 'Invalid BLZ format (must be 8 digits)';
|
|
return -1;
|
|
}
|
|
|
|
// Check if library is available
|
|
if (!$this->isLibraryAvailable()) {
|
|
$this->error = 'phpFinTS library not installed. Run: cd '.dirname(__DIR__).' && composer install';
|
|
return -1;
|
|
}
|
|
|
|
// Try to initialize connection
|
|
$result = $this->initConnection();
|
|
if ($result < 0) {
|
|
return -1;
|
|
}
|
|
|
|
try {
|
|
// Try to get TAN modes (this tests the connection)
|
|
$this->tanModes = $this->fints->getTanModes();
|
|
return 1;
|
|
} catch (Exception $e) {
|
|
$this->error = $e->getMessage();
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get available TAN modes
|
|
*
|
|
* @return array Array of TAN modes
|
|
*/
|
|
public function getTanModes()
|
|
{
|
|
if ($this->fints === null) {
|
|
$result = $this->initConnection();
|
|
if ($result < 0) {
|
|
return array();
|
|
}
|
|
}
|
|
|
|
try {
|
|
$modes = $this->fints->getTanModes();
|
|
$result = array();
|
|
foreach ($modes as $mode) {
|
|
$result[] = array(
|
|
'id' => $mode->getId(),
|
|
'name' => $mode->getName(),
|
|
'isDecoupled' => $mode->isDecoupled(),
|
|
);
|
|
}
|
|
return $result;
|
|
} catch (Exception $e) {
|
|
$this->error = $e->getMessage();
|
|
return array();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Login with selected TAN mode
|
|
*
|
|
* @param int $tanModeId TAN mode ID to use
|
|
* @return int 1 if success, 0 if TAN required, -1 if error
|
|
*/
|
|
public function login($tanModeId = null)
|
|
{
|
|
dol_syslog("BankImport FinTS: login() gestartet, tanModeId=".($tanModeId ?: 'auto'), LOG_DEBUG);
|
|
|
|
if ($this->fints === null) {
|
|
dol_syslog("BankImport FinTS: Keine bestehende Verbindung, rufe initConnection() auf", LOG_DEBUG);
|
|
$result = $this->initConnection();
|
|
dol_syslog("BankImport FinTS: initConnection() Ergebnis=".$result, LOG_DEBUG);
|
|
if ($result < 0) {
|
|
dol_syslog("BankImport FinTS: initConnection() FEHLGESCHLAGEN: ".$this->error, LOG_ERR);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
try {
|
|
// Get and select TAN mode
|
|
dol_syslog("BankImport FinTS: Rufe getTanModes() ab...", LOG_DEBUG);
|
|
$tanModes = $this->fints->getTanModes();
|
|
dol_syslog("BankImport FinTS: ".count($tanModes)." TAN-Modi verfuegbar", LOG_DEBUG);
|
|
|
|
foreach ($tanModes as $mode) {
|
|
dol_syslog("BankImport FinTS: TAN-Modus: ID=".$mode->getId()." Name=".$mode->getName()
|
|
." Decoupled=".($mode->isDecoupled() ? 'JA' : 'NEIN'), LOG_DEBUG);
|
|
}
|
|
|
|
if ($tanModeId !== null) {
|
|
foreach ($tanModes as $mode) {
|
|
if ($mode->getId() == $tanModeId) {
|
|
$this->selectedTanMode = $mode;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
// Auto-select decoupled mode (SecureGo Plus) if available
|
|
foreach ($tanModes as $mode) {
|
|
if ($mode->isDecoupled()) {
|
|
$this->selectedTanMode = $mode;
|
|
break;
|
|
}
|
|
}
|
|
// Fallback to first mode
|
|
if ($this->selectedTanMode === null && !empty($tanModes)) {
|
|
$this->selectedTanMode = $tanModes[0];
|
|
}
|
|
}
|
|
|
|
if ($this->selectedTanMode === null) {
|
|
$this->error = 'No TAN mode available';
|
|
dol_syslog("BankImport FinTS: KEIN TAN-Modus ausgewaehlt", LOG_ERR);
|
|
return -1;
|
|
}
|
|
|
|
dol_syslog("BankImport FinTS: Ausgewaehlter TAN-Modus: ID=".$this->selectedTanMode->getId()
|
|
." Name=".$this->selectedTanMode->getName()
|
|
." Decoupled=".($this->selectedTanMode->isDecoupled() ? 'JA' : 'NEIN'), LOG_DEBUG);
|
|
|
|
$this->fints->selectTanMode($this->selectedTanMode);
|
|
dol_syslog("BankImport FinTS: TAN-Modus gesetzt, starte fints->login()...", LOG_DEBUG);
|
|
|
|
// Perform login
|
|
$login = $this->fints->login();
|
|
dol_syslog("BankImport FinTS: fints->login() abgeschlossen, needsTan=".($login->needsTan() ? 'JA' : 'NEIN'), LOG_DEBUG);
|
|
|
|
if ($login->needsTan()) {
|
|
$this->pendingAction = $login;
|
|
$tanRequest = $login->getTanRequest();
|
|
$this->tanChallenge = $tanRequest->getChallenge();
|
|
dol_syslog("BankImport FinTS: TAN benoetigt, Challenge-Laenge=".strlen($this->tanChallenge), LOG_DEBUG);
|
|
return 0; // TAN required
|
|
}
|
|
|
|
dol_syslog("BankImport FinTS: Login erfolgreich (kein TAN benoetigt)", LOG_DEBUG);
|
|
return 1;
|
|
} catch (Exception $e) {
|
|
$this->error = $e->getMessage();
|
|
dol_syslog("BankImport FinTS: Login EXCEPTION: ".$e->getMessage()."\n".$e->getTraceAsString(), LOG_ERR);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get pending action for serialization
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function getPendingAction()
|
|
{
|
|
return $this->pendingAction;
|
|
}
|
|
|
|
/**
|
|
* Set pending action after deserialization
|
|
*
|
|
* @param mixed $action The pending action
|
|
* @return void
|
|
*/
|
|
public function setPendingAction($action)
|
|
{
|
|
$this->pendingAction = $action;
|
|
}
|
|
|
|
/**
|
|
* Check if decoupled TAN (SecureGo Plus) has been confirmed
|
|
*
|
|
* @return int 1 if confirmed, 0 if still waiting, -1 if error
|
|
*/
|
|
public function checkDecoupledTan()
|
|
{
|
|
if ($this->fints === null) {
|
|
$this->error = 'FinTS instance is null';
|
|
dol_syslog("BankImport: checkDecoupledTan - FinTS instance is null", LOG_ERR);
|
|
return -1;
|
|
}
|
|
|
|
if ($this->pendingAction === null) {
|
|
$this->error = 'Pending action is null';
|
|
dol_syslog("BankImport: checkDecoupledTan - Pending action is null", LOG_ERR);
|
|
return -1;
|
|
}
|
|
|
|
dol_syslog("BankImport: checkDecoupledTan - Checking TAN status, action type: ".get_class($this->pendingAction), LOG_DEBUG);
|
|
|
|
try {
|
|
$done = $this->fints->checkDecoupledSubmission($this->pendingAction);
|
|
if ($done) {
|
|
dol_syslog("BankImport: checkDecoupledTan - TAN confirmed!", LOG_DEBUG);
|
|
$this->pendingAction = null;
|
|
return 1;
|
|
}
|
|
dol_syslog("BankImport: checkDecoupledTan - Still waiting for TAN", LOG_DEBUG);
|
|
return 0;
|
|
} catch (Exception $e) {
|
|
$this->error = $e->getMessage();
|
|
dol_syslog("BankImport: checkDecoupledTan - Exception: ".$e->getMessage(), LOG_ERR);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get bank supported parameters (for diagnostics)
|
|
*
|
|
* @return array Array of supported parameter segments
|
|
*/
|
|
public function getBankParameters()
|
|
{
|
|
dol_syslog("BankImport FinTS: getBankParameters() aufgerufen", LOG_DEBUG);
|
|
|
|
if ($this->fints === null) {
|
|
dol_syslog("BankImport FinTS: getBankParameters() - fints===null", LOG_WARNING);
|
|
return array('error' => 'Not connected');
|
|
}
|
|
|
|
try {
|
|
// Use reflection to access protected BPD
|
|
$reflection = new \ReflectionClass($this->fints);
|
|
$bpdProperty = $reflection->getProperty('bpd');
|
|
$bpdProperty->setAccessible(true);
|
|
$bpd = $bpdProperty->getValue($this->fints);
|
|
|
|
if ($bpd === null) {
|
|
dol_syslog("BankImport FinTS: getBankParameters() - BPD ist null (noch kein Login?)", LOG_WARNING);
|
|
return array('error' => 'BPD not available');
|
|
}
|
|
|
|
dol_syslog("BankImport FinTS: BPD-Klasse: ".get_class($bpd), LOG_DEBUG);
|
|
|
|
// Get parameters property from BPD
|
|
$bpdReflection = new \ReflectionClass($bpd);
|
|
$paramsProperty = $bpdReflection->getProperty('parameters');
|
|
$paramsProperty->setAccessible(true);
|
|
$parameters = $paramsProperty->getValue($bpd);
|
|
|
|
dol_syslog("BankImport FinTS: BPD enthaelt ".count($parameters)." Segment-Typen", LOG_DEBUG);
|
|
|
|
$result = array();
|
|
foreach ($parameters as $type => $versions) {
|
|
$result[$type] = array_keys($versions);
|
|
}
|
|
|
|
dol_syslog("BankImport FinTS: BPD-Segmente: ".implode(', ', array_keys($result)), LOG_DEBUG);
|
|
|
|
return $result;
|
|
} catch (Exception $e) {
|
|
dol_syslog("BankImport FinTS: getBankParameters() Exception: ".$e->getMessage(), LOG_ERR);
|
|
return array('error' => $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Submit TAN for pending action
|
|
*
|
|
* @param string $tan The TAN entered by user
|
|
* @return int 1 if success, -1 if error
|
|
*/
|
|
public function submitTan($tan)
|
|
{
|
|
if ($this->fints === null || $this->pendingAction === null) {
|
|
$this->error = 'No pending action';
|
|
return -1;
|
|
}
|
|
|
|
try {
|
|
$this->fints->submitTan($this->pendingAction, $tan);
|
|
$this->pendingAction = null;
|
|
return 1;
|
|
} catch (Exception $e) {
|
|
$this->error = $e->getMessage();
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get SEPA accounts
|
|
*
|
|
* @return array|int Array of accounts or -1 on error
|
|
*/
|
|
public function getAccounts()
|
|
{
|
|
if ($this->fints === null) {
|
|
$this->error = 'Not connected';
|
|
return -1;
|
|
}
|
|
|
|
try {
|
|
$getAccounts = GetSEPAAccounts::create();
|
|
$this->fints->execute($getAccounts);
|
|
|
|
if ($getAccounts->needsTan()) {
|
|
$this->pendingAction = $getAccounts;
|
|
$tanRequest = $getAccounts->getTanRequest();
|
|
$this->tanChallenge = $tanRequest->getChallenge();
|
|
return 0; // TAN required
|
|
}
|
|
|
|
$accounts = $getAccounts->getAccounts();
|
|
$result = array();
|
|
|
|
foreach ($accounts as $account) {
|
|
$result[] = array(
|
|
'iban' => $account->getIban(),
|
|
'bic' => $account->getBic(),
|
|
'accountNumber' => $account->getAccountNumber(),
|
|
'blz' => $account->getBlz(),
|
|
);
|
|
}
|
|
|
|
return $result;
|
|
} catch (Exception $e) {
|
|
$this->error = $e->getMessage();
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Maximum days per fetch request (banks often limit this)
|
|
*/
|
|
const MAX_DAYS_PER_FETCH = 30;
|
|
|
|
/**
|
|
* Fetch account statements
|
|
*
|
|
* @param int $dateFrom Start date (timestamp)
|
|
* @param int $dateTo End date (timestamp)
|
|
* @return array|int Array of transactions or -1 on error, 0 if TAN required
|
|
*/
|
|
public function fetchStatements($dateFrom = 0, $dateTo = 0)
|
|
{
|
|
if (!$this->isConfigured()) {
|
|
$this->error = 'Configuration incomplete';
|
|
return -1;
|
|
}
|
|
|
|
if ($this->fints === null) {
|
|
$this->error = 'Not connected. Call login() first.';
|
|
return -1;
|
|
}
|
|
|
|
// Default: last 30 days
|
|
if (empty($dateFrom)) {
|
|
$dateFrom = strtotime('-30 days');
|
|
}
|
|
if (empty($dateTo)) {
|
|
$dateTo = time();
|
|
}
|
|
|
|
// Check if date range exceeds limit - if so, fetch in chunks
|
|
$daysDiff = ($dateTo - $dateFrom) / 86400;
|
|
if ($daysDiff > self::MAX_DAYS_PER_FETCH) {
|
|
dol_syslog("BankImport: Date range {$daysDiff} days exceeds limit, fetching in chunks", LOG_DEBUG);
|
|
return $this->fetchStatementsInChunks($dateFrom, $dateTo);
|
|
}
|
|
|
|
return $this->fetchStatementsForPeriod($dateFrom, $dateTo);
|
|
}
|
|
|
|
/**
|
|
* Fetch statements in chunks for long date ranges
|
|
*
|
|
* @param int $dateFrom Start date (timestamp)
|
|
* @param int $dateTo End date (timestamp)
|
|
* @return array|int Array of transactions or -1 on error, 0 if TAN required
|
|
*/
|
|
protected function fetchStatementsInChunks($dateFrom, $dateTo)
|
|
{
|
|
$allTransactions = array();
|
|
$lastBalance = array();
|
|
$chunkDays = self::MAX_DAYS_PER_FETCH;
|
|
$oldestDateReached = null;
|
|
|
|
// Start from the most recent date and go backwards
|
|
// This ensures we get the newest transactions even if old ones aren't available
|
|
$currentTo = $dateTo;
|
|
|
|
while ($currentTo > $dateFrom) {
|
|
$currentFrom = max($currentTo - ($chunkDays * 86400), $dateFrom);
|
|
|
|
dol_syslog("BankImport: Fetching chunk from ".date('Y-m-d', $currentFrom)." to ".date('Y-m-d', $currentTo), LOG_DEBUG);
|
|
|
|
$result = $this->fetchStatementsForPeriod($currentFrom, $currentTo);
|
|
|
|
if ($result === 0) {
|
|
// TAN required - save progress and return
|
|
$_SESSION['fints_chunk_progress'] = array(
|
|
'transactions' => $allTransactions,
|
|
'currentTo' => $currentFrom,
|
|
'dateFrom' => $dateFrom
|
|
);
|
|
return 0;
|
|
}
|
|
|
|
if ($result < 0) {
|
|
// Error fetching this chunk
|
|
// If we already have transactions from newer chunks, return those with a note
|
|
if (!empty($allTransactions)) {
|
|
dol_syslog("BankImport: Chunk failed for older dates, returning ".count($allTransactions)." transactions from recent period", LOG_WARNING);
|
|
return array(
|
|
'transactions' => $allTransactions,
|
|
'balance' => $lastBalance,
|
|
'partial' => true,
|
|
'oldestDate' => $oldestDateReached,
|
|
'info' => 'Ältere Daten (vor '.date('d.m.Y', $currentTo).') sind bei der Bank nicht mehr verfügbar.'
|
|
);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
if (is_array($result)) {
|
|
$newTransactions = $result['transactions'] ?? array();
|
|
$allTransactions = array_merge($allTransactions, $newTransactions);
|
|
|
|
// Track oldest date we successfully fetched
|
|
foreach ($newTransactions as $tx) {
|
|
if ($oldestDateReached === null || $tx['date'] < $oldestDateReached) {
|
|
$oldestDateReached = $tx['date'];
|
|
}
|
|
}
|
|
|
|
if (empty($lastBalance) && !empty($result['balance'])) {
|
|
$lastBalance = $result['balance'];
|
|
}
|
|
}
|
|
|
|
$currentTo = $currentFrom - 86400; // Previous day
|
|
}
|
|
|
|
// Remove duplicates based on transaction ID
|
|
$uniqueTransactions = array();
|
|
$seenIds = array();
|
|
foreach ($allTransactions as $tx) {
|
|
if (!isset($seenIds[$tx['id']])) {
|
|
$uniqueTransactions[] = $tx;
|
|
$seenIds[$tx['id']] = true;
|
|
}
|
|
}
|
|
|
|
// Sort by date descending (newest first)
|
|
usort($uniqueTransactions, function($a, $b) {
|
|
return $b['date'] - $a['date'];
|
|
});
|
|
|
|
dol_syslog("BankImport: Total fetched: ".count($uniqueTransactions)." unique transactions", LOG_DEBUG);
|
|
|
|
return array(
|
|
'transactions' => $uniqueTransactions,
|
|
'balance' => $lastBalance
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Fetch statements for a single period (internal)
|
|
*
|
|
* @param int $dateFrom Start date (timestamp)
|
|
* @param int $dateTo End date (timestamp)
|
|
* @return array|int Array of transactions or -1 on error, 0 if TAN required
|
|
*/
|
|
protected function fetchStatementsForPeriod($dateFrom, $dateTo)
|
|
{
|
|
try {
|
|
// Get accounts first
|
|
$getAccounts = GetSEPAAccounts::create();
|
|
$this->fints->execute($getAccounts);
|
|
|
|
if ($getAccounts->needsTan()) {
|
|
$this->pendingAction = $getAccounts;
|
|
$tanRequest = $getAccounts->getTanRequest();
|
|
$this->tanChallenge = $tanRequest->getChallenge();
|
|
return 0;
|
|
}
|
|
|
|
$accounts = $getAccounts->getAccounts();
|
|
|
|
// Debug: Log all available accounts
|
|
dol_syslog("BankImport: Looking for IBAN: ".$this->iban, LOG_DEBUG);
|
|
foreach ($accounts as $idx => $acc) {
|
|
dol_syslog("BankImport: Available account ".$idx.": IBAN=".$acc->getIban(), LOG_DEBUG);
|
|
}
|
|
|
|
// Find matching account by IBAN
|
|
$selectedAccount = null;
|
|
foreach ($accounts as $account) {
|
|
if ($account->getIban() === $this->iban) {
|
|
$selectedAccount = $account;
|
|
dol_syslog("BankImport: Selected account by IBAN match: ".$account->getIban(), LOG_DEBUG);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($selectedAccount === null) {
|
|
// Fallback: use first account
|
|
if (!empty($accounts)) {
|
|
$selectedAccount = $accounts[0];
|
|
dol_syslog("BankImport: WARNING - No IBAN match found! Using first account: ".$selectedAccount->getIban(), LOG_WARNING);
|
|
} else {
|
|
$this->error = 'No accounts found';
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
// Fetch statements
|
|
$from = new DateTime();
|
|
$from->setTimestamp($dateFrom);
|
|
$to = new DateTime();
|
|
$to->setTimestamp($dateTo);
|
|
|
|
$transactions = array();
|
|
|
|
// Log what we're trying
|
|
dol_syslog("BankImport: Fetching statements from ".$from->format('Y-m-d')." to ".$to->format('Y-m-d'), LOG_DEBUG);
|
|
|
|
// Try GetStatementOfAccount first (MT940/HKKAZ - most widely supported)
|
|
$mt940Success = false;
|
|
try {
|
|
dol_syslog("BankImport: Trying GetStatementOfAccount (MT940/HKKAZ)", LOG_DEBUG);
|
|
$getStatement = GetStatementOfAccount::create($selectedAccount, $from, $to);
|
|
$this->fints->execute($getStatement);
|
|
|
|
if ($getStatement->needsTan()) {
|
|
$this->pendingAction = $getStatement;
|
|
$tanRequest = $getStatement->getTanRequest();
|
|
$this->tanChallenge = $tanRequest->getChallenge();
|
|
return 0;
|
|
}
|
|
|
|
// Process MT940 results
|
|
$soa = $getStatement->getStatement();
|
|
foreach ($soa->getStatements() as $statement) {
|
|
foreach ($statement->getTransactions() as $tx) {
|
|
$sign = ($tx->getCreditDebit() === Transaction::CD_DEBIT) ? -1 : 1;
|
|
|
|
$transactions[] = array(
|
|
'id' => md5($tx->getValutaDate()->format('Y-m-d').$tx->getAmount().$tx->getName().$tx->getMainDescription()),
|
|
'date' => $tx->getValutaDate()->getTimestamp(),
|
|
'bookingDate' => $tx->getBookingDate() ? $tx->getBookingDate()->getTimestamp() : null,
|
|
'amount' => $sign * $tx->getAmount(),
|
|
'currency' => 'EUR',
|
|
'name' => $tx->getName(),
|
|
'iban' => $tx->getAccountNumber(),
|
|
'bic' => $tx->getBankCode(),
|
|
'reference' => $tx->getMainDescription(),
|
|
'bookingText' => $tx->getBookingText(),
|
|
'endToEndId' => $tx->getEndToEndID(),
|
|
);
|
|
}
|
|
}
|
|
dol_syslog("BankImport: MT940 successful, got ".count($transactions)." transactions", LOG_DEBUG);
|
|
|
|
// Return MT940 results
|
|
return array(
|
|
'transactions' => $transactions,
|
|
'balance' => array()
|
|
);
|
|
} catch (Exception $e) {
|
|
dol_syslog("BankImport: MT940 failed: ".$e->getMessage(), LOG_DEBUG);
|
|
|
|
// MT940 failed, try CAMT/XML
|
|
dol_syslog("BankImport: Trying GetStatementOfAccountXML (CAMT)", LOG_DEBUG);
|
|
try {
|
|
$getStatementXML = GetStatementOfAccountXML::create($selectedAccount, $from, $to);
|
|
$this->fints->execute($getStatementXML);
|
|
|
|
if ($getStatementXML->needsTan()) {
|
|
$this->pendingAction = $getStatementXML;
|
|
$tanRequest = $getStatementXML->getTanRequest();
|
|
$this->tanChallenge = $tanRequest->getChallenge();
|
|
return 0;
|
|
}
|
|
|
|
// Process CAMT XML results
|
|
$xmlStatements = $getStatementXML->getBookedXML();
|
|
|
|
// Debug: Log raw XML count
|
|
dol_syslog("BankImport: Got ".count($xmlStatements)." XML documents", LOG_DEBUG);
|
|
|
|
// Balance info from XML
|
|
$balanceInfo = array();
|
|
|
|
foreach ($xmlStatements as $idx => $camtDoc) {
|
|
// Debug: Save raw XML to temp file for analysis
|
|
$debugFile = DOL_DATA_ROOT.'/bankimport_debug_'.$idx.'.xml';
|
|
file_put_contents($debugFile, $camtDoc);
|
|
dol_syslog("BankImport: Saved XML to ".$debugFile, LOG_DEBUG);
|
|
|
|
// Parse CAMT XML
|
|
$xml = simplexml_load_string($camtDoc);
|
|
if ($xml === false) {
|
|
dol_syslog("BankImport: Failed to parse XML document ".$idx, LOG_ERR);
|
|
continue;
|
|
}
|
|
|
|
// Get all namespaces used in the document
|
|
$namespaces = $xml->getNamespaces(true);
|
|
dol_syslog("BankImport: Namespaces: ".json_encode($namespaces), LOG_DEBUG);
|
|
|
|
// Register all found namespaces
|
|
foreach ($namespaces as $prefix => $uri) {
|
|
if (empty($prefix)) {
|
|
$xml->registerXPathNamespace('ns', $uri);
|
|
} else {
|
|
$xml->registerXPathNamespace($prefix, $uri);
|
|
}
|
|
}
|
|
|
|
// Extract balance information from XML
|
|
$balances = $xml->xpath('//ns:Bal') ?: $xml->xpath('//Bal') ?: $xml->xpath('//*[local-name()="Bal"]') ?: [];
|
|
foreach ($balances as $bal) {
|
|
$balType = (string) ($bal->Tp->CdOrPrtry->Cd ?? '');
|
|
if ($balType === 'CLBD') { // Closing balance
|
|
$balAmount = (float) ($bal->Amt ?? 0);
|
|
$balCcy = (string) ($bal->Amt['Ccy'] ?? 'EUR');
|
|
$balSign = ((string) ($bal->CdtDbtInd ?? 'CRDT')) === 'DBIT' ? -1 : 1;
|
|
$balDate = (string) ($bal->Dt->Dt ?? date('Y-m-d'));
|
|
$balanceInfo = array(
|
|
'amount' => $balSign * $balAmount,
|
|
'currency' => $balCcy,
|
|
'date' => $balDate,
|
|
'type' => 'CLBD'
|
|
);
|
|
dol_syslog("BankImport: Found closing balance: ".$balanceInfo['amount']." ".$balCcy." at ".$balDate, LOG_DEBUG);
|
|
}
|
|
}
|
|
|
|
// Try multiple XPath patterns for different CAMT versions
|
|
$entries = $xml->xpath('//ns:Ntry') ?: $xml->xpath('//Ntry') ?: $xml->xpath('//*[local-name()="Ntry"]') ?: [];
|
|
dol_syslog("BankImport: Found ".count($entries)." entries", LOG_DEBUG);
|
|
|
|
foreach ($entries as $entry) {
|
|
$amount = (float) ($entry->Amt ?? 0);
|
|
$sign = ((string) ($entry->CdtDbtInd ?? 'CRDT')) === 'DBIT' ? -1 : 1;
|
|
$date = (string) ($entry->BookgDt->Dt ?? $entry->ValDt->Dt ?? date('Y-m-d'));
|
|
|
|
// Get counterparty name - CAMT.052.001.08 has Pty wrapper
|
|
$txDtls = $entry->NtryDtls->TxDtls ?? null;
|
|
$name = '';
|
|
if ($txDtls) {
|
|
// For DBIT (outgoing): counterparty is Cdtr (creditor)
|
|
// For CRDT (incoming): counterparty is Dbtr (debtor)
|
|
if ($sign < 0) {
|
|
// Outgoing payment - get creditor name
|
|
$name = (string) ($txDtls->RltdPties->Cdtr->Pty->Nm
|
|
?? $txDtls->RltdPties->Cdtr->Nm
|
|
?? '');
|
|
} else {
|
|
// Incoming payment - get debtor name
|
|
$name = (string) ($txDtls->RltdPties->Dbtr->Pty->Nm
|
|
?? $txDtls->RltdPties->Dbtr->Nm
|
|
?? '');
|
|
}
|
|
}
|
|
|
|
// Get reference/description
|
|
$reference = '';
|
|
if ($txDtls && isset($txDtls->RmtInf)) {
|
|
$reference = (string) ($txDtls->RmtInf->Ustrd ?? '');
|
|
}
|
|
// Fallback to AddtlNtryInf
|
|
if (empty($reference)) {
|
|
$reference = (string) ($entry->AddtlNtryInf ?? '');
|
|
}
|
|
|
|
$transactions[] = array(
|
|
'id' => md5($date . $amount . $name . $reference),
|
|
'date' => strtotime($date),
|
|
'bookingDate' => strtotime($date),
|
|
'amount' => $sign * $amount,
|
|
'currency' => (string) ($entry->Amt['Ccy'] ?? 'EUR'),
|
|
'name' => $name,
|
|
'iban' => '',
|
|
'bic' => '',
|
|
'reference' => $reference,
|
|
'bookingText' => (string) ($entry->AddtlNtryInf ?? ''),
|
|
'endToEndId' => (string) ($txDtls->Refs->EndToEndId ?? ''),
|
|
);
|
|
}
|
|
}
|
|
|
|
// Return transactions with balance info
|
|
return array(
|
|
'transactions' => $transactions,
|
|
'balance' => $balanceInfo
|
|
);
|
|
|
|
} catch (Exception $e2) {
|
|
// CAMT also failed - both methods failed
|
|
// Check if this is likely a historical data limit issue
|
|
$daysAgo = (time() - $dateFrom) / 86400;
|
|
$errorMsg = $e->getMessage() . ' ' . $e2->getMessage();
|
|
|
|
// Banks typically don't provide data older than 90 days
|
|
// Error messages containing "HIKAZS" or "not support" often indicate data unavailability
|
|
if ($daysAgo > 60 && (
|
|
stripos($errorMsg, 'HIKAZS') !== false ||
|
|
stripos($errorMsg, 'not support') !== false ||
|
|
stripos($errorMsg, 'nicht unterstützt') !== false ||
|
|
stripos($errorMsg, 'nicht verfügbar') !== false
|
|
)) {
|
|
$this->error = 'Die Bank stellt historische Kontodaten nur für ca. 2-3 Monate bereit. Bitte wählen Sie einen kürzeren Zeitraum.';
|
|
} else {
|
|
$this->error = 'Abruf fehlgeschlagen: ' . $e->getMessage();
|
|
}
|
|
dol_syslog("BankImport: Both MT940 and CAMT failed. MT940: ".$e->getMessage()." | CAMT: ".$e2->getMessage(), LOG_ERR);
|
|
return -1;
|
|
}
|
|
}
|
|
} catch (Exception $e) {
|
|
$this->error = $e->getMessage();
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Prueft ob die Bank elektronische Kontoauszuege (PDF) unterstuetzt
|
|
*
|
|
* Prueft sowohl HKEKP (HIEPS) als auch HKEKA (HIEKAS).
|
|
*
|
|
* @return bool True wenn HIEPS oder HIEKAS in den BPD vorhanden
|
|
*/
|
|
public function supportsStatementPdf()
|
|
{
|
|
if ($this->fints === null) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
$params = $this->getBankParameters();
|
|
return isset($params['HIEPS']) || isset($params['HIEKAS']);
|
|
} catch (Exception $e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Elektronische Kontoauszuege als PDF von der Bank abrufen (HKEKP)
|
|
*
|
|
* @param string|null $statementNumber Optionale Auszugsnummer
|
|
* @param string|null $year Optionales Jahr (JJJJ)
|
|
* @return array|int Array mit PDF-Daten oder -1 bei Fehler, 0 wenn TAN benoetigt
|
|
*/
|
|
public function fetchBankStatements($statementNumber = null, $year = null)
|
|
{
|
|
dol_syslog("BankImport HKEKP: ===== fetchBankStatements() START =====", LOG_DEBUG);
|
|
dol_syslog("BankImport HKEKP: Parameter: statementNumber=".($statementNumber ?: 'null').", year=".($year ?: 'null'), LOG_DEBUG);
|
|
|
|
if (!$this->isConfigured()) {
|
|
$this->error = 'Konfiguration unvollstaendig';
|
|
dol_syslog("BankImport HKEKP: ABBRUCH - nicht konfiguriert", LOG_ERR);
|
|
return -1;
|
|
}
|
|
|
|
if ($this->fints === null) {
|
|
$this->error = 'Nicht verbunden. Bitte zuerst login() aufrufen.';
|
|
dol_syslog("BankImport HKEKP: ABBRUCH - fints===null, kein Login", LOG_ERR);
|
|
return -1;
|
|
}
|
|
|
|
try {
|
|
// Konten abrufen
|
|
dol_syslog("BankImport HKEKP: Rufe GetSEPAAccounts ab...", LOG_DEBUG);
|
|
$getAccounts = GetSEPAAccounts::create();
|
|
$this->fints->execute($getAccounts);
|
|
dol_syslog("BankImport HKEKP: GetSEPAAccounts ausgefuehrt", LOG_DEBUG);
|
|
|
|
if ($getAccounts->needsTan()) {
|
|
dol_syslog("BankImport HKEKP: GetSEPAAccounts benoetigt TAN", LOG_DEBUG);
|
|
$this->pendingAction = $getAccounts;
|
|
$tanRequest = $getAccounts->getTanRequest();
|
|
$this->tanChallenge = $tanRequest->getChallenge();
|
|
return 0;
|
|
}
|
|
|
|
$accounts = $getAccounts->getAccounts();
|
|
dol_syslog("BankImport HKEKP: ".count($accounts)." SEPA-Konten gefunden", LOG_DEBUG);
|
|
|
|
foreach ($accounts as $idx => $acc) {
|
|
dol_syslog("BankImport HKEKP: Konto ".($idx+1).": IBAN=".$acc->getIban()
|
|
.", BIC=".$acc->getBic()
|
|
.", Ktonr=".$acc->getAccountNumber()
|
|
.", BLZ=".$acc->getSubAccount(), LOG_DEBUG);
|
|
}
|
|
|
|
// Passendes Konto per IBAN finden
|
|
$selectedAccount = null;
|
|
dol_syslog("BankImport HKEKP: Suche Konto mit IBAN=".$this->iban, LOG_DEBUG);
|
|
foreach ($accounts as $account) {
|
|
if ($account->getIban() === $this->iban) {
|
|
$selectedAccount = $account;
|
|
dol_syslog("BankImport HKEKP: IBAN-Match gefunden!", LOG_DEBUG);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($selectedAccount === null) {
|
|
if (!empty($accounts)) {
|
|
$selectedAccount = $accounts[0];
|
|
dol_syslog("BankImport HKEKP: KEIN IBAN-Match! Verwende erstes Konto: ".$selectedAccount->getIban(), LOG_WARNING);
|
|
} else {
|
|
$this->error = 'Keine Konten gefunden';
|
|
dol_syslog("BankImport HKEKP: ABBRUCH - keine Konten gefunden", LOG_ERR);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
// BPD (Bank Parameter Data) abfragen und loggen
|
|
dol_syslog("BankImport HKEKP: Rufe getBankParameters() ab...", LOG_DEBUG);
|
|
$params = $this->getBankParameters();
|
|
dol_syslog("BankImport HKEKP: BPD enthaelt ".count($params)." Geschaeftsvorfaelle", LOG_DEBUG);
|
|
dol_syslog("BankImport HKEKP: BPD-Segmente: ".implode(', ', array_keys($params)), LOG_DEBUG);
|
|
|
|
// Detailliert loggen welche Versionen unterstuetzt werden
|
|
foreach ($params as $segName => $versions) {
|
|
if (in_array($segName, ['HIEPS', 'HIEKAS', 'HIKEP', 'HIKAS', 'HIKAZS', 'HICAZS', 'HIPINS'])) {
|
|
dol_syslog("BankImport: BPD Detail: ".$segName." Versionen=[".implode(',', $versions)."]", LOG_DEBUG);
|
|
}
|
|
}
|
|
|
|
// Strategie: HKEKP (PDF-spezifisch) bevorzugen, Fallback auf HKEKA (generisch)
|
|
$useHkekp = isset($params['HIEPS']);
|
|
$useHkeka = isset($params['HIEKAS']);
|
|
|
|
dol_syslog("BankImport: HIEPS (HKEKP)=".($useHkekp ? 'JA' : 'NEIN')
|
|
.", HIEKAS (HKEKA)=".($useHkeka ? 'JA' : 'NEIN'), LOG_DEBUG);
|
|
|
|
if (!$useHkekp && !$useHkeka) {
|
|
$this->error = 'Die Bank unterstuetzt keine elektronischen Kontoauszuege (weder HKEKP/HIEPS noch HKEKA/HIEKAS in BPD)';
|
|
dol_syslog("BankImport: Weder HIEPS noch HIEKAS in BPD! Kontoauszuege nicht unterstuetzt.", LOG_WARNING);
|
|
dol_syslog("BankImport: Alle verfuegbaren BPD-Parameter: ".implode(', ', array_keys($params)), LOG_WARNING);
|
|
return -1;
|
|
}
|
|
|
|
dol_syslog("BankImport: Starte Abruf fuer ".$selectedAccount->getIban()
|
|
.($statementNumber ? " Nr.".$statementNumber : " (alle)")
|
|
.($year ? " Jahr ".$year : " (alle Jahre)"), LOG_DEBUG);
|
|
|
|
$pdfStatements = [];
|
|
|
|
if ($useHkekp) {
|
|
// === HKEKP: PDF-spezifischer Kontoauszug ===
|
|
dol_syslog("BankImport HKEKP: HIEPS gefunden! Versionen: [".implode(',', $params['HIEPS'])."]", LOG_DEBUG);
|
|
dol_syslog("BankImport HKEKP: Erstelle GetBankStatement Action...", LOG_DEBUG);
|
|
$getBankStatement = GetBankStatement::create($selectedAccount, $statementNumber, $year);
|
|
dol_syslog("BankImport HKEKP: Fuehre GetBankStatement aus (fints->execute)...", LOG_DEBUG);
|
|
$this->fints->execute($getBankStatement);
|
|
dol_syslog("BankImport HKEKP: GetBankStatement ausgefuehrt", LOG_DEBUG);
|
|
|
|
if ($getBankStatement->needsTan()) {
|
|
dol_syslog("BankImport HKEKP: GetBankStatement benoetigt TAN", LOG_DEBUG);
|
|
$this->pendingAction = $getBankStatement;
|
|
$tanRequest = $getBankStatement->getTanRequest();
|
|
$this->tanChallenge = $tanRequest->getChallenge();
|
|
return 0;
|
|
}
|
|
|
|
$pdfStatements = $getBankStatement->getPdfStatements();
|
|
dol_syslog("BankImport HKEKP: ".count($pdfStatements)." PDFs empfangen", LOG_DEBUG);
|
|
} else {
|
|
// === HKEKA: Generischer Kontoauszug (mit Format=PDF) ===
|
|
dol_syslog("BankImport HKEKA: HIEKAS gefunden! Versionen: [".implode(',', $params['HIEKAS'])."]", LOG_DEBUG);
|
|
dol_syslog("BankImport HKEKA: Verwende HKEKA mit Format=3 (PDF)", LOG_DEBUG);
|
|
dol_syslog("BankImport HKEKA: Erstelle GetElectronicStatement Action...", LOG_DEBUG);
|
|
|
|
$getStatement = GetElectronicStatement::create(
|
|
$selectedAccount,
|
|
GetElectronicStatement::FORMAT_PDF,
|
|
$statementNumber,
|
|
$year
|
|
);
|
|
|
|
dol_syslog("BankImport HKEKA: Fuehre GetElectronicStatement aus...", LOG_DEBUG);
|
|
$this->fints->execute($getStatement);
|
|
dol_syslog("BankImport HKEKA: GetElectronicStatement ausgefuehrt", LOG_DEBUG);
|
|
|
|
if ($getStatement->needsTan()) {
|
|
dol_syslog("BankImport HKEKA: GetElectronicStatement benoetigt TAN", LOG_DEBUG);
|
|
$this->pendingAction = $getStatement;
|
|
$tanRequest = $getStatement->getTanRequest();
|
|
$this->tanChallenge = $tanRequest->getChallenge();
|
|
return 0;
|
|
}
|
|
|
|
// Alle Statements holen, nach PDF filtern
|
|
$allStatements = $getStatement->getStatements();
|
|
dol_syslog("BankImport HKEKA: ".count($allStatements)." Statements empfangen", LOG_DEBUG);
|
|
|
|
foreach ($allStatements as $sIdx => $stmt) {
|
|
$format = $stmt['format'] ?? 'unbekannt';
|
|
$data = $stmt['data'];
|
|
dol_syslog("BankImport HKEKA: Statement ".($sIdx+1).": Format=".$format
|
|
.", ".strlen($data)." Bytes, Anfang=".substr($data, 0, 10), LOG_DEBUG);
|
|
}
|
|
|
|
// PDFs extrahieren (getPdfStatements filtert nach Format=3 oder %PDF-)
|
|
$pdfStatements = $getStatement->getPdfStatements();
|
|
dol_syslog("BankImport HKEKA: Davon ".count($pdfStatements)." PDFs", LOG_DEBUG);
|
|
|
|
// Falls keine PDFs, aber andere Formate: Hinweis
|
|
if (empty($pdfStatements) && !empty($allStatements)) {
|
|
$formats = array_map(function($s) { return $s['format'] ?? '?'; }, $allStatements);
|
|
dol_syslog("BankImport HKEKA: Keine PDFs! Empfangene Formate: ".implode(', ', $formats), LOG_WARNING);
|
|
$this->error = 'Kontoauszuege empfangen, aber nicht im PDF-Format (Formate: '.implode(', ', $formats).')';
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
foreach ($pdfStatements as $pIdx => $pdf) {
|
|
dol_syslog("BankImport: PDF ".($pIdx+1).": ".strlen($pdf)." Bytes, beginnt mit: ".substr($pdf, 0, 10), LOG_DEBUG);
|
|
}
|
|
|
|
dol_syslog("BankImport: ===== fetchBankStatements() ERFOLG - ".count($pdfStatements)." PDFs =====", LOG_DEBUG);
|
|
|
|
return array(
|
|
'count' => count($pdfStatements),
|
|
'pdfs' => $pdfStatements,
|
|
'iban' => $selectedAccount->getIban(),
|
|
);
|
|
} catch (\Fhp\Protocol\UnexpectedResponseException $e) {
|
|
// Bank unterstuetzt den Geschaeftsvorfall nicht
|
|
$this->error = 'Kontoauszug-Abruf nicht unterstuetzt: '.$e->getMessage();
|
|
dol_syslog("BankImport HKEKP: UnexpectedResponseException: ".$e->getMessage(), LOG_WARNING);
|
|
dol_syslog("BankImport HKEKP: Stack-Trace: ".$e->getTraceAsString(), LOG_DEBUG);
|
|
return -1;
|
|
} catch (Exception $e) {
|
|
$this->error = 'Fehler beim Kontoauszug-Abruf: '.$e->getMessage();
|
|
dol_syslog("BankImport HKEKP: Exception (".get_class($e)."): ".$e->getMessage(), LOG_ERR);
|
|
dol_syslog("BankImport HKEKP: Stack-Trace: ".$e->getTraceAsString(), LOG_DEBUG);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Close FinTS connection
|
|
*
|
|
* @return void
|
|
*/
|
|
public function close()
|
|
{
|
|
if ($this->fints !== null) {
|
|
try {
|
|
$this->fints->close();
|
|
} catch (Exception $e) {
|
|
// Ignore close errors
|
|
}
|
|
$this->fints = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Persist FinTS state (for web session handling)
|
|
*
|
|
* @return string|null Serialized state or null
|
|
*/
|
|
public function persist()
|
|
{
|
|
if ($this->fints === null) {
|
|
return null;
|
|
}
|
|
try {
|
|
return $this->fints->persist();
|
|
} catch (Exception $e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Restore FinTS from persisted state
|
|
*
|
|
* @param string $state Persisted state
|
|
* @return int 1 if success, -1 if error
|
|
*/
|
|
public function restore($state)
|
|
{
|
|
if (!$this->isConfigured()) {
|
|
$this->error = 'Configuration incomplete';
|
|
return -1;
|
|
}
|
|
|
|
if (!$this->isLibraryAvailable()) {
|
|
$this->error = 'phpFinTS library not found';
|
|
return -1;
|
|
}
|
|
|
|
if (empty($state)) {
|
|
$this->error = 'Empty state provided';
|
|
dol_syslog("BankImport: restore - Empty state provided", LOG_ERR);
|
|
return -1;
|
|
}
|
|
|
|
dol_syslog("BankImport: restore - Restoring FinTS state, length=".strlen($state), LOG_DEBUG);
|
|
|
|
try {
|
|
$options = new FinTsOptions();
|
|
$options->url = $this->fintsUrl;
|
|
$options->bankCode = $this->blz;
|
|
$options->productName = $this->productId;
|
|
$options->productVersion = '1.0';
|
|
|
|
$credentials = Credentials::create($this->username, $this->pin);
|
|
|
|
$this->fints = FinTs::new($options, $credentials, $state);
|
|
|
|
dol_syslog("BankImport: restore - FinTS state restored successfully", LOG_DEBUG);
|
|
return 1;
|
|
} catch (Exception $e) {
|
|
$this->error = $e->getMessage();
|
|
dol_syslog("BankImport: restore - Exception: ".$e->getMessage(), LOG_ERR);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get FinTS URL
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getFintsUrl()
|
|
{
|
|
return $this->fintsUrl;
|
|
}
|
|
|
|
/**
|
|
* Get BLZ
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getBlz()
|
|
{
|
|
return $this->blz;
|
|
}
|
|
|
|
/**
|
|
* Get IBAN
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getIban()
|
|
{
|
|
return $this->iban;
|
|
}
|
|
}
|