diff --git a/docs/architect/PCP_TGU_PLAN_STATUS_LIFECYCLE.md b/docs/architect/PCP_TGU_PLAN_STATUS_LIFECYCLE.md new file mode 100644 index 0000000..c2402c5 --- /dev/null +++ b/docs/architect/PCP_TGU_PLAN_STATUS_LIFECYCLE.md @@ -0,0 +1,143 @@ +# Жизненный цикл статусов плана ТГУ + +Справочник по статусам business-plan в расписании ТГУ: когда наступает каждый статус, что он +означает и какой переход его порождает. Источник правды — enum +[`PlannedPlanStatus`](../../services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/entity/PlannedPlanStatus.kt); +переходы — в `service/IssueDuePlanWorker`, `service/HandlePlanDecisionUseCase`, +`service/CreateDraftUseCase`, `service/CreateChainPlanUseCase`, `dto/PlanResponse`. + +Эти же статусы (кроме «зарезервированных», см. ниже) показаны в легенде вкладки «Планы» +([`tguStatus.ts`](../../services/pcp-tgu-ops-ui/src/features/tgu-planning/tguStatus.ts)) и задают +цвет плашек на таймлайне. + +## Диаграмма переходов (упрощённо) + +``` + создать черновик + (нет плана) ───────────────────────────────► DRAFT + │ promote («Применить») / авто-выдача + ▼ + проекция будущего ····(виртуально)····► PLANNED ──(подошёл срок, авто-выдача)──► ISSUING + │ BPMN-процесс стартовал + ▼ + WAITING_DECISION + ACCEPTED-решение │ │ REJECTED-решение + ▼ ▼ + AWAITING_LAYIN REJECTED (цепочка на паузе) + │ + ┌────────────────────────────────┼────────────────────────────────┐ + событие «заложен» событие «не заложен» событие «не принят» + (пока не реализовано) ▼ ▼ + ▼ LAYIN_FAILED NOT_ACCEPTED + LAID_IN (срочная перевыдача) (срочная перевыдача) + + при сбое старта 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. + +### AWAITING_LAYIN — принят, ждёт подтверждения закладки +- **Когда.** Пришло решение ACCEPTED (`acceptPlan`). +- **Что значит.** План принят и **оптимистично считается заложенным**; это текущий **базовый план + цепочки** (`state.currentBasePlanId = plan.planId`, `chainVersion += 1`). Запускается джоб + обновления слотов принятого плана (`acceptedPlanSlotsUpdateJobService.enqueue`). +- **Дальше.** При подтверждении закладки → `LAID_IN` (терминальный успех; **событие пока не + реализовано**, см. ниже). При негативе — `LAYIN_FAILED`/`NOT_ACCEPTED`. +- В легенде это де-факто «принятый/заложенный» статус (зелёный). Перевыдача в свой слот ещё + возможна, пока до старта больше `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`. +- **Что значит.** Подтверждённая закладка, терминальный успех; «точка невозврата» — дальше план + меняется только через fork-forward (используется в гардах `CreateDraftUseCase`, + `assertReissuableInSlot`). +- **⚠️ TODO — не реализовано.** Потребителя события «план заложен» сейчас **нет**, поэтому план в + `LAID_IN` не переходит и остаётся в `AWAITING_LAYIN`. Нужно: Kafka-consumer (по аналогии с + `LayinFailedConsumer`/`PlanNotAcceptedConsumer`) + `handleLayinConfirmed` в + `HandlePlanDecisionUseCase` (`AWAITING_LAYIN → LAID_IN` по текущему базовому плану). Маркер-TODO + стоит в коде `HandlePlanDecisionUseCase` рядом с обработчиками негативного фидбека. +- В легенде статус показан (зелёный, темнее `AWAITING_LAYIN`) — заранее, чтобы при появлении события + цвет уже был. + +## Зарезервированные / не возникающие сейчас статусы + +- **ACCEPTED** — в enum присутствует, но приёмка (`acceptPlan`) переводит план **сразу** в + `AWAITING_LAYIN`, минуя `ACCEPTED`. Фактически это легаси-значение, нигде не присваивается; в + легенду вкладки «Планы» намеренно не вынесено. + +## Связанные конфиги (`PlanningProperties`) +- `waitingDecisionGraceMinutes` — сколько ждём решение после старта, прежде чем «молчаливо» ехать + дальше (см. `WAITING_DECISION`). +- `layinFreezeMinutes` — за сколько минут до старта окно выдачи замораживается («точка невозврата»): + перевыдача в свой слот запрещена, только fork-forward. +- `notificationBeforeStartMinutes` — за сколько до старта авто-воркер начинает выдачу головы цепочки. + +## См. также +- [PCP_ARCHITECTURE.md](PCP_ARCHITECTURE.md) +- [PCP_TGU_OPS_UI_CAPABILITY_MAP.md](PCP_TGU_OPS_UI_CAPABILITY_MAP.md) diff --git a/services/pcp-tgu-ops-ui/src/features/tgu-planning/TguLegend.tsx b/services/pcp-tgu-ops-ui/src/features/tgu-planning/TguLegend.tsx index c4b6fac..b3a9b97 100644 --- a/services/pcp-tgu-ops-ui/src/features/tgu-planning/TguLegend.tsx +++ b/services/pcp-tgu-ops-ui/src/features/tgu-planning/TguLegend.tsx @@ -4,12 +4,11 @@ export function TguLegend() { return (