diff --git a/docs/tasks/TASK_tgu-plan-versioning-manual-reissue.md b/docs/tasks/TASK_tgu-plan-versioning-manual-reissue.md index a33f765..0a1e1fa 100644 --- a/docs/tasks/TASK_tgu-plan-versioning-manual-reissue.md +++ b/docs/tasks/TASK_tgu-plan-versioning-manual-reissue.md @@ -262,8 +262,13 @@ P2 = DRAFT (base_plan_id=P0, окно=W) - **Приёмка:** правка/удаление затрагивают только `DRAFT`; живая миссия неизменна; контур пересчитан корректно; каскады при `DELETE` (связь `drop_mode ↔ survey`) целы. - **Тесты:** PATCH меняет контур; DELETE с каскадами. -### Фаза 4 — Валидация-заглушка +### Фаза 4 — Валидация-заглушка — ✅ СДЕЛАНО **Модель: Sonnet.** + +> **Реализовано:** +> - `POST /api/missions/{id}/validate` в [MissionController](services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/controller/MissionController.kt) → [validateMission](services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/MissionPlaningService.kt): проверяет существование миссии (404, если нет) и возвращает корректный контракт с **пустым** списком конфликтов. Реальные правила — TODO раздела 6. +> - Контракт `MissionValidationResponse { missionId, conflicts: [ModeConflict] }`, где `ModeConflict { modeId, rule, severity, message, relatedModeId? }` ([MissionValidationDTOs.kt](services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/controller/MissionValidationDTOs.kt)). Коды правил (`ConflictRule`: TIME_OVERLAP/TARGET_VISIBILITY/COMMS_SESSION/DROP_ORDER/ONBOARD_LIMIT) и уровни (`ConflictSeverity`: error/warning) заведены сразу как стабильная схема — появление реальных правил не сломает фронт. Дифф по режимам (а не весь план). +> - Тесты: 2 кейса в `MissionEditModesTest` (валидация существующей миссии → пустой список; неизвестной → 404). Весь модуль зелёный. - `pcp-mission-planing-service`: `POST /api/missions/{id}/validate` → корректный контракт, пустой/тривиальный список конфликтов. Реальные правила — TODO раздела 6. - **Приёмка:** `200` с пустым diff; контракт стабилен для фронта. diff --git a/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/controller/MissionController.kt b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/controller/MissionController.kt index c550475..4f4adea 100644 --- a/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/controller/MissionController.kt +++ b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/controller/MissionController.kt @@ -252,6 +252,25 @@ class MissionController( return ResponseEntity.noContent().build() } + @Operation( + summary = "Проверить миссию-черновик на конфликты", + description = "Считает конфликты/наложения включений плана-черновика и возвращает дифф по " + + "режимам. MVP: валидатор-заглушка — контракт корректен, список конфликтов пуст; " + + "реальные правила (пересечения по времени, видимость цели, ЗРВ, порядок сброса, борт) — " + + "отдельной задачей." + ) + @ApiResponses( + value = [ + ApiResponse(responseCode = "200", description = "Валидация выполнена, конфликты в теле ответа"), + ApiResponse(responseCode = "404", description = "Миссия не найдена") + ] + ) + @PostMapping("/{mission_id}/validate") + fun validateMission( + @Parameter(description = "Идентификатор миссии-черновика") + @PathVariable("mission_id") id: UUID + ): MissionValidationResponse = missionPlaningService.validateMission(id) + @Operation(summary = "Создать миссию") diff --git a/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/controller/MissionValidationDTOs.kt b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/controller/MissionValidationDTOs.kt new file mode 100644 index 0000000..9ed7b6a --- /dev/null +++ b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/controller/MissionValidationDTOs.kt @@ -0,0 +1,55 @@ +package space.nstart.pcp.pcp_mission_planing_service.controller + +import java.util.UUID + +/** + * Коды правил валидации. MVP: валидатор-заглушка их **не вычисляет** — список существует как + * стабильный контракт для фронта, чтобы появление реальных правил (раздел 6 дизайн-дока) не ломало схему. + */ +object ConflictRule { + /** Пересечение по времени между включениями (съёмка↔съёмка, съёмка↔сброс, сброс↔сброс). */ + const val TIME_OVERLAP = "TIME_OVERLAP" + /** Окно съёмки вне окна видимости цели (баллистика). */ + const val TARGET_VISIBILITY = "TARGET_VISIBILITY" + /** Окно сброса вне зоны радиовидимости станции. */ + const val COMMS_SESSION = "COMMS_SESSION" + /** Сбрасываемая съёмка не завершается строго до начала зоны сброса. */ + const val DROP_ORDER = "DROP_ORDER" + /** Бортовые ограничения: длительность/скважность, энергетика, память. */ + const val ONBOARD_LIMIT = "ONBOARD_LIMIT" +} + +/** Уровень конфликта: [ERROR] блокирует «Применить», [WARNING] — информативный. */ +object ConflictSeverity { + const val ERROR = "error" + const val WARNING = "warning" +} + +/** + * Один конфликт на затронутом включении. Возвращается диффом по режимам (а не как весь план), + * чтобы фронт мог точечно подсвечивать проблемные включения. + * + * @param modeId id режима (survey или drop), к которому относится конфликт. + * @param rule код правила, см. [ConflictRule]. + * @param severity уровень, см. [ConflictSeverity]. + * @param message человекочитаемое описание для оператора. + * @param relatedModeId id второго участника парного правила (например, пересечения по времени), если есть. + */ +data class ModeConflict( + val modeId: Long, + val rule: String, + val severity: String, + val message: String, + val relatedModeId: Long? = null +) + +/** + * Результат валидации миссии-черновика. + * + * MVP: [conflicts] всегда пуст — реальные правила ещё не считаются (TODO раздела 6 дизайн-дока). + * Контракт стабилен: фронт уже завязывается на него и не сломается, когда правила появятся. + */ +data class MissionValidationResponse( + val missionId: UUID, + val conflicts: List +) diff --git a/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/MissionPlaningService.kt b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/MissionPlaningService.kt index a8eac66..eb4a246 100644 --- a/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/MissionPlaningService.kt +++ b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/MissionPlaningService.kt @@ -32,6 +32,7 @@ import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.SurveyRouteBuildResp import space.nstart.pcp.pcp_mission_planing_service.controller.CloneMode import space.nstart.pcp.pcp_mission_planing_service.controller.MissionCloneRequest import space.nstart.pcp.pcp_mission_planing_service.controller.MissionCloneResponse +import space.nstart.pcp.pcp_mission_planing_service.controller.MissionValidationResponse import space.nstart.pcp.pcp_mission_planing_service.entity.DropModeEntity import space.nstart.pcp.pcp_mission_planing_service.entity.DropModeSurveyEntity import space.nstart.pcp.pcp_mission_planing_service.entity.MissionEntity @@ -910,6 +911,21 @@ class MissionPlaningService( logger.info("Удалён сброс (drop_mode id={}), снято связей со съёмками={}", dropId, links.size) } + /** + * Валидация миссии-черновика на конфликты/наложения включений. + * + * MVP-заглушка: проверяет существование миссии (404, если нет) и возвращает корректный контракт + * с **пустым** списком конфликтов. Реальные правила (пересечения по времени, видимость цели, ЗРВ + * станции, порядок сброса, бортовые ограничения) — TODO раздела 6 дизайн-дока. Контракт + * ([MissionValidationResponse]/[ModeConflict]) стабилен, чтобы фронт не ломался при их появлении. + */ + fun validateMission(missionId: UUID): MissionValidationResponse { + if (!missionRepository.existsById(missionId)) { + throw notFound(missionId) + } + return MissionValidationResponse(missionId = missionId, conflicts = emptyList()) + } + /** * Строит контур полосы съёмки по параметрам видимости точки. * Чистый расчёт без персистенции; переиспользуется [buildSurveyRoute] и [saveSurveyRoute]. diff --git a/services/pcp-mission-planing-service/src/test/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/MissionEditModesTest.kt b/services/pcp-mission-planing-service/src/test/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/MissionEditModesTest.kt index 34b9916..f71d247 100644 --- a/services/pcp-mission-planing-service/src/test/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/MissionEditModesTest.kt +++ b/services/pcp-mission-planing-service/src/test/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/MissionEditModesTest.kt @@ -199,6 +199,25 @@ class MissionEditModesTest { assertEquals(HttpStatus.NOT_FOUND, ex.statusCode) } + // --- validate (заглушка) --- + + @Test + fun `validate of an existing mission returns an empty conflict list`() { + val missionId = UUID.randomUUID() + `when`(missionRepository.existsById(missionId)).thenReturn(true) + + val result = service.validateMission(missionId) + + assertEquals(missionId, result.missionId) + assertTrue(result.conflicts.isEmpty(), "MVP-заглушка не считает конфликтов") + } + + @Test + fun `validate of a missing mission yields 404`() { + val ex = assertThrows { service.validateMission(UUID.randomUUID()) } + assertEquals(HttpStatus.NOT_FOUND, ex.statusCode) + } + // --- helpers --- private fun survey(): SurveyModeEntity {