У бренда своё швейное производство, архив из трёхсот с лишним принтов и весь товарный учёт внутри кабинета маркетплейса. Магазин собран так, чтобы каталог не пришлось вести дважды: 64 модели и 307 расцветок приезжают из кабинета продавца сами, цена пересчитывается трижды в сутки по правилу владельца, а всё, что сотрудник поправил руками, синхронизация больше не трогает. Витрина, админка, заказы, уведомления в Telegram. Эквайринг вынесен в отдельный этап.
Бренд с собственным цехом продаёт через маркетплейс. Там же лежат все карточки: названия, цены, характеристики, фотографии. Свой сайт нужен не вместо площадки, а рядом с ней: прямая продажа без комиссии, свои условия и своё лицо. Главное ограничение сформулировал сам владелец: вести каталог в двух местах он не будет. Значит, всё, что уже описано в кабинете продавца, сайт обязан забирать сам.
Триста расцветок, у каждой название, цена, размерный ряд и десяток кадров. Ручное заведение занимает недели и устаревает в тот же день, когда владелец поменяет цену на площадке.
Цена меняется акциями и переоценкой, иногда несколько раз в неделю. Сайт с прошлогодним прайсом хуже, чем отсутствие сайта: покупатель сравнивает и уходит.
Площадка отдаёт одну карточку на каждый цвет. Показать их как есть значит вывалить на покупателя триста почти одинаковых футболок вместо шестидесяти четырёх моделей.
Без разработчика: описания, цены, порядок кадров, блоки главной, статусы заказов. И так, чтобы ночная синхронизация не затёрла сделанное руками.
Этап 1: сайт, админка, первая загрузка каталога. Этап 2: синхронизация с маркетплейсом. Этап 3: оплата, доставка, интеграция со службой доставки. Кейс описывает первые два: они сделаны и работают на боевом сервере. Эквайринг не подключён намеренно, заказ оформляется без списания средств.
Приложение одно, Next.js на App Router. Всё, что связано с внешними системами, живёт в отдельных модулях и ходит наружу только по расписанию: витрина и админка читают свою базу, а не чужой API. Так страница не зависит от того, отвечает ли сейчас маркетплейс.
Порядок товаров на витрине задаёт число заказов за тридцать дней. Очевидный путь - попросить у площадки сразу месяц. Так делать нельзя, и выяснилось это не из документации, а на живом API. Каждая из трёх находок сначала месяц ломала каталог молча.
Площадка сама говорит, сколько ждать, и называет это своим заголовком, а привычного стандартного заголовка в ответе нет вовсе. Пока читался только стандартный, паузу приходилось угадывать лесенкой: минута, пять, двадцать. Все попытки уходили в уже объявленный запрет и подтверждали его.
Каждый прогон берёт день, которого в истории нет вовсе, а если такого нет - самый давно обновлявшийся. Прогон исходит из того, что успеет ровно один запрос, и тратит его на самый полезный день.
Дни складываются в собственную таблицу и живут дольше окна. Отказ по одному дню не обнуляет товар: день остаётся с прежней записью, а в журнал уходит, какие дни не дались.
Пауза больше пяти минут не ждётся внутри прогона: срок ложится в настройки, прогон заканчивается, а следующий по расписанию просто не пойдёт, пока время не выйдет. Удачный запрос снимает запрет.
У площадки на одну и ту же ручку разные квоты в зависимости от типа токена: у части токенов запрос в минуту, у базового - один запрос в три часа. Наш оказался базовым, отсюда и всё остальное: один удачный запрос, потом полтора часа отказов. Это не перегрузка и не чужие сервисы, а объявленный лимит. Владельцу это сформулировано одним действием: пройти идентификацию в кабинете или перевыпустить токен, и история заказов наберётся за дни, а не за месяц.
Пауза по лимиту, срок и области токена, число прогонов за сутки и журнал последних двенадцати запусков стоят на отдельной странице админки. Без этого закрытый кабинет читается как поломка сайта или протухший токен, и владелец идёт звонить разработчику вместо того, чтобы подождать.
Владелец хотел простого: на сайте дешевле, чем на площадке. Формулировка выглядит однозначной ровно до того момента, когда выясняется, какую именно цену видит покупатель на витрине маркетплейса.
Считать можно от цены продавца или от витринной цены площадки. База выбирается владельцем, а не зашита в код, поэтому обещание "дешевле, чем на маркетплейсе" становится проверяемым.
Значение в коде - только запасное, на пустую базу. Правило задаёт владелец в админке, и его берут все три прогона. Если бы хоть один считал по коду, синхронизация возвращала бы цены к прежнему проценту, и правка отваливалась бы сама.
Разбор правила молча заменяет испорченное значение запасным. Ночной прогон не должен вставать из-за опечатки в одном числе: иначе цены отстанут от площадки на сутки.
Пока популярность считалась внутри удачного прогона цен, каждый отказ по лимиту оставлял каталог со вчерашним порядком, хотя заказы площадка отдавала прекрасно. Теперь это две независимые команды.
Маркетплейс отдаёт отдельную карточку на каждый цвет. Ключ, который связывает цвета одной вещи, приезжает отдельным полем - по нему карточки собираются в модель, а внутри модели остаются расцветками с переключателем. Без этой группировки витрина выглядела бы как триста почти одинаковых футболок.
| Решение | Как сделано | Почему так |
|---|---|---|
| Название расцветки | берём из артикула продавца, характеристика - запасной вариант | В характеристике "Цвет" площадка перечисляет все цвета принта: у футболки с лимонами там четыре значения. Это описание картинки, а не вариант. |
| Заголовок товара | слово в слово как на площадке | Покупатель, пришедший с маркетплейса, должен узнать вещь по названию. Короткая форма нужна ровно в одном месте - в адресе страницы. |
| Порядок расцветок | по возрастанию идентификатора карточки | Площадка отдаёт карточки в порядке обновления. Без явной сортировки цвета на витрине перемешивались бы после каждой синхронизации. |
| Кадры | копия у себя, ссылка площадки остаётся ключом сопоставления | Подменить ссылку локальным путём нельзя: ближайший прогон счёл бы все кадры пропавшими с карточки и удалил их вместе с расстановкой и отметками "скрыть". |
| Расстановка кадров | синхронизация правит только состав, не порядок | Порядок и скрытые кадры задал сотрудник. Раньше порядок переписывался каждым прогоном, и расстановка возвращалась к виду площадки за ночь. |
| Нулевой размер | остаётся на витрине перечёркнутым | Пропавший размер покупатель читает как "не для меня". Перечёркнутый говорит, что вещь такая есть и может вернуться. |
Сотрудник поправил название или описание - поле помечается замком, и синхронизация его больше не переписывает. Это то, что делает админку не декорацией: без замков любая ручная правка жила бы до ближайшей ночи, и владелец перестал бы править вообще.
У бренда своё производство и архив из трёхсот с лишним принтов, поэтому сайт устроен как каталог образцов, а не как лукбук: белый лист, печатный синий единственным акцентом, нулевые радиусы. Три шрифта делят работу - голос бренда, текст и данные. Артикулы, размеры, плотность и имена принтов набраны моноширинным: в каталоге образцов так и надо. Фирменный элемент - лента принтов под первым экраном: имена расцветок едут строкой и проговаривают масштаб архива именами, а не цифрой.
Разделы разные у разных ролей, и доступ решает каждая страница сама. Это не единственный рубеж: серверные действия проверяют разрешение ещё раз, потому что действие вызывается напрямую по HTTP, и проверка в оболочке при этом не выполняется вовсе.
Сначала то, что требует действия: расцветки без цены, новые заказы, размеры с нулевым остатком. Потом общие цифры, и только те, что открыты роли: выручка - это деньги владельца, а не общая справка.
Список, карточка, смена статуса только по разрешённым переходам. Заявки с формы обратной связи лежат в том же разделе: в них персональные данные, и отвечает на них тот же человек.
Поиск, правка названия и описания, цены по расцветкам, остатки по размерам, скрытие кадров и замки полей от перезаписи синхронизацией.
Рамка вокруг страницы, правка текста прямо на месте, кнопки сохранить и отменить. Отмена работает по журналу прежних значений, а не по памяти вкладки.
Блоки главной лежат в базе. Страница не знает, из чего она состоит: читает блоки по порядку и вычитывает разом всё, что им понадобится из каталога.
Заказ приходит в рабочий чат вебхуком. Очередь, напоминания и чистка идут отдельной командой из расписания каждые десять минут, чтобы недоступный мессенджер не ронял оформление заказа.
Список отдаёт статус селектом прямо в строке: открывать карточку ради одного перехода не нужно. В карточке лежит то, ради чего её открывают: состав, доставка, платёж и история.
Главная, страницы, документы и соцсети собраны под один пункт меню. Раньше они висели четырьмя строками, и меню разъезжалось.
Два счётчика, воронка и цели. Просмотр товара считается один раз за заход, а не дважды: событие висит на карточке, а не на каждом рендере.
Оформление перестало заканчиваться письмом менеджеру. Покупатель выбирает службу доставки и пункт на карте прямо на чекауте, платит картой или быстрым платежом, а оператор печатает накладную из админки и видит статусы, которые приезжают сами.
Оплата картой и быстрым платежом по QR. Уведомления банка приходят на свою ручку, подпись проверяется, и заказ меняет состояние только по подтверждённому уведомлению, а не по возврату покупателя на сайт.
Кнопка в карточке заказа возвращает деньги тем же методом, каким платили. Статус платежа при этом не едет назад: он идёт вперёд, в состояние возврата. Покупатель может отменить новый заказ сам, и деньги уходят обратно без оператора.
Курьерская сеть с пунктами на карте и накладной из админки, национальная почта с посылкой по индексу отделения и своей этикеткой, служба быстрой доставки с заявкой и статусами по опросу.
У одной службы вебхуки, у другой опрос по расписанию: у кого что есть. Оператор не ходит по кабинетам служб, а видит трек и состояние в строке заказа.
Если служба отказалась печатать этикетку, оператор видит её формулировку, а не голый код ошибки. Иначе разбор сводится к переписке с поддержкой, а заказ стоит.
Галочка на регистрации и на чекауте, отметка хранится вместе с заказом. Это не украшение: без неё нельзя ни писать покупателю, ни передавать адрес службе доставки.
Один модуль - одна папка, свой README, свои тесты рядом с кодом. Граница модуля это его индексный файл: импортировать внутренние файлы чужого модуля нельзя. Понадобилось - значит не хватает в публичном API, и добавлять надо туда.
В нём не то, что делает код - это видно из кода, - а какие решения приняты и почему. Половина находок из раздела про синхронизацию живёт именно там, и следующий человек не наступит на них заново.
Файл правил и файл тестов лежат в одной папке. Видно, что покрыто, а что нет, и тест переезжает вместе с кодом. Сеть в тестах не используется: HTTP-клиент принимает подмену снаружи.
Обвязка подменяет адрес базы до создания клиента и падает, если в адресе нет тестового имени. Без этой проверки неудачный запуск стёр бы каталог разработки.
Тесты, типы и сборка прогоняются на отдельной копии проекта с отдельной базой. Локальная база разработчику больше не нужна, а боевой магазин при этом не трогается.
Скрипт деплоя, сертификат с автопродлением, переадресация с www и с адреса сервера на основной домен. Расписание прогонов живёт в планировщике, а не внутри веб-запроса.
Так получается, когда чистые правила отделены от работы с базой и сетью. Разбор карточки, расчёт цены, переходы статусов заказа, разбор правила цены, группировка расцветок - всё это функции без побочных эффектов, и тест на них стоит недорого. Дорогие сквозные проверки остаются там, где без них нельзя.
| Что | Было | Стало |
|---|---|---|
| Каталог | только в кабинете маркетплейса | 307 расцветок на своём сайте, обновляются сами |
| Цены | меняются на площадке, сайта нет | пересчёт трижды в сутки по правилу владельца |
| Порядок товаров | - | по заказам за 30 дней, история набирается сама |
| Правка каталога | только через кабинет площадки | админка с замками полей, правка переживает ночь |
| Фотографии | раздаются складом маркетплейса | 4 478 кадров лежат у себя |
| Заказы | - | оформление, статусы, уведомление в Telegram |
| Лимиты площадки | отказы выглядели как поломка | объяснены на экране словами, прогоны ждут сами |
Магазин работает на боевом сервере под своим доменом, с сертификатом и автопродлением. Каталог, цены и порядок товаров обновляются по расписанию без участия человека. Владелец правит витрину и заказы сам. Следующий этап - оплата и доставка - отделён намеренно и оформляется отдельным заданием.
Бизнес-анализ и автоматизация: ERP, дашборды, интеграции с маркетплейсами и AI-агенты. Бывший операционный директор компании с оборотом 4 млн долларов в месяц, теперь строю такие системы под заказ.