pcp-tgu: «снять с учёта» терминально-негативный план (dismissed_at)

Оператор деактивирует отменённый/неудавшийся план, чтобы он не считался
проблемой и не красил индикатор КА. Косметико-статистический флаг
dismissed_at: статус плана и состояние цепочки не трогает, обратим.

- planned_plan.dismissed_at (V14) + поле в сущности и PlanResponse;
- DismissPlanUseCase.dismiss/restore с гардом на терминально-негативные
  статусы (REJECTED, START_AMBIGUOUS, LAYIN_FAILED, NOT_ACCEPTED,
  SUPERSEDED), идемпотентно; эндпоинты POST /api/plans/{id}/dismiss|restore;
- BFF (pcp-ui-service): проброс dismissedAt в TguPlanResponse и
  /api/tgu-planning/plans/{id}/dismiss|restore;
- тест DismissPlanUseCaseTest; починен предсуществующий TguPlanningServiceTest
  (мок-JSON не отдавал projected — WebClient падал на null→boolean).
This commit is contained in:
Дмитрий Соловьев
2026-06-04 16:59:13 +03:00
parent fae57e957e
commit e6f1219c9c
10 changed files with 280 additions and 4 deletions
@@ -47,6 +47,14 @@ class TguPlanningRestController(
@RequestParam(required = false) reason: String?
) = tguPlanningService.sendDecision(planId, decision, reason)
/** «Снять с учёта» терминально-негативный план (чтобы не красил индикатор КА). */
@PostMapping("/plans/{planId}/dismiss")
fun dismissPlan(@PathVariable planId: UUID) = tguPlanningService.dismissPlan(planId)
/** «Вернуть на учёт» план. */
@PostMapping("/plans/{planId}/restore")
fun restorePlan(@PathVariable planId: UUID) = tguPlanningService.restorePlan(planId)
/** ТЕСТ: подтвердить закладку плана (AWAITING_LAYIN → LAID_IN). */
@PostMapping("/plans/{planId}/confirm-layin")
fun confirmLayin(
@@ -30,7 +30,9 @@ data class TguPlanResponse(
/** Предшественник в цепочке (`null` — старт без предыстории). Проксируется из pcp-tgu-service. */
val basePlanId: UUID? = null,
/** `true` — проекция будущего (не persisted). Проксируется из pcp-tgu-service. */
val projected: Boolean = false
val projected: Boolean = false,
/** Время «снятия с учёта» (`null` — план на учёте). Проксируется из pcp-tgu-service. */
val dismissedAt: LocalDateTime? = null
)
enum class TguPlanDecision {
@@ -87,6 +87,28 @@ class TguPlanningService(webClientBuilderProvider: ObjectProvider<WebClient.Buil
throw error
}
/** «Снять с учёта» терминально-негативный план (pcp-tgu-service, реальный API, не тест-контроллер). */
fun dismissPlan(planId: UUID) {
webClientBuilder.build()
.post()
.uri(buildUri("/api/plans/{planId}/dismiss") { builder -> builder.buildAndExpand(planId) })
.retrieve()
.onStatus({ status -> status.isError }, ::mapReadError)
.bodyToMono(Void::class.java)
.block()
}
/** «Вернуть на учёт» план (pcp-tgu-service). */
fun restorePlan(planId: UUID) {
webClientBuilder.build()
.post()
.uri(buildUri("/api/plans/{planId}/restore") { builder -> builder.buildAndExpand(planId) })
.retrieve()
.onStatus({ status -> status.isError }, ::mapReadError)
.bodyToMono(Void::class.java)
.block()
}
fun confirmLayin(planId: UUID, reason: String?) {
runCatching {
webClientBuilder.build()
@@ -116,7 +116,7 @@ class TguPlanningServiceTest {
exchange.requestURI.path == "/api/plans" || exchange.requestURI.path == "/api/plans/56756" -> respond(
exchange,
200,
"""[{"planId":"6d0941e4-61d6-4f96-8fa8-90ba11db121e","spacecraftId":"56756","startTime":"2026-05-29T10:00:00Z","endTime":"2026-05-29T10:15:00Z","kppId":"KPP-1","status":"WAITING_DECISION"}]"""
"""[{"planId":"6d0941e4-61d6-4f96-8fa8-90ba11db121e","spacecraftId":"56756","startTime":"2026-05-29T10:00:00Z","endTime":"2026-05-29T10:15:00Z","kppId":"KPP-1","status":"WAITING_DECISION","basePlanId":null,"projected":false,"dismissedAt":null}]"""
)
exchange.requestURI.path.startsWith("/test/tgu/plans/") -> respond(
exchange,