Подключение к платежной системе CDEK Pay компании СДЭК

Подключение к платежной системе CDEK Pay компании СДЭК

20 сентября 2023

Для начала отметим важнейшие инструменты любого интернет-магазина на любой CMS: прием оплаты и доставка заказа. Не многие торговые компании могли бы похвастаться тем, что организовали собственную службу доставки, которая востребована на рынке как среди продавцов, так и среди покупателей. Чего не скажешь о компании CDEK, «на отлично» решившей проблему доставки грузов: более 20 лет успешной работы, 31 страна присутствия, 4200 пунктов выдачи заказов и более 10 миллионов активных пользователей. Впечатляющие результаты!

Офис CDEK аппарат
Пункт приема и выдачи грузов

Хорошо, с доставкой разобрались. А что делать с оплатой? Уже знакомая нам компания СДЭК представила платежную систему для интернет-магазинов — CDEK Pay. Доставлять можно и нужно не только чужие грузы, но и чужие деньги. В статье расскажем про подключение к платежной системе СДЭК Pay сразу нескольких систем управления контентом.

CDEK Pay — платежная система, подразделение международного логистического оператора экспресс-доставки CDEK. Первый платежный агрегатор России, созданный логистической компанией и зарегистрированный в реестре Банка России. Основная цель создания собственного сервиса — платежное решение для партнеров, клиентов и внешних пользователей, а также унификация платежных технологий внутри компании.

HTML
HTML
JavaScript
JavaScript
PHP
PHP
MySQL
MySQL
OpenCart
OpenCart
CS-Cart
CS-Cart
HikaShop
HikaShop
VirtueMart
VirtueMart
Joomla
Joomla
WooCommerce
WooCommerce
WordPress
WordPress
InSales
InSales

Как проводилась интеграция

Разработка, как водится, началась с технического задания. За что заказчику огромное спасибо. Ведущий разработчик тщательно изучил примеры, API интеграции и составил подробное техническое задание, по которому был реализован первый модуль для OpenCart. Наши инженеры использовали следующий инструментарий.

01 IDE IDE PhpStorm, Visual Studio Code
02 ospanel Среда Open Server Panel
03 Отладчик Отладчик XDebug

Главная проблема, с которой мы столкнулись при разработке, — отсутствие подробной документации по CMS, для которых нужно было написать модули интеграции. OpenCart, например, в «Developers Guide» на официальном сайте всего несколько страниц. Так что основной труд состоял в том, чтобы «перелопатить» форумы и статьи о различных CMS, собирая по крупицам необходимые решения. Когда нужной информации не находилось, в ход шло исследование кода CMS, кода модулей других систем оплаты и пошаговая отладка дебаггером для понимания внутренней логики CMS.

Разработано несколько модулей интеграции для различных CMS и SaaS: OpenCart, CS-Cart, HikaShop для Joomla, VirtueMart для Joomla, WooCommerce для WordPress, InSales. При разработке плагинов повторяются одни и те же действия.

01 Установка и настройка Установка и настройка плагина.
02 Инициализация платежа Инициализация платежа на стороне платежной системы.
03 Проверка статуса платежа Проверка статуса платежа, а также платежей, в том числе и по расписанию cron.
04 Инициализация возврата Инициализация возврата платежа.
05 Удаление плагина Удаление плагина и чистка данных за собой.

Установка и настройка плагина, а также его удаление полностью зависимы от используемой CMS. А вот инициализация платежа, проверка статуса платежа и инициализация возврата платежа идентичны, в результате чего само собой напрашивалось решение создания SDK, который можно (и нужно) использовать повторно при разработке решения для следующих платформ.

Так и сделали. Реализовали класс cdekpayPaymentSDK, в котором инкапсулировали весь опыт взаимодействия с платежным сервером, не зависящий от CMS. Класс cdekpayPaymentSDK очень прост и имеет три основных публичных метода:

  • initPayment() — инициализация платежа;
  • getPayments() — проверка статуса платежа или платежей;
  • initRefundPayment() — инициализация возврата платежа.

Большую часть времени разработчики занимаются исследованиями CMS и ее особенностей, о которых упоминалось ранее. Остальное дело техники: реализуем, тестируем и запускаем в продакшен.

Подключение CDEK Pay

API платежной системы устроен просто. Доступность описания всех эндпоинтов в Swagger по адресу https://api.cdekfin.ru сильно ускорила разработку. Благодарим разработчиков платежной системы за это.

Для оформления заказа первым делом формируется массив с данными по заказу, включающий номер заказа на стороне интеграции (например, сгенерированный CMS), e-mail пользователя, логин учетной записи магазина, зарегистрированного на стороне CDEK Pay, сумма заказа, перечень товаров, а также 2 ссылки на страницы, куда переводить пользователя в случаях успешной и неуспешной оплаты. По данным полученного массива формируется по определенным правилам строка. На основе полученной строки и секретного ключа, полученного от CDEK Pay, формируется электронная сигнатура по алгоритму SHA256. Сигнатура включается в массив данных, сформированных выше, и этот массив передается POST-запросом на эндпоинт /merchant_api/payment_orders для платежей в боевом режиме, или на /test_merchant_api/payment_orders для платежей в тестовом режиме.

Если верно указан логин магазина, и правильно подписан массив данных, пользователю возвращается код 200 OK и JSON, содержащий номер заказа на стороне СДЭК Pay вместе со ссылкой на оплату заказа. Далее номер заказа CDEK Pay сохраняется в базе данных на стороне интеграции (например, в CMS) и пользователь направляется на страницу оплаты заказа. Нужно включить ссылку на оплату в информацию по заказу на случай, если оплата не пройдет, и пользователь решит попробовать оплатить еще раз. Платежную ссылку возможно отправить пользователю на e-mail. Таким образом, данные по заказу на стороне интеграции сохраняются еще до фактической оплаты заказа: заказ формируется и ему присваивается статус «В ожидании».

Затем, если оплата прошла, покупатель попадает на страницу успешной оплаты, ссылка на которую передана ранее в массив данных по заказу. Соответственно, в случае неуспешной оплаты пользователь переводится на страницу неуспешной оплаты.

Cо стороны СДЭК Pay нам приходит информация заказа по вебхуку. Вебхук — это URL, который указывается в личном кабинете CDEK Pay для интернет-магазина, и на который приходят данные. Обработка вебхука на стороне интеграции происходит достаточно просто: определяется эндпоинт на стороне интеграции, на который могут прийти данные.

$data = json_decode(html_entity_decode(file_get_contents("php://input")), true);

Одна строка кода формирует массив $data с параметрами оплаченного заказа, которые можно проверить на валидность и обновить статус заказа в базе данных интернет-магазина. Данные подписаны секретным ключом, подобным образом, как в случае передачи данных по заказу на сторону CDEK Pay.

Данные при возврате средств тоже приходят по вебхуку. Статус заказа меняется на «Возмещен», и сумма возврата сохраняется в базе данных. Но что делать, если пользователь решил оплатить заказ по ссылке, пришедшей, например, ему на e-mail в тот момент, когда интернет-магазин обновляется, и сайт висит в нерабочем режиме обслуживания? В этот момент магазин не может принять данные по вебхуку. Конечно, CDEK Pay будет продолжать попытки отправить данные заказа торговцу. В отдельном случае данные магазином так и не будут получены.

Тяжелый случай помогает победить эндпоинт информации о заказе, где данные приходят не по вебхуку, а в ответ на запрос СДЭК Pay на проверку статуса заказа, благо номер заказа сохранен в базе данных. Эндпоинт (GET-запрос) для получения информации по платежу в боевом режиме — /merchant_api/payments, а в тестовом — /test_merchant_api/payments. Вызов контрольных эндпоинтов можно организовать на периодической основе, например, через планировщика задач cron.

Основная информация, передаваемая на указанные эндпоинты, — логин магазина, номер заказа CDEK Pay и сигнатура SHA256. В ответ появляется список заявок заказа cо статусами и суммами, которых достаточно для обновления базы мерчанта.

Офис CDEK уголок посетителя
Уголок посетителя

OpenCart 2.3, 3.0, 4.0

Для всех версий OpenCart в файле admin/controller/payment/cdekpay.php (указан путь OpenCart 4, для других версий путь немного отличается) создан метод install(), с действиями, которые нужно выполнить при установке модуля. К таким действиям относится настройка сессий.

$this->load->model("setting/setting");
// Настройка нужна, чтобы после редиректа со страницы оплаты пользователь не разлогинивался
$this->model_setting_setting->editValue("config", "config_session_samesite", "Lax");

Настройка сессий нужна, чтобы после редиректа с оплаты обратно в интернет-магазин сессия пользователя сохранялась, и не происходило «разлогинивания». Настройка сессий актуальна только в версии OpenCart 4. Также в методе install() создаем таблицу с данными по заказу.

$this->db->query("CREATE TABLE IF NOT EXISTS `" . DB_PREFIX . "cdekpay_order` (
        `id` int(11) NOT NULL,
        `status` varchar(50) DEFAULT NULL,
        `oc_order_id` int(11) DEFAULT NULL,
        `currency_code` CHAR(3) NOT NULL,
        `order_total` int(11) NOT NULL,
        `success_notified` boolean default false,
        `cancellation_requested_notified` boolean default false,
        `success_cancellation_notified` boolean default false,
        `cancelled_notified` boolean default false,
        `voided_notified` boolean default false,
        `refund_total` int(11) DEFAULT NULL,
        `updated_at` timestamp DEFAULT CURRENT_TIMESTAMP NULL on update CURRENT_TIMESTAMP,
        `created_at` timestamp DEFAULT CURRENT_TIMESTAMP NULL,
        PRIMARY KEY `id` (`id`)
    ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
");

В этой таблице будем сохранять соответствие номера заказа СДЭК Pay и OpenCart, другую полезную информацию по заказу. Например, стадии заказа, сумму возврата, уведомления пользователей по разным стадиям заказа.

Версия OpenCart 4 поддерживает cron-задачи из коробки. Для добавления задачи нужно в методе install() указать следующие строки. В метод addCron() передаем название задачи, описание задачи, периодичность, путь к задаче.

$this->load->model("setting/cron");
$this->model_setting_cron->addCron("cdekpay", "СДЭК Pay ежечасная проверка статусов заказов", "hour", "extension/cdekpay/cdekcronjob", true);

Помимо метода install() объявляется метод uninstall(), который подчистит созданное в install().

$this->db->query("DROP TABLE IF EXISTS `" . DB_PREFIX . "cdekpay_order`;");
$this->load->model("setting/cron");
$this->model_setting_cron->deleteCronByCode("cdekpay");

Удаляется таблица, созданная методом install(), и удаляется cron-задача модуля оплаты СДЭК Pay. Удаление cron-задачи актуально только для версии OpenCart 4.

В отличие от OpenCart 2.3, версии 3 и 4 поддерживают шаблонизатор Twig, удобный при написании шаблонов. Основной метод, который нам понадобится при интеграции, — это метод смены статуса заказа. В OpenCart делается следующим образом.

$this->load->model("checkout/order");

Сначала загружаем модель checkout/order.

$this->model_checkout_order->addHistory($cdekPayOrderInfo["oc_order_id"], $payment_cdekpay_setting["order_status"]["completed"]["id"], $comment, true);

Потом обращаемся к методу addHistory() для OpenCart 4.0 (для версии 2.3 будет addOrderHistory()), передавая параметры: номер заказа OpenCart, новый статус заказа, комментарий и булево значение в зависимости от того, нужно ли информировать пользователя о смене статуса. Вручную обновлять остатки на складе магазина не нужно. OpenCart может поправить автоматически, в зависимости от настроек статусов заказа.

OpenCart карточка товара
Карточка товара OpenCart

CS-Cart 4.16

CS-Cart — российское решение для электронной коммерции, подходящее для интернет-магазинов любого размера. CS-Cart, пожалуй, была самой удобной CMS для подключения модуля оплаты: хорошая документация (необходимые для работы методы легко найти), красивый дизайн из коробки не требует поиска тем оформления.

В отличие от OpenCart не нужно создавать отдельную таблицу для хранения состояний платежа. В CS-Cart есть функция fn_update_order_payment_info для добавления пользовательской информации по платежу. Например, при успешном оформлении заказа вызываем эту функцию.

fn_update_order_payment_info($order_id, [
    "cdekpay_order_id" => $data["order_id"],
    "payment_link" => $data["link"],
    "success_notified" => "no",
    "cancellation_requested_notified" => "no",
    "success_cancellation_notified" => "no",
    "cancelled_notified" => "no",
    "voided_notified" => "no",
]);

В примере сохраняются номер заказа CDEK Pay, ссылка на оплату и состояние уведомлений пользователя о статусе. Если понадобится получить, например, сохраненную ссылку на оплату — вызываем следующий код.

$order_info = fn_get_order_info($order_id);
$url = $order_info["payment_info"]["payment_link"];

С обновлением платежной информации разобрались. Далее, по мере взаимодействия с API и изменения статуса заказа нужно уметь обновлять этот статус. Для этого предназначена функция fn_change_order_status.

fn_change_order_status(
    $cdekpay_order["order_id"],
    $processor_settings["statuses"]["paid"]
);

Переменная $processor_settings содержит настройки платежного процессора, которые можно получить следующим образом.

$payments = fn_get_payments([processor_script" => "cdekpay.php"]);
$payment_id = array_key_first($payments);
$processor_settings = unserialize($payments[$payment_id]["processor_params"]);
CS-Cart карточка товара
Карточка товара CS-Cart
CS-Cart корзина
Корзина CS-Cart

VirtueMart 4 для Joomla 3

Бесплатное решение электронной коммерции с открытым исходным кодом VirtueMart нельзя использовать отдельно, поскольку это плагин для CMS Joomla. Разработкой VirtueMart занимается небольшая команда — пять разработчиков, помимо которых есть сообщество, которое так или иначе участвует в проекте.

В VirtueMart нет такой свободы именования методов платежного модуля, как в OpenCart. Класс платежного модуля наследуется от абстрактного класса vmPSPlugin, который, в свою очередь, наследуется от другого абстрактного класса. Так, для вывода информации о заказе на стороне бэкенда есть метод plgVmOnShowOrderBEPayment. Именно его будем переопределять в модуле оплаты, чтобы расширить отображение информации по заказу, например, для включения в него платежной информации.

Таблица платежной информации в VirtueMart создается специфическим образом. В конструкторе указывается:

$this->tableFields = array_keys($this->getTableSQLFields());

Далее определяется метод getTableSQLFields, содержащий поля платежа и заказа, в нашем случае выглядящий таким образом.

function getTableSQLFields()
{
    $SQLfields = array(
        "id"                              => "int(11) unsigned NOT NULL AUTO_INCREMENT",
        "virtuemart_order_id"         	  => "int(11) UNSIGNED DEFAULT NULL",
        "cdekpay_order_id"            	  => "int(11) UNSIGNED DEFAULT NULL",
        "order_number"                	  => "char(64)", // VirtueMart string order number
        "virtuemart_paymentmethod_id" 	  => "mediumint(1) UNSIGNED DEFAULT NULL",
        "paylink"                     	  => "char(255) DEFAULT NULL",
        "status"                      	  => "char(255) DEFAULT NULL",
        "currency_code"               	  => "char(3) NOT NULL DEFAULT 'TST' ",
        "total"                       	  => "bigint UNSIGNED DEFAULT NULL",
        "success_notified"            	  => "boolean DEFAULT false",
        "cancellation_requested_notified" => "boolean DEFAULT false",
        "success_cancellation_notified"   => "boolean DEFAULT false",
        "cancelled_notified"          	  => "boolean DEFAULT false",
        "voided_notified"             	  => "boolean DEFAULT false",
        "refund_total"                	  => "bigint UNSIGNED DEFAULT NULL",
    );

    return $SQLfields;
}

Для обновления статуса заказа используется функция updateStatusForOneOrder.

$orderModel = new VirtueMartModelOrders();
// получаем заказ по номеру заказа VirtueMart
$vmOrder = $orderModel->getOrder($oldOrder["virtuemart_order_id"]);
// берем статус «Оплачен» из настроек модуля
$vmOrder["order_status"] = $this->cdekpaySDK->getStatuses()->paid;
// обновляем статус заказа
$orderModel->updateStatusForOneOrder($oldOrder["virtuemart_order_id"], $vmOrder);

HikaShop для Joomla 3

HikaShop — электронная коммерция на базе Joomla с бесплатной базовой версией и двумя платными версиями с расширенным функционалом. Разработка оплаты из-под HikaShop похожа на разработку модуля для VirtueMart. Подобно VirtueMart, в HikaShop есть базовый класс платежного модуля hikashopPaymentPlugin, методы которого нужно реализовать.

Для предотвращения DDoS-атак ведущий разработчик проекта ввел ограничения на параллельный запуск cron-задач в модуле оплаты. Сами cron-задачи периодически опрашивают CDEK Pay и проверяют статусы платежей на тот случай, если не был получен ответ об успешной оплате или отмене заказа по вебхуку. Планируется в будущем включить функционал в остальные модули оплаты.

Первым делом, понадобится таблица хранения состояний cron-задач. Для этого в файле cdekpay.install.sql определяем таблицу следующим образом.

CREATE TABLE IF NOT EXISTS `#__hikashop_cdekpay_cron_jobs` (
    `job_name` varchar (256) NOT NULL,
    `job_process_id` int(10) unsigned NOT NULL,
    `job_started_at` datetime NOT NULL DEFAULT current_timestamp(),
    `job_completed_at` datetime NULL,
    `job_error` text NULL,
    PRIMARY KEY (`job_name`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

Чтобы создать таблицу при установке модуля, в файле настроек cdekpay.xml добавляется следующий код.

<install>
    <sql>
        <file driver="mysql" charset="utf8">cdekpay.install.sql</file>
    </sql>
</install>

Когда запускается cron-задача, либо при получении извне запроса на запуск, вычисляем, сколько прошло времени с предыдущего запуска. Для защиты от атак частота пусков ограничивается так, чтобы задержка составляла минимум 60 минут.

InSales

InSales никуда не нужно устанавливать, ведь он является SaaS, в котором:

01 Интерфейс разработчика доступен интерфейс разработчика;
02 Документация хорошая документация по реализации приложений;
03 Настройки надстройки расширяют возможности основного приложения.

По факту разрабатывается REST API-микросервис, который будет взаимодействовать с API InSales. Код интеграции CDEK Pay никуда не устанавливается, а публикуется на своем сервере и администрируется самостоятельно. Регистрируемся в реферальной программе. В личном кабинете появляется пустой раздел приложений. Зарегистрировать приложение довольно-таки просто: придумать уникальное название, заполнить описания и прописать доступы, которые потребуются для взаимодействия с InSales. На этапе тестирования можно указать полные права доступа. Но при публикации приложения в каталог не стоит брать полные права без явной надобности. Ведь чем больше прав попросим, тем больше внимания со стороны модератора, и больше вероятность того, что надстройку могут отклонить.

При регистрации надстройки указываются:

  • URL «установки» — https://insales.cdekfin.ru/install;
  • URL «удаления» — https://insales.cdekfin.ru/uninstall;
  • URL «авторизации» — https://insales.cdekfin.ru/auth.

Требуемые URL по началу могут ввести в ступор. Мы только задумали приложение, естественно никаких URL фактически не существует. Но не стоит отчаиваться: поля пока заполним произвольными значениями, а позже исправим. Нажимаем кнопку «создать», и приложение встроено в InSales.

Пользователь Степан создал на InSales магазин supershop.com и решил установить в него расширение из каталога. Степан кликает по кнопке «установить», InSales тем временем выполняет следующее.

  • Генерирует token — одноразовую строку, хранить которую не нужно.
  • Устанавливает пароль подключения к API: password = MD5 (token + secret_key), где seсret_key — секретный ключ приложения, который генерируется при регистрации приложения. Пароль у каждого магазина свой.
  • Отправляет GET-запрос на URL «установки» с параметрами token, shop и insales_id, где shop — адрес магазина (supershop.myinsales.ru); insales_id — внутренний идентификатор магазина, который не меняется.

InSales посчитает, что приложение успешно установлено, если получит ответ 200 OK. До тех пор API-запросы вызывать нельзя.

public function install(Request $request)
{
    $validator = Validator::make($request->all(), [
        "insales_id" => ["required", "numeric"],
        "token"      => ["required", "string"],
        "shop"       => ["required", "string"],
    ]);

    if ($validator->fails()) {
        return response()->json("HTTP_BAD_REQUEST", Response::HTTP_BAD_REQUEST);
    }

    $isInstalled = AppInSales::install($request->shop, $request->token, $request->insales_id);

    if ($isInstalled) {
        return response()->json("OK", Response::HTTP_OK);
    }

    return response()->json("", Response::HTTP_INTERNAL_SERVER_ERROR);
}

Аналогичным образом происходит и удаление надстройки. Если надстройка ответит 200 ОК, то приложение считается удаленным. После нажатия на соответствующую кнопку inSales отправит надстройке запрос:

https://insales.cdekfin.ru/uninstall?shop=shop&token=password&insales_id=insales_id, где password — пароль приложения; shop — адрес магазина в поддомене myinsales.ru; insales_id — внутренний уникальный идентификатор магазина.

InSales карточка товара
Карточка товара InSales

WooCommerce для WordPress

WordPress CMS, в отличие от OpenCart CMS, является, в первую очередь, системой управления контентом блога, а на электронную коммерцию и намека нет. Но расстраиваться не стоит, разработчики из компании Automattic постарались, чтобы исправить данную ситуацию, и сделали отличный плагин WooCommerce, который, в свою очередь, расширяет стандартные возможности WordPress до полноценного интернет-магазина. Таким образом, магазин есть, осталось только его настроить. В этой статье мы не будем углубляться в многочисленные тонкости настройки WooCommerce.

WordPress — универсальная CMS для сайта, а WooCommerce — универсальный международный плагин интернет-торговли для WordPress CMS. Известно, что в каждой стране свои «заморочки» с торговлей, законы, кассы, налоги, способы доставки и оплаты. Компания Automattic просто физически не способна реализовать решения для всех стран, поэтому был создан проект со «стройной» моделью данных и с довольно хорошей документацией. А мы как программисты реализовали способ доставки и способ оплаты для Российской Федерации.

Особенности реализации есть, и они относятся к самой CMS. Для тех, кто не программировал WordPress, «экшен» — набор определенных действий над некоей сущностью, «фильтр» — тот же экшен, изменяющий получаемый контент. Но это не все. Чтобы встроить фильтры и экшены, нужна еще точка входа в приложение, т.е. конкретное место или условие, при котором выполнится логика экшена или фильтра. Точка соприкосновения называется «хук» (с английского «крючок») — хуками мы прицепимся к определенным частям исполняющегося кода.

Другая особенность разработки под WordPress — зависимость от сторонних расширений. Без активации расширение не запускается. Но в нашем случае создается не плагин для CMS, а новый способ оплаты внутри WooCommerce, т.е. логика цепляется крючками к коду WooCommerce. Посему проверять наличие и статус активации плагина СДЭК Pay будем при активации плагина оплаты в методе activate_cdekpay с помощью метода check_woocommerce_status_cdekpay. Если получилось, что WooCommerce не установлен — выводим сообщение и предлагаем установить по ссылке.

function activate_cdekpay($network_wide) 
{
    // Check, that Woocommerce is installed and active
    if (!check_woocommerce_status_cdekpay()) {
        stop_activation_cdekpay();
    }

    require_once plugin_dir_path( __FILE__ ) . "includes/class-cdekpay-activator.php";

    if( is_multisite() && $network_wide ) {
        global $wpdb;

        $blogs = $wpdb->get_col("SELECT blog_id FROM $wpdb->blogs");
        foreach( $blogs as $blog_id ) {
            switch_to_blog($blog_id);

            Cdekpay_Activator::activate();

            restore_current_blog();
        }
    } else {
        Cdekpay_Activator::activate();
    }
}

Метод check_woocommerce_status_cdekpay возвращает true или false — признак наличия активированного плагина WooCommerce.

function check_woocommerce_status_cdekpay() 
{
    $woocommerce_plugin = "woocommerce/woocommerce.php";
    if (in_array($woocommerce_plugin, get_option("active_plugins"))) {
        return true;
    }

    if (!is_multisite()) {
        return false;
    }

    $plugins = get_site_option("active_sitewide_plugins");

    return isset($plugins[$woocommerce_plugin]);
}

Если проверка не прошла, вызывается stop_activation_cdekpay — метод, сообщающий об остановке активации плагина.

function stop_activation_cdekpay() {
    deactivate_cdekpay();

    $error_message = "Плагину CDEK Pay требуется установленный и активированный плагин ";
    $error_message .= "<a href=\"https://wordpress.org/extend/plugins/woocommerce/\" target=\"_blank\">WooCommerce</a>. ";
    $error_message .= "<a href=\"/wp-admin/plugins.php\">Вернуться</a>";
    $error_message = __($error_message, "cdekpay");

    wp_die($error_message);
}

В целом разработка под WooCommerce была приятной — как и в случае с CS-Cart хватало документации и примеров.

i в круге

Подключим сайт к CDEK Pay и любой другой платежной системе.