Как Intekey WMS 2.0
отличает заказчиков друг от друга
Около двадцати заказчиков, одна кодовая база. В легаси заказчик — это список
REQUIRE в его топ-модуле, зафиксированный при сборке. Дальше — почему так больше
нельзя, почему привязка к ролям не решение, и что принято вместо.
Что это
Архитектурное решение на уровне эпика: десять инвариантов (AD-12…AD-21) поверх базового правила AD-2.
Статус
Принято и реализовано в Epic 23. Перевод живых контуров на новую модель — Epic 18.
Как удерживается
Правила проверяются автоматически при каждой сборке, а не только на ревью.
Заказчик — это список модулей
Механизм простой, работающий, ему больше десяти лет. Он довёл легаси примерно до двадцати живых внедрений — и это надо признать прежде, чем его менять.
MODULE ClientC;
REQUIRE FusionWMS,
Replenishment, ReplenishmentSelection, ReplenishmentPlan,
ReplenishmentItemApi, InputControlReceipt,
SelectionLotBanned;
Что даёт
- Один взгляд — и видно, что у клиента включено
- Всё в git, всё воспроизводимо
- Нулевая стоимость рантайма
Чего не даёт
- Изменение = пересборка и релиз контура
- Своя сборка и своя версия у каждого заказчика, расходящиеся во времени
- Ничто не мешает одному клиенту тянуть модуль другого
Клиент перестал быть границей
Это не рассуждение — это коммит от 29 июля 2026.
#35261 FIX. Изменения по задаче 35261
custom-logics/client-b/ClientB.lsf
- SimpleCheckingTask, CheckingTaskUserShipment, ShipmentCheckingTaskClientA,
+ SimpleCheckingTask, CheckingTaskUserShipment,
custom-logics/client-a/processing/checkings/ShipmentCheckingTaskClientA.lsf
- удалён целиком
Топ-модуль Заказчика B подключал модуль Заказчика A. Граница, ради которой всё строилось, оказалась проходной: удаление модуля у одного клиента ломает другого.
Граница дырява и в обратную сторону
Тикет по клиенту «Заказчик C» правился в processing/selections/SelectionPlan.lsf
— общем модуле. Значит по расположению кода нельзя сказать, чей он.
без правила, что куда
владельца по коду
«А давайте привяжем к ролям»
Гипотеза, с которой начался разбор. Отвергнута — и вот счёт, который пришлось бы заплатить.
| Последствие | Что произойдёт |
|---|---|
| Расхождение данных | Один документ считается по-разному у кладовщика и у менеджера на одном складе. Не отображается — считается |
| Комбинаторный взрыв | ~20 заказчиков × 5 ролей ≈ 100 сущностей вместо ~20 профилей и 5 ролей |
| Аудит перестаёт читаться | В роли не отличить доступ от бизнес-правила; безопасник начинает читать бизнес-логику |
| Токен несёт логику | Смена правила расчёта = перевыпуск токенов |
Но интуиция была верной наполовину. Роль остаётся — она раздаёт доступ. Она просто не меняет расчёт. Это AD-17
Три ортогональные оси
Ядро не содержит понятия «какой клиент». Оно задаёт три независимых вопроса, у каждого свой источник и свой момент разрешения.
Capability
Что умеет этот контур
Источник: профиль развёртывания
Момент: старт процесса
RBAC
Что позволено этому человеку
Источник: claims токена + справочник
Момент: запрос
Scope
К каким данным относится действие
Источник: склад/зона по топологии
Момент: запрос
разрешено = capability(контур) AND permission(роль) AND scope(данные)
Каждый сомножитель вычисляется независимо. Оси не выводятся одна из другой — именно это и сломалось в старой модели.
Десять инвариантов
| AD | Правило |
|---|---|
| AD-12 | Тенант — константа развёртывания. Ни в DTO, ни в query, ни в заголовке, ни в claim, ни в сегменте пути |
| AD-13 | Типизированный scope: склад → контур → default. Уровень принадлежит настройке, а не вызывающему |
| AD-14 | Flyway = только схема. Значения — в смонтированном профиле, полный reconcile на каждом старте |
| AD-15 | Профиль состоит из capability. Частично включённая capability считается выключенной |
| AD-16 | Своя таблица → расширение у владельца; иначе флаг. Единица владения — донорский набор, а не один клиент |
| AD-17 | Роль раздаёт доступ, не меняет расчёт. Членство — у IdP, привязка — в профиле |
| AD-18 | Имя клиента в ядре валит сборку. Двумя механизмами, потому что одного не хватает |
| AD-19 | Клиентское приложение получает свои возможности с сервера и не выводит их само |
| AD-20 | Расширение мутирует ядро только через порты. Прямой доступ к таблицам запрещён |
| AD-21 | Переход живых контуров — явная процедура с записью «было → стало» |
Правило, которое держит сборка
Правило «ядро не знает имён заказчиков» не полагается на внимательность ревьюера. Его соблюдение проверяется автоматически — четырьмя независимыми способами.
Архитектурные тесты
При каждой сборке проверяется, что ядро не обращается к коду конкретного заказчика, а расширение заказчика не обращается к данным ядра в обход портов.
Проверка исходников
Отдельный текстовый проход ищет имена заказчиков в строках ядра — то, чего не видят архитектурные тесты, читающие байткод.
Список исключений только сокращается
Известные исключения можно убирать, но нельзя добавлять: попытка дописать новое ловится тестом. На 2026-11-09 список пуст.
Заказчик — свойство развёртывания
Контур не стартует без кода заказчика и проверяет, что смонтированный профиль принадлежит именно ему. Код заказчика не принимается из запросов к API.
Где решение можно оспорить
Четыре места, где решение принято сознательно и может быть оспорено. Остальное — следствия.
1. Полный reconcile на каждом старте
Ночная починка через API живёт до рестарта. Выбрано ради воспроизводимости before/after при кат-овере. Возражение: дежурному это неудобно.
2. Один клиент на контур
Соответствует сегодняшней реальности: свой сервер, своя БД, своя джоба деплоя. Возражение: закрывает дорогу к SaaS без переделки.
3. Уровень capability поверх флагов
При трёх заказчиках избыточен, окупается на двадцати. Возражение: лишний слой косвенности сейчас.
4. Красная сборка вместо отчёта
Правило в виде комментария на практике не удерживается. Возражение: сломанная сборка блокирует не того, кто виноват.
Полный перечень инвариантов — в документе «Intekey WMS 2.0 — целевая архитектура».