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:
+20
@@ -20,6 +20,7 @@ import space.nstart.pcp_tgu_service.dto.ZrvWindowResponse
|
|||||||
import space.nstart.pcp_tgu_service.service.CreateChainPlanUseCase
|
import space.nstart.pcp_tgu_service.service.CreateChainPlanUseCase
|
||||||
import space.nstart.pcp_tgu_service.service.CreateDraftUseCase
|
import space.nstart.pcp_tgu_service.service.CreateDraftUseCase
|
||||||
import space.nstart.pcp_tgu_service.service.DeleteDraftUseCase
|
import space.nstart.pcp_tgu_service.service.DeleteDraftUseCase
|
||||||
|
import space.nstart.pcp_tgu_service.service.DismissPlanUseCase
|
||||||
import space.nstart.pcp_tgu_service.service.IssueDuePlanWorker
|
import space.nstart.pcp_tgu_service.service.IssueDuePlanWorker
|
||||||
import space.nstart.pcp_tgu_service.service.PlanProjectionService
|
import space.nstart.pcp_tgu_service.service.PlanProjectionService
|
||||||
import space.nstart.pcp_tgu_service.service.PlanQueryService
|
import space.nstart.pcp_tgu_service.service.PlanQueryService
|
||||||
@@ -32,6 +33,7 @@ class PlanController(
|
|||||||
private val issueDuePlanWorker: IssueDuePlanWorker,
|
private val issueDuePlanWorker: IssueDuePlanWorker,
|
||||||
private val createDraftUseCase: CreateDraftUseCase,
|
private val createDraftUseCase: CreateDraftUseCase,
|
||||||
private val deleteDraftUseCase: DeleteDraftUseCase,
|
private val deleteDraftUseCase: DeleteDraftUseCase,
|
||||||
|
private val dismissPlanUseCase: DismissPlanUseCase,
|
||||||
private val createChainPlanUseCase: CreateChainPlanUseCase,
|
private val createChainPlanUseCase: CreateChainPlanUseCase,
|
||||||
private val planProjectionService: PlanProjectionService
|
private val planProjectionService: PlanProjectionService
|
||||||
) {
|
) {
|
||||||
@@ -118,4 +120,22 @@ class PlanController(
|
|||||||
deleteDraftUseCase.deleteDraft(draftPlanId)
|
deleteDraftUseCase.deleteDraft(draftPlanId)
|
||||||
return ResponseEntity.noContent().build()
|
return ResponseEntity.noContent().build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* «Снять с учёта» терминально-негативный план: проставляет `dismissedAt`, чтобы он перестал
|
||||||
|
* считаться проблемой и красить индикатор КА. Статус и цепочку не трогает.
|
||||||
|
* 404 — плана нет; 422 — статус не терминально-негативный.
|
||||||
|
*/
|
||||||
|
@PostMapping("/{planId}/dismiss")
|
||||||
|
fun dismissPlan(@PathVariable planId: UUID): ResponseEntity<Void> {
|
||||||
|
dismissPlanUseCase.dismiss(planId)
|
||||||
|
return ResponseEntity.noContent().build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** «Вернуть на учёт»: снимает `dismissedAt`. 404 — плана нет. */
|
||||||
|
@PostMapping("/{planId}/restore")
|
||||||
|
fun restorePlan(@PathVariable planId: UUID): ResponseEntity<Void> {
|
||||||
|
dismissPlanUseCase.restore(planId)
|
||||||
|
return ResponseEntity.noContent().build()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-2
@@ -45,7 +45,13 @@ data class PlanResponse(
|
|||||||
* У проекции предшественник не хранится, но цепочку она продолжает — фронт связывает её
|
* У проекции предшественник не хранится, но цепочку она продолжает — фронт связывает её
|
||||||
* соседние звенья по времени, поэтому начало цепочки определяется ТОЛЬКО для persisted.
|
* соседние звенья по времени, поэтому начало цепочки определяется ТОЛЬКО для persisted.
|
||||||
*/
|
*/
|
||||||
val projected: Boolean = false
|
val projected: Boolean = false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Время «снятия с учёта» (деактивации) терминально-негативного плана оператором. `null` — план
|
||||||
|
* на учёте. Фронт по этому полю гасит план и исключает его из индикатора проблем КА.
|
||||||
|
*/
|
||||||
|
val dismissedAt: LocalDateTime? = null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -62,7 +68,8 @@ data class PlanResponse(
|
|||||||
kppId = plan.kppId,
|
kppId = plan.kppId,
|
||||||
status = plan.status,
|
status = plan.status,
|
||||||
basePlanId = plan.basePlanId,
|
basePlanId = plan.basePlanId,
|
||||||
projected = false
|
projected = false,
|
||||||
|
dismissedAt = plan.dismissedAt
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+8
@@ -47,6 +47,14 @@ class PlannedPlanEntity(
|
|||||||
@Column(name = "accepted_at")
|
@Column(name = "accepted_at")
|
||||||
var acceptedAt: LocalDateTime? = null,
|
var acceptedAt: LocalDateTime? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* «Снято с учёта»: время деактивации терминально-негативного плана оператором. NULL — план на
|
||||||
|
* учёте. Косметико-статистический флаг ([space.nstart.pcp_tgu_service.service.DismissPlanUseCase]):
|
||||||
|
* не меняет статус и состояние цепочки, обратим.
|
||||||
|
*/
|
||||||
|
@Column(name = "dismissed_at")
|
||||||
|
var dismissedAt: LocalDateTime? = null,
|
||||||
|
|
||||||
@Column(name = "created_at", nullable = false)
|
@Column(name = "created_at", nullable = false)
|
||||||
var createdAt: LocalDateTime = LocalDateTime.now(),
|
var createdAt: LocalDateTime = LocalDateTime.now(),
|
||||||
|
|
||||||
|
|||||||
+77
@@ -0,0 +1,77 @@
|
|||||||
|
package space.nstart.pcp_tgu_service.service
|
||||||
|
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.http.HttpStatus
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
import org.springframework.web.server.ResponseStatusException
|
||||||
|
import space.nstart.pcp_tgu_service.entity.PlannedPlanStatus
|
||||||
|
import space.nstart.pcp_tgu_service.repository.PlannedPlanRepository
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* «Снять с учёта» / «Вернуть на учёт» терминально-негативный план.
|
||||||
|
*
|
||||||
|
* Оператор деактивирует отменённый/неудавшийся план, чтобы он перестал считаться проблемой и
|
||||||
|
* красить индикатор КА. Это чисто косметико-статистический флаг (`dismissed_at`): статус плана
|
||||||
|
* и состояние цепочки ([SpacecraftPlanningStateEntity]) НЕ трогаются — план остаётся в истории
|
||||||
|
* со своим терминальным статусом, цепочка не возобновляется и не паузится. Операция обратима
|
||||||
|
* ([restore]).
|
||||||
|
*
|
||||||
|
* Снимать с учёта разрешено только терминально-негативные статусы ([DISMISSIBLE_STATUSES]). Живые
|
||||||
|
* выдачи, успешный LAID_IN, DRAFT и виртуальный PLANNED этот путь не трогает (422) — у них «снятие
|
||||||
|
* с учёта» не имеет смысла.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
class DismissPlanUseCase(
|
||||||
|
private val plannedPlanRepository: PlannedPlanRepository
|
||||||
|
) {
|
||||||
|
private val log = LoggerFactory.getLogger(javaClass)
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
fun dismiss(planId: UUID) {
|
||||||
|
val plan = plannedPlanRepository.findById(planId)
|
||||||
|
.orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Plan $planId not found") }
|
||||||
|
if (plan.status !in DISMISSIBLE_STATUSES) {
|
||||||
|
throw ResponseStatusException(
|
||||||
|
HttpStatus.UNPROCESSABLE_ENTITY,
|
||||||
|
"План $planId в статусе ${plan.status} нельзя снять с учёта — только терминально-негативные " +
|
||||||
|
"($DISMISSIBLE_STATUSES)"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (plan.dismissedAt != null) {
|
||||||
|
log.info("План {} уже снят с учёта ({}) — идемпотентно", planId, plan.dismissedAt)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
plan.dismissedAt = LocalDateTime.now()
|
||||||
|
plan.updatedAt = LocalDateTime.now()
|
||||||
|
plannedPlanRepository.save(plan)
|
||||||
|
log.info("План {} (status={}) снят с учёта", planId, plan.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
fun restore(planId: UUID) {
|
||||||
|
val plan = plannedPlanRepository.findById(planId)
|
||||||
|
.orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Plan $planId not found") }
|
||||||
|
if (plan.dismissedAt == null) {
|
||||||
|
log.info("План {} уже на учёте — идемпотентно", planId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
plan.dismissedAt = null
|
||||||
|
plan.updatedAt = LocalDateTime.now()
|
||||||
|
plannedPlanRepository.save(plan)
|
||||||
|
log.info("План {} (status={}) возвращён на учёт", planId, plan.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
/** Терминально-негативные статусы, которые оператор может снять с учёта. */
|
||||||
|
val DISMISSIBLE_STATUSES = setOf(
|
||||||
|
PlannedPlanStatus.REJECTED,
|
||||||
|
PlannedPlanStatus.START_AMBIGUOUS,
|
||||||
|
PlannedPlanStatus.LAYIN_FAILED,
|
||||||
|
PlannedPlanStatus.NOT_ACCEPTED,
|
||||||
|
PlannedPlanStatus.SUPERSEDED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
-- «Снято с учёта»: оператор деактивирует терминально-негативный план (REJECTED, START_AMBIGUOUS,
|
||||||
|
-- LAYIN_FAILED, NOT_ACCEPTED, SUPERSEDED), чтобы он не считался проблемой и не красил индикатор КА.
|
||||||
|
-- Чисто косметико-статистический флаг: статус плана и состояние цепочки не трогает, обратим
|
||||||
|
-- (restore = dismissed_at -> NULL). NULL — план на учёте (как раньше).
|
||||||
|
ALTER TABLE planned_plan
|
||||||
|
ADD COLUMN IF NOT EXISTS dismissed_at TIMESTAMP NULL;
|
||||||
+126
@@ -0,0 +1,126 @@
|
|||||||
|
package space.nstart.pcp_tgu_service.service
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||||
|
import org.junit.jupiter.api.Assertions.assertNull
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.assertThrows
|
||||||
|
import org.mockito.ArgumentMatchers
|
||||||
|
import org.mockito.Mockito.doAnswer
|
||||||
|
import org.mockito.Mockito.mock
|
||||||
|
import org.mockito.Mockito.verify
|
||||||
|
import org.mockito.Mockito.`when`
|
||||||
|
import org.springframework.http.HttpStatus
|
||||||
|
import org.springframework.web.server.ResponseStatusException
|
||||||
|
import space.nstart.pcp_tgu_service.entity.PlannedPlanEntity
|
||||||
|
import space.nstart.pcp_tgu_service.entity.PlannedPlanStatus
|
||||||
|
import space.nstart.pcp_tgu_service.repository.PlannedPlanRepository
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.util.Optional
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
class DismissPlanUseCaseTest {
|
||||||
|
|
||||||
|
private val store = linkedMapOf<UUID, PlannedPlanEntity>()
|
||||||
|
private val repo = mock(PlannedPlanRepository::class.java).also { repo ->
|
||||||
|
`when`(repo.findById(anyUuid())).thenAnswer { inv -> Optional.ofNullable(store[inv.arguments[0]]) }
|
||||||
|
doAnswer { inv -> (inv.arguments[0] as PlannedPlanEntity).also { store[it.planId] = it } }
|
||||||
|
.`when`(repo).save(anyNonNull())
|
||||||
|
}
|
||||||
|
private val useCase = DismissPlanUseCase(repo)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `dismiss stamps dismissedAt on a REJECTED plan without changing status`() {
|
||||||
|
val plan = plan(PlannedPlanStatus.REJECTED)
|
||||||
|
store[plan.planId] = plan
|
||||||
|
|
||||||
|
useCase.dismiss(plan.planId)
|
||||||
|
|
||||||
|
assertNotNull(store[plan.planId]?.dismissedAt)
|
||||||
|
assertEquals(PlannedPlanStatus.REJECTED, store[plan.planId]?.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `dismiss is allowed for every terminal-negative status`() {
|
||||||
|
for (status in listOf(
|
||||||
|
PlannedPlanStatus.REJECTED,
|
||||||
|
PlannedPlanStatus.START_AMBIGUOUS,
|
||||||
|
PlannedPlanStatus.LAYIN_FAILED,
|
||||||
|
PlannedPlanStatus.NOT_ACCEPTED,
|
||||||
|
PlannedPlanStatus.SUPERSEDED
|
||||||
|
)) {
|
||||||
|
val plan = plan(status)
|
||||||
|
store[plan.planId] = plan
|
||||||
|
useCase.dismiss(plan.planId)
|
||||||
|
assertNotNull(store[plan.planId]?.dismissedAt, "expected $status to be dismissible")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `dismiss refuses a live or successful plan with 422`() {
|
||||||
|
for (status in listOf(
|
||||||
|
PlannedPlanStatus.DRAFT,
|
||||||
|
PlannedPlanStatus.ISSUING,
|
||||||
|
PlannedPlanStatus.WAITING_DECISION,
|
||||||
|
PlannedPlanStatus.AWAITING_LAYIN,
|
||||||
|
PlannedPlanStatus.LAID_IN
|
||||||
|
)) {
|
||||||
|
val plan = plan(status)
|
||||||
|
store[plan.planId] = plan
|
||||||
|
val ex = assertThrows<ResponseStatusException> { useCase.dismiss(plan.planId) }
|
||||||
|
assertEquals(HttpStatus.UNPROCESSABLE_ENTITY, ex.statusCode, "expected $status to be refused")
|
||||||
|
assertNull(store[plan.planId]?.dismissedAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `dismiss of an unknown plan yields 404`() {
|
||||||
|
val ex = assertThrows<ResponseStatusException> { useCase.dismiss(UUID.randomUUID()) }
|
||||||
|
assertEquals(HttpStatus.NOT_FOUND, ex.statusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `dismiss is idempotent — second call does not re-save`() {
|
||||||
|
val plan = plan(PlannedPlanStatus.REJECTED)
|
||||||
|
store[plan.planId] = plan
|
||||||
|
useCase.dismiss(plan.planId)
|
||||||
|
val firstStamp = store[plan.planId]?.dismissedAt
|
||||||
|
|
||||||
|
useCase.dismiss(plan.planId)
|
||||||
|
|
||||||
|
// повторный вызов не трогает запись (save вызван ровно один раз)
|
||||||
|
verify(repo, org.mockito.Mockito.times(1)).save(anyNonNull())
|
||||||
|
assertEquals(firstStamp, store[plan.planId]?.dismissedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `restore clears dismissedAt`() {
|
||||||
|
val plan = plan(PlannedPlanStatus.REJECTED).also { it.dismissedAt = LocalDateTime.now() }
|
||||||
|
store[plan.planId] = plan
|
||||||
|
|
||||||
|
useCase.restore(plan.planId)
|
||||||
|
|
||||||
|
assertNull(store[plan.planId]?.dismissedAt)
|
||||||
|
assertEquals(PlannedPlanStatus.REJECTED, store[plan.planId]?.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `restore of an unknown plan yields 404`() {
|
||||||
|
val ex = assertThrows<ResponseStatusException> { useCase.restore(UUID.randomUUID()) }
|
||||||
|
assertEquals(HttpStatus.NOT_FOUND, ex.statusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun plan(status: PlannedPlanStatus): PlannedPlanEntity =
|
||||||
|
PlannedPlanEntity(
|
||||||
|
planId = UUID.randomUUID(),
|
||||||
|
spacecraftId = "25544",
|
||||||
|
startTime = LocalDateTime.now().plusHours(3),
|
||||||
|
endTime = LocalDateTime.now().plusHours(4),
|
||||||
|
kppId = "KPP-1",
|
||||||
|
status = status
|
||||||
|
)
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
private fun <T> anyNonNull(): T { ArgumentMatchers.any<T>(); return null as T }
|
||||||
|
private fun anyUuid(): UUID { ArgumentMatchers.any<UUID>(); return UUID.randomUUID() }
|
||||||
|
}
|
||||||
+8
@@ -47,6 +47,14 @@ class TguPlanningRestController(
|
|||||||
@RequestParam(required = false) reason: String?
|
@RequestParam(required = false) reason: String?
|
||||||
) = tguPlanningService.sendDecision(planId, decision, reason)
|
) = 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). */
|
/** ТЕСТ: подтвердить закладку плана (AWAITING_LAYIN → LAID_IN). */
|
||||||
@PostMapping("/plans/{planId}/confirm-layin")
|
@PostMapping("/plans/{planId}/confirm-layin")
|
||||||
fun confirmLayin(
|
fun confirmLayin(
|
||||||
|
|||||||
+3
-1
@@ -30,7 +30,9 @@ data class TguPlanResponse(
|
|||||||
/** Предшественник в цепочке (`null` — старт без предыстории). Проксируется из pcp-tgu-service. */
|
/** Предшественник в цепочке (`null` — старт без предыстории). Проксируется из pcp-tgu-service. */
|
||||||
val basePlanId: UUID? = null,
|
val basePlanId: UUID? = null,
|
||||||
/** `true` — проекция будущего (не persisted). Проксируется из pcp-tgu-service. */
|
/** `true` — проекция будущего (не persisted). Проксируется из pcp-tgu-service. */
|
||||||
val projected: Boolean = false
|
val projected: Boolean = false,
|
||||||
|
/** Время «снятия с учёта» (`null` — план на учёте). Проксируется из pcp-tgu-service. */
|
||||||
|
val dismissedAt: LocalDateTime? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class TguPlanDecision {
|
enum class TguPlanDecision {
|
||||||
|
|||||||
+22
@@ -87,6 +87,28 @@ class TguPlanningService(webClientBuilderProvider: ObjectProvider<WebClient.Buil
|
|||||||
throw error
|
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?) {
|
fun confirmLayin(planId: UUID, reason: String?) {
|
||||||
runCatching {
|
runCatching {
|
||||||
webClientBuilder.build()
|
webClientBuilder.build()
|
||||||
|
|||||||
+1
-1
@@ -116,7 +116,7 @@ class TguPlanningServiceTest {
|
|||||||
exchange.requestURI.path == "/api/plans" || exchange.requestURI.path == "/api/plans/56756" -> respond(
|
exchange.requestURI.path == "/api/plans" || exchange.requestURI.path == "/api/plans/56756" -> respond(
|
||||||
exchange,
|
exchange,
|
||||||
200,
|
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.requestURI.path.startsWith("/test/tgu/plans/") -> respond(
|
||||||
exchange,
|
exchange,
|
||||||
|
|||||||
Reference in New Issue
Block a user