Подключение оплаты через Tinkoff Bank

Всем привет. Только закончил прикручивать оплату через Tinkoff Bank и пока еще не все забыл хочу поделить наработками.
Предполагается, что у вас уже настроен Shopkeeper и остается только прикрутить оплату. В Тинькове вы тоже зарегались и получили Идентификатор терминала и Пароль.
В репозитарии ModX есть модуль Тинькова, но он там очень сырой и без документации, в общем то его я и переписал почти полностью.

Первым делом необходимо перейти на страницу Оформления заказа или Корзину и установить в вызове снипета FormIt параметр &redirectTo=100 равным ID страницы где вызывается снипет Тинькова. У меня эта страница называется Оплата и содержит вызов снипета [[!Tinkoff]]

Добавим в настройки Shopkeeper`a способ оплаты Картой (card).

payment.zip
Качаем архив и закидываем в папку /assets/components/ там же и распакуем. Появится папка payment в которой найдите файл config.php и настройте под себя

Содержимое файла config.php

<?php
define('PAY_URL', 'https://securepay.tinkoff.ru/rest/Init');// URL запроса
define('TERMINAL_KEY', '1479721120904DEMO');// Идентификатор терминала
define('PAY_PASSWORD', '73mq7zfg30hhhwde');	// пароль Тинькова
define('PAY_NETMASK', '91.194.226.0/23');	// подсеть с которой приходит нотификация

Содержимое файла result.php

<?php
	require_once '/home/u67838/foodseasons.ru/www/config.core.php'; // указать ваш абсолютный путь до файла 
	require_once MODX_CORE_PATH.'model/modx/modx.class.php';
	require_once MODX_CORE_PATH.'../assets/components/payment/config.php';

	$ip = ip2long($_SERVER['REMOTE_ADDR']); 
	list($net,$mask) = explode('/',PAY_NETMASK);
	$net = ip2long($net);
	$mask = pow(2, 32 - $mask) - 1;
	$net = $net&~$mask;
	if (!(($ip^$net)&~$mask)) { // Проверяем принадлежит ли IP к подсети Тинькова
		if ($_SERVER["REQUEST_METHOD"] == "POST") {
			if($_POST["TerminalKey"] == TERMINAL_KEY) { // Сверяем Идентификатор терминала
				$modx = new modX();
				$modx->initialize('web');
				$modx->addPackage('shopkeeper3', $modx->getOption('core_path').'components/shopkeeper3/model/');
				$order_id = $_POST['OrderId'];
				$order = $modx->getObject('shk_order', $order_id);
				if ( (isset($order)) && ($order > 0) ) {
					$order_status = $order->get('status');
					$amount = $order->get('price');
					$amount = $amount*100; // Сумма в копейках
					$email = $order->get('email');

					$args['OrderId'] = $order_id;
					$args['Amount'] = $amount;
					$args['Description'] = "Оплата счета №".$order_id;
					$args['DATA'] = "Email=".$email;
					$args['TerminalKey'] = TERMINAL_KEY;
					$args['Password'] = PAY_PASSWORD;
					// генерируем наш токен, чтобы сверить с тем что пришел в нотификации
					$token = '';
					ksort($args);
					foreach ($args as $arg){$token .= $arg;}
					//echo '<p>'.$token.'</p>';
					$token = hash('sha256', $token);
					unset($args);

					if( ($_POST["Token"] == $token) && ($_POST["ErrorCode"] == 0) && ($_POST["Success"]) ){ // Сверяем токены наш и нотификации из банка
						$change_status = $order->set('status', 6);
						$order->save();
						$modx->invokeEvent('OnSHKChangeStatus',array('order_id'=>$order_id,'status'=>6));
						echo "OK";
					}
				} else {
					echo "Order not found";
				}
			}
		}
	}

Далее создаем сам снипет Tinkoff

<?php
if ( ($_SESSION['shk_lastOrder']['payment'] != 'card') && (!$_GET['ord_id']) ){ // Проверяем что у нас способ оплаты указан card
	$modx->sendRedirect('/checkout/success.html', 0, 'REDIRECT_HEADER'); // иначе редиректим на страницу какую хотите
}

if ( ($_GET['ord_id']) && ($_GET['payment'] == 'card') ){
	$order_id = $_GET['ord_id'];
}else{
	$order_id = $_SESSION['shk_lastOrder']['id']; // получаем ID заказа
}
require_once MODX_BASE_PATH."assets/components/payment/config.php"; подтягиваем конфиг Тинькова

$order = $modx->getObject('shk_order', $order_id);
if ( isset($order) ){
	$order_status = $order->get('status');
	$amount = $order->get('price');
	$amount = $amount*100; // Сумма в копейках
	$email = $order->get('email');

	$args['OrderId'] = $order_id?:'';
	$args['Amount'] = $amount?:'';
	$args['Description'] = "Оплата счета №".$order_id;
	$args['DATA'] = "Email=".$email;
	$args['TerminalKey'] = TERMINAL_KEY;
	$args['Password'] = PAY_PASSWORD;
	//token generation
	$token = '';
	ksort($args);
	foreach ($args as $arg){$token .= $arg;}
	//echo '<p>'.$token.'</p>';
	$token = hash('sha256', $token);
	$args['Token'] = $token;
	unset($args['Password']);

	$args = http_build_query($args);
	//return print_r($args,true);
	if ($curl = curl_init()){
		curl_setopt($curl, CURLOPT_URL, PAY_URL);
		curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
		curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
		curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
		curl_setopt($curl, CURLOPT_POST, true);
		curl_setopt($curl, CURLOPT_POSTFIELDS, $args);
		$out = curl_exec($curl);

		$json = json_decode($out);
		//echo '<p>'.print_r($json,true).'</p>';

		curl_close($curl);

		if ($json->PaymentURL){
		  $change_status = $order->set('status', 2);
		  $order->save();
		  $modx->invokeEvent('OnSHKChangeStatus',array('order_id'=>$order_id,'status'=>2));
			$modx->sendRedirect($json->PaymentURL);
		}
		return $json->PaymentURL?:$out;
	}else{
		$modx->sendRedirect('/checkout/error.html', 0, 'REDIRECT_HEADER');
	}
}

В личный кабинет Тинькова добавим URL для нотификации http://site.ru/assets/components/payment/result.php

Вот собственно и вся настройка. Теперь идем в магазин, добавляем товар в корзину, выбираем способ оплаты Картой (card) и попадаем на страницу где Тиньков просит ввести данные карты, после успешной оплаты статус заказа изменится на Оплачен

Спасибо, что поделились!

Действительно, спасибо.
@Gulik, Tinkoff рекомендует логировать все входящие параметры. Считаю, что нужно обязательно добавить логирование.
Вот пример того, как я это делаю. Код очень кривой, но работает.

// логирование
class Logger {
 
	protected $fh;
 
	public function __construct() {
		$this->fh = fopen('core/logs/log.log', 'a+');
	}
 
	public function log($msg) {
		if(!$this->fh) {
			throw new Exception('Unable to open log file for writing');
		}
		if(fwrite($this->fh, $msg . "\n") === false) {
			throw new Exception('Unable to write to log file.');
		}
	}
 
	public function __destruct() {
		fclose($this->fh);
	}
}
 
$logger = new Logger();
$logger->log(date('m-d-Y H:i:s') . ' ' . $_SERVER['REMOTE_ADDR']);
$logger->log('$_POST: ' . print_r($_POST, true));
$logger->log('$_GET: ' . print_r($_GET, true));

Я его вставил перед проверкой токена. Предлагаю вам включить логирование в ваш снипет. Если получится, прошу вас поделиться кодом того, что получилось.

@Gulik, Так что на счет логирования?

@alexanderr Я использую просто запись нужных мне данных в файл.
Собираю в процессе выполнения скрипта данные в переменную, а потом (в конце) пишу в файл. Но это нужно только для отладки.

Похоже, подключение к Форум | MODX Shopkeeper было разорвано, подождите, пока мы пытаемся восстановить соединение.