Skip to content

Commit

Permalink
wa-plugins/payment/tinkoff v.1.1.0
Browse files Browse the repository at this point in the history
  * Поддержка оплаты по QR-коду на сайте и в мобильной кассе.
  * Поддержка централизованного контроля фискализации платежей.
  * Передача номера заказа в чек об оплате.
  • Loading branch information
Leonix committed Sep 24, 2024
1 parent f0a85d7 commit 38a7c28
Show file tree
Hide file tree
Showing 14 changed files with 222 additions and 32 deletions.
1 change: 1 addition & 0 deletions wa-plugins/payment/tinkoff/img/qr-test.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified wa-plugins/payment/tinkoff/img/tinkoff.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions wa-plugins/payment/tinkoff/img/tinkoff.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified wa-plugins/payment/tinkoff/img/tinkoff16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions wa-plugins/payment/tinkoff/lib/config/guide.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@
'value' => '%RELAY_URL%',
'title' => 'URL для нотификации по HTTP',
'description' => 'Значение настройки URL для оповещения активного протокола<br>
<strong>Указанный в этом поле адрес скопируйте и сохраните в анкете подключения к интернет-эквайрингу банка Тинькофф</strong>',
<strong>Указанный в этом поле адрес скопируйте и сохраните в анкете подключения к интернет-эквайрингу от Т-Кассы (Тинькофф)</strong>',
),
array(
'value' => '%RELAY_URL%',
'title' => 'Страница успешного платежа',
'description' => 'URL возврата покупателя обратно на сайт после успешной оплаты<br>
<strong>Указанный в этом поле адрес скопируйте и сохраните в анкете подключения к интернет-эквайрингу банка Тинькофф</strong>',
<strong>Указанный в этом поле адрес скопируйте и сохраните в анкете подключения к интернет-эквайрингу от Т-Кассы (Тинькофф)</strong>',
),
array(
'value' => '%RELAY_URL%',
'title' => 'Страница ошибки оплаты',
'description' => 'URL возврата покупателя обратно на сайт в случае ошибки оплаты<br>
<strong>Указанный в этом поле адрес скопируйте и сохраните в анкете подключения к интернет-эквайрингу банка Тинькофф</strong>',
<strong>Указанный в этом поле адрес скопируйте и сохраните в анкете подключения к интернет-эквайрингу от Т-Кассы (Тинькофф)</strong>',
),
);
10 changes: 5 additions & 5 deletions wa-plugins/payment/tinkoff/lib/config/plugin.php
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<?php
return array(
'name' => 'Банк Тинькофф',
'description' => 'Оплата картами VISA, MasterCard и Maestro через интернет-эквайринг банка Тинькофф',
'icon' => 'img/tinkoff16.png',
'logo' => 'img/tinkoff.png',
'name' => 'Т-Касса',
'description' => 'Интернет-эквайринг от «Т-Банка»: банковские карты, СБП, SberPay, T-Pay, MirPay',
'icon' => 'img/tinkoff.svg',
'logo' => 'img/tinkoff.png?v2',
'vendor' => 'webasyst',
'version' => '1.0.22',
'version' => '1.1.0',
'type' => waPayment::TYPE_ONLINE,
'partial_refund' => true,
'partial_capture' => true,
Expand Down
2 changes: 1 addition & 1 deletion wa-plugins/payment/tinkoff/lib/config/requirements.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?php
return array(
'app.installer' => array(
'version' => '>=1.13',
'version' => '>=3.2.0',
'strict' => true,
),
'php.hash' => array(
Expand Down
8 changes: 7 additions & 1 deletion wa-plugins/payment/tinkoff/lib/config/settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
'value' => '',
'title' => /*_wp*/('Пароль'),
'control_type' => waHtmlControl::PASSWORD,
'description' => <<<HTML
<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;">
Подключайтесь к Т-Кассе <a href="https://www.tbank.ru/kassa/form/partner/webasyst" target="_blank" style="color: #09f;"><b>через Webasyst по этой ссылке</b></a> и получите данные для заполнения Terminal ID и пароля.
</span>
HTML
,
),
'currency_id' => array(
'value' => '',
Expand All @@ -23,7 +29,7 @@
'two_steps' => array(
'value' => false,
'title' => 'Схема подключения',
'description' => /*_wp*/('Вариант обработки платежей, выбранный при заключении договора с банком Тинькофф.<br>Двухстадийную схему подключения можно использовать только с поддерживаемым приложением, например, Shop-Script версии не ниже 8.6.'),
'description' => /*_wp*/('Вариант обработки платежей, выбранный при заключении договора с Т-Кассой.<br>Двухстадийную схему подключения можно использовать только с поддерживаемым приложением, например, Shop-Script версии не ниже 8.6.'),
'control_type' => waHtmlControl::RADIOGROUP,
'options' => array(
'0' => 'Одностадийная',
Expand Down
2 changes: 1 addition & 1 deletion wa-plugins/payment/tinkoff/lib/config/shop_support.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"support_premium": "yes",
"support_premium_description": "<p><strong>Дробное количество</strong> товаров передаётся в «Тинькофф Банк» <em>точно так, как указано в заказе</em>.</p><p><strong>Единицы измерения</strong> количества товаров <em>не передаются</em> — такая возможность не предусмотрена платёжной системой.</p>"
"support_premium_description": "<p><strong>Дробное количество</strong> товаров передаётся в «Т-Кассу» <em>точно так, как указано в заказе</em>.</p><p><strong>Единицы измерения</strong> количества товаров <em>не передаются</em> — такая возможность не предусмотрена платёжной системой.</p>"
}
205 changes: 191 additions & 14 deletions wa-plugins/payment/tinkoff/lib/tinkoffPayment.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
* @property-read string $payment_method_type
*
*/
class tinkoffPayment extends waPayment implements waIPayment, waIPaymentRefund, waIPaymentRecurrent, waIPaymentCancel, waIPaymentCapture
class tinkoffPayment extends waPayment implements waIPayment, waIPaymentRefund, waIPaymentRecurrent, waIPaymentCancel, waIPaymentCapture, waIPaymentImage
{
private $order_id;
private $receipt;
Expand Down Expand Up @@ -163,17 +163,25 @@ private function genToken($args)
* @throws waPaymentException
*/
private function checkToken($args)
{
$token = ifset($args, 'Token', false);
unset($args['Token']);

$expected_token = $this->calculateToken($args);

if (empty($token) || ($token !== $expected_token)) {
throw new waPaymentException('Invalid token');
}
}

private function calculateToken($args)
{
$args['Password'] = trim($this->getSettings('terminal_password'));

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

$token = ifset($args, 'Token', false);
unset($args['Token']);


ksort($args);
foreach ($args as $k => &$arg) {
if (is_bool($arg)) {
Expand All @@ -184,17 +192,12 @@ private function checkToken($args)
}
unset($arg);

$expected_token = hash('sha256', implode('', $args));

if (empty($token) || ($token !== $expected_token)) {
throw new waPaymentException('Invalid token');
}
return hash('sha256', implode('', $args));
}

protected function callbackInit($request)
{
$request = $this->sanitizeRequest($request);

$pattern = '/^([a-z]+)_(\d+)_(.+)$/';
if (!empty($request['OrderId']) && preg_match($pattern, $request['OrderId'], $match)) {
$this->app_id = $match[1];
Expand Down Expand Up @@ -314,13 +317,20 @@ protected function callbackHandler($data)
// Verify token
$this->checkToken($data);

if (isset($data['SBPQR'])) {
$this->sbpQrImage($data);
exit;
}

$transaction_data = $this->formalizeData($data);

$app_payment_method = null;
$declare_fiscalization = false;

switch ($transaction_data['type']) {
case self::OPERATION_AUTH_ONLY:
if ($transaction_data['result']) {
$declare_fiscalization = true;
$app_payment_method = self::CALLBACK_AUTH;
} else {
$app_payment_method = self::CALLBACK_DECLINE;
Expand All @@ -329,6 +339,7 @@ protected function callbackHandler($data)

case self::OPERATION_AUTH_CAPTURE:
if ($transaction_data['result']) {
$declare_fiscalization = true;
$app_payment_method = self::CALLBACK_PAYMENT;
} else {
$app_payment_method = self::CALLBACK_DECLINE;
Expand All @@ -340,6 +351,7 @@ protected function callbackHandler($data)
break;

case self::OPERATION_CAPTURE:
$declare_fiscalization = true;
$app_payment_method = self::CALLBACK_CAPTURE;
break;

Expand Down Expand Up @@ -370,6 +382,10 @@ protected function callbackHandler($data)
//Save transaction and run app callback only if it not repeated callback;
$transaction_data = $this->saveTransaction($transaction_data, $data);
$this->execAppCallback($app_payment_method, $transaction_data);

if ($declare_fiscalization && $this->getSettings('check_data_tax')) {
$this->getAdapter()->declareFiscalization($transaction_data['order_id'], $this, ['id' => $transaction_data['native_id']]);
}
} else {
$log = array(
'message' => 'silent skip callback as repeated',
Expand Down Expand Up @@ -417,7 +433,10 @@ public function refund($transaction_raw_data)
}

$res = $this->apiQuery('Cancel', $args);

if (in_array(ifset($res['Status']), ['ASYNC_REFUNDING', 'REFUNDING'])) {
sleep(1);
$res = $this->apiQuery('GetState', ['PaymentId' => $args['PaymentId']]);
}

$response = array(
'result' => 0,
Expand Down Expand Up @@ -472,7 +491,7 @@ public function refund($transaction_raw_data)
return $response;
} catch (Exception $ex) {
$message = sprintf("Error occurred during %s: %s", __METHOD__, $ex->getMessage());
self::log($this->id, $message);
self::log($this->id, [$message, $ex->getTraceAsString()]);
return array(
'result' => -1,
'data' => null,
Expand Down Expand Up @@ -544,6 +563,156 @@ public function recurrent($order_data)

}

public function sbp($order_data)
{
$order_data = waOrder::factory($order_data);

// https://www.tbank.ru/kassa/dev/payments/#tag/Oplata-cherez-SBP
$args = array(
'Amount' => round($order_data['amount'] * 100),
'Currency' => ifset(self::$currencies[$this->currency_id]),
'OrderId' => $this->app_id.'_'.$this->merchant_id.'_'.$order_data['order_id'],
'Description' => ifempty($order_data, 'description', ''),
'PayType' => $this->two_steps ? 'T' : 'O',
'DATA' => [],
);

if ($this->getSettings('check_data_tax')) {
$full_order_data = $order_data;
if (!$full_order_data->items) {
$full_order_data = $this->getAdapter()->getOrderData($order_data['order_id']);
}
$args['Receipt'] = $this->getReceiptData($full_order_data, $this);
if (!$args['Receipt']) {
return 'Данный вариант платежа недоступен. Воспользуйтесь другим способом оплаты.';
}
}

if (!empty($order_data['customer_contact_id'])) {
$args['CustomerKey'] = $order_data['customer_contact_id'];
try {
$c = new waContact($order_data['customer_contact_id']);
$email = $c->get('email', 'default');
$phone = $c->get('phone', 'default');
} catch (waException $e) {
// contact is deleted
}
if (empty($email)) {
//$email = $this->getDefaultEmail();
}
if (!empty($email)) {
$args['DATA']['Email'] = $email;
}
if (!empty($phone)) {
$args['DATA']['Phone'] = $phone;
}
}
if (empty($args['DATA'])) {
unset($args['DATA']);
}

try {
$payment_id = null;
$cache_key = 'tinkoff/sbp/' . md5('SBP'.$args['OrderId'].$args['Amount']);
$cache = new waSerializeCache($cache_key, -1, $this->app_id);
if ($cache->isCached()) {
$payment_id = $cache->get();
$check_payment_data = $this->apiQuery('GetState', ['PaymentId' => $payment_id]);
if (ifset($check_payment_data, 'ErrorCode', 0) != 0 || !in_array(ifset($check_payment_data, 'State', ''), ['NEW', 'FORM_SHOWED'])) {
unset($payment_id);
}
}

if (empty($payment_id)) {
$payment_data = $this->apiQuery('Init', $args);
$payment_id = ifset($payment_data, 'PaymentId', '');
}

if (empty($payment_id)) {
$cache->delete();
return null;
} else {
$cache->set($payment_id);
}

if ($this->isTestMode()) {
try {
// Запрашивает успешную оплату по СБП для текущего счёта
// https://www.tbank.ru/kassa/dev/payments/#tag/Oplata-cherez-SBP/operation/SbpPayTest
$test_sbp_result = $this->apiQuery('SbpPayTest', [
'PaymentId' => $payment_id,
]);
} catch (Exception $ex) {
self::log($this->id, ['Unable create test QR code, using hardcoded stub', $ex->getMessage(), $ex->getTraceAsString()]);
return [
'svg' => file_get_contents($this->path.'/img/qr-test.svg'),
'url' => wa()->getRootUrl().'wa-plugins/payment/tinkoff/img/qr-test.svg',
];
}
}

$qr_data = $this->apiQuery('GetQr', [
'PaymentId' => $payment_id,
'DataType' => 'IMAGE'
]);
if (ifset($qr_data, 'Success', false)) {
$qr_link = $this->apiQuery('GetQr', [
'PaymentId' => $payment_id,
'DataType' => 'PAYLOAD'
]);

if (ifset($qr_link, 'Success', false)) {
return [
'svg' => $qr_data['Data'],
'url' => $qr_link['Data'],
];
}
}
$cache->delete();
return null;
} catch (Exception $ex) {
self::log($this->id, [$ex->getMessage(), $ex->getTraceAsString()]);
$cache->delete();
return false;
}
}

private function sbpQrImage($params)
{
$order_data = [
'order_id' => $this->order_id,
'amount' => $params['amount'],
'customer_contact_id' => $params['customer_contact_id'],
];

$sbp = $this->sbp($order_data);
if (empty($sbp['svg'])) {
throw new waException('Не удалось получить QR-код');
}

$response = wa()->getResponse();
$response->addHeader('Content-Type', 'image/svg+xml', true);
echo $sbp['svg'];
exit;
}

public function image($order_data)
{
$args = array(
'OrderId' => $this->app_id.'_'.$this->merchant_id.'_'.$order_data['order_id'],
'amount' => $order_data['amount'],
'description' => ifempty($order_data, 'description', ''),
'customer_contact_id' => $order_data['customer_contact_id'],
'SBPQR' => 1,
);
$args['Token'] = $this->calculateToken($args);
return [
// At least one of keys `image_url` and `image_data_url` is required. Both are ok, too.
'image_url' => wa()->getRootUrl(true) . 'payments.php/tinkoff/?' . http_build_query($args),
//'image_data_url' => 'data:image/png;base64,........',
];
}

public function cancel($transaction_raw_data)
{
try {
Expand All @@ -553,6 +722,10 @@ public function cancel($transaction_raw_data)
);

$data = $this->apiQuery('Cancel', $args);
if (in_array(ifset($data['Status']), ['ASYNC_REFUNDING', 'REFUNDING'])) {
sleep(1);
$data = $this->apiQuery('GetState', ['PaymentId' => $args['PaymentId']]);
}
$transaction_data = $this->formalizeData($data);

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

} catch (Exception $ex) {
$message = sprintf("Error occurred during %s: %s", __METHOD__, $ex->getMessage());
self::log($this->id, $message);
self::log($this->id, [$message, $ex->getTraceAsString()]);
return array(
'result' => -1,
'description' => $ex->getMessage(),
Expand Down Expand Up @@ -940,6 +1113,10 @@ private function getReceiptData(waOrder $order)
'Items' => array(),
'Taxation' => $this->getSettings('taxation'),
'Email' => $email,
'AddUserProp' => [
'Name' => 'Номер заказа',
'Value' => $order->id_str
]
);
if ($phone = $order->getContactField('phone')) {
$this->receipt['Phone'] = sprintf('+%s', preg_replace('/^8/', '7', $phone));
Expand Down
Binary file not shown.
Loading

0 comments on commit 38a7c28

Please sign in to comment.