diff --git a/wa-plugins/payment/tinkoff/img/qr-test.svg b/wa-plugins/payment/tinkoff/img/qr-test.svg new file mode 100644 index 000000000..cf747dc19 --- /dev/null +++ b/wa-plugins/payment/tinkoff/img/qr-test.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/wa-plugins/payment/tinkoff/img/tinkoff.png b/wa-plugins/payment/tinkoff/img/tinkoff.png index c9224b1c0..b4ef7be7a 100644 Binary files a/wa-plugins/payment/tinkoff/img/tinkoff.png and b/wa-plugins/payment/tinkoff/img/tinkoff.png differ diff --git a/wa-plugins/payment/tinkoff/img/tinkoff.svg b/wa-plugins/payment/tinkoff/img/tinkoff.svg new file mode 100644 index 000000000..a888c9ff0 --- /dev/null +++ b/wa-plugins/payment/tinkoff/img/tinkoff.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/wa-plugins/payment/tinkoff/img/tinkoff16.png b/wa-plugins/payment/tinkoff/img/tinkoff16.png index 16b6881c2..f59107ce7 100644 Binary files a/wa-plugins/payment/tinkoff/img/tinkoff16.png and b/wa-plugins/payment/tinkoff/img/tinkoff16.png differ diff --git a/wa-plugins/payment/tinkoff/lib/config/guide.php b/wa-plugins/payment/tinkoff/lib/config/guide.php index ddfe095f0..ec661fb7b 100644 --- a/wa-plugins/payment/tinkoff/lib/config/guide.php +++ b/wa-plugins/payment/tinkoff/lib/config/guide.php @@ -5,18 +5,18 @@ 'value' => '%RELAY_URL%', 'title' => 'URL для нотификации по HTTP', 'description' => 'Значение настройки URL для оповещения активного протокола
-Указанный в этом поле адрес скопируйте и сохраните в анкете подключения к интернет-эквайрингу банка Тинькофф', +Указанный в этом поле адрес скопируйте и сохраните в анкете подключения к интернет-эквайрингу от Т-Кассы (Тинькофф)', ), array( 'value' => '%RELAY_URL%', 'title' => 'Страница успешного платежа', 'description' => 'URL возврата покупателя обратно на сайт после успешной оплаты
-Указанный в этом поле адрес скопируйте и сохраните в анкете подключения к интернет-эквайрингу банка Тинькофф', +Указанный в этом поле адрес скопируйте и сохраните в анкете подключения к интернет-эквайрингу от Т-Кассы (Тинькофф)', ), array( 'value' => '%RELAY_URL%', 'title' => 'Страница ошибки оплаты', 'description' => 'URL возврата покупателя обратно на сайт в случае ошибки оплаты
-Указанный в этом поле адрес скопируйте и сохраните в анкете подключения к интернет-эквайрингу банка Тинькофф', +Указанный в этом поле адрес скопируйте и сохраните в анкете подключения к интернет-эквайрингу от Т-Кассы (Тинькофф)', ), ); diff --git a/wa-plugins/payment/tinkoff/lib/config/plugin.php b/wa-plugins/payment/tinkoff/lib/config/plugin.php index f7efc0c38..be31c49c2 100644 --- a/wa-plugins/payment/tinkoff/lib/config/plugin.php +++ b/wa-plugins/payment/tinkoff/lib/config/plugin.php @@ -1,11 +1,11 @@ 'Банк Тинькофф', - '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, diff --git a/wa-plugins/payment/tinkoff/lib/config/requirements.php b/wa-plugins/payment/tinkoff/lib/config/requirements.php index d054b2f23..e16e29332 100644 --- a/wa-plugins/payment/tinkoff/lib/config/requirements.php +++ b/wa-plugins/payment/tinkoff/lib/config/requirements.php @@ -1,7 +1,7 @@ array( - 'version' => '>=1.13', + 'version' => '>=3.2.0', 'strict' => true, ), 'php.hash' => array( diff --git a/wa-plugins/payment/tinkoff/lib/config/settings.php b/wa-plugins/payment/tinkoff/lib/config/settings.php index 6c04b6255..54c0c95d5 100644 --- a/wa-plugins/payment/tinkoff/lib/config/settings.php +++ b/wa-plugins/payment/tinkoff/lib/config/settings.php @@ -10,6 +10,12 @@ 'value' => '', 'title' => /*_wp*/('Пароль'), 'control_type' => waHtmlControl::PASSWORD, + 'description' => << +Подключайтесь к Т-Кассе через Webasyst по этой ссылке и получите данные для заполнения Terminal ID и пароля. + +HTML + , ), 'currency_id' => array( 'value' => '', @@ -23,7 +29,7 @@ 'two_steps' => array( 'value' => false, 'title' => 'Схема подключения', - 'description' => /*_wp*/('Вариант обработки платежей, выбранный при заключении договора с банком Тинькофф.
Двухстадийную схему подключения можно использовать только с поддерживаемым приложением, например, Shop-Script версии не ниже 8.6.'), + 'description' => /*_wp*/('Вариант обработки платежей, выбранный при заключении договора с Т-Кассой.
Двухстадийную схему подключения можно использовать только с поддерживаемым приложением, например, Shop-Script версии не ниже 8.6.'), 'control_type' => waHtmlControl::RADIOGROUP, 'options' => array( '0' => 'Одностадийная', diff --git a/wa-plugins/payment/tinkoff/lib/config/shop_support.json b/wa-plugins/payment/tinkoff/lib/config/shop_support.json index c2004d817..e38c21e47 100644 --- a/wa-plugins/payment/tinkoff/lib/config/shop_support.json +++ b/wa-plugins/payment/tinkoff/lib/config/shop_support.json @@ -1,4 +1,4 @@ { "support_premium": "yes", - "support_premium_description": "

Дробное количество товаров передаётся в «Тинькофф Банк» точно так, как указано в заказе.

Единицы измерения количества товаров не передаются — такая возможность не предусмотрена платёжной системой.

" + "support_premium_description": "

Дробное количество товаров передаётся в «Т-Кассу» точно так, как указано в заказе.

Единицы измерения количества товаров не передаются — такая возможность не предусмотрена платёжной системой.

" } diff --git a/wa-plugins/payment/tinkoff/lib/tinkoffPayment.class.php b/wa-plugins/payment/tinkoff/lib/tinkoffPayment.class.php index ec83a49d4..1d2c8e922 100644 --- a/wa-plugins/payment/tinkoff/lib/tinkoffPayment.class.php +++ b/wa-plugins/payment/tinkoff/lib/tinkoffPayment.class.php @@ -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; @@ -163,6 +163,18 @@ 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')); @@ -170,10 +182,6 @@ private function checkToken($args) throw new waPaymentException('Password misconfiguration'); } - $token = ifset($args, 'Token', false); - unset($args['Token']); - - ksort($args); foreach ($args as $k => &$arg) { if (is_bool($arg)) { @@ -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]; @@ -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; @@ -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; @@ -340,6 +351,7 @@ protected function callbackHandler($data) break; case self::OPERATION_CAPTURE: + $declare_fiscalization = true; $app_payment_method = self::CALLBACK_CAPTURE; break; @@ -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', @@ -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, @@ -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, @@ -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 { @@ -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); @@ -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(), @@ -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)); diff --git a/wa-plugins/payment/tinkoff/locale/en_US/LC_MESSAGES/payment_tinkoff.mo b/wa-plugins/payment/tinkoff/locale/en_US/LC_MESSAGES/payment_tinkoff.mo new file mode 100644 index 000000000..4043db5c7 Binary files /dev/null and b/wa-plugins/payment/tinkoff/locale/en_US/LC_MESSAGES/payment_tinkoff.mo differ diff --git a/wa-plugins/payment/tinkoff/locale/en_US/LC_MESSAGES/payment_tinkoff.po b/wa-plugins/payment/tinkoff/locale/en_US/LC_MESSAGES/payment_tinkoff.po index cfd8afc38..9bfee1310 100644 --- a/wa-plugins/payment/tinkoff/locale/en_US/LC_MESSAGES/payment_tinkoff.po +++ b/wa-plugins/payment/tinkoff/locale/en_US/LC_MESSAGES/payment_tinkoff.po @@ -5,18 +5,19 @@ msgstr "" "PO-Revision-Date: \n" "Last-Translator: wa-plugins/payment/tinkoff\n" "Language-Team: wa-plugins/payment/tinkoff\n" +"Language: en_US\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Poedit-Language: en_US\n" "X-Poedit-SourceCharset: utf-8\n" "X-Poedit-Basepath: utf-8\n" +"X-Generator: Poedit 3.3.2\n" "X-Poedit-SearchPath-0: .\n" "X-Poedit-SearchPath-1: .\n" msgid "Redirecting to «Tinkoff Bank» site for payment..." -msgstr "" +msgstr "Redirecting to T-Bank for payment..." msgid "Pay for your order on «Tinkoff Bank» site" -msgstr "" +msgstr "Pay via T-Bank" diff --git a/wa-plugins/payment/tinkoff/locale/ru_RU/LC_MESSAGES/payment_tinkoff.mo b/wa-plugins/payment/tinkoff/locale/ru_RU/LC_MESSAGES/payment_tinkoff.mo index 1a2dad8a1..c3b3176f1 100644 Binary files a/wa-plugins/payment/tinkoff/locale/ru_RU/LC_MESSAGES/payment_tinkoff.mo and b/wa-plugins/payment/tinkoff/locale/ru_RU/LC_MESSAGES/payment_tinkoff.mo differ diff --git a/wa-plugins/payment/tinkoff/locale/ru_RU/LC_MESSAGES/payment_tinkoff.po b/wa-plugins/payment/tinkoff/locale/ru_RU/LC_MESSAGES/payment_tinkoff.po index f1ef14c42..854aea98a 100644 --- a/wa-plugins/payment/tinkoff/locale/ru_RU/LC_MESSAGES/payment_tinkoff.po +++ b/wa-plugins/payment/tinkoff/locale/ru_RU/LC_MESSAGES/payment_tinkoff.po @@ -5,18 +5,19 @@ msgstr "" "PO-Revision-Date: \n" "Last-Translator: wa-plugins/payment/tinkoff\n" "Language-Team: wa-plugins/payment/tinkoff\n" +"Language: ru_RU\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=((((n%10)==1)&&((n%100)!=11))?(0):(((((n%10)>=2)&&((n%10)<=4))&&(((n%100)<10)||((n%100)>=20)))?(1):2));\n" -"X-Poedit-Language: ru_RU\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n" "X-Poedit-SourceCharset: utf-8\n" "X-Poedit-Basepath: utf-8\n" +"X-Generator: Poedit 3.3.2\n" "X-Poedit-SearchPath-0: .\n" "X-Poedit-SearchPath-1: .\n" msgid "Redirecting to «Tinkoff Bank» site for payment..." -msgstr "Перенаправление на сайт «Тинькофф Банка» для оплаты..." +msgstr "Перенаправление на сайт «Т-Банка» для оплаты..." msgid "Pay for your order on «Tinkoff Bank» site" -msgstr "Оплатить заказ на сайте «Тинькофф Банка»" +msgstr "Оплатить заказ на сайте «Т-Банка»"