Перейти к содержанию

06. Решения, открытые вопросы, план работ

6.1. Принятые решения (ADR)

ADR-001. Модульный монолит на Spring Modulith вместо набора сервисов

Контекст. Legacy: PHP site, Go menu, Go payment, Go user, Node notification. Меню из iiko импортировалось дважды — в site/app/Models/Import/* и в menu — в две разные схемы с разной моделью данных. Решение. Один процесс, одна БД, модули с явными границами; границы держит Spring Modulith. Следствия. Один импорт, одна модель данных, одна транзакция. Границы проверяются тестом (ApplicationModules.verify()), а не договорённостями. Транзакционный outbox получаем из коробки (Event Publication Registry) — событие «меню импортировано → пересобрать снапшот» не теряется при падении процесса. Цена — общий деплой и требования Modulith к структуре пакетов.

ADR-011. JDK 25 и актуальный Spring Boot

Решение. JDK 25 (LTS), Spring Boot 4.x, Kotlin 2.x; версии проверяются на пустом каркасе в первый день. Обоснование. Виртуальные потоки без ограничений pinning — прямая выгода для IO-bound импорта (до 10 параллельных запросов к iiko) и для API витрины; generational ZGC и compact object headers уменьшают пиковую память при разборе 19 МБ JSON. Проект новый, легаси-зависимостей нет — брать устаревший рантайм не на чем.

ADR-002. Разделение слоёв «источник» и «витрина»

Решение. Схема iiko пишется только импортом; схема catalog — только админкой. Переопределения — отдельные nullable-колонки, чтение через coalesce. Следствия. Импорт не может затереть ручную работу; ручная работа не может исказить данные, уходящие в заказ.

ADR-003. iiko — источник истины по цене, составу и наличию

Решение. Цены не редактируются в админке. Обоснование. Цена, по которой продано, должна совпадать с ценой в чеке. Любое переопределение на стороне сайта создаёт класс расхождений, который дороже гибкости. Следствия. Акции и скидки (этап 2) реализуются как отдельный слой поверх цены iiko со своими правилами, а не правкой цены товара.

ADR-004. Собственное дерево категорий витрины

Решение. catalog.category создаётся вручную; itemCategories и productCategories из iiko — только сырьё. Обоснование. Переименование категории в iiko («Роллы Доставка») не должно менять URL и SEO; витрине нужны вложенность, картинки, тексты и порядок, которых в iiko нет.

ADR-005. Товар с вариантами, привязка вручную

Решение. Один товар = одна карточка, вариант = одна позиция iiko, привязку делает человек с автоподсказкой. Обоснование. В iiko размеры пиццы — 4 разных блюда в 4 разных категориях; автоматика по названию ошибается на нестандартных наименованиях, а ошибка привязки означает неверную цену. Инвариант. unique (iiko_item_size_id) — позиция iiko принадлежит максимум одному товару.

ADR-006. Модификаторы: состав и цена — на уровне блюда

Решение. iiko.item_modifier_group привязана к размеру блюда; цена модификатора хранится в разрезе (группа, модификатор, меню, организация). Обоснование (данные). Группа aee0fab6… встречается 163 раза с двумя разными наборами позиций; 11 групп приходят без itemGroupId; три модификатора имеют разную цену в разных блюдах (35 ₽ / 60 ₽). Схема legacy (modifier_prices по modifier_id + price_category + org) все три факта теряет.

ADR-007. Один generic-товар вместо таблиц на каждый тип

Решение. Никаких dish_pizza / dish_snacks / dish_drinks / dish_non_food. Различия типов выражаются данными: категория, ось вариативности, набор модификаторов, бейджи. Обоснование. В legacy добавление типа товара = миграция + модель + репозиторий + экран + ветка в сборке меню. Мерч и «пиво разливное» из выгрузки в такую модель не помещаются вовсе.

ADR-008. Снапшот меню вместо сборки на каждый запрос

Решение. Материализованный JSON на (организация, внешнее меню) в Postgres + Redis, пересборка по событию, отдача с ETag. Обоснование. Сборка меню — это join по 8 таблицам с 21 тыс. связок модификаторов; на каждый запрос витрины это неприемлемо, а данные меняются несколько раз в сутки.

ADR-009. Админка — отдельное SPA

Решение. React+AntD собирается независимо, общается только через /api/admin/v1. Обоснование. Orchid в legacy связал вёрстку админки с релизом бэкенда. Отдельное SPA позволяет менять админку без деплоя бэкенда и наоборот, а также переиспользовать API для интеграций.

ADR-012. Каталог проектируется и заполняется с нуля, legacy-данные не мигрируются

Контекст. В legacy товары разложены по таблицам dish_pizza, dish_snacks, dish_drinks, dish_non_food с десятками таблиц-спутников (dish_pizza_ingredients, dish_pizza_dough, dish_snack_sauces, dish_drink_toppings …), два параллельных дерева категорий и два импорта iiko. Решение. Каталог не мигрируется: категории и карточки клиент собирает заново в новой админке, изображения загружаются заново, заказы удаляются. Мигрируются только пользователи, и это задача следующего этапа. Обоснование. Перенос данных потянул бы за собой разбиение товаров по типам — ровно ту структуру, от которой отказываемся (ADR-007). Экран «Новые позиции iiko» делает пересборку обозримой: вся номенклатура лежит готовой очередью с автоподсказкой вариантов. Следствия. Нужен список URL, важных для SEO, и редиректы. Legacy используется только как источник разбора ошибок, не как источник данных и не как ориентир схемы.

ADR-013. Снимок привязки к позиции iiko

Контекст. Сотрудники случайно удаляют и пересоздают блюда в iiko; пересозданное блюдо приходит с новым itemId. Ссылка варианта на позицию становится нерабочей, товар молча пропадает. Решение. Вариант хранит слепок позиции iiko на момент привязки и последнее известное живое состояние (catalog.variant_binding), пропажа порождает инцидент (catalog.binding_incident), замена подбирается по SKU и названию, перепривязка — одно действие. Обоснование. Карточка — это ручной труд: тексты, фото, SEO, сортировка. Ошибка в iiko не должна стоить его повторения. Плюс появляется точный ответ на вопрос «почему товара нет на сайте». Следствия. Удаление в iiko никогда не удаляет витринные сущности; окно ожидания (missing_grace) отсекает шум от неполных выгрузок.

ADR-010. Публичное API отдаёт нормализованное меню

Решение. Модификаторы — отдельным словарём, товары ссылаются по id; группы с одинаковым составом схлопываются. Обоснование. 19 МБ ответа iiko при 243 блюдах — прямое следствие денормализации. Витрина должна получать сотни килобайт.

6.2. Открытые вопросы

ОВ-1. Мультибрендовость и франшиза — отложено, ждём уточнения от клиента

Клиент планирует франшизу, но модель не определена. Три сценария и их цена:

Сценарий Что означает Влияние на модель
A. Франчайзи — организации в том же аккаунте iiko одна номенклатура, одни карточки, отличаются цены и стоп-листы по организациям изменений не требуется — текущая модель это уже умеет: цена и наличие в разрезе организации, видимость товара по организациям
B. Франчайзи — свой аккаунт iiko, общий бренд и каталог свой apiLogin, свои организации, общие карточки нужна сущность «подключение к iiko» (iiko.connection) и connection_id в таблицах слоя iiko; витрина не меняется
C. Франчайзи — свой бренд, свой каталог и домен всё своё: каталог, дизайн, домен нужен brand_id почти во всех таблицах catalog, изоляция в API и правах, выбор бренда в админке

Что делаем сейчас, чтобы не переделывать потом:

  1. Слой iiko проектируется с расчётом на несколько подключений: поле connection_id вводится сразу со значением по умолчанию 1, apiLogin берётся из конфигурации подключения, а не из глобальной настройки. Это дешёвая страховка, закрывающая сценарий B.
  2. Витринные ключи (slug категорий и товаров) уникальны глобально — при сценарии C уникальность станет составной (brand_id, slug), что является механической миграцией.
  3. Публичное API уже принимает organizationId в каждом запросе — при любом сценарии точкой входа остаётся организация.

Вопрос клиенту, который нужно задать: будут ли у франчайзи собственные юрлица и собственные аккаунты iiko, или они заводятся организациями внутри существующего аккаунта? Ответ различает A/B (почти бесплатно) и C (значимая работа, лучше знать до начала реализации).

ОВ-2. Семантика startRevision в /api/2/menu/by_id

Отдаёт ли iiko при указании startRevision только изменения или полное меню. До проверки на боевом ключе удаление позиций выполняется только на полном ночном импорте. Проверяется в первую неделю разработки.

ОВ-3. Ценовые категории

В коде legacy — комментарий «в iiko изменилось API, теперь ценовая категория не обязательна» и хардкод соответствия меню → ценовая категория. Нужно подтвердить у клиента актуальную схему ценообразования: используется ли механизм ценовых категорий сейчас, или цены полностью определяются внешним меню и организацией.

ОВ-4. Поведение при стоп-листе

Скрывать товар или показывать неактивным с пометкой «нет в наличии»? Влияет на конверсию и на SEO (исчезающие товары). Предлагается настройка с умолчанием «показывать неактивным».

ОВ-5. Несколько внешних меню на организацию

В legacy их два (41188, 50865) — предположительно доставка и зал. Если витрина использует только одно, второе не нужно синхронизировать. Требуется подтверждение, чтобы не гонять лишний объём каждые 5 минут.

ОВ-6. Комбо и наборы

В выгрузке comboCategories пуст, но механизм в iiko есть. Если сеты («Сеты» есть в учётных группах) собираются как комбо, потребуется отдельная модель. Сейчас предполагается, что сет — обычное блюдо. Требуется подтверждение.

ОВ-7. Судьба legacy-сервисов

payment, user, notification — переносятся в монолит или остаются? На этап каталога не влияет, но определяет план миграции. Предложение: перенести в монолит модулями на своих этапах, notification может остаться отдельным (у него другой профиль нагрузки и внешние интеграции).

6.3. План работ по этапу «Каталог»

# Работа Результат Оценка
0 Инвентаризация размеров изображений (см. 07-media.md §7.1) — совместно с фронтендом витрины заполненная таблица мест использования и утверждённый список пресетов 1–2 дн, блокирует п. 6
1 Каркас монолита: Gradle, JDK 25, Spring Boot + Modulith, Flyway, Docker, CI, структура модулей, ApplicationModules.verify() собирается и деплоится «hello world» с миграциями и проверкой границ 3–4 дн
2 Схема БД: iiko, catalog, media, platform миграции + тесты на Testcontainers 3 дн
3 iiko-клиент: токен, организации, терминалы, ценовые категории, внешние меню зелёные тесты на WireMock, данные в БД 4 дн
4 Импорт меню: потоковый парсер, bulk-запись, ревизии, mark-and-sweep, предохранитель полный импорт response.json ≤ 60 с, ≤ 512 МБ heap 6–8 дн
5 Импорт стоп-листов + события + diff стоп-лист на витрине ≤ 60 с 2 дн
Снимки привязок и инциденты: детект пропаж, окно ожидания, подбор замены, перепривязка сценарий «удалили блюдо в iiko → перепривязал в один клик» проходит на тестах 3 дн
6 Модуль media: загрузка, S3, нормализация, нарезка по утверждённым пресетам, точка фокуса, дедупликация, перегенерация загрузка картинки, все пресеты, отдача через CDN, srcset в API 4–5 дн
7 Домен каталога: категории, товары, варианты, размещения, публикация с правилами покрытые тестами сценарии 5 дн
8 Сборка снапшота + публичное API витрины /api/v1/menu p95 ≤ 80 мс, ETag 4 дн
9 Админский API + OpenAPI + RBAC + аудит спецификация, по которой пишется фронт 5 дн
10 Автоподсказка вариантов и триаж новых позиций экран «Новые позиции iiko» работает 3 дн
11 Админка: каркас SPA, авторизация, layout, генерация клиента ходит в API, показывает /me 3 дн
12 Админка: категории (дерево, DnD, формы, медиа) экран готов 4 дн
13 Админка: список товаров, фильтры, массовые операции экран готов 4 дн
14 Админка: карточка товара (все вкладки) экран готов 6–8 дн
15 Админка: новые позиции iiko, проблемы привязок, модификаторы, импорты, справочники экраны готовы 6 дн
16 Загрузка изображений в админке: кроп, точка фокуса, предпросмотр во всех пресетах, понятные ошибки валидации экран, на котором невозможно «загрузить и не увидеть результат» 3 дн
17 Нагрузочное тестирование, наблюдаемость, алерты, документация, рантбук дашборд и рантбук 3 дн
18 Первичное наполнение каталога вместе с клиентом (сопровождение, исправления по ходу) каталог собран, витрина работает 3 дн сопровождения

Итого по этапу каталога: ≈ 68–76 человеко-дней. При двух разработчиках (бэкенд + фронт) с параллельной работой начиная с п. 9 — примерно 8–9 недель календарно.

Критический путь: п. 2 → 4 → 7 → 8 → 9 → 14. Работы 11–16 упираются в готовность спецификации (п. 9), поэтому OpenAPI фиксируется раньше реализации — фронт стартует на моках. Пункт 0 не на критическом пути, но блокирует п. 6 и 16, поэтому запускается в первый же день.

Наполнение каталога — работа клиента, а не разработки: примерно 250 позиций, из них ~80 пицц, собираемых в ~20 карточек с вариантами. При наличии готовых фотографий это несколько дней работы контент-менеджера, и это единственная цена отказа от миграции legacy-данных.

6.4. Что нужно от клиента для старта

  1. Доступ к боевому и тестовому apiLogin iiko (для проверки ОВ-2 и ОВ-3).
  2. Ответ по франшизе (ОВ-1).
  3. Подтверждение назначения внешних меню 41188 и 50865 (ОВ-5).
  4. Размеры изображений от фронтенда витрины — таблица 07-media.md §7.1. Блокирует нарезку.
  5. Список организаций, которые должны быть на витрине в момент запуска.
  6. Список URL каталога, которые важно сохранить для SEO (каталог собирается заново, старые адреса автоматически не наследуются — нужны редиректы).
  7. Готовность контент-менеджера к первичному наполнению каталога и наличие фотографий блюд в исходном разрешении.

6.5. Пользователи из legacy

Единственные данные, которые переносятся. К этапу каталога отношения не имеют — переносятся вместе с модулем клиентов на следующем этапе. Что нужно решить заранее:

  • как хранились пароли в legacy (алгоритм хеша) — от этого зависит, переживут ли пользователи миграцию без сброса пароля;
  • авторизация по телефону с кодом (как сейчас) означает, что пароли могут быть вообще не нужны — тогда мигрируются телефон, имя, история адресов и бонусы;
  • бонусы и накопления, если они хранятся в iiko, а не в legacy, вообще не мигрируются.

Заказы, по решению клиента, не переносятся.