Files
kupshop/class/payments/class.Essox2Splatky.php
2025-08-02 16:30:27 +02:00

425 lines
15 KiB
PHP

<?php
declare(strict_types=1);
use KupShop\AdminBundle\Util\ActivityLog;
use KupShop\KupShopBundle\Config;
use KupShop\KupShopBundle\Query\JsonOperator;
use KupShop\KupShopBundle\Util\StringUtil;
use KupShop\OrderingBundle\Exception\PaymentException;
use Query\Operator;
// Dokumentace ESSOX API: https://drive.google.com/drive/folders/1j7OFhsrUl1F3ZQt3Lo7d8yOyvLW7ioN5?usp=sharing
class Essox2Splatky extends Payment
{
public static $name = 'Essox - splátky';
public $class = 'Essox2Splatky';
protected string $apiUrl = 'https://apiv32.essox.cz';
protected string $apiTestUrl = 'https://testapiv32.essox.cz';
protected $templateOrderView = 'payment.Essox2Splatky.orderView.tpl';
protected $pay_method = Payment::METHOD_ONLINE;
public function getPaymentUrl()
{
return createScriptURL([
's' => 'payment',
'IDo' => $this->order->id,
'cf' => $this->order->getSecurityCode(),
'step' => 1,
'class' => $this->class,
'absolute' => true,
]);
}
protected function getSpreadedInstalments(): bool
{
return false;
}
public function processStep_1()
{
$proposalData = $this->getProposalData();
$redirectData = $this->getProposalRedirect($proposalData);
$contractId = $redirectData['contractId'];
$this->createPayment($contractId, $proposalData['price'], ['paymentClass' => $this->class]);
redirection($redirectData['redirectionUrl']);
}
/** Return from gateway */
public function processStep_2()
{
$this->info(translate('payment_waiting_for_confirmation', 'payment'));
}
public function checkPaidOrders()
{
$orderPayments = sqlQueryBuilder()->select('op.id, op.id_order, op.payment_data')
->from('order_payments', 'op')
->where(\Query\Operator::inIntArray([
static::STATUS_CREATED,
static::STATUS_PENDING,
static::STATUS_UNKNOWN,
], 'op.status'))
->andWhere('op.date > (DATE_SUB(CURDATE(), INTERVAL 1 MONTH))')
->andWhere(Operator::inStringArray([$this->class],
JsonOperator::value('op.payment_data', 'paymentClass')))
->andWhere(JsonOperator::exists('op.payment_data', 'session'))
->execute();
foreach ($orderPayments as $orderPayment) {
$paymentData = json_decode($orderPayment['payment_data'], true);
if (empty($paymentData['session'])) {
continue;
}
$this->setOrder($orderPayment['id_order']);
$contractId = (int) $paymentData['session'];
$response = $this->getPaymentStatus($contractId);
if (!empty($response['errorCollection'])) {
foreach ($response['errorCollection'] as $error) {
addActivityLog(ActivityLog::SEVERITY_ERROR,
ActivityLog::TYPE_COMMUNICATION,
translate('returnFailedMessage', 'orderPayment'),
$error);
}
}
foreach ($response['businessCases'] as $businessCase) {
$businessCaseStatus = (int) $businessCase['contractStatusId'];
switch ($businessCaseStatus) {
case 1: // Zakaz. opustil před odesláním
case 2: // Čeká na posouzení
case 3: // Posuzuje se
case 4: // Odložen
case 18: // Čeká na nahrání dokumentů
case 19: // Čeká na platbu
case 9:
case 13: // Ke kontrole 9,13
$this->setStatus(self::STATUS_PENDING, $contractId);
break;
case 5:
case 10:
case 11: // Reklamace 5,10,11
case 7: // Zamítnuto / Neschválený návrh
$this->setStatus(self::STATUS_STORNO, $contractId);
break;
case 12: // V pořádku doručeno do ESSOXu proplaceno / Zkontrolováno
$newStatus = ($this->config['setPaymentCompletedManually'] ?? false) ? self::STATUS_PENDING : self::STATUS_FINISHED;
$this->setStatus($newStatus, $contractId);
break;
default:
$this->setStatus(self::STATUS_UNKNOWN, $contractId);
}
}
}
}
protected function getPaymentStatus(int $contractId)
{
$response = $this->requestEssoxCurl('/consumergoods/v1/api/consumergoods/status?ContractId='.$contractId, [
'Accept: application/json',
'Authorization: Bearer '.$this->getAccessToken(),
], '', false);
return json_decode($response, true);
}
protected function getAccessToken()
{
$loginHash = md5(($this->config['clientKey'] ?? '').':'.($this->config['clientSecret'] ?? '').':'.($this->config['test'] ?? ''));
$tokenCacheKey = 'payment_essox_access_token'.$loginHash;
$token = getCache($tokenCacheKey);
if (!$token) {
$retrievedToken = $this->retrieveAccessToken();
$token = $retrievedToken['access_token'];
setCache($tokenCacheKey, $token, $retrievedToken['expires_in'] - 60);
}
return $token;
}
protected function retrieveAccessToken(): array
{
$data = [
'grant_type' => 'client_credentials',
'scope' => 'scopeFinit.consumerGoods.eshop',
];
$response = $this->requestEssoxCurl('/token', [
'Content-Type: application/x-www-form-urlencoded',
'accept: application/json',
'Authorization: Basic '.base64_encode($this->config['clientKey'].':'.$this->config['clientSecret']),
], http_build_query($data));
$decodedData = json_decode($response, true);
return ['access_token' => (string) $decodedData['access_token'], 'expires_in' => (int) $decodedData['expires_in']];
}
public function getProposalData(): array
{
$phoneNumber = $this->order->invoice_phone;
$checkPhonePrefixes = ['+420'];
$phonePrefix = '';
foreach ($checkPhonePrefixes as $checkPrefix) {
if (StringUtil::startsWith($phoneNumber, $checkPrefix)) {
$phoneNumber = str_replace($checkPrefix, '', $phoneNumber);
$phonePrefix = $checkPrefix;
break;
}
}
return [
'firstName' => $this->order->invoice_name,
'surname' => $this->order->invoice_surname,
'mobilePhonePrefix' => $phonePrefix,
'mobilePhoneNumber' => $phoneNumber,
'email' => $this->order->invoice_email,
'price' => $this->order->getTotalPrice()->getPriceWithVat()->asFloat(),
'orderId' => $this->order->order_no,
'customerId' => $this->order->id_user,
'transactionId' => 1,
'shippingAddress' => [
'street' => $this->order->delivery_street,
'houseNumber' => '',
'city' => $this->order->delivery_city,
'zip' => $this->order->delivery_zip,
],
'callbackUrl' => $this->getGenericPaymentUrl(2),
'spreadedInstalments' => $this->getSpreadedInstalments(),
];
}
protected function getProposalRedirect($proposalData)
{
$response = $this->requestEssoxCurl('/consumergoods/v1/api/consumergoods/proposal', [
'Content-Type: application/json',
'Authorization: Bearer '.$this->getAccessToken(),
], json_encode($proposalData));
return json_decode($response, true);
}
public function getEssoxCalcDetail(Decimal $price)
{
$price = roundPrice($price, -1, 'DB', 0)->asInteger();
if (!($this->config['productDetail'] ?? false) || $price < ($this->config['minPrice'] ?? 2000) || (!empty($this->config['maxPrice']) && $price > $this->config['maxPrice'])) {
return null;
}
return path('kupshop_ordering_payment_legacypayment', [
'step' => 5,
'class' => $this->class,
'price' => $price,
]);
}
public function processStep_5()
{
$price = roundPrice(getVal('price'), -1, 'DB', 0)->asInteger();
if ($price < ($this->config['minPrice'] ?? 2000) || (!empty($this->config['maxPrice']) && $price > $this->config['maxPrice'])) {
return null;
}
redirection($this->getCalculatorUrl($price));
}
protected function getCalculatorUrl($price)
{
$body = [
'price' => $price,
'productId' => 0,
];
$response = $this->requestEssoxCurl('/consumergoods/v1/api/consumergoods/calculator', [
'Content-Type: application/json',
'accept: application/json',
'Authorization: Bearer '.$this->getAccessToken(),
], json_encode($body));
$decodedData = json_decode($response, true);
return $decodedData['redirectionUrl'];
}
protected function requestEssoxCurl(string $path, array $headers, string $encodedBody, $post = true)
{
$url = $this->getApiUrl().$path;
$initTimeout = 30;
$step = 1;
$ch = curl_init();
if ($post) {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $encodedBody);
}
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_TCP_KEEPALIVE, 1);
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
while ($step <= 2) {
curl_setopt($ch, CURLOPT_TIMEOUT, $initTimeout);
$response = curl_exec($ch);
$responseInfo = curl_getinfo($ch);
$curl_errno = curl_errno($ch);
curl_close($ch);
if ($responseInfo['http_code'] === 0 && $step === 1) {
$initTimeout = 10;
$step++;
continue;
}
if ($responseInfo['http_code'] != 200) {
$debugInfo = ['url' => $url, 'responseInfo' => $responseInfo, 'curl_errno' => $curl_errno, 'RESPONSE' => $response];
$this->handleError($debugInfo);
throw new PaymentException('Komunikace s platební bránou selhala. Zkuste prosím znovu později.');
}
break;
}
return $response;
}
public function accept($totalPrice, $freeDelivery)
{
$price = $totalPrice->getPriceWithVat()->asFloat();
if ($price <= 0 && $this->order) {
$price = $this->order->total_price;
}
// price has to be greater than 2000 Kč according to the documentation
return parent::accept($totalPrice, $freeDelivery) && $price >= 2000;
}
public function hasOnlinePayment()
{
return true;
}
protected function getApiUrl(): string
{
return ($this->config['test'] ?? false) ? $this->apiTestUrl : $this->apiUrl;
}
public static function isEnabled($className)
{
$cfg = Config::get();
if (empty($cfg['Modules']['payments'][$className])) {
return false;
}
return true;
}
public static function getSettingsConfiguration(): array
{
return [
'fields' => [
'clientKey' => [
'title' => 'ID klienta (client_id)',
'type' => 'text',
],
'clientSecret' => [
'title' => 'Secret (clientSecret)',
'type' => 'text',
],
'setPaymentCompletedManually' => [
'title' => 'Manuální potvrzení platby',
'tooltip' => 'Pokud je vypnuto, platba objednávky se nastaví jako uhrazená automaticky po schválení úvěru. Pokud je zapnuto, přepnutí platby na uhrazenou je potřeba udělat manuálně.',
'type' => 'toggle',
],
'minPrice' => [
'title' => 'Minimální částka',
'type' => 'number',
'placeholder' => 2000,
'tooltip' => 'Minimální částka u které tuto možnost zobrazit na detailu produktu. (min. 2000 Kč)',
],
'maxPrice' => [
'title' => 'Maximální částka',
'type' => 'number',
'tooltip' => 'Maximální částka u které tuto možnost zobrazit na detailu produktu.',
],
'productDetail' => [
'title' => 'Zobrazit na detailu produktu',
'type' => 'toggle',
],
'test' => [
'title' => 'Testovací režim',
'type' => 'toggle',
],
],
];
}
protected function handleError($debugInfo)
{
$message = $this->getName().': admin info - Komunikace s platební bránou selhala';
$sentry = false;
$csobFault = false;
switch ($debugInfo['responseInfo']['http_code'] ?? -1) {
case 400:
$reason = 'Nevalidní request. V dotazu chybí povinné pole nebo je v
nevhodném / nevalidním formátu.
Nevalidní uživatel.
Neplatné pověření.
Uživatel není oprávněný používat autorizační typ';
$sentry = true;
break;
case 401:
unset($debugInfo['responseInfo']);
$reason = 'Přístup odepřen (špatné přihlašovací údaje)';
$message .= ' - '.$reason;
break;
case 403:
$reason = 'Klient není oprávněný provádět tento dotaz.';
$sentry = true;
break;
case 500:
$reason = 'Chyba na straně CSOB serveru';
$csobFault = true;
break;
case 503:
$reason = 'CSOB služba je dočasně nedostupná. Může být způsobena přetížením serveru nebo z důvodu údržby';
$csobFault = true;
break;
case 0:
$reason = 'CSOB API timeout';
$csobFault = true;
break;
}
$debugInfo = array_merge(['reason' => $reason ?? 'Neznámá chyba'], $debugInfo);
if ($sentry) {
getRaven()->captureMessage('Essox API error', [], ['extra' => $debugInfo]);
}
if ($csobFault) {
$message .= ' - chyba na straně CSOB';
}
addActivityLog(ActivityLog::SEVERITY_ERROR, ActivityLog::TYPE_COMMUNICATION, $message, $debugInfo);
}
}