Доступен — Локальные цифровые продукты
HCA · Studio
TR Контакты ↗
Заметка 01 · WooCommerce

Оплата обеденными картами в WooCommerce

Сайт, принимающий заказы еды и не принимающий обеденные карты, теряет значительную часть аудитории на шаге оплаты. WooCommerce не поддерживает эти карты из коробки; решение — собственный платёжный шлюз.

Тема
Разработка платёжного шлюза
Платформа
WooCommerce
Уровень
Средний — продвинутый
Короткий ответ

Готового плагина для приёма Multinet, Sodexo или Setcard в WooCommerce нет; нужен собственный шлюз, расширяющий WC_Payment_Gateway по документации провайдера. Критично никогда не доверять параметрам URL, на который вернулся клиент: факт оплаты должен подтверждаться отдельным серверным запросом при возврате. Только после этого подтверждения заказ помечается оплаченным.

Почему нет готового плагина

Платёжная экосистема WooCommerce построена вокруг международных провайдеров и распространённых локальных виртуальных POS. Провайдеры обеденных карт — другая категория: работа по договору, обязательное одобрение мерчанта и документация по интеграции, которая передаётся только после соглашения.

Поэтому решения «установил и работает» в каталоге плагинов нет. К плагинам, утверждающим обратное, стоит относиться настороженно: платёжный процесс зависит от версии API провайдера и от специфичных для вас параметров мерчанта.

Первый шаг — коммерческий, а не технический: заключается договор мерчанта, берутся тестовые доступы и документация. Код, написанный без них, — догадка.

Каркас шлюза

Способ оплаты в WooCommerce — это класс, расширяющий WC_Payment_Gateway и регистрируемый через фильтр woocommerce_payment_gateways.

add_filter( 'woocommerce_payment_gateways', function ( $gateways ) {
    $gateways[] = 'WC_Gateway_Meal_Card';
    return $gateways;
} );

class WC_Gateway_Meal_Card extends WC_Payment_Gateway {

    public function __construct() {
        $this->id                 = 'meal_card';
        $this->method_title       = 'Обеденная карта';
        $this->has_fields         = false;

        $this->init_form_fields();
        $this->init_settings();

        $this->title   = $this->get_option( 'title' );
        $this->enabled = $this->get_option( 'enabled' );

        add_action( 'woocommerce_update_options_payment_gateways_' . $this->id,
            array( $this, 'process_admin_options' ) );
    }

    public function process_payment( $order_id ) {
        $order = wc_get_order( $order_id );

        // Инициируем транзакцию у провайдера и получаем адрес перенаправления.
        $redirect = $this->start_transaction( $order );

        if ( is_wp_error( $redirect ) ) {
            wc_add_notice( $redirect->get_error_message(), 'error' );
            return array( 'result' => 'failure' );
        }

        return array(
            'result'   => 'success',
            'redirect' => $redirect,
        );
    }
}

Обратите внимание: process_payment не помечает заказ оплаченным. Он только отправляет клиента на экран подтверждения провайдера. Произошла ли оплата, на этом этапе неизвестно.

Процесс оплаты

  1. Клиент выбирает обеденную карту при оформлении и подтверждает заказ.
  2. Сайт с сервера отправляет провайдеру запрос на инициацию транзакции с суммой, ссылкой на заказ и адресом возврата.
  3. Провайдер возвращает идентификатор транзакции и адрес перенаправления.
  4. Клиент проходит подтверждение на стороне провайдера.
  5. Провайдер возвращает клиента на адрес возврата.
  6. Сайт делает второй, серверный запрос к провайдеру о реальном состоянии транзакции.
  7. При подтверждении заказ помечается оплаченным и уходит в работу.

Шестой шаг — и есть тема этой заметки.

Самая частая ошибка безопасности

Самая распространённая ошибка: провайдер возвращает клиента на адрес вида /vozvrat/?status=success&order=1234, код читает status=success и считает заказ оплаченным.

Этот адрес проходит через браузер клиента. Любой, кто наберёт параметр вручную, пометит заказ оплаченным, ничего не заплатив. В ресторанных заказах это прямая потеря товара: заказ уходит на кухню, готовится и выдаётся.

Правильное поведение: шаг возврата сообщает только о том, что клиент вернулся. Реальное решение запрашивается у провайдера.

public function handle_return() {
    $order_id = absint( $_GET['order_id'] ?? 0 );
    $order    = wc_get_order( $order_id );

    if ( ! $order || $order->is_paid() ) {
        return; // нет заказа или уже оплачен — не обрабатываем повторно
    }

    // Решение принимает провайдер, а не браузер:
    $status = $this->query_transaction_status( $order->get_id() );

    if ( is_wp_error( $status ) || 'approved' !== $status['state'] ) {
        $order->update_status( 'failed', 'Оплата обеденной картой не подтверждена.' );
        wp_safe_redirect( wc_get_checkout_url() );
        exit;
    }

    // Сумму тоже нужно проверить — подтверждение на меньшую сумму недопустимо.
    if ( ! $this->amounts_match( $status['amount'], $order->get_total() ) ) {
        $order->update_status( 'on-hold', 'Расхождение суммы, требуется ручная проверка.' );
        return;
    }

    $order->payment_complete( $status['transaction_id'] );
    wp_safe_redirect( $this->get_return_url( $order ) );
    exit;
}

Три защиты сразу: состояние запрашивается у провайдера, сумма сверяется, уже оплаченный заказ не обрабатывается повторно. Последнее важно: обновление страницы возврата или повторное уведомление провайдера не должны завершать заказ дважды.

Статусы заказа

СтатусКогдаПочему
pendingЗаказ создан, оплата начатаДенег ещё нет; на кухню попадать не должен
failedПровайдер отклонил или клиент отменилКлиент может попробовать снова
on-holdНеоднозначность, например расхождение суммыАвтоматически решить нельзя, нужен человек
processingОплата подтвержденаpayment_complete() ставит его сам

Частая ошибка — помечать оплату вручную через update_status('processing'). Используйте payment_complete(): он записывает номер транзакции, списывает остатки, запускает нужные хуки и выбирает правильный статус для типа магазина.

Совместимость с блочным оформлением

Блочная страница оформления WooCommerce работает иначе классической: способы оплаты перечисляются на стороне React. Шлюз, написанный по-классически, может вообще не появиться в блочном оформлении.

add_action( 'woocommerce_blocks_loaded', function () {
    if ( ! class_exists( 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\AbstractPaymentMethodType' ) ) {
        return;
    }
    require_once __DIR__ . '/class-meal-card-blocks.php';

    add_action(
        'woocommerce_blocks_payment_method_type_registration',
        function ( $registry ) {
            $registry->register( new WC_Meal_Card_Blocks_Support() );
        }
    );
} );

Так как блочная сторона регистрируется через JavaScript, шлюзу нужен небольшой JS-файл. Этот файл нельзя откладывать (defer) или объединять — иначе способ оплаты не появится в списке. Подробности этой ловушки — в заметке о пустой корзине.

Тестирование и запуск

  • Начните с тестовой среды. С тестовыми доступами провайдера пройдите минимум: успешная оплата, недостаточный баланс, отказ клиента, обновление страницы возврата.
  • Недостаточный баланс критичен. На обеденных картах баланс часто ниже суммы заказа. Клиенту нужно понятное сообщение, заказ должен получить статус failed, а корзина — сохраниться.
  • Ведите записи. Пишите краткое содержание каждого запроса к провайдеру и от него в примечания заказа WooCommerce. В платёжном споре это единственное доказательство.
  • Не храните ключи в коде. Ключи мерчанта — в настройках плагина или в константах wp-config.php, но не в системе контроля версий.

Итог

Интеграция обеденных карт — стандартное применение разработки платёжного шлюза WooCommerce; сложность в том, чтобы верно прочитать документацию провайдера и поставить границу доверия в правильное место. Ничто, приходящее из браузера клиента, не является доказательством оплаты.

Этот подход работает в продакшене в системе заказов Her Mutfak.

Чек-лист
Предусловие
Договор мерчанта с провайдером + документация по интеграции
Класс
Расширить WC_Payment_Gateway, зарегистрировать через woocommerce_payment_gateways
Граница доверия
НИКОГДА не доверять параметрам адреса возврата — статус запрашивать у провайдера
Проверка суммы
Сверять подтверждённую сумму с суммой заказа
Защита от повтора
Не обрабатывать повторно уже оплаченный заказ
Завершение
Использовать payment_complete(), а не update_status
Блочное оформление
Нужна отдельная регистрация; JS-файл нельзя откладывать
Секреты
Ключи в настройках или константах wp-config, не в коде
Частые вопросы

Об интеграции обеденных карт.

Есть ли готовый плагин для оплаты Multinet в WooCommerce?

На практике нет. Провайдеры обеденных карт передают данные интеграции только компаниям с договором мерчанта, и параметры специфичны для каждой. Решение — собственный платёжный шлюз, расширяющий WC_Payment_Gateway по документации провайдера.

Почему нельзя доверять параметрам адреса возврата после оплаты?

Потому что этот адрес проходит через браузер клиента и может быть изменён. Система, читающая из URL только слово «успешно», позволяет создавать заказы вообще без оплаты. Правильный способ — отдельный серверный запрос при возврате с проверкой состояния транзакции и суммы.

Мой способ оплаты не виден в блочном оформлении — почему?

Блочное оформление перечисляет способы оплаты на стороне JavaScript. Шлюз, написанный для классической страницы, должен дополнительно зарегистрировать поддержку блоков. Кроме того, если JS-файл шлюза откладывается или объединяется плагином оптимизации, способ вообще не появится в списке.

Почему payment_complete() вместо статуса processing?

payment_complete() делает больше, чем меняет статус: записывает номер транзакции, списывает остатки, запускает нужные хуки и выбирает корректный статус для типа магазина. Ручная установка статуса пропускает эти шаги и оставляет тихие ошибки в отчётности и остатках.

Доступность и смета

Виден в локальном поиске.
Убедителен в продукте.