Перейти к основному содержимому

ТЗ: Переход глубины деревьев 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. Бизнес-логика

Глубина дерева — не косметический параметр. Она:

  1. определяет, когда матрица считается закрытой (isMatrixClosed, isOwnerMatrixClosed) и, следовательно, куда попадёт следующий зарегистрированный пользователь;
  2. уходит на блокчейн как referralLines и делит выплату между аплайнами: apps/contracts-v2/contracts/diamond/libs/DistributionsLib.sol:99baseAmount = 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:110findOne().lean() на каждый вызов, без кэша. Значит правка документа применяется мгновенно, рестарт не нужен.
  • Запись: POST /levels-config (programLevelsConfig.controller.ts:21), guard JwtAuthGuard + 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:208SET root.lines в initOwnerMatrices
programs-helper.service.ts:1310lines: при создании AIR-дерева
programs-helper.service.ts:1381SET n.lines при создании REC-дерева

Даже API, отдающий фронту lines по каждому дереву, считает его из конфига, а не читает из базы: apps/p2-backend/src/tree.service.ts:776.

Вывод: сохранённая глубина — мёртвые денормализованные данные. Опереться на них «как есть» нельзя, но они верны и пригодны как основа миграции (см. 3.2).

Инвентарь мест, где глубина выводится из конфига

Все перечисленные точки сломаются при смене cycle, т.к. пересчитывают глубину заново:

Файл:строкаЧто
programs-helper.service.ts:671getTargetMatrixIndex — выбор матрицы для размещения
programs-helper.service.ts:747isOwnerMatrixClosed — закрытие матриц владельца (+ баг)
apps/multisig-backend/src/verification.service.ts:291referralLines → блокчейн (денежный путь)
apps/p2-backend/src/tree.service.ts:718getMatrixCountlinesToClosePerPage
apps/p2-backend/src/tree.service.ts:776getP2TreesSliderInfolines на дерево (API фронта)
apps/p2-backend/src/tree.service.ts:1166обход дерева
apps/p2-backend/src/tree.service.ts:1382обход дерева
apps/p2-backend/src/tree.service.ts:1516nextTreeLinesCount — глубина следующего дерева
apps/p2-backend/src/tree.service.ts:1715getLinesToCloseByLevelAndMatrix → эндпоинты /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 засеянных корня, пользователей нет.
leveltreeIndexroot.matrixIndexroot.linescycle[ti%3]нодmaxDepth
1005565
1114464
1223343
1335544
2..150/1/20/1/25/4/35/4/311

Инварианты, подтверждённые запросами:

  • 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. Известные ограничения

  1. Конфиг применяется мгновенно, без окна безопасности

    • getLevelsConfig() не кэширует — правка документа действует на следующем же запросе.
    • «Мы ещё не передеплоились» защитой не является.
  2. Фаза цикла привязана к абсолютному matrixIndex

    • getOwnerLineForMatrixIndex (: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:291referralLines → контракт, где 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 → достаточно замены массива.
    • Кому адресовано: постановщик задачи / бизнес.
  • Вопрос 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:291apps/contracts-v2/contracts/diamond/libs/DistributionsLib.sol:99