Files
kupshop/bundles/KupShop/BankAutoPaymentBundle/PaymentSources/FioBankApi.php
2025-08-02 16:30:27 +02:00

325 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace KupShop\BankAutoPaymentBundle\PaymentSources;
use KupShop\AdminBundle\Util\ActivityLog;
use KupShop\BankAutoPaymentBundle\BankAutoPaymentBundle;
use KupShop\BankAutoPaymentBundle\Entity\BankTransaction;
use KupShop\BankAutoPaymentBundle\Exceptions\FioPaymentExeption;
use KupShop\KupShopBundle\Config;
use KupShop\KupShopBundle\Util\StringUtil;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
class FioBankApi extends AbstractPaymentSource
{
public const SETTINGS = 'fio_check';
protected static string $name = 'FioBank';
protected static string $logTag = BankAutoPaymentBundle::LOG_TAG_FIO;
protected array $allowedPaymentTypes = [
'Okamžitá příchozí platba',
'Bezhotovostní příjem',
'Příjem',
'Převod mezi bankovními konty (příjem)',
'Příjem převodem uvnitř banky',
'Posel příjem',
'Neidentifikovaný příjem na bankovní konto',
'Okamžitá příchozí Europlatba',
'Vklad pokladnou',
];
protected array $lastPaymentIDs = [];
public function checkPayments(?\DateTime $forceDate = null): bool
{
if (!$this->loadConfig()) {
return false;
}
$result = $this->forToken(function ($token) use ($forceDate) {
$lastTransactionId = $this->lastPaymentIDs[$token] ?? null;
$data = $this->downloadPaymentsData($token, $lastTransactionId, $forceDate);
if (!$data) {
return false;
}
$transactions = $data->transactionList->transaction ?? [];
foreach ($transactions as $transaction) {
try {
$bankTransaction = (new BankTransaction())
->setType($transaction->column8->value ?? false)
->setPrice($transaction->column1->value ?? false)
->setCurrency($transaction->column14->value ?? false)
->setId($transaction->column22->value ?? false)
->setComment($transaction->column25->value ?? false)
->setAccountNumber($transaction->column2->value ?? false)
->setBankCode($transaction->column3->value ?? false)
->setVariableSymbol($transaction->column5->value ?? false)
->setRef($transaction->column27->value ?? false)
->setMsg($transaction->column16->value ?? false);
$this->checkPayment($bankTransaction);
$this->lastPaymentIDs[$token] = $bankTransaction->getId();
} catch (\Throwable $exception) {
// capture to sentry
$this->sentryLogger->captureException(new \Exception('Failure in FioBankApi payment processing!', 0, $exception),
['extra' => (array) $transaction]);
}
}
return true;
});
\Settings::getDefault()->saveValue(self::SETTINGS, $this->lastPaymentIDs, false);
return $result;
}
protected function downloadPaymentsData($token, ?int $lastTransactionId = null, ?\DateTime $fromDate = null)
{
$client = $this->curlUtil->getClient(options: ['max_duration' => 60]);
try {
// Podle data má přednost, když kontroluju z adminu, nechci limitovat podle ID
if ($fromDate) {
$response = $client->request('GET', "https://fioapi.fio.cz/v1/rest/set-last-date/{$token}/{$fromDate->format('Y-m-d')}/");
$response->getContent();
}
// Nastavit zarážku na poslední přečtené ID
elseif ($lastTransactionId) {
$response = $client->request('GET', "https://fioapi.fio.cz/v1/rest/set-last-id/{$token}/{$lastTransactionId}/");
$response->getContent();
}
// Stáhnout seznam transakcí
$response = $client->request('GET', "https://fioapi.fio.cz/v1/rest/last/{$token}/transactions.json");
$data = json_decode_strict($response->getContent());
$data = $data->accountStatement;
getLogger()->notice('Fio downloadPaymentsData', ['lastTransactionId' => $lastTransactionId, 'token' => $token, 'transactionCount' => count($data->transactionList->transaction ?? [])]);
} catch (ClientExceptionInterface|RedirectionExceptionInterface|ServerExceptionInterface|TransportExceptionInterface|\JsonException|DecodingExceptionInterface $e) {
if ($e instanceof ServerExceptionInterface) {
// neexistující nebo neaktivní token - k cemu taky mame 401, ze?
if ($e->getCode() == 500) {
addActivityLog(ActivityLog::SEVERITY_ERROR,
ActivityLog::TYPE_SYNC,
'FIO API: Neexistující nebo neaktivní token. Zkontrolujte nastavení a internetové bankovnictví.',
tags: [BankAutoPaymentBundle::LOG_TAG_FIO]);
return false;
}
}
addActivityLog(ActivityLog::SEVERITY_ERROR,
ActivityLog::TYPE_SYNC,
'FIO API: Chyba při stahování plateb.',
['exception' => $e->getMessage()],
[BankAutoPaymentBundle::LOG_TAG_FIO]);
return false;
}
return $data;
}
protected function forToken(callable $callback)
{
$tokenString = str_replace("\r", '', $this->config['tokens'] ?? '');
$tokens = explode(PHP_EOL, $tokenString);
$responses = [];
foreach ($tokens as $token) {
$token = StringUtil::unicode_trim($token);
if (!empty($token)) {
$responses[] = $callback($token);
}
}
return !in_array(false, $responses);
}
protected function loadConfig()
{
if (!empty($this->config)) {
return true;
}
$cfg = Config::get();
$dbcfg = \Settings::getDefault();
$dbConfig = $dbcfg->payments[self::getName()] ?? [];
$config = $cfg['Modules']['bank_auto_payments']['fio_bank'] ?? [];
if (!is_array($config)) {
$config = [];
}
$this->config = array_merge($config, $dbConfig);
$this->lastPaymentIDs = $dbcfg->loadValue(self::SETTINGS) ?? [];
return !empty($this->config);
}
public function sendPayments($czk = [], $eur = [])
{
// Pořadí je daný APIčkem - domácí platby, euro platby a ostatní
$toSend = [
'DomesticTransaction' => $czk,
'T2Transaction' => $eur,
];
$types = [
'DomesticTransaction' => 'CZK',
'T2Transaction' => 'EUR',
];
$accounts = [];
// Načti config, aby se odeslalo všechno, nebo nic
if (!empty($czk)) {
$accounts['CZK'] = [
'token' => $this->getTokenForCurrency('CZK'),
'account' => $this->getAccountForCurrency('CZK'),
];
}
if (!empty($eur)) {
$accounts['EUR'] = [
'token' => $this->getTokenForCurrency('EUR'),
'account' => $this->getAccountForCurrency('EUR'),
];
}
$return = [];
foreach ($toSend as $type => $values) {
if (count($values) == 0) {
continue;
}
$currency = $types[$type];
$xml = new \SimpleXMLElement('<Import xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://www.fio.cz/schema/importIB.xsd"><Orders></Orders></Import>');
foreach ($values as $id => $item) {
$transactionElement = $xml->Orders->addChild($type);
// accountFrom musí být na začátku elementu
foreach (['accountFrom' => $accounts[$currency]['account']] + $item as $key => $value) {
$transactionElement->addChild($key, strval($value));
}
}
$tmpFilename = tempnam('data/tmp', 'fio_');
if (!$tmpFilename) {
throw new \Exception('Nepodařilo se vytvořit tmp soubor');
}
$xml->asXml($tmpFilename);
$token = $accounts[$currency]['token'];
$url = 'https://fioapi.fio.cz/v1/rest/import/';
$fields = [
'type' => 'xml',
'token' => $token,
'file' => new \CURLFile($tmpFilename, 'application/xml'),
];
if (!isLocalDevelopment()) {
$res = curl_init();
curl_setopt($res, CURLOPT_RETURNTRANSFER, true);
curl_setopt($res, CURLOPT_TIMEOUT, 60);
curl_setopt($res, CURLOPT_URL, $url);
curl_setopt($res, CURLOPT_HTTPHEADER, ['Content-Type: multipart/form-data']);
curl_setopt($res, CURLOPT_POST, true);
curl_setopt($res, CURLOPT_POSTFIELDS, $fields);
$result = curl_exec($res);
$statusCode = curl_getinfo($res, CURLINFO_RESPONSE_CODE);
curl_close($res);
} else {
continue;
}
if ($statusCode != 200) {
addActivityLog(ActivityLog::SEVERITY_ERROR,
ActivityLog::TYPE_SYNC,
'FIO API: Nepodařilo se odeslat platby do Fio.',
tags: [BankAutoPaymentBundle::LOG_TAG_FIO]);
throw new FioPaymentExeption('Nepodařilo se nahrát platby do Fio.');
}
addActivityLog(ActivityLog::SEVERITY_NOTICE, ActivityLog::TYPE_SYNC, 'FIO API: Odeslány Platby do Fio', [
'request' => $xml->asXML(),
'response' => $result,
], tags: [BankAutoPaymentBundle::LOG_TAG_FIO]);
$responseXml = simplexml_load_string($result);
$ids = [];
$index = 0;
foreach ($values as $id => $item) {
$detail = $responseXml->ordersDetails->detail[$index++];
$success = true;
$msg = [];
foreach ($detail->messages->message as $message) {
if ($message['errorCode']->__toString() != 0) {
$success = false;
$msg[] = $message->__toString();
$ids[] = $id;
}
}
$return[$types[$type]][$id] = [
'success' => $success,
'message' => $msg,
];
}
if (!empty($ids)) {
addActivityLog(ActivityLog::SEVERITY_WARNING,
ActivityLog::TYPE_SYNC,
'FIO API: při zpracování některých plateb došlo k chybě.',
[implode(', ', $ids)],
[BankAutoPaymentBundle::LOG_TAG_FIO]);
}
@unlink($tmpFilename);
}
return $return;
}
protected function getAccountForCurrency($currency)
{
$dbcfg = \Settings::getDefault();
$account = $dbcfg['returns']['fio'][$currency]['account'];
if (empty($account)) {
throw new FioPaymentExeption("Měna {$currency} nemá nastavený účet.");
}
return explode('/', $account)[0];
}
protected function getTokenForCurrency($currency)
{
$dbcfg = \Settings::getDefault();
$token = $dbcfg['returns']['fio'][$currency]['token'];
if (empty($token)) {
throw new FioPaymentExeption("Měna {$currency} nemá nastavený token.");
}
return $token;
}
}