Skip to content

Commit 38a7c28

Browse files
author
Leonix
committed
wa-plugins/payment/tinkoff v.1.1.0
* Поддержка оплаты по QR-коду на сайте и в мобильной кассе. * Поддержка централизованного контроля фискализации платежей. * Передача номера заказа в чек об оплате.
1 parent f0a85d7 commit 38a7c28

14 files changed

+222
-32
lines changed

wa-plugins/payment/tinkoff/img/qr-test.svg

Lines changed: 1 addition & 0 deletions
Loading
1.73 KB
Loading
Lines changed: 4 additions & 0 deletions
Loading
341 Bytes
Loading

wa-plugins/payment/tinkoff/lib/config/guide.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,18 @@
55
'value' => '%RELAY_URL%',
66
'title' => 'URL для нотификации по HTTP',
77
'description' => 'Значение настройки URL для оповещения активного протокола<br>
8-
<strong>Указанный в этом поле адрес скопируйте и сохраните в анкете подключения к интернет-эквайрингу банка Тинькофф</strong>',
8+
<strong>Указанный в этом поле адрес скопируйте и сохраните в анкете подключения к интернет-эквайрингу от Т-Кассы (Тинькофф)</strong>',
99
),
1010
array(
1111
'value' => '%RELAY_URL%',
1212
'title' => 'Страница успешного платежа',
1313
'description' => 'URL возврата покупателя обратно на сайт после успешной оплаты<br>
14-
<strong>Указанный в этом поле адрес скопируйте и сохраните в анкете подключения к интернет-эквайрингу банка Тинькофф</strong>',
14+
<strong>Указанный в этом поле адрес скопируйте и сохраните в анкете подключения к интернет-эквайрингу от Т-Кассы (Тинькофф)</strong>',
1515
),
1616
array(
1717
'value' => '%RELAY_URL%',
1818
'title' => 'Страница ошибки оплаты',
1919
'description' => 'URL возврата покупателя обратно на сайт в случае ошибки оплаты<br>
20-
<strong>Указанный в этом поле адрес скопируйте и сохраните в анкете подключения к интернет-эквайрингу банка Тинькофф</strong>',
20+
<strong>Указанный в этом поле адрес скопируйте и сохраните в анкете подключения к интернет-эквайрингу от Т-Кассы (Тинькофф)</strong>',
2121
),
2222
);

wa-plugins/payment/tinkoff/lib/config/plugin.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
<?php
22
return array(
3-
'name' => 'Банк Тинькофф',
4-
'description' => 'Оплата картами VISA, MasterCard и Maestro через интернет-эквайринг банка Тинькофф',
5-
'icon' => 'img/tinkoff16.png',
6-
'logo' => 'img/tinkoff.png',
3+
'name' => 'Т-Касса',
4+
'description' => 'Интернет-эквайринг от «Т-Банка»: банковские карты, СБП, SberPay, T-Pay, MirPay',
5+
'icon' => 'img/tinkoff.svg',
6+
'logo' => 'img/tinkoff.png?v2',
77
'vendor' => 'webasyst',
8-
'version' => '1.0.22',
8+
'version' => '1.1.0',
99
'type' => waPayment::TYPE_ONLINE,
1010
'partial_refund' => true,
1111
'partial_capture' => true,

wa-plugins/payment/tinkoff/lib/config/requirements.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php
22
return array(
33
'app.installer' => array(
4-
'version' => '>=1.13',
4+
'version' => '>=3.2.0',
55
'strict' => true,
66
),
77
'php.hash' => array(

wa-plugins/payment/tinkoff/lib/config/settings.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@
1010
'value' => '',
1111
'title' => /*_wp*/('Пароль'),
1212
'control_type' => waHtmlControl::PASSWORD,
13+
'description' => <<<HTML
14+
<span class="js-tkassa-registration-link" style="background-color: #fea; display: block; margin: 10px 0; padding: 10px 15px; font-weight: normal; font-size: 14px;color: black; width: 80%; border-radius: 8px;">
15+
Подключайтесь к Т-Кассе <a href="https://www.tbank.ru/kassa/form/partner/webasyst" target="_blank" style="color: #09f;"><b>через Webasyst по этой ссылке</b></a> и получите данные для заполнения Terminal ID и пароля.
16+
</span>
17+
HTML
18+
,
1319
),
1420
'currency_id' => array(
1521
'value' => '',
@@ -23,7 +29,7 @@
2329
'two_steps' => array(
2430
'value' => false,
2531
'title' => 'Схема подключения',
26-
'description' => /*_wp*/('Вариант обработки платежей, выбранный при заключении договора с банком Тинькофф.<br>Двухстадийную схему подключения можно использовать только с поддерживаемым приложением, например, Shop-Script версии не ниже 8.6.'),
32+
'description' => /*_wp*/('Вариант обработки платежей, выбранный при заключении договора с Т-Кассой.<br>Двухстадийную схему подключения можно использовать только с поддерживаемым приложением, например, Shop-Script версии не ниже 8.6.'),
2733
'control_type' => waHtmlControl::RADIOGROUP,
2834
'options' => array(
2935
'0' => 'Одностадийная',
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
22
"support_premium": "yes",
3-
"support_premium_description": "<p><strong>Дробное количество</strong> товаров передаётся в «Тинькофф Банк» <em>точно так, как указано в заказе</em>.</p><p><strong>Единицы измерения</strong> количества товаров <em>не передаются</em> — такая возможность не предусмотрена платёжной системой.</p>"
3+
"support_premium_description": "<p><strong>Дробное количество</strong> товаров передаётся в «Т-Кассу» <em>точно так, как указано в заказе</em>.</p><p><strong>Единицы измерения</strong> количества товаров <em>не передаются</em> — такая возможность не предусмотрена платёжной системой.</p>"
44
}

wa-plugins/payment/tinkoff/lib/tinkoffPayment.class.php

Lines changed: 191 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
* @property-read string $payment_method_type
2323
*
2424
*/
25-
class tinkoffPayment extends waPayment implements waIPayment, waIPaymentRefund, waIPaymentRecurrent, waIPaymentCancel, waIPaymentCapture
25+
class tinkoffPayment extends waPayment implements waIPayment, waIPaymentRefund, waIPaymentRecurrent, waIPaymentCancel, waIPaymentCapture, waIPaymentImage
2626
{
2727
private $order_id;
2828
private $receipt;
@@ -163,17 +163,25 @@ private function genToken($args)
163163
* @throws waPaymentException
164164
*/
165165
private function checkToken($args)
166+
{
167+
$token = ifset($args, 'Token', false);
168+
unset($args['Token']);
169+
170+
$expected_token = $this->calculateToken($args);
171+
172+
if (empty($token) || ($token !== $expected_token)) {
173+
throw new waPaymentException('Invalid token');
174+
}
175+
}
176+
177+
private function calculateToken($args)
166178
{
167179
$args['Password'] = trim($this->getSettings('terminal_password'));
168180

169181
if (!strlen($args['Password'])) {
170182
throw new waPaymentException('Password misconfiguration');
171183
}
172184

173-
$token = ifset($args, 'Token', false);
174-
unset($args['Token']);
175-
176-
177185
ksort($args);
178186
foreach ($args as $k => &$arg) {
179187
if (is_bool($arg)) {
@@ -184,17 +192,12 @@ private function checkToken($args)
184192
}
185193
unset($arg);
186194

187-
$expected_token = hash('sha256', implode('', $args));
188-
189-
if (empty($token) || ($token !== $expected_token)) {
190-
throw new waPaymentException('Invalid token');
191-
}
195+
return hash('sha256', implode('', $args));
192196
}
193197

194198
protected function callbackInit($request)
195199
{
196200
$request = $this->sanitizeRequest($request);
197-
198201
$pattern = '/^([a-z]+)_(\d+)_(.+)$/';
199202
if (!empty($request['OrderId']) && preg_match($pattern, $request['OrderId'], $match)) {
200203
$this->app_id = $match[1];
@@ -314,13 +317,20 @@ protected function callbackHandler($data)
314317
// Verify token
315318
$this->checkToken($data);
316319

320+
if (isset($data['SBPQR'])) {
321+
$this->sbpQrImage($data);
322+
exit;
323+
}
324+
317325
$transaction_data = $this->formalizeData($data);
318326

319327
$app_payment_method = null;
328+
$declare_fiscalization = false;
320329

321330
switch ($transaction_data['type']) {
322331
case self::OPERATION_AUTH_ONLY:
323332
if ($transaction_data['result']) {
333+
$declare_fiscalization = true;
324334
$app_payment_method = self::CALLBACK_AUTH;
325335
} else {
326336
$app_payment_method = self::CALLBACK_DECLINE;
@@ -329,6 +339,7 @@ protected function callbackHandler($data)
329339

330340
case self::OPERATION_AUTH_CAPTURE:
331341
if ($transaction_data['result']) {
342+
$declare_fiscalization = true;
332343
$app_payment_method = self::CALLBACK_PAYMENT;
333344
} else {
334345
$app_payment_method = self::CALLBACK_DECLINE;
@@ -340,6 +351,7 @@ protected function callbackHandler($data)
340351
break;
341352

342353
case self::OPERATION_CAPTURE:
354+
$declare_fiscalization = true;
343355
$app_payment_method = self::CALLBACK_CAPTURE;
344356
break;
345357

@@ -370,6 +382,10 @@ protected function callbackHandler($data)
370382
//Save transaction and run app callback only if it not repeated callback;
371383
$transaction_data = $this->saveTransaction($transaction_data, $data);
372384
$this->execAppCallback($app_payment_method, $transaction_data);
385+
386+
if ($declare_fiscalization && $this->getSettings('check_data_tax')) {
387+
$this->getAdapter()->declareFiscalization($transaction_data['order_id'], $this, ['id' => $transaction_data['native_id']]);
388+
}
373389
} else {
374390
$log = array(
375391
'message' => 'silent skip callback as repeated',
@@ -417,7 +433,10 @@ public function refund($transaction_raw_data)
417433
}
418434

419435
$res = $this->apiQuery('Cancel', $args);
420-
436+
if (in_array(ifset($res['Status']), ['ASYNC_REFUNDING', 'REFUNDING'])) {
437+
sleep(1);
438+
$res = $this->apiQuery('GetState', ['PaymentId' => $args['PaymentId']]);
439+
}
421440

422441
$response = array(
423442
'result' => 0,
@@ -472,7 +491,7 @@ public function refund($transaction_raw_data)
472491
return $response;
473492
} catch (Exception $ex) {
474493
$message = sprintf("Error occurred during %s: %s", __METHOD__, $ex->getMessage());
475-
self::log($this->id, $message);
494+
self::log($this->id, [$message, $ex->getTraceAsString()]);
476495
return array(
477496
'result' => -1,
478497
'data' => null,
@@ -544,6 +563,156 @@ public function recurrent($order_data)
544563

545564
}
546565

566+
public function sbp($order_data)
567+
{
568+
$order_data = waOrder::factory($order_data);
569+
570+
// https://www.tbank.ru/kassa/dev/payments/#tag/Oplata-cherez-SBP
571+
$args = array(
572+
'Amount' => round($order_data['amount'] * 100),
573+
'Currency' => ifset(self::$currencies[$this->currency_id]),
574+
'OrderId' => $this->app_id.'_'.$this->merchant_id.'_'.$order_data['order_id'],
575+
'Description' => ifempty($order_data, 'description', ''),
576+
'PayType' => $this->two_steps ? 'T' : 'O',
577+
'DATA' => [],
578+
);
579+
580+
if ($this->getSettings('check_data_tax')) {
581+
$full_order_data = $order_data;
582+
if (!$full_order_data->items) {
583+
$full_order_data = $this->getAdapter()->getOrderData($order_data['order_id']);
584+
}
585+
$args['Receipt'] = $this->getReceiptData($full_order_data, $this);
586+
if (!$args['Receipt']) {
587+
return 'Данный вариант платежа недоступен. Воспользуйтесь другим способом оплаты.';
588+
}
589+
}
590+
591+
if (!empty($order_data['customer_contact_id'])) {
592+
$args['CustomerKey'] = $order_data['customer_contact_id'];
593+
try {
594+
$c = new waContact($order_data['customer_contact_id']);
595+
$email = $c->get('email', 'default');
596+
$phone = $c->get('phone', 'default');
597+
} catch (waException $e) {
598+
// contact is deleted
599+
}
600+
if (empty($email)) {
601+
//$email = $this->getDefaultEmail();
602+
}
603+
if (!empty($email)) {
604+
$args['DATA']['Email'] = $email;
605+
}
606+
if (!empty($phone)) {
607+
$args['DATA']['Phone'] = $phone;
608+
}
609+
}
610+
if (empty($args['DATA'])) {
611+
unset($args['DATA']);
612+
}
613+
614+
try {
615+
$payment_id = null;
616+
$cache_key = 'tinkoff/sbp/' . md5('SBP'.$args['OrderId'].$args['Amount']);
617+
$cache = new waSerializeCache($cache_key, -1, $this->app_id);
618+
if ($cache->isCached()) {
619+
$payment_id = $cache->get();
620+
$check_payment_data = $this->apiQuery('GetState', ['PaymentId' => $payment_id]);
621+
if (ifset($check_payment_data, 'ErrorCode', 0) != 0 || !in_array(ifset($check_payment_data, 'State', ''), ['NEW', 'FORM_SHOWED'])) {
622+
unset($payment_id);
623+
}
624+
}
625+
626+
if (empty($payment_id)) {
627+
$payment_data = $this->apiQuery('Init', $args);
628+
$payment_id = ifset($payment_data, 'PaymentId', '');
629+
}
630+
631+
if (empty($payment_id)) {
632+
$cache->delete();
633+
return null;
634+
} else {
635+
$cache->set($payment_id);
636+
}
637+
638+
if ($this->isTestMode()) {
639+
try {
640+
// Запрашивает успешную оплату по СБП для текущего счёта
641+
// https://www.tbank.ru/kassa/dev/payments/#tag/Oplata-cherez-SBP/operation/SbpPayTest
642+
$test_sbp_result = $this->apiQuery('SbpPayTest', [
643+
'PaymentId' => $payment_id,
644+
]);
645+
} catch (Exception $ex) {
646+
self::log($this->id, ['Unable create test QR code, using hardcoded stub', $ex->getMessage(), $ex->getTraceAsString()]);
647+
return [
648+
'svg' => file_get_contents($this->path.'/img/qr-test.svg'),
649+
'url' => wa()->getRootUrl().'wa-plugins/payment/tinkoff/img/qr-test.svg',
650+
];
651+
}
652+
}
653+
654+
$qr_data = $this->apiQuery('GetQr', [
655+
'PaymentId' => $payment_id,
656+
'DataType' => 'IMAGE'
657+
]);
658+
if (ifset($qr_data, 'Success', false)) {
659+
$qr_link = $this->apiQuery('GetQr', [
660+
'PaymentId' => $payment_id,
661+
'DataType' => 'PAYLOAD'
662+
]);
663+
664+
if (ifset($qr_link, 'Success', false)) {
665+
return [
666+
'svg' => $qr_data['Data'],
667+
'url' => $qr_link['Data'],
668+
];
669+
}
670+
}
671+
$cache->delete();
672+
return null;
673+
} catch (Exception $ex) {
674+
self::log($this->id, [$ex->getMessage(), $ex->getTraceAsString()]);
675+
$cache->delete();
676+
return false;
677+
}
678+
}
679+
680+
private function sbpQrImage($params)
681+
{
682+
$order_data = [
683+
'order_id' => $this->order_id,
684+
'amount' => $params['amount'],
685+
'customer_contact_id' => $params['customer_contact_id'],
686+
];
687+
688+
$sbp = $this->sbp($order_data);
689+
if (empty($sbp['svg'])) {
690+
throw new waException('Не удалось получить QR-код');
691+
}
692+
693+
$response = wa()->getResponse();
694+
$response->addHeader('Content-Type', 'image/svg+xml', true);
695+
echo $sbp['svg'];
696+
exit;
697+
}
698+
699+
public function image($order_data)
700+
{
701+
$args = array(
702+
'OrderId' => $this->app_id.'_'.$this->merchant_id.'_'.$order_data['order_id'],
703+
'amount' => $order_data['amount'],
704+
'description' => ifempty($order_data, 'description', ''),
705+
'customer_contact_id' => $order_data['customer_contact_id'],
706+
'SBPQR' => 1,
707+
);
708+
$args['Token'] = $this->calculateToken($args);
709+
return [
710+
// At least one of keys `image_url` and `image_data_url` is required. Both are ok, too.
711+
'image_url' => wa()->getRootUrl(true) . 'payments.php/tinkoff/?' . http_build_query($args),
712+
//'image_data_url' => 'data:image/png;base64,........',
713+
];
714+
}
715+
547716
public function cancel($transaction_raw_data)
548717
{
549718
try {
@@ -553,6 +722,10 @@ public function cancel($transaction_raw_data)
553722
);
554723

555724
$data = $this->apiQuery('Cancel', $args);
725+
if (in_array(ifset($data['Status']), ['ASYNC_REFUNDING', 'REFUNDING'])) {
726+
sleep(1);
727+
$data = $this->apiQuery('GetState', ['PaymentId' => $args['PaymentId']]);
728+
}
556729
$transaction_data = $this->formalizeData($data);
557730

558731
$this->saveTransaction($transaction_data, $data);
@@ -565,7 +738,7 @@ public function cancel($transaction_raw_data)
565738

566739
} catch (Exception $ex) {
567740
$message = sprintf("Error occurred during %s: %s", __METHOD__, $ex->getMessage());
568-
self::log($this->id, $message);
741+
self::log($this->id, [$message, $ex->getTraceAsString()]);
569742
return array(
570743
'result' => -1,
571744
'description' => $ex->getMessage(),
@@ -940,6 +1113,10 @@ private function getReceiptData(waOrder $order)
9401113
'Items' => array(),
9411114
'Taxation' => $this->getSettings('taxation'),
9421115
'Email' => $email,
1116+
'AddUserProp' => [
1117+
'Name' => 'Номер заказа',
1118+
'Value' => $order->id_str
1119+
]
9431120
);
9441121
if ($phone = $order->getContactField('phone')) {
9451122
$this->receipt['Phone'] = sprintf('+%s', preg_replace('/^8/', '7', $phone));
Binary file not shown.

0 commit comments

Comments
 (0)