У магазині 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 кілометрів не туди.
Тариф перевізника диктує інтерфейс
Це головне, чого я не очікував: умови тарифу визначають, які кнопки взагалі можна показувати. Три обмеження Укрпошти прямо перетворилися на правила в коді.
- Поштомати й точки видачі. Післяплата для них неможлива, а доставку туди може оплатити лише відправник — тобто магазин. Для магазину з середнім чеком у кілька сотень гривень це мінус із кожного замовлення, тому канал не вмикали зовсім.
- Отримувач-ФОП. Укрпошта не оформлює післяплату, коли одержувач — ФОП або компанія. Тому в чекауті є питання про тип отримувача, і при виборі «ФОП» накладений платіж зникає з поясненням чому, а оплата перемикається на передоплату.
- Оголошена цінність. У документації тричі повторено, що на тарифі «Стандарт» платником за пересилання можна зробити одержувача тільки за наявності оголошеної цінності. Практичний наслідок: якщо її не вказати, за доставку платить відправник — навіть у відділення. Це вже не код, а гроші власника, тож це окремий пункт інструкції й окреме попередження клієнту.
Ключ живе на сервері — і тільки там
Довідник вимагає bearer-токен за договором. Це означає, що запит із браузера неможливий: ключ не можна віддавати вітрині ні в якому вигляді. Між покупцем і Укрпоштою стоїть проксі на бекенді.
У проксі кожне обмеження — конкретне число, а не побажання, і на кожне є тест: таймаут 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, тож для власника все працювало, а покупець бачив підтвердження на екрані одразу після оформлення й не чекав листа. Дефект прожив місяці рівно тому, що поруч був другий, справний канал.
Звідси правило, яке я тепер застосовую скрізь: якщо подія має два канали, моніторинг потрібен кожному окремо. «Нам приходять замовлення» не означає «покупцям приходять листи».
Що лишилось на наступну фазу
Накладні поки створюються руками в кабінеті перевізника — так само, як це було з Новою Поштою. Наступний крок — автоматичне створення накладної при замовленні й трекінг посилки на сайті, щоб покупець бачив статус і не дзвонив власнику з питанням «де моя посилка». Ключі для цього вже є, тож це робота, а не нові перемовини з перевізником.
Якщо ви робите те саме
Коротко, що я б сказав собі на початку:
- не вірте булевим полям чужого довідника, поки не перерахували їх на вибірці;
- перепис робіть не по столиці — рідкісні типи живуть у селах;
- спершу прочитайте умови тарифу, і лише потім малюйте інтерфейс: вони визначають, які способи оплати взагалі можна показати;
- ключ — тільки на сервері, з лімітами в числах і тестом на кожне число;
- усе, що показав інтерфейс, перевірте ще раз на сервері перед створенням замовлення;
- вмикайте через прапорець, який можна вимкнути за дві хвилини.