* * 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/bankimportcron.class.php * \ingroup bankimport * \brief Cron job class for automatic bank statement import */ require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php'; dol_include_once('/bankimport/class/fints.class.php'); dol_include_once('/bankimport/class/banktransaction.class.php'); /** * Class BankImportCron * Handles automatic bank statement import via scheduled task */ class BankImportCron { /** * @var DoliDB Database handler */ public $db; /** * @var string Error message */ public $error = ''; /** * @var array Error messages */ public $errors = array(); /** * @var string Output message for cron log */ public $output = ''; /** * Constructor * * @param DoliDB $db Database handler */ public function __construct($db) { $this->db = $db; } /** * Execute the automatic import cron job * Called by Dolibarr's scheduled task system * * @return int 0 if OK, < 0 if error */ public function doAutoImport() { global $conf, $langs, $user; $langs->load('bankimport@bankimport'); dol_syslog("BankImportCron::doAutoImport - Starting automatic import", LOG_INFO); // Check if automatic import is enabled if (!getDolGlobalInt('BANKIMPORT_AUTO_ENABLED')) { $this->output = $langs->trans('AutoImportDisabled'); dol_syslog("BankImportCron::doAutoImport - Auto import is disabled", LOG_INFO); return 0; } // Initialize FinTS $fints = new BankImportFinTS($this->db); if (!$fints->isConfigured()) { $this->error = $langs->trans('AutoImportNotConfigured'); dol_syslog("BankImportCron::doAutoImport - FinTS not configured", LOG_WARNING); $this->setNotification('config_error'); return -1; } if (!$fints->isLibraryAvailable()) { $this->error = $langs->trans('FinTSLibraryNotFound'); dol_syslog("BankImportCron::doAutoImport - Library not found", LOG_ERR); return -1; } // Check for stored session state (from previous successful TAN) $storedState = getDolGlobalString('BANKIMPORT_CRON_STATE'); $sessionRestored = false; try { // Try to restore previous session if (!empty($storedState)) { dol_syslog("BankImportCron::doAutoImport - Attempting to restore previous session (state length: ".strlen($storedState).")", LOG_INFO); $restoreResult = $fints->restore($storedState); if ($restoreResult < 0) { // Session expired, need fresh login dol_syslog("BankImportCron::doAutoImport - Session restore failed: ".$fints->error.", trying fresh login", LOG_WARNING); $storedState = ''; // Clear stale session dolibarr_del_const($this->db, 'BANKIMPORT_CRON_STATE', $conf->entity); } else { dol_syslog("BankImportCron::doAutoImport - Session restored successfully", LOG_INFO); $sessionRestored = true; } } else { dol_syslog("BankImportCron::doAutoImport - No stored session found", LOG_INFO); } // If no stored session or restore failed, try fresh login if (empty($storedState)) { dol_syslog("BankImportCron::doAutoImport - Attempting fresh login", LOG_INFO); $loginResult = $fints->login(); if ($loginResult < 0) { $this->error = $langs->trans('LoginFailed').': '.$fints->error; dol_syslog("BankImportCron::doAutoImport - Login failed: ".$fints->error, LOG_ERR); $this->setNotification('login_error'); return -1; } if ($loginResult == 0) { // TAN required - can't proceed automatically $this->output = $langs->trans('TANRequired'); dol_syslog("BankImportCron::doAutoImport - TAN required for login, cannot proceed automatically", LOG_WARNING); $this->setNotification('tan_required'); // Store the state so user can complete TAN manually $state = $fints->persist(); if (!empty($state)) { dolibarr_set_const($this->db, 'BANKIMPORT_CRON_PENDING_STATE', $state, 'chaine', 0, '', $conf->entity); dolibarr_set_const($this->db, 'BANKIMPORT_CRON_PENDING_ACTION', serialize($fints->getPendingAction()), 'chaine', 0, '', $conf->entity); } return 0; // Not an error, just can't proceed } dol_syslog("BankImportCron::doAutoImport - Fresh login successful", LOG_INFO); } // Login successful or session restored - fetch statements $daysToFetch = getDolGlobalInt('BANKIMPORT_AUTO_DAYS') ?: 30; $dateFrom = strtotime("-{$daysToFetch} days"); $dateTo = time(); dol_syslog("BankImportCron::doAutoImport - Fetching statements from ".date('Y-m-d', $dateFrom)." to ".date('Y-m-d', $dateTo)." ({$daysToFetch} days)", LOG_INFO); $result = $fints->fetchStatements($dateFrom, $dateTo); // Log what we got back if (is_array($result)) { $txCount = count($result['transactions'] ?? array()); $hasBalance = !empty($result['balance']); $isPartial = !empty($result['partial']); dol_syslog("BankImportCron::doAutoImport - fetchStatements returned array: transactions={$txCount}, hasBalance=".($hasBalance?'yes':'no').", partial=".($isPartial?'yes':'no'), LOG_INFO); } else { dol_syslog("BankImportCron::doAutoImport - fetchStatements returned: ".var_export($result, true), LOG_INFO); } if ($result === 0) { // TAN required for statements $this->output = $langs->trans('TANRequired'); dol_syslog("BankImportCron::doAutoImport - TAN required for statements", LOG_WARNING); $this->setNotification('tan_required'); // Store state for manual completion $state = $fints->persist(); if (!empty($state)) { dolibarr_set_const($this->db, 'BANKIMPORT_CRON_PENDING_STATE', $state, 'chaine', 0, '', $conf->entity); dolibarr_set_const($this->db, 'BANKIMPORT_CRON_PENDING_ACTION', serialize($fints->getPendingAction()), 'chaine', 0, '', $conf->entity); } return 0; } if ($result < 0) { $this->error = $langs->trans('FetchFailed').': '.$fints->error; dol_syslog("BankImportCron::doAutoImport - Fetch failed: ".$fints->error, LOG_ERR); $this->setNotification('fetch_error'); return -1; } // Success - import transactions $transactions = $result['transactions'] ?? array(); $fetchedCount = count($transactions); $importedCount = 0; $skippedCount = 0; dol_syslog("BankImportCron::doAutoImport - Bank returned {$fetchedCount} transactions", LOG_INFO); // If restored session returned 0 transactions, the session might be stale // Try fresh login as fallback if ($fetchedCount == 0 && $sessionRestored) { dol_syslog("BankImportCron::doAutoImport - Restored session returned 0 transactions, clearing stale session", LOG_WARNING); dolibarr_del_const($this->db, 'BANKIMPORT_CRON_STATE', $conf->entity); // Note: We don't retry here because a fresh login would require TAN // Just mark that the session was stale $this->output = $langs->trans('AutoImportNoTransactions').' (Session abgelaufen - nächster Lauf erfordert TAN)'; $this->setNotification('session_expired'); return 0; } if (!empty($transactions)) { // Get a system user for the import $importUser = new User($this->db); $importUser->fetch(1); // Admin user $iban = $fints->getIban(); // Use the importFromFinTS method for correct field mapping $transImporter = new BankImportTransaction($this->db); $importResult = $transImporter->importFromFinTS($transactions, $iban, $importUser); $importedCount = $importResult['imported'] ?? 0; $skippedCount = $importResult['skipped'] ?? 0; dol_syslog("BankImportCron::doAutoImport - Import result: imported={$importedCount}, skipped={$skippedCount}", LOG_INFO); } // Update last fetch info dolibarr_set_const($this->db, 'BANKIMPORT_LAST_FETCH', time(), 'chaine', 0, '', $conf->entity); dolibarr_set_const($this->db, 'BANKIMPORT_LAST_FETCH_COUNT', $importedCount, 'chaine', 0, '', $conf->entity); // Store session for next run (might avoid TAN) $state = $fints->persist(); if (!empty($state)) { dolibarr_set_const($this->db, 'BANKIMPORT_CRON_STATE', $state, 'chaine', 0, '', $conf->entity); } // Clear any pending state dolibarr_del_const($this->db, 'BANKIMPORT_CRON_PENDING_STATE', $conf->entity); dolibarr_del_const($this->db, 'BANKIMPORT_CRON_PENDING_ACTION', $conf->entity); // Clear notification flag on success $this->clearNotification(); $fints->close(); if ($importedCount > 0) { $this->output = $langs->trans('AutoImportSuccess', $importedCount); } elseif ($skippedCount > 0) { $this->output = $langs->trans('AutoImportNoTransactions').' ('.$skippedCount.' bereits vorhanden)'; } else { $this->output = $langs->trans('AutoImportNoTransactions'); } dol_syslog("BankImportCron::doAutoImport - Completed: imported={$importedCount}, skipped={$skippedCount}", LOG_INFO); return 0; } catch (Exception $e) { $this->error = 'Exception: '.$e->getMessage(); dol_syslog("BankImportCron::doAutoImport - Exception: ".$e->getMessage(), LOG_ERR); $this->setNotification('error'); return -1; } } /** * Set notification flag for admin users * * @param string $type Notification type (tan_required, error, etc.) * @return void */ private function setNotification($type) { global $conf; dolibarr_set_const($this->db, 'BANKIMPORT_NOTIFICATION', $type, 'chaine', 0, '', $conf->entity); dolibarr_set_const($this->db, 'BANKIMPORT_NOTIFICATION_DATE', time(), 'chaine', 0, '', $conf->entity); } /** * Clear notification flag * * @return void */ private function clearNotification() { global $conf; dolibarr_del_const($this->db, 'BANKIMPORT_NOTIFICATION', $conf->entity); dolibarr_del_const($this->db, 'BANKIMPORT_NOTIFICATION_DATE', $conf->entity); } /** * Check if there's a pending notification * * @return array|null Notification info or null */ public static function getNotification() { $type = getDolGlobalString('BANKIMPORT_NOTIFICATION'); $date = getDolGlobalInt('BANKIMPORT_NOTIFICATION_DATE'); if (empty($type)) { return null; } return array( 'type' => $type, 'date' => $date ); } /** * Resume a pending TAN action (called from web interface) * * @return int 1 if TAN confirmed and import done, 0 if still waiting, -1 if error */ public function resumePendingAction() { global $conf, $langs, $user; $pendingState = getDolGlobalString('BANKIMPORT_CRON_PENDING_STATE'); $pendingAction = getDolGlobalString('BANKIMPORT_CRON_PENDING_ACTION'); if (empty($pendingState) || empty($pendingAction)) { $this->error = 'No pending action'; return -1; } $fints = new BankImportFinTS($this->db); $restoreResult = $fints->restore($pendingState); if ($restoreResult < 0) { $this->error = $fints->error; // Clear expired state dolibarr_del_const($this->db, 'BANKIMPORT_CRON_PENDING_STATE', $conf->entity); dolibarr_del_const($this->db, 'BANKIMPORT_CRON_PENDING_ACTION', $conf->entity); return -1; } $action = @unserialize($pendingAction); if ($action === false) { $this->error = 'Could not restore pending action'; return -1; } $fints->setPendingAction($action); // Check if TAN was confirmed $checkResult = $fints->checkDecoupledTan(); if ($checkResult == 0) { // Still waiting // Update state $newState = $fints->persist(); if (!empty($newState)) { dolibarr_set_const($this->db, 'BANKIMPORT_CRON_PENDING_STATE', $newState, 'chaine', 0, '', $conf->entity); } return 0; } if ($checkResult < 0) { $this->error = $fints->error; return -1; } // TAN confirmed - now run the import dolibarr_del_const($this->db, 'BANKIMPORT_CRON_PENDING_STATE', $conf->entity); dolibarr_del_const($this->db, 'BANKIMPORT_CRON_PENDING_ACTION', $conf->entity); // Store the confirmed session and run import $state = $fints->persist(); if (!empty($state)) { dolibarr_set_const($this->db, 'BANKIMPORT_CRON_STATE', $state, 'chaine', 0, '', $conf->entity); } return $this->doAutoImport(); } }