87905311b1
Базой цепочки план становится на ЗАКЛАДКЕ (LAID_IN), а не на приёмке.
После принятого плана можно отправить коррекцию — она тоже примется;
несколько выдач «в полёте» по одному КА теперь норма, а не конфликт.
- ACCEPT больше не двигает currentBasePlanId/chainVersion и не запирает
слот — только переводит план в AWAITING_LAYIN.
- Новый позитивный топик layin-confirmed + LayinConfirmedConsumer →
handleLayinConfirmed: AWAITING_LAYIN → LAID_IN, план становится базой
(chainVersion++), конкуренты на пересекающемся окне гасятся в новый
терминальный статус SUPERSEDED. Гард ±10 мин (LAYIN_MIN_SEPARATION_MINUTES):
нельзя заложить план со стартом в пределах 10 мин от уже заложенного (409).
- «В полёте» считается из статусов планов (ISSUING/WAITING_DECISION), а не
из единственного active-слота; снят гард «идёт выдача другого плана».
IssueDuePlanWorker гейтится hasBlockingInFlight, базу нового плана берёт
как latestCommittedPlanId; grace больше не двигает базу.
- PlanProjectionService якорит по самому дальнему committed-окну
{ISSUING, WAITING_DECISION, AWAITING_LAYIN, LAID_IN}.
- REJECT ставит паузу только если по КА не осталось живых кандидатов.
- CreateDraftUseCase: replace-черновик наследует ПРЕДШЕСТВЕННИКА исходного
плана (base.basePlanId), а не сам план — коррекция занимает тот же слот;
fork-forward по-прежнему ссылается на базу. Идемпотентность черновиков —
по слоту (КА + окно), а не по basePlanId.
- Тест-контроллер: POST /test/tgu/plans/{id}/confirm-layin (через BFF
pcp-ui-service) — имитация события «план заложен».
- Док PCP_TGU_PLAN_STATUS_LIFECYCLE обновлён (LAID_IN-переход, SUPERSEDED).
14 KiB
14 KiB
Жизненный цикл статусов плана ТГУ
Справочник по статусам business-plan в расписании ТГУ: когда наступает каждый статус, что он
означает и какой переход его порождает. Источник правды — enum
PlannedPlanStatus;
переходы — в service/IssueDuePlanWorker, service/HandlePlanDecisionUseCase,
service/CreateDraftUseCase, service/CreateChainPlanUseCase, dto/PlanResponse.
Эти же статусы (кроме «зарезервированных», см. ниже) показаны в легенде вкладки «Планы»
(tguStatus.ts) и задают
цвет плашек на таймлайне.
Диаграмма переходов (упрощённо)
создать черновик
(нет плана) ───────────────────────────────► DRAFT
│ promote («Применить») / авто-выдача
▼
проекция будущего ····(виртуально)····► PLANNED ──(подошёл срок, авто-выдача)──► ISSUING
│ BPMN-процесс стартовал
▼
WAITING_DECISION
ACCEPTED-решение │ │ REJECTED-решение
▼ ▼
AWAITING_LAYIN REJECTED (цепочка на паузе)
│
┌────────────────────────────────┼────────────────────────────────┐
событие «заложен» событие «не заложен» событие «не принят»
▼ ▼ ▼
LAID_IN LAYIN_FAILED NOT_ACCEPTED
(база цепочки; конкуренты (срочная перевыдача) (срочная перевыдача)
того же окна → SUPERSEDED)
при сбое старта BPMN из ISSUING ─────────────────────────────────────────► START_AMBIGUOUS
Статусы
DRAFT — черновик
- Когда. Оператор создаёт план через «Создать план» (
CreateChainPlanUseCase) или «Добавить»/ редактирование (CreateDraftUseCase). - Что значит. План в стадии правки. Персистится, но не является головой цепочки, не попадает в проекцию/read-путь как запланированный слот и не выдаётся авто-воркером.
- Дальше. Становится активным только после promote («Применить») — переходит в
ISSUING. Удаление черновика (abandon) убирает его и клон-миссию. - В UI помечен как «ЧЕРНОВИК», виден только при включённом режиме «Черновики».
PLANNED — проекция будущего (виртуальный)
- Когда. Вычисляется на лету
PlanProjectionService/PlanResponse.projected()при чтении расписания. Никогда не пишется в БД. - Что значит. Предполагаемый будущий слот цепочки (следующие ЗРВ) — «как пойдёт цепочка, если
ничего не менять». Не несёт
basePlanId, стартом цепочки не считается. - Дальше. Когда подходит срок выдачи, по этому слоту авто-воркер материализует реальный план
сразу в
ISSUING(materializeIssuingPlan→insertIssuingPlan). - В UI виден только при включённом тумблере «Возможные будущие планы».
ISSUING — идёт выдача
- Когда.
IssueDuePlanWorkerначинает выдачу: promote черновика (prepareDraftPromotion), ручная (пере)выдача конкретного плана (prepareSpecificPlan) или авто-материализация головы проекции (materializeIssuingPlan). - Что значит. Запущен BPMN-процесс выдачи плана на борт; создан attempt со статусом
STARTING. - Дальше. Успешный старт процесса →
WAITING_DECISION(markStarted). Сбой старта BPMN →START_AMBIGUOUS(markAmbiguous).
WAITING_DECISION — ожидает решения
- Когда. BPMN-процесс выдачи успешно стартовал, attempt →
STARTED(markStarted). - Что значит. План выдан, ждём решения оператора/борта: ACCEPTED либо REJECTED.
- Дальше.
- ACCEPTED →
AWAITING_LAYIN(acceptPlan). - REJECTED →
REJECTED(rejectPlan). - Grace-окно. Если решения нет дольше
waitingDecisionGraceMinutesпослеstartTime, цепочка «молчаливо» едет дальше: якорь сдвигается на этот план, блокировка снимается, но статус остаётсяWAITING_DECISION(tryAdvancePastUnconfirmedPlan). Запоздалое решение запишется на attempt и ушедшую вперёд цепочку не тронет. Поэтому в UI кнопки решения гасятся после grace.
- ACCEPTED →
AWAITING_LAYIN — принят, ждёт подтверждения закладки
- Когда. Пришло решение ACCEPTED (
acceptPlan). - Что значит. План принят и ждёт закладки. ACCEPT больше НЕ делает план базой и не двигает
chainVersion— это разъединено намеренно: после приёмки можно отправить и принять коррекцию того же окна, поэтому по одному окну может одновременно «лететь» несколько AWAITING_LAYIN/ WAITING_DECISION. Базой и версией цепочки управляет только закладка (LAID_IN). На ACCEPT запускается джоб обновления слотов принятого плана (acceptedPlanSlotsUpdateJobService.enqueue). - Дальше. При подтверждении закладки →
LAID_IN(становится базой). При негативе —LAYIN_FAILED/NOT_ACCEPTED. Если базой стала другая коррекция того же окна — этот план гасится вSUPERSEDED. - В легенде это «принятый, ждёт закладки» статус (зелёный). Перевыдача/коррекция в свой слот ещё
возможна, пока до старта больше
layinFreezeMinutes(после — «точка невозврата», только fork-forward).
REJECTED — отклонён
- Когда. Пришло решение REJECTED (
rejectPlan). - Что значит. План признан неправильным. Авто-перевыдача не взводится (повторять тот же план
бессмысленно). Цепочка ставится на паузу (
pausedAfterReject = true) — только если по КА не осталось других живых кандидатов (в полёте/приняты, но не заложены): при нескольких выдачах отказ одной не глушит цепочку, пока жив другой кандидат. На паузе проекция пуста, воркер ничего не выдаёт. Базовый план не трогается. - Дальше. Оператор вручную создаёт исправленный план; его promote/ручная выдача снимают паузу.
- В UI у КА с паузой рисуется пометка «⏸ на паузе».
NOT_ACCEPTED — не принят (негативный фидбек)
- Когда. Пришло внешнее событие «не принят» по текущему базовому плану в
AWAITING_LAYIN(handleNotAccepted→applyNegativeFeedback). - Что значит. Терминальный негатив по базовому плану. В отличие от REJECTED — это технический
негатив, поэтому запускается срочная перевыдача на ближайшую ЗРВ (
reissueOnNearestWindow). - Событие, пришедшее не по текущему базовому плану (устаревшее/дубль), игнорируется.
LAYIN_FAILED — не заложен (негативный фидбек)
- Когда. Пришло внешнее событие «не заложен» по текущему базовому плану в
AWAITING_LAYIN(handleLayinFailed→applyNegativeFeedback). - Что значит. Терминальный негатив: закладка не удалась. Поведение сейчас идентично
NOT_ACCEPTED— срочная перевыдача на ближайшую ЗРВ.
START_AMBIGUOUS — неопределённый старт
- Когда. Из
ISSUINGне удалось достоверно стартовать BPMN-процесс выдачи (исключение вstartPreparedAttempt→markAmbiguous); attempt →AMBIGUOUS. - Что значит. Неизвестно, ушёл план на борт или нет — требуется ручной разбор оператором. Опасный статус, в UI выделен пунктиром.
LAID_IN — заложен (терминальный успех, база цепочки)
- Когда. Пришло внешнее событие «план заложен» по плану в
AWAITING_LAYIN(LayinConfirmedConsumer→handleLayinConfirmed, топикlayin-confirmed). - Что значит. Подтверждённая закладка — именно она делает план базой цепочки
(
state.currentBasePlanId = plan.planId,chainVersion += 1, снимается пауза/срочная перевыдача). «Точка невозврата»: дальше план меняется только через fork-forward (гардыCreateDraftUseCase,assertReissuableInSlot). - Supersede конкурентов. Все ещё не заложенные планы КА (ISSUING/WAITING_DECISION/AWAITING_LAYIN)
с окном, пересекающим окно заложенного, переводятся в
SUPERSEDED(supersedeCompetingPlans). - Событие по плану не в
AWAITING_LAYIN(устаревшее/дубль/уже вытеснён) — игнорируется. - В легенде — зелёный, темнее
AWAITING_LAYIN.
SUPERSEDED — вытеснён заложенной коррекцией (терминал)
- Когда. По тому же окну КА другой план ушёл в
LAID_IN, а этот был конкурирующей выдачей/ коррекцией (supersedeCompetingPlans). - Что значит. Терминал, цепочку не двигает. Запоздавшее решение по его attempt записывается, но игнорируется (план уже не in-flight). Базовый план не трогается.
Зарезервированные / не возникающие сейчас статусы
- ACCEPTED — в enum присутствует, но приёмка (
acceptPlan) переводит план сразу вAWAITING_LAYIN, минуяACCEPTED. Фактически это легаси-значение, нигде не присваивается; в легенду вкладки «Планы» намеренно не вынесено.
Связанные конфиги (PlanningProperties)
waitingDecisionGraceMinutes— сколько ждём решение после старта, прежде чем «молчаливо» ехать дальше (см.WAITING_DECISION).layinFreezeMinutes— за сколько минут до старта окно выдачи замораживается («точка невозврата»): перевыдача в свой слот запрещена, только fork-forward.notificationBeforeStartMinutes— за сколько до старта авто-воркер начинает выдачу головы цепочки.