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

318 lines
12 KiB
PHP

<?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\EverifinPaymentException;
use Psr\Log\LoggerInterface;
use Symfony\Component\Routing\Router;
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;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\Service\Attribute\Required;
class EverifinApi extends AbstractPaymentSource
{
protected static string $name = 'Everifin';
protected static string $logTag = BankAutoPaymentBundle::LOG_TAG_EVERIFIN;
// testovací údaje
public const clientId = 'wpj-cz-test';
public const clientSecret = '11asXDRO8egAchUftnOCx0BThM9IgY3n';
private const TRANSACTION_URL = 'https://api.everifin.com/v1/transactions';
private const REFRESH_TRANSACTION_URL = 'https://api.everifin.com/v1/transactions/refresh';
private const LOGOUT_URL = 'https://app.everifin.com/auth/realms/everifin_app/protocol/openid-connect/logout';
private const LOGIN_EXPIRATION_DIFF = 1209600; // 14 days
protected array $allowedPaymentTypes = [
// 'PAYMENT',
// 'INSTANT_PAYMENT',
];
protected LoggerInterface $logger;
public function getRedirectUrl(): string
{
// https://app.everifin.com/auth/realms/everifin_app/protocol/openid-connect/auth?client_id=wpj-cz-test&redirect_uri=http://www.kupshop.local/admin/_everifin&response_type=code&scope=ais
$redirect_uri = path('kupshop_bankautopayment_admin_everifin_handleoauth', referenceType: Router::ABSOLUTE_URL);
// https://app.everifin.com/auth/realms/everifin_app/protocol/openid-connect/auth?client_id=everifin-api-test&redirect_uri=http%3A%2F%2Fmyweb.example&response_type=code&scope=ais
return 'https://app.everifin.com/auth/realms/everifin_app/protocol/openid-connect/auth?client_id='.$this->getClientId()."&redirect_uri={$redirect_uri}&response_type=code&scope=ais";
}
public function checkPayments(?\DateTime $forceDate = null): bool
{
if (!$this->loadConfig()) {
return false;
}
$settings = \Settings::getDefault();
$startTimestamp = null;
$everifinSettings = $settings->loadValue('everifin');
if ($forceDate) {
$dateFrom = $forceDate;
} elseif ($everifinSettings['last_check'] ?? false) {
$dateFrom = \DateTime::createFromFormat('Y-m-d\TH:i:s.vP', $everifinSettings['last_check']);
} else {
$dateFrom = new \DateTime();
}
$dateFrom->modify('+1 ms');
$dateFrom = $dateFrom->format('Y-m-d\TH:i:s.vP');
$currentPage = 1;
$maxPage = 100;
$transactions = [];
// Refresh token if it's expired or expires soon
if (time() + 30 >= $this->config['expires_in']) {
if (isProduction()) {
$this->refreshToken();
}
}
// download paymentsl
try {
$downloadResponse = $this->curlUtil->getClient(['Authorization' => "Bearer {$this->config['access_token']}"], ['max_duration' => 30])
->request('GET', self::REFRESH_TRANSACTION_URL);
$downloaded = $downloadResponse->toArray();
$this->logger->notice('Everifin API: refresh payments', ['data' => $downloaded]);
if (($downloaded['meta']['status'] ?? false) != 'SUCCESS') {
addActivityLog(ActivityLog::SEVERITY_ERROR,
ActivityLog::TYPE_SYNC,
'Everifin API: Nepodařilo se aktualizovat platby',
['error' => 'Refresh token expired'],
[BankAutoPaymentBundle::LOG_TAG_EVERIFIN]);
}
if (isset($downloaded['data']) && $downloaded['data'] == []) {
addActivityLog(ActivityLog::SEVERITY_ERROR,
ActivityLog::TYPE_SYNC,
'Everifin API: S Everifinem není propojen žádný bankovní účet. Obnovte připojení mezi Everifinem a bankou.',
['error' => 'No bank account connected'], [BankAutoPaymentBundle::LOG_TAG_EVERIFIN]
);
}
} catch (ClientExceptionInterface|RedirectionExceptionInterface|ServerExceptionInterface|TransportExceptionInterface|DecodingExceptionInterface $e) {
$this->logger->notice('Everifin API: refresh payments error', ['error' => $e->getMessage()]);
}
foreach (range(1, $maxPage) as $page) {
if ($page > $maxPage) {
break;
}
try {
$request = $this->makePaymentsRequest($dateFrom, $page);
$payments = $request->toArray();
$this->logger->notice('Everifin API: payments', ['data' => $payments]);
} catch (ClientExceptionInterface|RedirectionExceptionInterface|ServerExceptionInterface|TransportExceptionInterface|DecodingExceptionInterface $e) {
// Unauthorized, probably expired token
if ($e->getCode() == 401) {
if (isProduction()) {
$this->refreshToken();
}
// TODO: fix
// try current page again
// $page--;
continue;
}
$this->logger->notice('Everifin API: refresh payments error', ['error' => $e->getMessage()]);
return false;
}
$maxPage = $payments['meta']['last'];
$transactions = [...$transactions, ...$payments['data']];
}
$this->logger->notice('Everifin API: payments downloaded', ['data' => $transactions]);
foreach ($transactions as $transaction) {
$bankTransaction = (new BankTransaction())
->setType($transaction['type'])
->setPrice($transaction['amount'])
->setCurrency($transaction['currency'])
->setId($transaction['id'])
->setIban($transaction['userIban'])
->setVariableSymbol($transaction['variableSymbol'])
->setRef($transaction['reference'])
->setMsg($transaction['description']);
if ($startTimestamp === null) {
$startTimestamp = \DateTime::createFromFormat('Y-m-d\TH:i:s.vP', $transaction['createdAt']);
} else {
$currentTimestamp = \DateTime::createFromFormat('Y-m-d\TH:i:s.vP', $transaction['createdAt']);
if ($currentTimestamp > $startTimestamp) {
$startTimestamp = $currentTimestamp;
}
}
$this->checkPayment($bankTransaction);
}
if ($startTimestamp !== null) {
$this->saveSettings(['last_check' => $startTimestamp->format('Y-m-d\TH:i:s.vP')]);
}
return true;
}
protected function loadConfig()
{
if (!empty($this->config)) {
return $this->config;
}
$settings = \Settings::getDefault();
return $this->config = array_merge($settings->payments['Everifin'] ?? [], $settings->loadValue('everifin') ?? []);
}
protected function refreshToken()
{
if (time() >= $this->config['refresh_expires_in']) {
addActivityLog(ActivityLog::SEVERITY_ERROR,
ActivityLog::TYPE_SYNC,
'Everifin API: Vypršel přihlašovací token, obnovte přihlášení v nastavení eshopu',
['error' => 'Refresh token expired'],
[BankAutoPaymentBundle::LOG_TAG_EVERIFIN]);
throw new EverifinPaymentException('Refresh token expired');
}
$refreshBody = [
'grant_type' => 'refresh_token',
'client_id' => $this->getClientId(),
'client_secret' => $this->getClientSecret(),
'refresh_token' => $this->config['refresh_token'],
];
$this->logger->notice('Everifin API: refreshing token with body', ['data' => $refreshBody, 'config' => $this->config]);
$curlResponse = $this->curlUtil->getClient([
'Content-Type' => 'application/x-www-form-urlencoded',
])->request('POST', 'https://app.everifin.com/auth/realms/everifin_app/protocol/openid-connect/token', [
'body' => $refreshBody,
]);
$curlResponse = $curlResponse->toArray();
$this->logger->notice('Everifin API: refresh token', ['data' => $curlResponse]);
$this->saveSettings($curlResponse, true);
$this->loadConfig();
}
public function saveSettings(array $data, bool $updateTimestamp = false): void
{
$settings = \Settings::getDefault();
$oldState = $settings->loadValue('everifin') ?? [];
// Set new timestamps only when saving new tokens
if ($updateTimestamp) {
// Calculate expiration time
$data['expires_in'] = time() + $data['expires_in'];
$data['refresh_expires_in'] = time() + $data['refresh_expires_in'];
}
$data = array_merge($oldState, $data);
$settings->saveValue('everifin', $data, false);
}
protected function makePaymentsRequest($dateFrom, $page = 1): ResponseInterface
{
return $this->curlUtil->getClient(['Authorization' => "Bearer {$this->config['access_token']}"])
->request('GET', self::TRANSACTION_URL, [
'query' => [
'sort' => 'createdAt:desc',
'countPerPage' => 100,
'direction' => 'IN',
'createdFrom' => $dateFrom,
'page' => $page,
],
]);
}
public function logout()
{
$this->loadConfig();
$response = $this->curlUtil->getClient([
'Content-Type' => 'application/x-www-form-urlencoded',
])->request('POST', self::LOGOUT_URL, [
'body' => [
'client_id' => $this->getClientId(),
'client_secret' => $this->getClientSecret(),
'refresh_token' => $this->config['refresh_token'],
],
]);
if ($response->getStatusCode() == 204) {
$settings = \Settings::getDefault();
$settings->saveValue('everifin', [], false);
} else {
throw new EverifinPaymentException('Logout failed');
}
}
public function checkEverifinLogin(): void
{
if (!$this->loadConfig()) {
return;
}
// Nothing to do
if (empty($this->config['refresh_expires_in'])) {
return;
}
$timeDiff = $this->config['refresh_expires_in'] - time();
// Less than X days remaining to relogin
if ($timeDiff < self::LOGIN_EXPIRATION_DIFF) {
// Calculate remaining days
$days = (int) floor($timeDiff / 86400);
addActivityLog(ActivityLog::SEVERITY_ERROR, ActivityLog::TYPE_SYNC,
"Everifin: k vypršení přihlášení zbývá {$days} dní", ['timeDiff' => $timeDiff], [$this::getLogTag()]);
}
}
public function getClientId(): ?string
{
if (!$this->loadConfig()) {
return null;
}
return $this->config['clientId'] ?? null;
}
public function getClientSecret(): ?string
{
if (!$this->loadConfig()) {
return null;
}
return $this->config['clientSecret'] ?? null;
}
#[Required]
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
}