first commit
This commit is contained in:
424
class/payments/class.Essox2Splatky.php
Normal file
424
class/payments/class.Essox2Splatky.php
Normal file
@@ -0,0 +1,424 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user