Сайт, принимающий заказы еды и не принимающий обеденные карты, теряет значительную часть аудитории на шаге оплаты. 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 не помечает заказ оплаченным. Он только отправляет клиента на экран подтверждения провайдера. Произошла ли оплата, на этом этапе неизвестно.
Шестой шаг — и есть тема этой заметки.
Самая распространённая ошибка: провайдер возвращает клиента на адрес вида /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, а корзина — сохраниться.wp-config.php, но не в системе контроля версий.Интеграция обеденных карт — стандартное применение разработки платёжного шлюза WooCommerce; сложность в том, чтобы верно прочитать документацию провайдера и поставить границу доверия в правильное место. Ничто, приходящее из браузера клиента, не является доказательством оплаты.
Этот подход работает в продакшене в системе заказов Her Mutfak.
На практике нет. Провайдеры обеденных карт передают данные интеграции только компаниям с договором мерчанта, и параметры специфичны для каждой. Решение — собственный платёжный шлюз, расширяющий WC_Payment_Gateway по документации провайдера.
Потому что этот адрес проходит через браузер клиента и может быть изменён. Система, читающая из URL только слово «успешно», позволяет создавать заказы вообще без оплаты. Правильный способ — отдельный серверный запрос при возврате с проверкой состояния транзакции и суммы.
Блочное оформление перечисляет способы оплаты на стороне JavaScript. Шлюз, написанный для классической страницы, должен дополнительно зарегистрировать поддержку блоков. Кроме того, если JS-файл шлюза откладывается или объединяется плагином оптимизации, способ вообще не появится в списке.
payment_complete() делает больше, чем меняет статус: записывает номер транзакции, списывает остатки, запускает нужные хуки и выбирает корректный статус для типа магазина. Ручная установка статуса пропускает эти шаги и оставляет тихие ошибки в отчётности и остатках.