ТЗ: Переход глубины деревьев P2 с цикла 5-4-3 на 4-3-2
Метаданные
| Параметр | Значение |
|---|---|
| Дата создания | 2026-07-14 |
| Дата последнего изменения | 2026-07-14 |
| Статус апрува | ⏳ На рассмотрении |
| Дата апрува | — |
В программе 2 (Sandbox) каждое дерево (матрица) имеет предельную глубину — количество линий,
после заполнения которых матрица считается закрытой. Сейчас глубина задаётся циклом [5, 4, 3]:
дерево 0 — 5 линий, дерево 1 — 4, дерево 2 — 3, дерево 3 — снова 5 и так далее.
Требуется, чтобы новые деревья создавались по схеме 4-3-2, а уже существующие деревья
сохранили свою исходную глубину.
Прямая замена массива в конфиге эту задачу не решает и является деструктивной — см. раздел 4.
2.1. Пользовательские сценарии
- Существующий пользователь. Деревья, открытые до перехода, продолжают закрываться на своих 5/4/3 линиях. Выплаты по ним не меняются.
- Новое дерево (реинвест). Дерево, открытое после перехода, закрывается на 4/3/2 линиях.
В уведомлении
OpenNewMatrixпользователь видит корректное число линий нового дерева. - Смешанный случай. У одного п ользователя одновременно есть старые деревья на 5/4/3 и новые на 4/3/2 — интерфейс карьеры должен корректно отрисовать оба.
2.2. Бизнес-логика
Глубина дерева — не косметический параметр. Она:
- определяет, когда матрица считается закрытой (
isMatrixClosed,isOwnerMatrixClosed) и, следовательно, куда попадёт следующий зарегистрированный пользователь; - уходит на блокчейн как
referralLinesи делит выплату между аплайнами:apps/contracts-v2/contracts/diamond/libs/DistributionsLib.sol:99—baseAmount = totalReferralFund / referralLines. Значение неизменяемо после записи.
Отсюда главное требование: историческая глубина дерева не должна меняться никогда.
3.1. Архитектура (как есть на 2026-07-14)
Где лежит конфигурация
MongoDB, база PROGRAM_LEVELS_CONFIG, коллекция programlevelsconfigs — ровно один документ:
{ "levels": { "1": { "cycle": [5,4,3], "qualification": [1,1,1], "openPattern": ["AIR"] },
"2": { "cycle": [5,4,3], ... }, // ... и так до уровня 15
} }
- Схема:
packages/reusable-magnet-backend/src/schemas/programLevelsConfig.schema.ts - Интерфейс
LevelCfg:packages/reusable-magnet-backend/src/schemas/interfaces/programLevel.interfaces.ts - Чтение:
apps/p2-backend/src/programLevelsConfig/programLevelsConfig.service.ts:110—findOne().lean()на каждый вызов, без кэша. Значит правка документа применяется мгновенно, рестарт не нужен. - Запись:
POST /levels-config(programLevelsConfig.controller.ts:21), guardJwtAuthGuard + RolesGuard, ролиAdmin | LevelsConfigWrite. Валидируется только длина массива, не значения. apps/backendрегистрирует это подключение, но модель не инжектит (мёртвое подключение).apps/multisig-backendберёт конфиг по HTTP (GET /levels-configу p2), то есть наследует любое изменение автоматически.
Как считается глубина
Единственный источник истины — вычисление на лету:
// packages/reusable-magnet-backend/src/programs-helper/programs-helper.service.ts:631
getLinesToClose(treeIndex, config) { return config.cycle[treeIndex % config.cycle.length]; }
// :637 — байт-в-байт такая же getLinesToCloseOwnerMatrix(treeIndex, config)
Ключевой факт: root.lines — write-only
В Neo4j у корня каждого дерева (uid = 1, depth = 1) уже записано свойство lines.
Но по всему монорепозиторию есть три записи и ноль чтений:
| Запись | Что делает |
|---|---|
programs-helper.service.ts:208 | SET root.lines в initOwnerMatrices |
programs-helper.service.ts:1310 | lines: при создании AIR-дерева |
programs-helper.service.ts:1381 | SET n.lines при создании REC-дерева |
Даже API, отдающий фронту lines по каждому дереву, считает его из конфига, а не читает из базы:
apps/p2-backend/src/tree.service.ts:776.
Вывод: сохранённая глубина — мёртвые денормализованные данные. Опереться на них «как есть» нельзя, но они верны и пригодны как основа миграции (см. 3.2).
Инвентарь мест, где глубина выводится из конфига
Все перечисленные точки сломаются при смене cycle, т.к. пересчитывают глубину заново:
| Файл:строка | Что |
|---|---|
programs-helper.service.ts:671 | getTargetMatrixIndex — выбор матрицы для размещения |
programs-helper.service.ts:747 | isOwnerMatrixClosed — закрытие матриц владельца (+ баг) |
apps/multisig-backend/src/verification.service.ts:291 | referralLines → блокчейн (денежный путь) |
apps/p2-backend/src/tree.service.ts:718 | getMatrixCount → linesToClosePerPage |
apps/p2-backend/src/tree.service.ts:776 | getP2TreesSliderInfo → lines на дерево (API фронта) |
apps/p2-backend/src/tree.service.ts:1166 | обход дерева |
apps/p2-backend/src/tree.service.ts:1382 | обход дерева |
apps/p2-backend/src/tree.service.ts:1516 | nextTreeLinesCount — глубина следующего дерева |
apps/p2-backend/src/tree.service.ts:1715 | getLinesToCloseByLevelAndMatrix → эндпоинты /lines-to-close, /lines-to-close-by-level |
Создание новых деревьев: getOwnerLineForMatrixIndex (:1126) → openOwnerNextMatrix (:1295).
Состояние живых данных (проверено 2026-07-14, только чтение)
Neo4j (78.17.44.15:17687, БД prod-контура; креды — вне репозитория, у DevOps):
- Метки
Node1..Node15,NodeM,OwnerRuntime,RouteSeq. Связиleft,right,childOf,lastReferral,previousLastReferral. - Уровень 1 — единственный с реальными пользователями: 20 нод, 4 дерева.
- Уровни 2–15 — только по 3 засеянных корня, пользователей нет.
| level | treeIndex | root.matrixIndex | root.lines | cycle[ti%3] | нод | maxDepth |
|---|---|---|---|---|---|---|
| 1 | 0 | 0 | 5 | 5 | 6 | 5 |
| 1 | 1 | 1 | 4 | 4 | 6 | 4 |
| 1 | 2 | 2 | 3 | 3 | 4 | 3 |
| 1 | 3 | 3 | 5 | 5 | 4 | 4 |
| 2..15 | 0/1/2 | 0/1/2 | 5/4/3 | 5/4/3 | 1 | 1 |
Инварианты, подтверждённые запросами:
linesесть у каждого корня каждого уровня и совпадает с текущим конфигом;linesесть только у корней владельца (uid=1, depth=1), у пользовательских нод его нет;treeIndexесть у каждой ноды, и для каждогоtreeIndexнаходится корень владельца (сирот и NULL-ов нет);maxDepth <= root.linesвыполняется везде.
OwnerRuntime: уровень 1 → nextMatrixIndex = 4; уровни 2–15 → nextMatrixIndex = 3.
Следствие: бэкфилл данных не нужен. Достаточно резолвить treeIndex → корень владельца → lines.
Запросы для повторной проверки (read-only):
// глубина по деревьям уровня N
MATCH (n:Node1)
WITH n.treeIndex AS ti, count(n) AS nodes, max(n.depth) AS maxDepth
OPTIONAL MATCH (root:Node1 { uid: 1, treeIndex: ti, depth: 1 })
RETURN ti, root.matrixIndex AS rmi, root.lines AS lines, nodes, maxDepth ORDER BY ti;
// ноды с lines, не являющиеся корнями владельца (ожидается 0)
MATCH (n:Node1) WHERE n.lines IS NOT NULL AND NOT (n.uid = 1 AND n.depth = 1) RETURN count(n);
3.2. Описание технической реализации
Целевая модель: источник истины по глубине — сохранённый root.lines. Конфиг используется
только в момент создания нового дерева. Тогда любая будущая смена цикла автоматически
перестаёт быть ретроактивной.
Шаг 1. Резолвер глубины. Добавить в ProgramsHelperService:
getTreeLines(tx, level, treeIndex, config): Promise<number>
// читает root.lines у (:Node{level} { uid: 1, treeIndex, depth: 1 })
// fallback на config.cycle[treeIndex % len] — только если свойства нет
Бэкфилл не требуется (см. 3.1), фоллбэк — страховка.
Шаг 2. Перевести на резолвер все точки из инвентаря (programs-helper.service.ts:671, :747;
tree.service.ts:718, :776, :1166, :1382, :1715; verification.service.ts:291).
Шаг 3. Исправить баг isOwnerMatrixClosed (programs-helper.service.ts:747) — см. 4.3.
Шаг 4. Глубина следующего (ещё не существующего) дерева. tree.service.ts:1516
(getLinesToClose(treeIndex + 1, config)) и openOwnerNextMatrix (:1295) должны остаться
config-derived — это единственные места, которые обязаны начать отдавать новую глубину.
Шаг 5. Обезвредить initOwnerMatrices (programs-helper.service.ts:207-216) — см. 4.3.
Шаг 6. Фронтенд — см. 4.3 (падение на глубине 2).
Шаг 7. Конфиг. Только после деплоя кода: POST /levels-config с [4,3,2].
Длина остаётся 3, поэтому qualification, openPattern, размер группы, рекурсия и счётчики
OwnerRuntime не затрагиваются — валидации проходят.
4. Проблемы и компромиссы
4.1. Известные ограничения
-
Конфиг применяется мгновенно, без окна безопасности
getLevelsConfig()не кэширует — правка документа действует на следующем же запросе.- «Мы ещё не передеплоились» защитой не является.
-
Фаза цикла привязана к абсолютному
matrixIndexgetOwnerLineForMatrixIndex(:1126) =cycle[matrixIndex % 3].- У уровня 1
nextMatrixIndex = 4, значит следующее дерево возьмётcycle[1]— при[4,3,2]это 3, а не 4. Простая замена массива промахнётся мимо начала новой схемы. - Если нужно, чтобы новая схема стартовала именно с 4, требуется якорь переключения в конфиге
(например
switchAtMatrixIndex+ фазаcycleV2[(matrixIndex - switchAt) % 3]). См. раздел 5.
4.2. Технический долг
-
apps/p2-backend/src/program1/program1.service.ts:64-66— мёртвый код с захардкоженнымиlines: 5/4/3в трёхMERGE. Удалить, чтобы старая схема не воскресла. -
apps/p2-backend/src/tree.service.spec.ts:84мокаетcycle: [4,3,2]и тестирует вывод глубины из конфига — это не страховка. Самые дорогие пути (закрытие матриц, размещение,referralLines) не покрыты тестами вообще. -
initOwnerMatricesбезусловно сбрасываетforceOpenedиactivatedна каждом старте — отдельный латентный баг, всплывший при разборе.
4.3. Риски
Р1. Ретроактивное переопределение существующих деревьев — БЛОКЕР.
Так как root.lines никем не читается, смена cycle на [4,3,2] мгновенно превращает дерево 0
из пятилинейного в четырёхлинейное. Уже заполненные деревья становятся «переполненными»/закрытыми.
Хуже всего денежный путь: verification.service.ts:291 → referralLines → контракт, где
baseAmount = totalReferralFund / referralLines. Бэкенд начнёт подписывать referralLines = 4
для пользователей, попадающих в существующее 5-линейное дерево → необратимая недоплата 5-й линии.
Митигация: шаги 1–2 (чтение сохранённой глубины) до любой правки конфига.
Р2. initOwnerMatrices перезаписывает историю на каждом старте.
Вызывается из Program1Service.onModuleInit (apps/p2-backend/src/program1/program1.service.ts:36),
то есть на каждом бутстрапе. В programs-helper.service.ts:207-216 за ON CREATE спрятаны только
chunk/localPosition/position/hasFreePlace, а SET root.lines / mode / treeIndex / forceOpened / activated — безусловный. После смены конфига первый же рестарт перепишет корням деревьев
0/1/2 глубину 5,4,3 → 4,3,2 на всех уровнях. Деревья 3+ не затрагиваются (цикл идёт
до cycle.length).
Митигация: перенести root.lines, root.treeIndex, root.forceOpened, root.activated
под ON CREATE SET.
Р3. Подтверждённый баг: isOwnerMatrixClosed передаёт level вместо treeIndex.
programs-helper.service.ts:747 вызывает getLinesToCloseOwnerMatrix(level, config), хотя сигнатура
(:637) ожидает treeIndex. Для уровня 1 это cycle[1 % 3] = 4 — то есть дерево владельца 0
всегда закрывалось на 4 линиях вместо 5, а дерево 2 — на 4 вместо 3. Баг существует сейчас;
при [4,3,2] он просто сместится (уровень 1 → 3).
Митигация: чинить в рамках этой же работы, вместе с пламбингом treeIndex.
Р4. Фронт падает на глубине 2.
apps/frontend/src/entities/career/ui/PreprodCardMatrix.tsx:38 — словарь iconByLineToClose
знает только 3/4/5/closed и индексируется без защиты (:128). Ассета
public/images/icons/reinvest/circle_2.svg не существует. Первое же дерево глубины 2 роняет
диалог матриц с TypeError.
Митигация: добавить circle_2.svg, ключ 2: и безопасный фоллбэк. Проверить таблицу settings
в CardCircleTree.tsx:97 (для глубины 2 сейчас уедет в settings[6]).
Р5. Админка — живой футган.
POST /levels-config валидирует только длину массива, поэтому [4,3,2] проходит все проверки
и применяется мгновенно. Пока не выполнены шаги 1–2, любой админ может ретроактивно переопределить
все деревья одним нажатием «Сохранить».
Митигация: до конца работ не трогать эндпоинт; после — расширить валидацию.
5. Вопросы на дополнительное обсуждение
-
Вопрос 1 (блокирующий): С какой глубины должна начинаться новая схема?
- Из-за Р1/4.1.2 следующее дерево уровня 1 (
matrixIndex = 4) при простой замене массива получитcycle[1] = 3, а не 4. - Вариант А: новая схема начинается ровно с 4 → нужен якорь переключения
(
switchAtMatrixIndex) в конфиге и правка фазовой формулы. Объём работ больше. - Вариант Б: цикл продолжает фазу, следующее дерево = 3 → достаточно замены массива.
- Кому адресовано: постановщик задачи / бизнес.
- Из-за Р1/4.1.2 следующее дерево уровня 1 (
-
Вопрос 2: Меняется ли
qualificationвместе с циклом? Сейчас предполагается, что нет.- Кому адресовано: постановщик задачи / бизнес.
-
Вопрос 3: Уровни 2–15 практически пусты (только засеянные корни). Применять переход сразу на всех 15 уровнях или поэтапно?
- Кому адресовано: постановщик задачи.
6. План реализации
6.1. Этапы разработки
Порядок строгий — нарушение порядка приводит к Р1.
- Этап 0. Заморозить
POST /levels-config(Р5). Конфиг не трогать. - Этап 1. Резолвер
getTreeLines(чтениеroot.linesиз Neo4j). - Этап 2. Перевести на него 8 call-sites из инвентаря 3.1.
- Этап 3. Починить
isOwnerMatrixClosed(Р3) иinitOwnerMatrices(Р2). - Этап 4. Фронт:
circle_2.svg, ключ2:, безопасный индекс (Р4). - Этап 5. Тесты: закрытие матриц, размещение,
referralLines(сейчас покрытие нулевое). - Этап 6. Деплой кода. Проверить, что старые деревья по-прежнему 5/4/3.
- Этап 7.
POST /levels-configс[4,3,2](+ якорь переключения, если выбран вариант А). - Этап 8. Проверить глубину следующего с озданного дерева.
6.2. Критические зависимости
- Ответ на Вопрос 1 блокирует этапы 1–2 (от него зависит форма конфига и сигнатура фазы).
- Этап 7 нельзя выполнять раньше этапа 6 ни при каких условиях (Р1).
8. Документация
- Обновить engineering-раздел описанием того, что глубина дерева хранится в
root.linesи является источником истины. - Задокументировать формат конфига
LevelCfg(включая якорь переключения, если он появится).
9. Ссылки
- Конфиг:
packages/reusable-magnet-backend/src/schemas/programLevelsConfig.schema.ts - Ядро логики:
packages/reusable-magnet-backend/src/programs-helper/programs-helper.service.ts - Денежный путь:
apps/multisig-backend/src/verification.service.ts:291→apps/contracts-v2/contracts/diamond/libs/DistributionsLib.sol:99