Кастомний модуль оплати для PrestaShop: як уникнути помилок інтеграції
При інтеграції стороннього платіжного шлюзу в PrestaShop багато хто стикається з проблемою некоректної обробки вебхуків. Типова помилка — статус замовлення не оновлюється після callback через відсутність перевірки підпису HMAC. Якщо не валідувати callback, можна отримати «завислі» замовлення, коли гроші списалися, а статус залишився «очікування». Інша поширена проблема — неправильна передача суми в мінімальних одиницях валюти (копійках/центах), що призводить до розбіжностей в обліку. На прикладі інтеграції MyPay розберемо правильне налаштування Payment API, щоб цього уникнути.
Крім того, важливо коректно формувати URL callback з урахуванням безпеки. Ми використовуємо лише POST-запити з підписом у заголовку X-Signature. Такий підхід виключає підміну даних. Додатково реалізуємо idempotency key, щоб повторний callback не змінював статус замовлення.
Як уникнути помилок при інтеграції платіжного шлюзу в PrestaShop?
Ключовий момент — валідація замовлення до редиректу та обробка webhook з HMAC-підписом. Для запобігання timing attack використовуємо hash_equals при перевірці підпису. У модулі потрібно зареєструвати три хуки: paymentOptions, paymentReturn та actionOrderStatusUpdate. Останній обов'язковий для автоматичних повернень через адмінку. Ідемпотентність досягається перевіркою унікального ключа в callback. Крім цього, необхідно перевіряти поточний статус замовлення: якщо воно вже оплачено, повторний callback не повинен змінювати статус.
Чому кастомний модуль краще готового плагіна?
| Параметр | Кастомний модуль | Готовий плагін |
|---|---|---|
| Гнучкість | 100% під бізнес-логіку | Обмежений налаштуваннями |
| Безпека | Повний контроль коду | Можливі вразливості |
| Підтримка | Прямий зв'язок з розробником | Через тікети вендора |
| Продуктивність | Оптимізований під ваш стек | Часто надлишковий код |
Кастомна розробка окупається, якщо у вас нестандартна валюта, кілька складів або складні сценарії повернень. Наприклад, у готовому плагіні для Fondy немає підтримки часткових повернень. У кастомному модулі ми реалізували API виклику повернення через адмінку PrestaShop. Готові плагіни часто не підтримують мультивалютність і не адаптовані під специфіку магазину. У кастомному модулі ми передаємо код валюти (ISO 4217) у запиті до шлюзу, що особливо важливо для регіонів з різними валютами.
Які типові помилки допускають при інтеграції?
Найчастіша — ігнорування перевірки HMAC. Без неї зловмисник може підробити callback і змінити статус замовлення. Друга — неправильна обробка помилок API: якщо шлюз повернув 500, модуль повинен коректно відкотити замовлення, а не залишати його в підвішеному стані. Третя — некоректна робота з таймаутами: при довгій відповіді шлюзу користувач бачить нескінченне завантаження. Ми використовуємо async-запити з таймаутом 30 секунд і показуємо повідомлення про помилку.
Які шлюзи ми інтегруємо?
Ми працюємо зі Stripe, PayPal, Fondy, LiqPay, а також з будь-якими іншими, що надають REST API. У таблиці нижче — порівняння за ключовими параметрами.
| Шлюз | Комісія (приблизно) | Підтримка 3DS | Webhook HMAC | SDK для PHP |
|---|---|---|---|---|
| Stripe | 2.9% + 0.30€ | Так | Так | stripe/stripe-php |
| PayPal | 3.49% + 0.30€ | Так | Так | paypal/rest-api-sdk-php |
| Fondy | 2.3% | Так | Так | fondy/fondy-php |
| LiqPay | 2.5% | Так | Так | liqpay/liqpay-php |
Комісія вказана орієнтовно, точні тарифи уточнюйте у провайдера.
Структура та реалізація модуля
Для реєстрації модуля в системі оплати ми використовуємо хуки paymentOptions, paymentReturn та actionOrderStatusUpdate. Додатково можна підключити displayPaymentTop для виведення інформації перед вибором оплати.
Структура файлів
modules/mypay/
├── controllers/front/
│ ├── payment.php # Ініціалізація платежу
│ └── callback.php # Webhook від провайдера
├── views/templates/front/
│ └── payment_infos.tpl
├── mypay.php # Основний клас модуля
└── logo.png
Основний клас модуля
class MyPay extends PaymentModule
{
public function __construct()
{
$this->name = 'mypay';
$this->tab = 'payments_gateways';
$this->version = '1.0.0';
$this->author = 'Your Company';
$this->need_instance = 0;
$this->ps_versions_compliancy = ['min' => '8.0.0', 'max' => _PS_VERSION_];
$this->bootstrap = true;
parent::__construct();
$this->displayName = $this->trans('MyPay', [], 'Modules.Mypay.Admin');
$this->description = $this->trans('Оплата карткою через MyPay', [], 'Modules.Mypay.Admin');
}
public function install(): bool
{
return parent::install()
&& $this->registerHook('paymentOptions')
&& $this->registerHook('paymentReturn')
&& $this->registerHook('actionOrderStatusUpdate');
}
public function hookPaymentOptions(array $params): array
{
if (!$this->active) return [];
$this->context->smarty->assign([
'mypay_action_url' => $this->context->link->getModuleLink('mypay', 'payment', [], true),
]);
$option = new \PrestaShop\PrestaShop\Core\Payment\PaymentOption();
$option->setCallToActionText($this->trans('Оплата карткою', [], 'Modules.Mypay.Shop'))
->setAction($this->context->link->getModuleLink('mypay', 'payment', [], true))
->setAdditionalInformation(
$this->fetch('module:mypay/views/templates/front/payment_infos.tpl')
);
return [$option];
}
}
Контролер ініціалізації платежу
class MyPayPaymentModuleFrontController extends ModuleFrontController
{
public function postProcess(): void
{
$cart = $this->context->cart;
if (!$this->module->checkCurrency($cart)) {
Tools::redirect('index.php?controller=order');
}
$total = (int) round($cart->getOrderTotal(true) * 100);
$customer = new Customer($cart->id_customer);
$currency = new Currency($cart->id_currency);
$this->module->validateOrder(
$cart->id,
Configuration::get('MYPAY_OS_PENDING'),
$cart->getOrderTotal(true),
$this->module->displayName,
null,
[],
(int) $currency->id,
false,
$customer->secure_key
);
$orderId = Order::getIdByCartId($cart->id);
$client = new MyPayApiClient(
Configuration::get('MYPAY_API_KEY'),
Configuration::get('MYPAY_SECRET_KEY')
);
$payment = $client->createPayment([
'amount' => $total,
'currency' => $currency->iso_code,
'order_id' => $orderId,
'callback_url' => $this->context->link->getModuleLink('mypay', 'callback', [], true),
'success_url' => $this->context->link->getPageLink('order-confirmation', true, null, [
'id_cart' => $cart->id,
'id_module' => $this->module->id,
'id_order' => $orderId,
'key' => $customer->secure_key,
]),
]);
$order = new Order($orderId);
$order->reference = $payment['payment_id'];
$order->save();
Tools::redirect($payment['payment_url']);
}
}
Обробка callback
class MyPayCallbackModuleFrontController extends ModuleFrontController
{
public function postProcess(): void
{
$raw = file_get_contents('php://input');
$data = json_decode($raw, true);
$secret = Configuration::get('MYPAY_SECRET_KEY');
if (!hash_equals(hash_hmac('sha256', $raw, $secret), $_SERVER['HTTP_X_SIGNATURE'] ?? '')) {
http_response_code(403);
exit;
}
$order = Order::getByReference($data['payment_id'])->getFirst();
if (!$order) {
http_response_code(404);
exit('Order not found');
}
$statusMap = [
'succeeded' => Configuration::get('MYPAY_OS_PAID'),
'failed' => Configuration::get('PS_OS_ERROR'),
'cancelled' => Configuration::get('PS_OS_CANCELED'),
];
if (isset($statusMap[$data['status']])) {
$history = new OrderHistory();
$history->id_order = $order->id;
$history->changeIdOrderState((int) $statusMap[$data['status']], $order);
$history->addWithemail(true);
}
http_response_code(200);
echo 'OK';
exit;
}
}
Що входить в роботу
- Документація: опис встановлення, конфігурації та API-методів модуля.
- Доступи: передача вихідного коду, файлів модуля та дампів БД (при необхідності).
- Підтримка: 2 тижні безкоштовних доробок після здачі проекту. Можлива постгарантійна підтримка за домовленістю.
Ми документуємо кожен крок, щоб ви могли самостійно вносити зміни.
Процес і терміни
- Аналітика (2–4 години): вивчаємо документацію шлюзу, узгоджуємо вимоги, оцінюємо ризики.
- Проектування (1 день): схема інтеграції, визначення необхідних хунків, прототип модуля.
- Розробка (2–3 дні): написання коду з використанням Symfony-компонентів та Dependency Injection, реалізація API-клієнта та хунків, написання PHPUnit-тестів.
- Тестування (1 день): інтеграційне тестування з пісочницею шлюзу, перевірка всіх сценаріїв (успіх, відмова, повернення).
- Деплой (2 години): встановлення на staging, потім на production за допомогою CI/CD.
Кожен етап завершується демонстрацією замовнику. Ми використовуємо Git для контролю версій та Code Review для забезпечення якості. Термін розробки — від 2 до 5 робочих днів залежно від складності шлюзу. Вартість розраховується індивідуально та включає аудит поточної конфігурації магазину.
Зв'яжіться з нами для оцінки вашого проекту — це займе 2 години. Отримайте консультацію з інтеграції — це безкоштовно. Замовте інтеграцію і ми налаштуємо прийом платежів без головного болю.
Довіра та гарантії
Ми інтегрували платіжні шлюзи для 50+ магазинів на PrestaShop. Досвід роботи з платформою — більше 5 років. На всі модулі надається гарантія 3 місяці. Наші інженери завжди на зв'язку.







