У магазині Atlas — це магазин аксесуарів на Medusa з вітриною на Next.js — була одна доставка: Нова Пошта. Клієнт попросив додати Укрпошту, бо частина покупців живе там, де відділення Укрпошти є, а Нової Пошти немає.

Готового модуля Укрпошти для Medusa не існує, тому все писалося з нуля: проксі до довідника, класифікація пунктів видачі, крок вибору перевізника в чекауті й серверні перевірки. Нижче — те, що я дізнався вже по ходу, і чого немає в документації перевізника.

Що вийшло

Покупець на чекауті обирає перевізника, вводить місто, бачить список відділень із індексом і адресою та вказує, хто отримувач — фізична особа чи ФОП. Список тягнеться напряму з довідника Укрпошти, тож нічого не треба вносити руками й нічого не застаріває.

Запуск у цифрах: 707 пунктів видачі переписано для класифікації, тести бекенду зросли зі 107 до 240, тести вітрини — з 644 до 687. За три хвилини до мого тестового замовлення справжній покупець уже оформив доставку Укрпоштою — перевізник запрацював на живому трафіку одразу.

Довідник бреше прапорцями

Перша спокуса — класифікувати пункти за булевими полями довідника: є POSTERMINAL, є IS_CASH, усе ніби готове. Так робити не можна: ці поля не заповнені.

Перепис по Києву дав цифри, які закрили питання: POSTTERMINAL дорівнює 0 у всіх 141 поштоматі міста, а IS_CASH дорівнює 0 у всіх 342 пунктах — включно з відділеннями, де готівку беруть щодня. Єдине надійне джерело типу — TYPE_ACRONYM.

Друга пастка поруч: TYPE_SHORT і TYPE_LONG поводяться всупереч власним назвам, і їхній вміст залежить від ендпоінта. Один маршрут віддає в TYPE_SHORT код «МВ», інший у тому самому полі — «Міське відділення». Якщо десь у коді є fallback з TYPE_ACRONYM на TYPE_SHORT, він вистрелить рівно тоді, коли ви зміните ендпоінт.

І третє: перепису по Києву замало. Сільських і вантажних відділень у столиці просто немає, тому довелося додати Львів, Одесу, Ужгород, Коломию й кілька сіл — інакше три типи з семи не траплялися взагалі.

КодЩо цеРішення
МВміське відділенняпоказуємо
СВсільське відділенняпоказуємо
ВВвантажне відділенняпоказуємо
П-тпоштоматвимкнено (економіка тарифу)
PUDOточка видачівимкнено (економіка тарифу)
ЦКДцентр кур'єрської доставкивиключено — туди покупець не приходить
ПВпересувне відділеннявиключено — це автомобіль за графіком, а не адреса

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

«Порожньо» — не помилка, а 500 — не завжди ваша

Довідник відповідає 200 OK з тілом {"Entries":{}}, коли нічого не знайшов. Це нормальний стан, а не збій: у селі на Луганщині пунктів немає взагалі, і порожній список має бути легальним екраном в інтерфейсі, з поясненням, а не помилкою.

Навпаки, два ендпоінти довідника стабільно віддавали 500 з боку Укрпошти — їх я просто не використовую. Години роботи й зона кур'єрської доставки відповідають 200, але порожнім тілом, тому покупцю вони не показуються: обіцяти час роботи, якого ми не знаємо, гірше, ніж не обіцяти нічого.

Міста з однаковими назвами

На запит «Київ» довідник повертає десять варіантів, і серед них Київ у Миколаївській області. На «Подільськ» — вісім у різних областях. Тому в списку міст обов'язково показується область і район: без них покупець обере не те місто, а ви дізнаєтесь про це, коли посилка поїде за 400 кілометрів не туди.

Тариф перевізника диктує інтерфейс

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

  1. Поштомати й точки видачі. Післяплата для них неможлива, а доставку туди може оплатити лише відправник — тобто магазин. Для магазину з середнім чеком у кілька сотень гривень це мінус із кожного замовлення, тому канал не вмикали зовсім.
  2. Отримувач-ФОП. Укрпошта не оформлює післяплату, коли одержувач — ФОП або компанія. Тому в чекауті є питання про тип отримувача, і при виборі «ФОП» накладений платіж зникає з поясненням чому, а оплата перемикається на передоплату.
  3. Оголошена цінність. У документації тричі повторено, що на тарифі «Стандарт» платником за пересилання можна зробити одержувача тільки за наявності оголошеної цінності. Практичний наслідок: якщо її не вказати, за доставку платить відправник — навіть у відділення. Це вже не код, а гроші власника, тож це окремий пункт інструкції й окреме попередження клієнту.

Ключ живе на сервері — і тільки там

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

Браузер → проксі на Medusa → довідник Укрпошти Браузер місто, відділення Проксі на Medusa ключ · кеш · ліміти класифікація пунктів fail-closed Довідник Укрпошти
Ключ бачить лише середній блок. Браузер не знає ні токена, ні відповідей довідника в сирому вигляді.

У проксі кожне обмеження — конкретне число, а не побажання, і на кожне є тест: таймаут 6 секунд, не більше 120 звернень до довідника за хвилину, 6 одночасних, обрив читання відповіді на 8 МБ (саме під час читання, а не після), кеш міст на тиждень і кеш пунктів на 15 хвилин — короткий, бо статус відділення змінюється на «тимчасово не працює».

Окремо — поведінка на 401. Якщо ключ перестав працювати, проксі не вдає, що все гаразд: покупцю віддається зрозуміла відмова, власнику магазину йде повідомлення в Telegram, яке починається словами «Укрпошта відхиляє наш ключ». Браузер при цьому ніколи не бачить тіла відповіді перевізника — лише тип проблеми.

Вибір покупця перевіряється вдруге — на сервері

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

Правило просте: підроблений запит має отримати відмову, а не пройти. Поштомати вимкнені в інтерфейсі, але й на бекенді запит із поштоматом відхиляється — навіть якщо UI їх колись знову покаже. Запит із перевізником «Укрпошта» і способом доставки Нової Пошти не створює замовлення зовсім.

Три пастки самої Medusa

Жодна з них не видна в тестах — усі три вилізли на реальному середовищі.

Один обробник на хук. Medusa дозволяє рівно один обробник на хук воркфлоу, а я зареєстрував другий — сервіс просто не піднявся б: Cannot define multiple hook handlers for the validate hook. Тести цього не ловили, бо кожен тест мокає core-flows і реєструє свій обробник окремо. Довелося злити обидві перевірки в один файл і додати тест, який вантажить усі хуки проти справжніх core-flows.

Валідується payload, а не підсумкова сутність. Увімкнення правила способу доставки впало на Rule must have an attribute, an operator and a value: оновлення { id, value } відхиляється, бо Medusa перевіряє саме те, що ви надіслали. Атрибут і оператор тепер їдуть у кожному записі.

CLI з'їдає ваші прапорці. medusa exec розбирає все, що починається з --, як власні опції й падає на «Unknown argument», а виклик через npx із подвійним тире ковтає прапорець тихо — через що «бойовий» запуск скрипта виявився сухим прогоном. Прапорці стали звичайними словами, а їхній розбір — окремим модулем під тестом.

Дві пастки деплою

scp не видаляє зайве. Старий файл перевірки лишився б поруч із новим і дав би той самий конфлікт обробників. Синхронізація тепер — rsync -a --delete з перевіркою, що в теці хуків рівно один файл.

Вітрина й бекенд їдуть різними поїздами. Next.js на Vercel деплоїться автоматично з main, а бекенд — вручну. Я відправив вітрину з новим форматом даних раніше за бекенд, який ще читав старий: у тому вікні замовлення з передоплатою показалося б власнику як накладений платіж. Виправив за кілька хвилин, але висновок на майбутнє твердий: під час зміни контракту першим їде той, хто читає, а не той, хто пише. І прапорець функції на Vercel треба ставити до пушу — інакше збирається білд без нього, а відрізнити два білди за хешем коміту неможливо, бо коміт той самий.

Як вмикали

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

Перевірка — у браузері на живому сайті: вибір перевізника, пошук міста з областями, три відділення з індексами, зникнення накладеного платежу для ФОПа. Вимикається все так само швидко: одна змінна — і магазин повертається до однієї Нової Пошти, а вже створені замовлення Укрпоштою лишаються цілими.

Побічний урок: перевіряйте другий канал

Коли я проходив увесь шлях замовлення вручну, спливла річ, яка не мала стосунку до Укрпошти: лист-підтвердження покупцеві не надсилався — і не надсилався з самого запуску магазину. Поштовий сервіс на сервері не був налаштований узагалі, кожне замовлення тихо писало в лог «пропускаю» і йшло далі.

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

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

Що лишилось на наступну фазу

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

Якщо ви робите те саме

Коротко, що я б сказав собі на початку:

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