Перейти до змісту
ECOMLARAVEL9 хв читання

Інтеграція Нової Пошти та оплати: де найчастіше ламається.

ЄвгенійТех-лід, 11 років у PHP / Laravel
Опубліковано
Оновлено

Інтеграція Нової Пошти та онлайн-оплати ламається не там, де її чекають. Сам виклик API зазвичай працює з першого разу. Проблеми починаються на межах: коли відділення зникло з довідника, колбек платіжки прийшов двічі, а ТТН створилася на неправильну вагу. Ця стаття для власників магазинів і розробників на Laravel: 10 найчастіших місць поломок і те, як ми їх перевіряємо.

Нижче все базується на практиці Laravel-проєктів з доставкою Новою Поштою і оплатою через LiqPay або схожі сервіси. Конкретні назви полів перевіряйте за актуальною документацією провайдера: API змінюються, а принципи ні.

Чому саме ці інтеграції створюють найбільше проблем

Доставка й оплата живуть поза вашим кодом. Ви не контролюєте їхню доступність, формат відповідей і час реакції. До того ж обидві інтеграції стосуються грошей: помилка у відображенні банера не коштує нічого, а помилка в оплаті коштує замовлення і довіри клієнта.

Є ще одна причина. Ці інтеграції рідко тестують на повному шляху. Розробник перевіряє «happy path» у sandbox, замовник клікає один раз у тестовому режимі, і все виглядає добре. Реальні клієнти роблять інше: закривають вкладку під час 3-D Secure, обирають поштомат для посилки на 30 кг, вводять номер телефону з пробілами.

Якщо ви замовляєте розробку інтернет-магазину, запитайте підрядника, як саме він перевіряє ці сценарії. Відповідь «ми тестували в sandbox» недостатня.

Нова Пошта: довідники та вибір відділення

1. Застарілі довідники міст і відділень

Типова помилка: розробник один раз завантажує список відділень у базу і забуває про нього. Відділення відкриваються, закриваються, переїжджають, тимчасово не працюють. Клієнт обирає відділення, якого вже немає, а ТТН не створюється.

Що робити:

  • Оновлюйте довідник за розкладом (php artisan schedule), а не вручну. Для невеликого магазину достатньо раз на добу вночі.
  • Зберігайте в замовленні не лише warehouse_ref, а й текстовий знімок: місто, номер відділення, адресу. Через місяць відділення може змінитися, а менеджеру треба бачити, що вибрав клієнт.
  • Позначайте відділення, що зникли з довідника, а не видаляйте. Інакше старі замовлення втратять зв'язок.

2. Обмеження за вагою та габаритами

Не кожне відділення приймає вантаж будь-якої ваги. Поштомати мають жорсткі обмеження за розміром і вагою посилки, а частина відділень не приймає великогабаритні вантажі. У довіднику відділень є поля з максимальною вагою і габаритами, і їх треба використовувати.

Класичний баг: у кошику важкий товар, у чекауті пропонуються всі відділення, клієнт обирає поштомат, а менеджер потім телефонує і просить змінити відділення. Це втрачений час і частина замовлень, які клієнт просто скасує.

Правильна поведінка: порахувати сумарну вагу кошика, відфільтрувати відділення за обмеженнями, а якщо жодне не підходить, показати зрозуміле повідомлення. Для цього в товарів мають бути заповнені вага й габарити. Це вимога до контенту, а не лише до коду.

3. Пошук міста та населені пункти з однаковою назвою

В Україні багато населених пунктів з однаковими назвами в різних областях. Пошук по тексту без уточнення області повертає не те село. Використовуйте метод пошуку населених пунктів (Address/searchSettlements) і зберігайте Ref, а не назву. У випадаючому списку показуйте область і район, щоб клієнт бачив, яке саме «Лісове» він обирає.

Окремо перевіряйте введення з помилками: «Днепр» замість «Дніпро», зайві пробіли, апостроф у «Кам'янське» (є три різні символи апострофа). Автокомпліт має це переживати.

Створення накладної (ТТН)

4. Помилки валідації при створенні ТТН

Накладна створюється методом InternetDocument/save, і тут найбільше «дрібних» відмов: телефон отримувача у неправильному форматі, не заповнене прізвище, оголошена вартість нуль, вага менша за допустиму мінімальну. Якщо ваш код створює ТТН прямо в момент оформлення замовлення, клієнт бачить помилку, якої не розуміє.

Наша практика така:

  1. Нормалізуйте телефон при введенні: приберіть пробіли, дужки, дефіси, приведіть до формату 380XXXXXXXXX.
  2. Створюйте ТТН не в HTTP-запиті клієнта, а у фоновому завданні (queue job).
  3. Якщо API повернув помилку, зберігайте текст помилки в замовленні й показуйте менеджеру в адмінці. Не ковтайте виняток.
  4. Додайте кнопку «Створити ТТН повторно» для менеджера.

Приклад каркаса завдання:

class CreateShipmentJob implements ShouldQueue
{
    public int $tries = 5;
    public array $backoff = [30, 120, 600, 1800];

    public function __construct(public int $orderId) {}

    public function handle(NovaPoshtaClient $np): void
    {
        $order = Order::findOrFail($this->orderId);

        // Idempotency: do not create a second waybill for the same order
        if ($order->ttn_number) {
            return;
        }

        $response = $np->createWaybill($order);

        if (! $response['success']) {
            $order->update(['shipment_error' => json_encode($response['errors'], JSON_UNESCAPED_UNICODE)]);
            throw new ShipmentFailed($response['errors']);
        }

        $order->update([
            'ttn_number' => $response['data'][0]['IntDocNumber'],
            'shipment_error' => null,
        ]);
    }
}

Ключове тут не синтаксис, а перевірка ttn_number на початку. Без неї повторний запуск завдання створить другу накладну і ви заплатите за дві.

5. Накладений платіж і зворотна доставка грошей

Накладений платіж додає складність: сума зворотної доставки має збігатися із сумою замовлення мінус уже сплачена частина. Типові баги:

  • клієнт оплатив частково (знижка, промокод, бонуси), а ТТН створена з повною сумою накладеного платежу;
  • сума округлена по-різному в магазині і в накладній, і виникає розбіжність у копійки, яку бухгалтерія потім шукає тижнями;
  • для замовлень, оплачених карткою, накладений платіж взагалі не має створюватися, але через умову в коді все одно створюється.

Тестуйте комбінації: передоплата повна, передоплата частина, накладений платіж, промокод плюс накладений платіж. Це десять хвилин ручної перевірки, яка рятує від скарг.

6. Недоступність API та таймаути

API Нової Пошти буває повільним, іноді недоступним, іноді повертає помилку лише на певні запити. Якщо пошук відділень у чекауті йде прямо в API без кешу й обмеження часу очікування, повільна відповідь зависає всю форму.

Мінімум, який варто мати:

  • явний таймаут HTTP-клієнта (у Laravel Http::timeout(8)->retry(2, 200));
  • кеш довідників, щоб чекаут не залежав від API під час пошуку;
  • запасний сценарій: якщо довідник недоступний, дозволити ввести адресу вручну й передати замовлення менеджеру на уточнення.

Це рішення про бізнес, а не лише про код: що важливіше, не втратити замовлення чи отримати ідеальні дані. Для магазину майже завжди першим є замовлення.

Онлайн-оплата: колбеки, статуси, дублі

7. Довіра до сторінки повернення замість серверного колбеку

Найнебезпечніша помилка в оплаті. Після платежу клієнт повертається на сторінку «Дякуємо за замовлення» (result_url). Якщо ви позначаєте замовлення оплаченим саме тут, то ризикуєте двічі: клієнт може закрити вкладку до повернення, і оплата не зафіксується, а зловмисник може відкрити цю адресу вручну і вдати оплату.

Правильна схема така: статус замовлення змінює лише серверний колбек платіжної системи (server_url у LiqPay), а сторінка повернення тільки показує поточний статус і за потреби опитує його.

8. Непідтверджений підпис і повторні колбеки

Платіжна система надсилає колбек із підписом. Якщо ви його не перевіряєте, будь-хто може надіслати POST-запит і позначити замовлення оплаченим. У LiqPay підпис розраховується від приватного ключа і даних запиту; формула є в документації, і її треба реалізувати та покрити тестом.

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

public function handle(Request $request): Response
{
    $data = $request->input('data');
    $signature = $request->input('signature');

    if (! hash_equals($this->sign($data), $signature)) {
        abort(403);
    }

    $payload = json_decode(base64_decode($data), true);

    DB::transaction(function () use ($payload) {
        $order = Order::where('public_id', $payload['order_id'])
            ->lockForUpdate()
            ->firstOrFail();

        // Terminal state reached already: ignore repeated callbacks
        if ($order->payment_status === 'paid') {
            return;
        }

        if ($payload['status'] === 'success' && (float) $payload['amount'] === (float) $order->total) {
            $order->markPaid($payload['payment_id']);
        }
    });

    return response('OK');
}

Зверніть увагу на дві речі: lockForUpdate() не дає двом одночасним колбекам обробити замовлення паралельно, а порівняння суми захищає від підміни. Про суму кажуть рідко, а марно: клієнт, який змінив параметри на стороні браузера, має отримати відмову.

9. Не оброблені проміжні та невдалі статуси

Платіж має більше станів, ніж «успіх» і «помилка». Є очікування підтвердження, 3-D Secure, обробка, повернення, відхилення банком. Якщо код знає лише success, то все інше зависає в невизначеності: клієнт заплатив, але замовлення «очікує оплати», або навпаки.

Для кожного статусу прийміть рішення і запишіть його в тест:

Статус платежуЩо робимо із замовленням
успіхпозначаємо оплаченим, резервуємо товар, шлемо підтвердження
очікування / обробказалишаємо «в обробці», не скасовуємо за таймером занадто рано
відхилено / помилкадаємо клієнту повторити оплату тим самим замовленням
повернення коштівповертаємо залишки, оновлюємо статус, повідомляємо менеджера

Окремий випадок: клієнт заплатив, а колбек не дійшов. Нехай раз на кілька хвилин працює завдання, яке перевіряє «зависшим» замовленням статус у платіжній системі. Це дешевше, ніж розбиратися вручну за виписками.

10. Розбіжність між замовленням і платежем

Коли клієнт змінює кошик після відкриття сторінки оплати, або менеджер редагує замовлення вже після створення платежу, сума в платіжній системі й у магазині розходиться. Результат: оплачено 1 200, замовлення на 1 450, бухгалтерія в паніці.

Рішення: після створення платежу замовлення блокується для змін суми. Якщо зміна потрібна, створюйте новий платіж, а старий анулюйте. Також додайте звірку: щодня порівнюйте оплачені замовлення з реєстром платежів провайдера.

Хочете, щоб інтеграції пройшли перевірку до запуску? Ми проганяємо checkout із доставкою й оплатою на реальних сценаріях і повертаємо баг-репорти з відео. Замовити QA-аудит інтернет-магазину, від 6 000 грн.

Як тестувати такі інтеграції

Окремий список, який ми використовуємо як мінімум перед запуском магазину:

  1. Sandbox-режим усіх провайдерів: тестові картки, тестові ТТН. Перевірте, що тестовий режим не залишився увімкненим на проді (це трапляється частіше, ніж хочеться).
  2. Негативні сценарії: відмова банку, закрита вкладка на 3-D Secure, недостатньо коштів, закінчений термін картки.
  3. Повторні колбеки: надішліть той самий колбек двічі вручну (curl або Postman) і переконайтеся, що нічого не задвоїлося.
  4. Підроблений колбек: змініть суму або підпис і переконайтеся, що отримали відмову.
  5. Недоступний API: вимкніть мережу для API Нової Пошти в тесті й подивіться, що бачить клієнт.
  6. Різні пристрої: оплата і вибір відділення на iOS Safari й Android Chrome. Рекламні вбудовані браузери (Instagram, Telegram) нерідко ведуть себе інакше.
  7. Автотести: підпис колбеку, ідемпотентність і розрахунок накладеного платежу легко покрити тестами на Pest/PHPUnit, а повний шлях checkout автоматизувати на Playwright.

Для Laravel зручно фіксувати відповіді зовнішніх API через Http::fake(): тест стає швидким і не залежить від мережі, а ви перевіряєте саме вашу логіку, а не чужий сервер. Про автоматизацію ключових шляхів докладніше на сторінці автоматизації тестування.

Про те, чому замовлення губляться ще до оплати, ми писали окремо: 7 типових багів checkout. А повний перелік перевірок перед релізом зібраний у чек-листі тестування інтернет-магазину.

Моніторинг після запуску

Інтеграція не закінчується релізом. Зовнішні API змінюються, ключі закінчуються, ліміти запитів вичерпуються. Мінімум, який варто налаштувати:

  • сповіщення в Telegram чи на пошту, коли завдання створення ТТН провалилося після всіх повторів;
  • щоденний звіт: кількість замовлень без ТТН і кількість оплат без підтвердженого колбеку;
  • логування запитів і відповідей до платіжки та Нової Пошти (без повних номерів карток і приватних ключів) із прив'язкою до номера замовлення;
  • нагадування про закінчення дії ключів і токенів.

Це дрібниці, але саме вони перетворюють «ми дізналися від клієнта» на «ми виправили до того, як клієнт помітив».

Підсумок: що перевірити сьогодні

Якщо магазин уже працює, є п'ять швидких перевірок:

  • Чи оновлюється довідник відділень автоматично, і коли востаннє?
  • Чи змінює статус оплати лише серверний колбек, і чи перевіряється підпис?
  • Що станеться, якщо той самий колбек прийде двічі?
  • Чи видно менеджеру помилку створення ТТН, чи вона лише в логах?
  • Чи збігається сума накладеного платежу із залишком до сплати при знижках?

Якщо хоча б на одне питання відповідь «не знаємо», це привід для перевірки. Саме такі місця ми розбираємо в технічному аудиті коду, коли магазин уже живий і треба зрозуміти, де втрачаються гроші.

Магазин працює, а замовлення губляться? Опишіть проблему в брифі, і ми скажемо, з чого почати перевірку, безкоштовно й без зобов'язань. Заповнити бриф.

АВТОРЄвгенійЗасновник і тех-лід Tochno. 11 років у PHP / Laravel, десятки аудитів інтернет-магазинів. Пише про те, що бачить у реальних проєктах.Про команду

Коротко про головне

Не знайшли відповідь? Напишіть у Telegram.

Чому на сайті не завантажуються відділення Нової Пошти?

Найчастіше довідник відділень не оновлюється або запит до API Нової Пошти падає без обробки помилки. Зберігайте локальну копію довідника й оновлюйте її за розкладом, а при збої API показуйте запасний варіант вибору. Також перевірте, чи чинний ключ API.

Чому оплата пройшла, а замовлення залишилось неоплаченим?

Платіжна система повідомляє про результат окремим запитом-колбеком, і якщо його не прийнято або не підтверджено підпис, статус не оновиться. Колбек має бути ідемпотентним: повторний виклик не повинен створювати дубль. Регулярно звіряйте замовлення з виписками платіжної системи.

Як уникнути дублів накладних ТТН у Новій Пошті?

Перед створенням накладної перевіряйте, чи вона вже існує для цього замовлення, і блокуйте повторні запити на сервері. Зберігайте номер ТТН і статус одразу після відповіді API. Так повторний клік чи повторне виконання завдання в черзі не створить другу накладну.

Як протестувати інтеграцію з оплатою без реальних грошей?

Використовуйте тестовий режим платіжної системи: більшість провайдерів дають тестові картки та sandbox для успішних і відхилених платежів. Окремо перевірте збій, затримку й повторний колбек. Перед запуском зробіть один реальний платіж на мінімальну суму. Так ви побачите, як сайт поводиться в реальних умовах.

Впізнали свій проєкт?

Опишіть задачу в брифі — відповімо з оцінкою і планом за один робочий день.

Що потрібно

Надсилаючи форму, ви погоджуєтесь з політикою конфіденційності.