Назад к блогу
08.04.2026 14:37:09

Интеграция интернет-эквайринга ВТБ на сайте Yii: как подключить оплату и не сломать логику заказов

Подключение интернет-эквайринга к действующему сайту редко сводится к простой отправке запроса в API банка. Особенно если проект уже работает, принимает заказы, бронирует билеты, меняет внутренние статусы и отправляет клиентам подтверждения. В такой задаче важно не только получить ссылку на оплату, но и правильно встроить банк в существующий бизнес-процесс. Чтобы исключить кассовые разрывы и зависшие заказы, на крупных проектах закладывается профессиональная интеграция платежных систем с полным контролем жизненного цикла транзакций.

В этом материале разберем реальный пример интеграции эквайринга ВТБ в проект на Yii: с получением токена, созданием платежного заказа, обработкой серверных уведомлений, проверкой статуса через API и безопасным переводом локального заказа в оплаченный статус.

Такая задача относится к области сложных кастомных PHP-доработок: нужно не просто написать запрос к API, а аккуратно встроить новую логику в существующий проект. Похожие задачи мы выполняем в рамках услуги доработки Bitrix / PHP-задачи.

Задача

Нужно было подключить новый способ оплаты через ВТБ к сайту на Yii, где уже была реализована старая логика оплаты через другой банк. То есть задача заключалась не просто в том, чтобы добавить еще одну кнопку оплаты, а в том, чтобы встроить новый банк в уже работающий процесс оформления заказа.

Для клиента всё должно выглядеть просто: пользователь выбрал билет, нажал кнопку оплаты, оплатил заказ и получил подтверждение. Но внутри сайта этот сценарий состоит из нескольких связанных этапов:

  • получение access token (OAuth2 авторизация);
  • создание платежного заказа в ВТБ;
  • редирект клиента на страницу оплаты банка;
  • возврат клиента на сайт и параллельная обработка асинхронного callback-сервера;
  • проверка статуса заказа через API банка;
  • смена внутреннего статуса заказа;
  • смена статусов связанных билетов;
  • запуск дальнейшей логики выдачи билета и отправки письма.

Что было на руках

Для подключения интернет-эквайринга были получены стандартные реквизиты:

  • client_id и client_secret;
  • заголовок авторизации мерчанта (Merchant-Authorization);
  • test / prod endpoints для token API и работы с заказами (order API).

На этом этапе часто кажется, что большая часть интеграции уже решена. Есть доступы, есть адреса API, остается только отправить запрос. Но на практике реквизиты — это только стартовая точка. Самая сложная часть начинается дальше: нужно понять, какие заголовки реально ожичает банк, какие поля обязательны в JSON, какие статусы приходят после оплаты и как эти статусы связать с внутренней логикой сайта.

Почему одной документации оказалось недостаточно

Формально документация ВТБ дает общую схему взаимодействия: как получить токен, как создать заказ, куда отправить пользователя и как получить информацию о платеже. Но в реальной интеграции важны детали, которые не всегда очевидны из инструкции.

В процессе подключения нужно было точно понять:

  • нужно ли передавать Merchant-Authorization при получении токена;
  • как правильно формировать обязательный заголовок X-IBM-Client-Id;
  • какой набор полей должен быть в теле запроса при создании заказа;
  • откуда брать ссылку на оплату;
  • какие статусы реально возвращает ВТБ после оплаты;
  • что считать успешной оплатой: статус заказа, статус транзакции или оба значения вместе.

Именно такие детали часто приводят к ошибкам вроде 403 Forbidden, unauthorized_client или к ситуации, когда заказ фактически оплачен, но внутри сайта остается в неправильном статусе.

С какими проблемами пришлось столкнуться

1. Ошибки соединения и SSL

Первый этап интеграции — получение токена — показал, что даже корректный endpoint может не заработать с первого раза. Причина крылась в окружении проекта: настройках curl, SSL-сертификатах и проверке цепочки сертификатов на сервере. Поэтому при интеграции платежных систем важно проверять не только код, но и серверную среду.

2. 403 Forbidden при создании заказа

После успешного получения токена следующая ошибка появилась на этапе создания заказа. Это важный момент: если токен уже получен, значит базовая авторизация работает. Проблему нужно искать в конкретном ресурсе, заголовках, формате запроса или связке client_id и Merchant-Authorization.

3. Разница между test и prod окружением

Если использовать боевые реквизиты с тестовым endpoint или тестовые реквизиты с боевым endpoint, integration не заработает. Поэтому в конфигурационный файл Yii были вынесены раздельные параметры для test и prod режима, чтобы исключить путаницу.

4. Ограничения старой версии PHP

Проект работал на сервере со старой версией PHP. Подготовленный нами класс клиента ВТБ изначально использовал современный синтаксис с типизированными свойствами (private string $clientId;). Старое окружение выдавало синтаксическую ошибку при парсинге. Код клиента пришлось адаптировать под старый синтаксис, убрав строгие конструкции.

Это доказывает, что перед сложными интеграциями полезно делать полный технический аудит проекта: проверять версию PHP, расширения, SSL, права доступа и логи. Для таких задач у нас есть отдельная услуга технический аудит и оптимизация сайта.

Архитектурное решение на Yii: Выделенный компонент

Чтобы не размазывать обращения к банку по контроллерам и действиям, мы разработали изолированный компонент. Логика получения токена была оптимизирована: токен кэшируется внутри встроенного компонента кэширования Yii, чтобы сайт не тратил сетевое время на лишние OAuth-запросы при каждом действии пользователя.

Пример структуры компонента для работы с API ВТБ:

<?php

namespace app\components;

use Yii;
use yii\base\Component;
use yii\httpclient\Client;

class VtbMerchantClient extends Component
{
    public $baseUrl;
    public $tokenUrl;
    public $clientId;
    public $clientSecret;
    public $merchantAuth;

    public function getAccessToken()
    {
        $cacheKey = 'vtb_access_token_' . $this->clientId;
        $token = Yii::$app->cache->get($cacheKey);

        if ($token !== false) {
            return $token;
        }

        $client = new Client();
        $response = $client->createRequest()
            ->setMethod('POST')
            ->setUrl($this->tokenUrl)
            ->setHeaders([
                'Content-Type' => 'application/x-www-form-urlencoded',
                'Authorization' => 'Basic ' . base64_encode($this->clientId . ':' . $this->clientSecret)
            ])
            ->setData([
                'grant_type' => 'client_credentials',
                'scope' => 'vtb_pay'
            ])
            ->send();

        if (!$response->isOk) {
            throw new \Exception('VTB OAuth Error: ' . $response->content);
        }

        $data = $response->data;
        $token = $data['access_token'];
        
        // Кэшируем токен за пару минут до его официального истечения (TTL)
        $duration = isset($data['expires_in']) ? ((int)$data['expires_in'] - 60) : 3500;
        Yii::$app->cache->set($cacheKey, $token, $duration);

        return $token;
    }

    public function checkOrderStatus($bankOrderId)
    {
        $token = $this->getAccessToken();
        $client = new Client();
        
        $response = $client->createRequest()
            ->setMethod('GET')
            ->setUrl($this->baseUrl . '/orders/' . $bankOrderId)
            ->setHeaders([
                'Authorization' => 'Bearer ' . $token,
                'X-IBM-Client-Id' => $this->clientId,
                'Merchant-Authorization' => $this->merchantAuth,
                'Accept' => 'application/json'
            ])
            ->send();

        if (!$response->isOk) {
            return null;
        }

        return $response->data;
    }
}
?>

Почему редирект на result-страницу — это уязвимость

В старой логике сайта статус оплаты проверялся в момент, когда покупатель возвращался на страницу /order/result?orderId=.... Для надежного эквайринга это уязвимость. Если пользователь закроет вкладку сразу после списания денег банком (или у него отключится интернет), он не попадет на страницу возврата. Деньги уйдут мерчанту, а заказ останется висеть в неоплаченных.

Правильное решение: Безопасное изменение статусов должно происходить через асинхронный фоновый Callback-URL банка, не зависящий от браузера пользователя. Если банк не поддерживает вебхуки, пишется консольная команда Yii, которая по Cron раз в 10 минут опрашивает статусы всех незакрытых платежей.

Пример безопасного контроллера обработки статусов в Yii:

<?php

namespace app\controllers;

use Yii;
use yii\web\Controller;
use yii\web\NotFoundHttpException;
use app\models\Order;

class PaymentController extends Controller
{
    // Отключаем CSRF-валидацию для серверного вебхука банка
    public $enableCsrfValidation = false;

    public function actionCallback()
    {
        $request = Yii::$app->request;
        $bankOrderId = $request->post('orderId'); // Идентификатор заказа в банке
        
        if (empty($bankOrderId)) {
            return 'Empty order';
        }

        // Ищем локальный заказ по идентификатору транзакции банка
        $order = Order::findOne(['bank_order_id' => $bankOrderId]);
        if (!$order) {
            throw new NotFoundHttpException('Order not found');
        }

        // Если заказ уже оплачен — прекращаем выполнение (принцип идемпотентности)
        if ($order->status === Order::STATUS_PAID) {
            return 'OK';
        }

        // Выполняем встречный запрос верификации в API ВТБ (защита от фейковых запросов)
        /** @var \app\components\VtbMerchantClient $vtbClient */
        $vtbClient = Yii::$app->vtbMerchant;
        $bankData = $vtbClient->checkOrderStatus($bankOrderId);

        $bankOrderValue = $bankData['response']['object']['status']['value'] ?? null;
        $paymentStatus  = $bankData['transactions']['payments'][0]['object']['status']['value'] ?? null;

        // Только если статус заказа PAID, а статус транзакции CONFIRMED — меняем логику
        if ($bankOrderValue === 'PAID' &lt;= $paymentStatus === 'CONFIRMED') {
            
            // Запускаем транзакцию СУБД для обновления статуса и билетов
            $transaction = Yii::$app->db->beginTransaction();
            try {
                $order->status = Order::STATUS_PAID;
                $order->paid_at = date('Y-m-d H:i:s');
                
                if ($order->save()) {
                    // Переводим связанные билеты в статус оплаченных
                    $order->updateRelatedTicketsStatus(Order::STATUS_PAID);
                    
                    // Триггерим генерацию билетов и отправку почты клиенту
                    $order->generateTicketsAndSendEmail();
                }
                
                $transaction->commit();
            } catch (\Exception $e) {
                $transaction->rollBack();
                Yii::error('Payment processing failed: ' . $e->getMessage());
                return 'Fail';
            }
        }

        return 'OK';
    }
}
?>

Почему бронь и оплата — не одно и то же

На проекте уже существовала разветвленная логика внутренних статусов: Новый заказ, Бронь, Оплачен, Аннулирован.

На этапе интеграции выяснилось, что при успешном ответе от банка ВТБ система переводила локальный заказ не в конечный статус «Оплачен», а ошибочно сбрасывала его в статус «Забронирован». Для бизнеса это критичная ошибка: клиент оплатил билет, деньги списались, но сам билет на почту не ушел, так как робот отправки писем реагировал исключительно на флаг полной оплаты.

Благодаря внедрению транзакционности в Callback-контроллере (код которого представлен выше), мы жестко разделили эти процессы: статус брони ставится в момент редиректа на банк, а статус «Оплачен» — строго по факту подтверждения от СУБД.

Сопоставление строковых статусов в логах оплат

Дополнительная техническая сложность заключалась в логах. Старая база логов проектировалась под старый банк, работавший на целочисленных кодах ошибок (например, 1 — успех, 0 — отказ). ВТБ возвращает развернутые текстовые статусы: CREATED, PAID, CONFIRMED.

Мы создали слой маппинга, который сохраняет сырые строковые логи банка для быстрой диагностики программистами, но транслирует системные integer-коды для старой административной панели сайта. В логах теперь четко фиксируется связка: order_status=PAID; payment_status=CONFIRMED.

Что в итоге получает владелец сайта

Подключение интернет-эквайринга — это не просто заполнение полей реквизитов в админке. Внутренняя логика затрагивает распределение остатков на складах, бронирование мест, транзакционные email-оповещения и серверное окружение. После профессиональной доработки бизнес получает отказоустойчивый цикл:

  1. Клиент выбирает места и бронирует билет.
  2. Сайт генерирует ордер и открывает платежный шлюз ВТБ.
  3. Вся цепочка — от проверки асинхронного вебхука до генерации PDF-файла билета — работает автоматически.
  4. Исключены риски «зависших» транзакций, когда деньги списаны, а услуга не оказана.

Посмотреть примеры аналогичных технических задач по финтех-автоматизации и разработке личных кабинетов можно в разделе примеры работ.

Итог

Интеграция эквайринга ВТБ на базе фреймворка Yii доказала: техническая документация шлюзов — лишь вершина айсберга. Ключевой результат безопасной работы платежей достигается за счет кэширования токенов авторизации, изоляции API-клиента в отдельный компонент и обязательной асинхронной верификации транзакций.

If вам необходима качественная интеграция платежных систем, подключение эквайринга ВТБ, доработка логики статусов, настройка безопасных вебхуков или адаптация платежного клиента под фреймворк Yii, отправьте нам задачу через страницу контактов. Подробнее о стандартах нашей работы и поддержке проектов читайте на странице о студии.