Ограничение maker-бренда.
Изделия ручной работы из Украины с доставкой в семь стран. Финансовая и логистическая инфраструктура, на которой держится этот рынок, — Monobank для платежей, Nova Post для доставки, ежедневные курсы Национального банка Украины для ценообразования — не имеет аналога в типовом европейском ecommerce-стеке.
Одна только платёжная модель исключает Stripe. Split-платёж Monobank берёт предоплату при подтверждении заказа и инициирует списание остатка, когда заказ переходит в статус «готов к отгрузке». Это два платёжных этапа, привязанных к производственному состоянию, с подписями вебхуков по ECDSA на каждом переходе. У Stripe нет примитива для этого. Пришлось бы имитировать это двумя несвязанными списаниями и самостоятельно сверять состояние — а любой разрыв в сверке становится поверхностью для мошенничества.
Логистическая модель исключает Shippo. Nova Post, доминирующий перевозчик Украины, отдаёт справочник городов, отделений и классификаторов, который нужно синхронизировать и запрашивать локально. Накладные формируются программно в момент отгрузки, а итоговый PDF-этикетки должен храниться там, откуда его можно получить. Ничто в мире абстракций типовых перевозчиков не ложится на это чисто.
Модель ценообразования исключает валютные настройки на уровне региона. Часть товаров maker продаёт по всему миру в EUR, другие — только в UAH, потому что цепочка поставок локальна. У нескольких товаров есть явные переопределения в UAH или USD, отличающиеся от базы, пересчитанной по курсу. Правила на уровне региона не могут выразить «у этого конкретного товара своя цена в UAH независимо от локали покупателя». Переопределения на уровне товара — могут.
Связка Stripe + Shippo + WooCommerce не способна описать «предоплата сейчас в UAH, остаток по готовности в EUR, доставка Nova Post» без кастомного кода на каждом стыке. Кастомная разработка была не предпочтением — это было требование рынка.
Что мы построили.
Одна платформа: клиентская витрина на 7 локалей, производственный жизненный цикл (draft → in-progress → ready → shipped) и операционная панель, которой основатель управляет в одиночку.
Платёжный слой реализует полный split-платёжный сценарий Monobank. Предоплата собирается при подтверждении заказа. Списание остатка инициируется, когда состояние заказа переходит в «готов к отгрузке». Каждый вебхук проверяется по кэшированному публичному PEM-ключу Monobank через ECDSA SHA256 с ротацией. Платёжные этапы — это записи в базе данных, привязанные к состоянию заказа, а не сверяемые постфактум.
Слой ценообразования сначала читает переопределения валюты на уровне товара и лишь затем переходит к базе, пересчитанной по курсу. У каждой строки товара есть базовая цена в EUR и опциональные колонки переопределений в UAH и USD. Когда переопределение задано, витрина использует его. Когда его нет, ежедневный курс Национального банка Украины пересчитывает базу в EUR. Курс NBU синхронизируется по cron каждый день.
Логистический слой работает на синхронизированном справочнике Nova Post. Города, отделения и классификаторы постранично подтягиваются из API Nova Post и хранятся локально — выбор адреса при оформлении запрашивает локальную копию, а не живой API. В момент отгрузки платформа формирует накладную через API Nova Post и сохраняет итоговый PDF-этикетки как presigned-объект в Cloudflare R2.
Производственный жизненный цикл — это каркас, связывающий платёжные этапы и отгрузку. Машина состояний заказа — пять состояний, журнал событий, лента фото прогресса — существует для того, чтобы переход от предоплаты к остатку имел чётко определённый триггер, а формирование накладной — чётко определённый момент. Машина состояний — не продукт. Это несущая рама для финансовых и логистических потоков.
- F · 01Split-платёж Monobank
- Предоплата собирается при подтверждении заказа; остаток срабатывает по статусу «готов к отгрузке». Платёжные этапы — записи в базе, привязанные к состоянию заказа. Проверка вебхуков по ECDSA SHA256 с ротацией PEM-ключа.
- F · 02Мультивалютность на уровне товара
- Базовая цена в EUR с опциональными колонками переопределений в UAH и USD для каждого товара. Резолвер цены сначала читает переопределение и лишь затем переходит к ежедневному курсу NBU. Управление на уровне maker, а не региона.
- F · 03Интеграция с Nova Post
- Справочник городов и отделений синхронизируется локально плановой задачей. Накладные формируются в момент отгрузки через API Nova Post. PDF-этикетки хранятся как presigned-объекты в Cloudflare R2.
- F · 04Производственный жизненный цикл
- Машина состояний заказа из пяти состояний (draft → in-progress → ready → shipped → cancelled) с журналом событий и лентой фото прогресса. Каркас для платёжных этапов и формирования накладных — не центральный объект.
- F · 05Витрина на 7 локалей
- Клиентский магазин на семи языках. Переопределения валюты на уровне товара и резервный курс NBU означают, что в каждой локали отображается контекстно корректная цена без настройки на уровне региона.
- F · 06Операционная панель под задачу
- Производственный канбан, спарклайн выручки в EUR за 30 дней, индикаторы состояния интеграций (Monobank / Nova Post / R2 / почта), отображение свежести cron и полоса внимания. Один экран для всей операции.
Инженерные решения финтех-уровня.
Проверка вебхуков по ECDSA. Monobank подписывает платёжные вебхуки приватным ключом; получатель обязан проверять их по кэшированному публичному ключу, обновлять кэш, когда Monobank публикует новый, и отклонять любой вебхук, не прошедший проверку подписи. Игрушечная аутентификация — это проверка значения заголовка. Настоящая аутентификация — это ECDSA SHA256 против PEM-ключа с ротацией.
Платёжные этапы на уровне данных. Split-платёжная модель — это не конфигурация платёжного шлюза. Это модель данных: у заказа один или несколько платёжных этапов, у каждого — сумма, валюта, статус и триггер. Этап предоплаты срабатывает при подтверждении заказа. Этап остатка срабатывает при переходе состояния. Журнал событий несёт аудиторский след. Сверять два несвязанных API-списания постфактум означало бы, что источником истины становится панель Monobank, а не ваша собственная база данных. Мы отказались от такого размена.
Переопределения валюты на уровне товара. В таблице цен помимо базы в EUR есть три опциональные колонки: UAH, USD и флаг «цена по запросу». Резолвер цены читает их в порядке приоритета: явное переопределение → база, пересчитанная по курсу → запрос. Maker управляет ценой на уровне товара, а не на уровне региона.
Специфика локального рынка — это ров. Конкурент может скопировать дизайн витрины. Но он не сможет запустить ту же платёжную модель, не переписав платёжный слой под конкретный API Monobank.
Синхронизация справочника Nova Post. Обращение к живому API Nova Post при каждом вводе адреса на оформлении связало бы процесс покупки с внешней зависимостью, чей SLA платформа не контролирует. Синхронизация справочника выполняется как плановая задача: страницы городов, отделений и классификаторов записываются локально, а оформление запрашивает локальную копию. Единственный живой вызов Nova Post в критическом пути покупки — это формирование накладной в момент отгрузки, и происходит он уже после оплаты заказа.
Результат.
Основатель управляет всей операцией с одного экрана. Производственный канбан по состоянию заказа, спарклайн выручки за 30 дней в EUR, индикаторы состояния интеграций для Monobank, Nova Post, R2 и почты, отображение свежести cron и полоса внимания, выводящая всё, что требует действий. Около 1–2K строк компонента панели — построено под задачу, а не типовое решение.
145 коммитов за 3 недели. 659 тестовых файлов, покрывающих путь Monobank ECDSA, сверку справочника Nova Post, семантику бандлов в корзине, фиксацию валютных снимков и переходы состояний платёжных этапов. Трейсы Sentry на 5% в продакшене со скруббером PII. Axiom для структурированных событий heartbeat по cron. Наблюдаемость продакшен-уровня не прикручивалась задним числом — она была частью первого спринта.
Стек кастомный не потому, что нам хотелось писать кастомный код. Он кастомный потому, что у рынка нет готовых рельсов.
