diff --git a/docs/standards/CHECKLIST.md b/docs/standards/CHECKLIST.md
index 88e0b19..4dff708 100644
--- a/docs/standards/CHECKLIST.md
+++ b/docs/standards/CHECKLIST.md
@@ -21,8 +21,8 @@
- существующие публичные контракты не сломаны без явного запроса;
- добавлены или обновлены релевантные тесты;
- не внесено несвязанных правок;
-- время на API — UTC; backend использует UTC-`now` (`Instant.now()` / `LocalDateTime.now(ZoneOffset.UTC)`), фронт парсит/форматирует через `utils/utcTime` (см. `docs/standards/DATETIME_UTC.md`);
-- чтение времени на бэке толерантно к обоим форматам (naive=UTC и `Z`) — модуль наследуется из `pcp-types-lib` (`PcpTimeModule`) авто-конфигурацией; ручной `ObjectMapper`/не-pcp-types-lib сервис регистрирует модуль явно;
+- время на API — naive `LocalDateTime` без зоны; backend штампует `now` через `LocalDateTime.now(ZoneOffset.UTC)`, фронт парсит/форматирует через `utils/utcTime` без `new Date("<строка>")`;
+- egress фронта к бэку — naive ISO (`YYYY-MM-DDTHH:mm:ss`, без Z);
- validation выполнен по правилам `AGENTS.md`, либо пропуски явно объяснены.
### Дополнительно для BPM / Kafka / Zeebe / external integration изменений
diff --git a/docs/standards/DATETIME_UTC.md b/docs/standards/DATETIME_UTC.md
deleted file mode 100644
index de1aaac..0000000
--- a/docs/standards/DATETIME_UTC.md
+++ /dev/null
@@ -1,182 +0,0 @@
-# DATETIME_UTC.md — единообразие времени (UTC) в группе сервисов `pcp`
-
-## Назначение
-
-Этот документ фиксирует соглашение по работе со временем во всей группе сервисов `pcp`
-и состояние сервисов на момент аудита. На него ссылаются KDoc у DTO и комментарии в коде.
-Цель — убрать неоднозначность парсинга/форматирования и неоднородность формата на проводе,
-которая уже приводила к сдвигу времени на офсет пояса (для МСК — +3 ч).
-
-## 1. Соглашение (canonical)
-
-1. **Всё время — UTC.** На любом API времена представляют момент в UTC. В БД — тоже UTC.
-2. **Формат на проводе — ISO-8601 с явной зоной `Z`** (предпочтительно). Где сервис исторически
- отдаёт naive `LocalDateTime` (ISO без зоны, напр. `"2026-05-29T01:00:00"`), это допустимо
- **только** при железном правиле «naive = UTC»; такой эндпоинт — кандидат на миграцию к `Z`.
-3. **Backend:**
- - «сейчас» — только UTC: `Instant.now()` или `LocalDateTime.now(ZoneOffset.UTC)`; **не** `LocalDateTime.now()`;
- - конвертация `Instant`/`OffsetDateTime` → `LocalDateTime` для naive-полей — только через UTC
- (`LocalDateTime.ofInstant(instant, ZoneOffset.UTC)`, `withOffsetSameInstant(ZoneOffset.UTC)`),
- **никогда** через `ZoneId.systemDefault()` / `atZone(systemDefault())`;
- - Jackson во всех сервисах настроен с `JavaTimeModule` + `disable(WRITE_DATES_AS_TIMESTAMPS)`,
- поэтому `LocalDateTime` уходит как ISO без зоны, а `OffsetDateTime`/`Instant` — с `Z`/офсетом.
-4. **Frontend (`pcp-tgu-ops-ui`):**
- - парсинг строк бэка — только через `src/utils/utcTime.ts` (`parseUtc`); никаких «голых»
- `new Date(string)` / `Date.parse(string)`;
- - отображение — только UTC (`formatUtcDateTime/Date/Time` либо `toLocale*` с `timeZone: "UTC"`);
- - `` — значение через `toUtcInputValue`, чтение через `parseUtc`,
- отправка на offset-бэк — через `toUtcIso`.
- - Инвариант закреплён тестом-стражем `src/utils/utcTime.guard.test.ts` (падает на запрещённых паттернах).
-5. **Никакой неявной локализации** под пояс машины ни на фронте, ни в логах/ответах бэка.
-
-## 2. Эталоны переиспользования
-
-- Frontend: `services/pcp-tgu-ops-ui/src/utils/utcTime.ts` — единая точка парсинга/форматирования.
-- Backend (чтение): `libs/pcp-types-lib` `utils/json/PcpTimeModule` — слой толерантности
- (бэковый аналог `parseUtc`): любой потребитель читает и naive(=UTC), и `Z`/офсет (см. раздел ниже).
-- Backend (запись): `pcp-request-service` отдаёт `OffsetDateTime` (строки с `Z`) и конвертирует вход
- через `OffsetDateTime.withOffsetSameInstant(ZoneOffset.UTC).toLocalDateTime()` — корректный образец.
-
-## 2bis. Слой толерантности времени (включатель безопасного флипа)
-
-`PcpTimeModule` (`libs/pcp-types-lib/.../utils/json/PcpTimeModule.kt`) — Jackson-модуль с
-толерантными **десериализаторами** для `LocalDateTime`, `OffsetDateTime`, `Instant`. Любой
-потребитель с ним читает время в **обоих** форматах на проводе и нормализует к UTC:
-
-| Тип поля | naive `…T01:00:00` | с зоной `…T01:00:00Z` / `+03:00` |
-|---|---|---|
-| `LocalDateTime` | как есть (= UTC) | приводится к UTC, зона отбрасывается |
-| `OffsetDateTime` | `atOffset(UTC)` | `withOffsetSameInstant(UTC)` |
-| `Instant` | трактуется как момент UTC | штатно |
-
-Модуль трогает **только чтение** — формат записи определяется типом поля и не меняется, поэтому
-подключение слоя само по себе не сдвигает ни один контракт.
-
-**Зачем.** Микросервисы деплоятся независимо. Раньше перевод продюсера `LocalDateTime` → `OffsetDateTime`
-(на проводе появляется `Z`) ломал ещё не обновлённого потребителя — штатный `LocalDateTimeDeserializer`
-кидает исключение на офсете. С толерантностью во всём флоте продюсера можно флипать **в любом порядке,
-без синхронного деплоя**. Это снимает требование «продюсер + все потребители одним коммитом».
-
-**Подключение (три механизма, чтобы покрыть все мапперы):**
-- Spring Boot auto-configuration `PcpTimeJacksonAutoConfiguration` (`@Bean Module`) +
- `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports` — Boot-managed
- `ObjectMapper` каждого из 12 сервисов, зависящих от pcp-types-lib (правок в сервисах не требуется).
-- Jackson SPI `META-INF/services/com.fasterxml.jackson.databind.Module` — ручные мапперы с
- `findAndRegisterModules()` (напр. `pcp-request-service`).
-- Точечная регистрация в мапперах, что обходят оба: `pcp-dynamic-plan-service/JacksonConfig`.
-
-**Особый случай — pcp-tgu-service.** Сервис мигрирован с WebFlux на **servlet/MVC** (см. ниже
-«Миграция tgu на MVC») и теперь зависит от pcp-types-lib, как остальной флот. Но из-за Boot 4 на
-HTTP-границе всё равно работает **Jackson 3**, поэтому время покрывается двумя путями:
-- Jackson-2 модуль **из либы** (`pcp_types_lib.utils.json.PcpTimeModule`) — явно зарегистрирован в
- ручном маппере `CamundaJacksonConfig` (Camunda/внутренний парсинг); своего зеркала больше нет.
-- `config/PcpTimeModuleV3.kt` (**Jackson 3**, `tools.jackson.*`) — REST HTTP message converters (MVC),
- которые Boot 4 авто-конфигурирует на Jackson 3 `JsonMapper`. Регистрируется бином
- `tools.jackson.databind.JacksonModule` (`RestJacksonConfig`); `JacksonAutoConfiguration` собирает
- все такие бины в общий `JsonMapper` (`builder.addModules(...)`). Лежит локально, т.к. либа даёт
- только Jackson-2 модуль; поведение обязано совпадать с библиотечным, правки синхронизировать.
-
-Контракт слоя закреплён `PcpTimeModuleTest` (pcp-types-lib): оба формата → один и тот же UTC-момент,
-плюс проверка авто-подхвата через SPI.
-
-## 3. Состояние сервисов (срез аудита)
-
-Формат на проводе определяется Jackson-сериализацией: `LocalDateTime` → ISO без зоны (naive, = UTC),
-`OffsetDateTime`/`Instant` → ISO с `Z`. «Потребители» — кто читает время этого сервиса.
-
-| Сервис | Преобладает на границе API | Naive? | Вердикт |
-|---|---|---|---|
-| pcp-request-service | `OffsetDateTime` (`Z`) | нет | **эталон**: явная зона на проводе |
-| pcp-tgu-service | REST `PlanResponse` — `OffsetDateTime` (`Z`); Kafka/прочие DTO — `LocalDateTime` | частично | `PlanResponse` **мигрирован на `Z`**; баги конвертаций исправлены; Kafka-DTO остаются naive (=UTC) |
-| pcp-ui-service | `LocalDateTime` (агрегатор/CZML, summaries); план ТГУ — `OffsetDateTime` (`Z`) | частично | `TguPlanResponse` **мигрирован на `Z`** вслед за tgu; прочие naive DTO — задокументировать |
-| pcp-complex-mission-service | `LocalDateTime` (+ часть `Instant`) | смешано | проверить смешение; задокументировать |
-| pcp-dynamic-plan-service | `LocalDateTime` (+ offset на входе клиента) | да | задокументировать «=UTC» |
-| pcp-ballistics-service | `LocalDateTime` | да | задокументировать «=UTC» |
-| pcp-mission-planing-service | `LocalDateTime` (вход — `OffsetDateTime`) | да | задокументировать «=UTC» |
-| slots-service | `LocalDateTime` | да | задокументировать «=UTC»; проверить `rs.getTimestamp().toLocalDateTime()` |
-| pcp-coverage-scheme-service | `LocalDateTime` | да | задокументировать «=UTC» |
-| pcp-route-processing-service | нет времени на границе API | — | — |
-| pcp-satellite-catalog-service | нет времени на границе API | — | — |
-| pcp-stations-service | нет времени на границе API | — | — |
-| tle-monitoring-service | нет времени на границе API | — | — |
-
-## 4. Известные расхождения и под-задачи (миграция к `Z` — не «большим взрывом»)
-
-Систематический класс: `LocalDateTime.now()` без UTC-зоны (~80 мест: entity-дефолты `createdAt/updatedAt`,
-воркеры, use case'ы). На не-UTC JVM пишет локальное «сейчас» в UTC-поля. Низкий риск для audit-колонок,
-но формально неверно. Рекомендованные под-задачи по сервисам:
-
-- **pcp-tgu-service** — заменить оставшиеся `LocalDateTime.now()` (воркеры выдачи/решений, `PlanQueryService`
- cutoff, entity-дефолты) на `LocalDateTime.now(ZoneOffset.UTC)`; тест на cutoff истории.
-- **slots-service** — проверить `SlotRepositoryImpl.rs.getTimestamp("tn").toLocalDateTime()` (зависит от
- зоны драйвера); зафиксировать чтение в UTC.
-- **pcp-mission-planing-service / pcp-dynamic-plan-service** — `OffsetDateTime.toLocalDateTime()` теряет
- офсет; убедиться, что вход всегда UTC, иначе конвертировать через `withOffsetSameInstant(UTC)`.
-- **Все сервисы с naive `LocalDateTime` на границе** — добавить сериализационный тест, фиксирующий формат
- на проводе (есть `Z` / нет зоны), затем по согласованию мигрировать DTO к `OffsetDateTime`/`Instant` (UTC).
- Фронтовый `parseUtc` уже совместим с обоими форматами, поэтому миграция безопасна при пер-сервисном раскате.
-
-### Контрактные тесты формата (зафиксировано)
-
-Оба формата на проводе закреплены сериализационными тестами (mapper зеркалит конфиг Spring Boot —
-JavaTimeModule + `disable(WRITE_DATES_AS_TIMESTAMPS)`):
-
-- `PlanResponseContractTest` (pcp-tgu-service) — `OffsetDateTime` с `Z` (после миграции, см. ниже).
-- `RequestResponseDtoContractTest` (pcp-request-service) — `OffsetDateTime` с `Z` (эталон).
-- `TguPlanningDtoContractTest` (pcp-ui-service) — план ТГУ `OffsetDateTime` с `Z` + `Instant` с `Z`.
-
-Прочие сервисы с naive-временем (ballistics, complex-mission, dynamic-plan, slots, coverage-scheme,
-mission-planing) используют тот же дефолтный Jackson-конфиг → тот же naive-формат; при желании
-пер-сервисный страж копируется по образцу `RequestResponseDtoContractTest`.
-
-### Миграция к `Z` — выполнено
-
-- **План ТГУ (tgu → ui-service → фронт).** `PlanResponse` (pcp-tgu-service) и зависящий от него
- `TguPlanResponse` (pcp-ui-service) переведены с naive `LocalDateTime` на `OffsetDateTime` в UTC →
- на проводе `Z`. Домен/БД (`PlannedPlanEntity`/`CalculatedPlan`) остались naive `LocalDateTime` (=UTC);
- конвертация только на границе (`atOffset(ZoneOffset.UTC)`). Потребитель один (ui-service), обновлён
- синхронно; фронт `parseUtc` совместим. Контракт закреплён тестами выше.
-
-- **Слой толерантности (`PcpTimeModule`) — раскатан по флоту.** 12 сервисов на pcp-types-lib
- получают его авто-конфигурацией/SPI; dynamic-plan — точечно; tgu — модулем из либы (Camunda,
- Jackson 2) плюс локальное Jackson-3 зеркало для REST-границы MVC. Это включатель: дальнейший флип
- продюсеров на `Z` больше не требует синхронного деплоя.
-
-После толерантности продюсеров можно флипать **в любом порядке**. Кандидаты: остальные naive-DTO
-ui-service, ballistics, complex-mission, dynamic-plan, slots, coverage-scheme.
-
-### Исправленные баги формата
-
-- **`KafkaMessage.time` (общий Kafka-конверт, pcp-types-lib) — исправлено.** Было `time = LocalDateTime.now()`
- (локальное!) + сериализация `LocalDateTimeTimeZoneSerializer` через `ZoneId.systemDefault()`: конверт
- нёс МСК-стенные часы с `+03:00`, а потребитель (формат `…SSS[xxx]` в `LocalDateTime`) отбрасывал зону
- → сдвиг на пояс. Стало `now(ZoneOffset.UTC)` + сериализация `atOffset(ZoneOffset.UTC)` → на проводе
- `…+00:00`. **Инстант не изменился** (тот же момент), но стенные часы теперь совпадают с UTC, поэтому
- потребитель, дропающий зону, получает корректное время. Закреплено `LocalDateTimeTimeZoneSerializerTest`.
-
-- **pcp-tgu-service REST-граница на Jackson 3** — теперь толерантна (см. 2bis). Добавлен Jackson-3
- вариант модуля `config/PcpTimeModuleV3.kt` и его регистрация бином `JacksonModule`
- (`config/RestJacksonConfig.kt`); входящий REST-JSON читается в обоих форматах (naive=UTC и `Z`).
- Закреплено `PcpTimeModuleV3Test`.
-
-### Миграция tgu на MVC
-
-- **pcp-tgu-service: WebFlux → servlet/MVC.** Сервис не имел реального реактивного кода — JPA
- блокирующая, контроллеры оборачивали блокирующие вызовы в `Mono.fromCallable{}.subscribeOn(
- boundedElastic())`, WebClient-вызовы `.block()`-ались сразу. Это был единственный WebFlux-only
- сервис флота. Переведён на MVC: подключён pcp-types-lib (тянет `starter-web` → тип приложения
- servlet), `springdoc` → `webmvc-ui`, контроллеры возвращают значения напрямую (без `Mono`).
- `starter-webflux` оставлен только ради WebClient. Побочный эффект для времени: убрано локальное
- Jackson-2 зеркало (`config/PcpTimeModule.kt`) в пользу модуля из либы; Jackson-3 зеркало остаётся
- (Boot 4 держит Jackson 3 на HTTP-границе и для MVC).
-
-### Остаток (под-задачи)
-
-- Обе Jackson-среды tgu толерантны; отдельных остатков по чтению времени нет. При миграции
- оставшихся продьюсеров на `Z` (раздел 4) добавлять сериализационные тесты контракта.
-
-## 5. Правило для нового кода
-
-- Frontend: только `utils/utcTime` (страж `utcTime.guard.test.ts` не даст просочиться нарушителю).
-- Backend: UTC-`now`; на новых API предпочтителен явный `Z` (`OffsetDateTime`/`Instant`).
-- Меняешь формат на проводе — обнови потребителей и добавь сериализационный тест (контракт не должен «уехать» молча).
diff --git a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/configuration/PcpTimeJacksonAutoConfiguration.kt b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/configuration/PcpTimeJacksonAutoConfiguration.kt
deleted file mode 100644
index 6d1ae8f..0000000
--- a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/configuration/PcpTimeJacksonAutoConfiguration.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-package space.nstart.pcp.pcp_types_lib.configuration
-
-import com.fasterxml.jackson.databind.Module
-import org.springframework.boot.autoconfigure.AutoConfiguration
-import org.springframework.context.annotation.Bean
-import space.nstart.pcp.pcp_types_lib.utils.json.PcpTimeModule
-
-/**
- * Регистрирует [PcpTimeModule] в Boot-managed `ObjectMapper` каждого сервиса, который подключает
- * pcp-types-lib. Spring Boot собирает все бины типа [Module] из контекста в авто-конфигурируемый
- * маппер, поэтому отдельный `@Bean` доезжает до всех сервисов без явного component-scan пакета либы.
- *
- * Ручные мапперы (вызывающие `findAndRegisterModules()`) подхватывают модуль через Jackson SPI —
- * см. META-INF/services/com.fasterxml.jackson.databind.Module.
- *
- * Слой толерантности — см. [PcpTimeModule] и docs/standards/DATETIME_UTC.md.
- */
-@AutoConfiguration
-class PcpTimeJacksonAutoConfiguration {
-
- @Bean
- fun pcpTimeModule(): Module = PcpTimeModule()
-}
diff --git a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/mission/ModeResponseDTO.kt b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/mission/ModeResponseDTO.kt
index a14befc..9ca3e7f 100644
--- a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/mission/ModeResponseDTO.kt
+++ b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/mission/ModeResponseDTO.kt
@@ -3,7 +3,7 @@ package space.nstart.pcp.pcp_types_lib.dto.satellite.mission
import com.fasterxml.jackson.annotation.JsonSubTypes
import com.fasterxml.jackson.annotation.JsonTypeInfo
import io.swagger.v3.oas.annotations.media.Schema
-import java.time.OffsetDateTime
+import java.time.LocalDateTime
@Schema(
description = "Базовая модель режима",
@@ -28,7 +28,7 @@ sealed interface ModeResponseDTO {
val planId: Long
@get:Schema(description = "Время начала режима")
- val timeStart: OffsetDateTime?
+ val timeStart: LocalDateTime?
@get:Schema(description = "Номер витка")
val revolution: Long
@@ -41,7 +41,7 @@ sealed interface ModeResponseDTO {
data class SurveyModeResponseDTO(
override val id: Long = 0,
override val planId: Long = 0,
- override val timeStart: OffsetDateTime? = null,
+ override val timeStart: LocalDateTime? = null,
override val revolution: Long = 0,
override val type: String = "SURVEY",
val status: String = "",
@@ -60,7 +60,7 @@ data class SurveyModeResponseDTO(
data class DropModeResponseDTO(
override val id: Long = 0,
override val planId: Long = 0,
- override val timeStart: OffsetDateTime? = null,
+ override val timeStart: LocalDateTime? = null,
override val revolution: Long = 0,
override val type: String = "DROP",
val station: String? = "",
diff --git a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/message/KafkaMessage.kt b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/message/KafkaMessage.kt
index 55a7589..d2349a2 100644
--- a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/message/KafkaMessage.kt
+++ b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/message/KafkaMessage.kt
@@ -2,8 +2,6 @@ package space.nstart.pcp.pcp_types_lib.message
import com.fasterxml.jackson.annotation.JsonFormat
import com.fasterxml.jackson.annotation.JsonAlias
-import com.fasterxml.jackson.databind.annotation.JsonSerialize
-import space.nstart.pcp.pcp_types_lib.utils.json.LocalDateTimeTimeZoneSerializer
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.*
@@ -21,11 +19,9 @@ class KafkaMessage (
val id = UUID.randomUUID().toString()
var source = "Example"
- // Время конверта в UTC: naive LocalDateTime трактуется как UTC (см. docs/standards/DATETIME_UTC.md),
- // сериализатор помечает его офсетом +00:00. Раньше now() + systemDefault давали локальные часы и
- // несовпадение момента у потребителей, дропающих зону.
- @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS[xxx]")
- @JsonSerialize(using = LocalDateTimeTimeZoneSerializer::class)
+ // Время конверта — naive LocalDateTime, штампуется по настенным часам UTC (now(ZoneOffset.UTC)),
+ // сериализуется как ISO без зоны.
+ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS")
val time: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC)
val dataContentType = "application/json"
diff --git a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/utils/json/LocalDateTimeTimeZoneSerializer.kt b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/utils/json/LocalDateTimeTimeZoneSerializer.kt
deleted file mode 100755
index 65b5373..0000000
--- a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/utils/json/LocalDateTimeTimeZoneSerializer.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-package space.nstart.pcp.pcp_types_lib.utils.json
-
-import com.fasterxml.jackson.core.JsonGenerator
-import com.fasterxml.jackson.databind.JsonSerializer
-import com.fasterxml.jackson.databind.SerializerProvider
-import java.time.LocalDateTime
-import java.time.ZoneOffset
-import java.time.format.DateTimeFormatter
-
-/**
- * Сериализует naive [LocalDateTime] как момент в **UTC** (офсет `+00:00`).
- * По соглашению группы `pcp` naive LocalDateTime = UTC (см. docs/standards/DATETIME_UTC.md),
- * поэтому зона навешивается через [ZoneOffset.UTC], а не через пояс машины.
- * Прежняя реализация (`ZoneId.systemDefault()`) помечала те же часы локальным офсетом, из-за чего
- * потребитель, дропающий зону, получал сдвиг на пояс.
- */
-class LocalDateTimeTimeZoneSerializer: JsonSerializer() {
-
- companion object {
- val fmt: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSxxx")
- }
-
- override fun serialize(p0: LocalDateTime?, p1: JsonGenerator, p2: SerializerProvider) {
- p1.writeString(p0?.atOffset(ZoneOffset.UTC)?.format(fmt))
- }
-}
\ No newline at end of file
diff --git a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/utils/json/PcpTimeModule.kt b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/utils/json/PcpTimeModule.kt
deleted file mode 100644
index 3ad19eb..0000000
--- a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/utils/json/PcpTimeModule.kt
+++ /dev/null
@@ -1,124 +0,0 @@
-package space.nstart.pcp.pcp_types_lib.utils.json
-
-import com.fasterxml.jackson.core.JsonParser
-import com.fasterxml.jackson.core.JsonToken
-import com.fasterxml.jackson.databind.DeserializationContext
-import com.fasterxml.jackson.databind.JsonDeserializer
-import com.fasterxml.jackson.databind.module.SimpleModule
-import com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer
-import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer
-import java.time.Instant
-import java.time.LocalDateTime
-import java.time.OffsetDateTime
-import java.time.ZoneOffset
-import java.time.format.DateTimeParseException
-
-/**
- * Слой толерантности времени для всей группы `pcp` (бэковый аналог фронтового `parseUtc`).
- *
- * Любой потребитель с этим модулем читает время в **обоих** форматах на проводе:
- * naive ISO без зоны (исторический формат, трактуется как UTC по соглашению
- * docs/standards/DATETIME_UTC.md) и ISO с явной зоной/`Z` (формат после миграции).
- * Все значения нормализуются к UTC.
- *
- * Зачем: микросервисы деплоятся независимо. Если включить толерантность во всём флоте,
- * продюсер можно перевести `LocalDateTime` → `OffsetDateTime` (на проводе появляется `Z`)
- * в любом порядке — потребитель не упадёт на парсинге офсета. Без этого слоя миграция
- * формата требовала бы синхронного деплоя продюсера и всех потребителей.
- *
- * Модуль трогает **только десериализацию** (чтение). Формат записи определяется типом поля
- * (`LocalDateTime` → naive, `OffsetDateTime`/`Instant` → `Z`) и здесь не меняется, чтобы
- * подключение слоя само по себе не сдвигало ни один контракт.
- *
- * Подключается через Spring Boot auto-configuration ([PcpTimeJacksonAutoConfiguration]) для
- * Boot-managed `ObjectMapper` и через Jackson SPI
- * (`META-INF/services/com.fasterxml.jackson.databind.Module`) для ручных мапперов,
- * вызывающих `findAndRegisterModules()`.
- */
-class PcpTimeModule : SimpleModule(NAME) {
-
- init {
- addDeserializer(LocalDateTime::class.java, TolerantLocalDateTimeDeserializer())
- addDeserializer(OffsetDateTime::class.java, TolerantOffsetDateTimeDeserializer())
- addDeserializer(Instant::class.java, TolerantInstantDeserializer())
- }
-
- companion object {
- const val NAME = "PcpTimeModule"
- }
-}
-
-/**
- * Читает [LocalDateTime] из обоих форматов: строка с зоной/`Z` нормализуется к UTC и теряет зону,
- * naive-строка парсится как есть (= UTC). Нестроковые токены (массивы/таймстемпы) делегируются
- * штатному [LocalDateTimeDeserializer], чтобы не сломать прочие представления.
- */
-class TolerantLocalDateTimeDeserializer : JsonDeserializer() {
-
- private val default = LocalDateTimeDeserializer.INSTANCE
-
- override fun deserialize(p: JsonParser, ctxt: DeserializationContext): LocalDateTime? {
- if (p.currentToken() == JsonToken.VALUE_STRING) {
- val text = p.text.trim()
- if (text.isEmpty()) return null
- tryParseToUtc(text)?.let { return it.toLocalDateTime() }
- }
- return default.deserialize(p, ctxt)
- }
-
- /** Возвращает момент в UTC, если строка несёт зону/офсет; иначе null (строка naive). */
- private fun tryParseToUtc(text: String): OffsetDateTime? =
- try {
- OffsetDateTime.parse(text).withOffsetSameInstant(ZoneOffset.UTC)
- } catch (_: DateTimeParseException) {
- null
- }
-}
-
-/**
- * Читает [OffsetDateTime] из обоих форматов: naive-строка получает зону UTC,
- * строка с зоной приводится к UTC.
- */
-class TolerantOffsetDateTimeDeserializer : JsonDeserializer() {
-
- @Suppress("UNCHECKED_CAST")
- private val default = InstantDeserializer.OFFSET_DATE_TIME as JsonDeserializer
-
- override fun deserialize(p: JsonParser, ctxt: DeserializationContext): OffsetDateTime? {
- if (p.currentToken() == JsonToken.VALUE_STRING) {
- val text = p.text.trim()
- if (text.isEmpty()) return null
- try {
- return OffsetDateTime.parse(text).withOffsetSameInstant(ZoneOffset.UTC)
- } catch (_: DateTimeParseException) {
- // naive-строка без зоны — трактуем как UTC
- return LocalDateTime.parse(text).atOffset(ZoneOffset.UTC)
- }
- }
- return default.deserialize(p, ctxt)
- }
-}
-
-/**
- * Читает [Instant] из обоих форматов: строка с `Z`/офсетом — штатно, naive-строка трактуется
- * как момент в UTC.
- */
-class TolerantInstantDeserializer : JsonDeserializer() {
-
- @Suppress("UNCHECKED_CAST")
- private val default = InstantDeserializer.INSTANT as JsonDeserializer
-
- override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Instant? {
- if (p.currentToken() == JsonToken.VALUE_STRING) {
- val text = p.text.trim()
- if (text.isEmpty()) return null
- try {
- return OffsetDateTime.parse(text).toInstant()
- } catch (_: DateTimeParseException) {
- // naive-строка без зоны — трактуем как UTC
- return LocalDateTime.parse(text).toInstant(ZoneOffset.UTC)
- }
- }
- return default.deserialize(p, ctxt)
- }
-}
diff --git a/libs/pcp-types-lib/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module b/libs/pcp-types-lib/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module
deleted file mode 100644
index 6e96a8a..0000000
--- a/libs/pcp-types-lib/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module
+++ /dev/null
@@ -1 +0,0 @@
-space.nstart.pcp.pcp_types_lib.utils.json.PcpTimeModule
diff --git a/libs/pcp-types-lib/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/libs/pcp-types-lib/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
deleted file mode 100644
index 32f1b41..0000000
--- a/libs/pcp-types-lib/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++ /dev/null
@@ -1 +0,0 @@
-space.nstart.pcp.pcp_types_lib.configuration.PcpTimeJacksonAutoConfiguration
diff --git a/libs/pcp-types-lib/src/test/kotlin/space/nstart/pcp/pcp_types_lib/utils/json/LocalDateTimeTimeZoneSerializerTest.kt b/libs/pcp-types-lib/src/test/kotlin/space/nstart/pcp/pcp_types_lib/utils/json/LocalDateTimeTimeZoneSerializerTest.kt
deleted file mode 100644
index 69de0d6..0000000
--- a/libs/pcp-types-lib/src/test/kotlin/space/nstart/pcp/pcp_types_lib/utils/json/LocalDateTimeTimeZoneSerializerTest.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-package space.nstart.pcp.pcp_types_lib.utils.json
-
-import com.fasterxml.jackson.annotation.JsonFormat
-import com.fasterxml.jackson.databind.annotation.JsonSerialize
-import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
-import com.fasterxml.jackson.module.kotlin.readValue
-import org.junit.jupiter.api.Assertions.assertEquals
-import org.junit.jupiter.api.Assertions.assertTrue
-import org.junit.jupiter.api.Test
-import java.time.LocalDateTime
-
-/**
- * Контракт сериализатора времени Kafka-конверта ([LocalDateTimeTimeZoneSerializer]):
- * naive LocalDateTime помечается офсетом UTC (`+00:00`), часы не сдвигаются.
- * Закрывает баг прежней реализации через `ZoneId.systemDefault()`
- * (см. docs/standards/DATETIME_UTC.md).
- */
-class LocalDateTimeTimeZoneSerializerTest {
-
- private val mapper = jacksonObjectMapper().findAndRegisterModules()
-
- // Зеркалит аннотации реального поля KafkaMessage.time (паттерн с опциональным офсетом).
- private data class Holder(
- @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS[xxx]")
- @JsonSerialize(using = LocalDateTimeTimeZoneSerializer::class)
- val t: LocalDateTime
- )
-
- @Test
- fun `serializes naive LocalDateTime with UTC offset, no wall-clock shift`() {
- val json = mapper.writeValueAsString(Holder(LocalDateTime.of(2026, 5, 29, 1, 0, 0)))
-
- assertTrue(
- json.contains("2026-05-29T01:00:00.000+00:00"),
- "ожидался UTC-офсет без сдвига часов, получено: $json"
- )
- }
-
- @Test
- fun `round-trips through the tolerance layer to the same UTC moment`() {
- val original = LocalDateTime.of(2026, 5, 29, 1, 0, 0)
-
- val json = mapper.writeValueAsString(Holder(original))
- val back: Holder = mapper.readValue(json)
-
- assertEquals(original, back.t)
- }
-}
diff --git a/libs/pcp-types-lib/src/test/kotlin/space/nstart/pcp/pcp_types_lib/utils/json/PcpTimeModuleTest.kt b/libs/pcp-types-lib/src/test/kotlin/space/nstart/pcp/pcp_types_lib/utils/json/PcpTimeModuleTest.kt
deleted file mode 100644
index c14c4fe..0000000
--- a/libs/pcp-types-lib/src/test/kotlin/space/nstart/pcp/pcp_types_lib/utils/json/PcpTimeModuleTest.kt
+++ /dev/null
@@ -1,81 +0,0 @@
-package space.nstart.pcp.pcp_types_lib.utils.json
-
-import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
-import com.fasterxml.jackson.module.kotlin.readValue
-import org.junit.jupiter.api.Assertions.assertEquals
-import org.junit.jupiter.api.Test
-import org.junit.jupiter.api.assertDoesNotThrow
-import java.time.Instant
-import java.time.LocalDateTime
-import java.time.OffsetDateTime
-import java.time.ZoneOffset
-
-/**
- * Контракт слоя толерантности времени ([PcpTimeModule]): любой потребитель читает время
- * в обоих форматах на проводе (naive=UTC и `Z`/офсет), всё нормализуется к UTC.
- * Это и есть то, что делает безопасным пер-сервисный флип продюсера на `Z`
- * без синхронного деплоя (см. docs/standards/DATETIME_UTC.md).
- */
-class PcpTimeModuleTest {
-
- private val mapper = jacksonObjectMapper().registerModule(PcpTimeModule())
-
- private data class LdtHolder(val t: LocalDateTime)
- private data class OdtHolder(val t: OffsetDateTime)
- private data class InstantHolder(val t: Instant)
-
- @Test
- fun `LocalDateTime field reads naive and Z as the same UTC moment`() {
- val naive: LdtHolder = mapper.readValue("""{"t":"2026-05-29T01:00:00"}""")
- val zoned: LdtHolder = mapper.readValue("""{"t":"2026-05-29T01:00:00Z"}""")
-
- val expected = LocalDateTime.of(2026, 5, 29, 1, 0, 0)
- assertEquals(expected, naive.t)
- assertEquals(expected, zoned.t, "Z-строка должна дать тот же UTC-момент, что naive")
- }
-
- @Test
- fun `LocalDateTime field converts explicit offset to UTC`() {
- // +03:00 01:00 == 22:00 UTC предыдущего дня
- val moscow: LdtHolder = mapper.readValue("""{"t":"2026-05-29T01:00:00+03:00"}""")
-
- assertEquals(LocalDateTime.of(2026, 5, 28, 22, 0, 0), moscow.t)
- }
-
- @Test
- fun `LocalDateTime field accepts Z without throwing (the regression the layer prevents)`() {
- // Штатный LocalDateTimeDeserializer кинул бы исключение на офсете/Z.
- assertDoesNotThrow {
- mapper.readValue("""{"t":"2026-05-29T01:00:00Z"}""")
- }
- }
-
- @Test
- fun `OffsetDateTime field reads naive as UTC and normalizes offset to UTC`() {
- val naive: OdtHolder = mapper.readValue("""{"t":"2026-05-29T01:00:00"}""")
- val moscow: OdtHolder = mapper.readValue("""{"t":"2026-05-29T04:00:00+03:00"}""")
-
- assertEquals(OffsetDateTime.of(2026, 5, 29, 1, 0, 0, 0, ZoneOffset.UTC), naive.t)
- assertEquals(OffsetDateTime.of(2026, 5, 29, 1, 0, 0, 0, ZoneOffset.UTC), moscow.t)
- }
-
- @Test
- fun `Instant field reads naive as UTC and Z as is`() {
- val naive: InstantHolder = mapper.readValue("""{"t":"2026-05-29T01:00:00"}""")
- val zoned: InstantHolder = mapper.readValue("""{"t":"2026-05-29T01:00:00Z"}""")
-
- assertEquals(Instant.parse("2026-05-29T01:00:00Z"), naive.t)
- assertEquals(Instant.parse("2026-05-29T01:00:00Z"), zoned.t)
- }
-
- @Test
- fun `module is auto-discovered via Jackson SPI (findAndRegisterModules)`() {
- // Подтверждает, что META-INF/services/com.fasterxml.jackson.databind.Module работает:
- // ручной маппер с findAndRegisterModules() получает толерантность без явной регистрации.
- val spiMapper = jacksonObjectMapper().findAndRegisterModules()
-
- val holder: LdtHolder = spiMapper.readValue("""{"t":"2026-05-29T01:00:00Z"}""")
-
- assertEquals(LocalDateTime.of(2026, 5, 29, 1, 0, 0), holder.t)
- }
-}
diff --git a/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/dto/ComplexPlanRunResponseDTO.kt b/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/dto/ComplexPlanRunResponseDTO.kt
index 56c5887..42ed9c9 100644
--- a/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/dto/ComplexPlanRunResponseDTO.kt
+++ b/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/dto/ComplexPlanRunResponseDTO.kt
@@ -2,7 +2,6 @@ package space.nstart.pcp.pcp_satellites_service.dto
import space.nstart.pcp.pcp_satellites_service.entity.ComplexPlanRunStatus
import java.time.LocalDateTime
-import java.time.OffsetDateTime
data class ComplexPlanRunResponseDTO(
val runId: Long,
@@ -11,9 +10,9 @@ data class ComplexPlanRunResponseDTO(
val intervalEnd: LocalDateTime,
val satelliteIds: List,
val requestedBy: String?,
- val createdAt: OffsetDateTime,
- val startedAt: OffsetDateTime?,
- val finishedAt: OffsetDateTime?,
+ val createdAt: LocalDateTime,
+ val startedAt: LocalDateTime?,
+ val finishedAt: LocalDateTime?,
val durationMs: Long?,
val errorMessage: String?,
val calculatedModesCount: Int,
diff --git a/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/dto/ComplexPlanRunSummaryDTO.kt b/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/dto/ComplexPlanRunSummaryDTO.kt
index d401132..aa5d252 100644
--- a/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/dto/ComplexPlanRunSummaryDTO.kt
+++ b/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/dto/ComplexPlanRunSummaryDTO.kt
@@ -2,7 +2,6 @@ package space.nstart.pcp.pcp_satellites_service.dto
import space.nstart.pcp.pcp_satellites_service.entity.ComplexPlanRunStatus
import java.time.LocalDateTime
-import java.time.OffsetDateTime
data class ComplexPlanRunSummaryDTO(
val runId: Long,
@@ -11,9 +10,9 @@ data class ComplexPlanRunSummaryDTO(
val intervalEnd: LocalDateTime,
val satelliteIds: List,
val requestedBy: String?,
- val createdAt: OffsetDateTime,
- val startedAt: OffsetDateTime?,
- val finishedAt: OffsetDateTime?,
+ val createdAt: LocalDateTime,
+ val startedAt: LocalDateTime?,
+ val finishedAt: LocalDateTime?,
val durationMs: Long?,
val errorMessage: String?,
val calculatedModesCount: Int,
diff --git a/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/service/ComplexPlanRunMapper.kt b/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/service/ComplexPlanRunMapper.kt
index 7d6bfd1..37d2c55 100644
--- a/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/service/ComplexPlanRunMapper.kt
+++ b/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/service/ComplexPlanRunMapper.kt
@@ -4,9 +4,6 @@ import org.springframework.stereotype.Component
import space.nstart.pcp.pcp_satellites_service.dto.ComplexPlanRunResponseDTO
import space.nstart.pcp.pcp_satellites_service.dto.ComplexPlanRunSummaryDTO
import space.nstart.pcp.pcp_satellites_service.entity.ComplexPlanRunEntity
-import java.time.LocalDateTime
-import java.time.OffsetDateTime
-import java.time.ZoneOffset
@Component
class ComplexPlanRunMapper {
@@ -19,9 +16,9 @@ class ComplexPlanRunMapper {
intervalEnd = entity.intervalEnd,
satelliteIds = satelliteIds(entity),
requestedBy = entity.requestedBy,
- createdAt = entity.createdAt.toUtcOffset(),
- startedAt = entity.startedAt?.toUtcOffset(),
- finishedAt = entity.finishedAt?.toUtcOffset(),
+ createdAt = entity.createdAt,
+ startedAt = entity.startedAt,
+ finishedAt = entity.finishedAt,
durationMs = entity.durationMs,
errorMessage = entity.errorMessage,
calculatedModesCount = entity.calculatedModesCount,
@@ -46,9 +43,9 @@ class ComplexPlanRunMapper {
intervalEnd = entity.intervalEnd,
satelliteIds = satelliteIds(entity),
requestedBy = entity.requestedBy,
- createdAt = entity.createdAt.toUtcOffset(),
- startedAt = entity.startedAt?.toUtcOffset(),
- finishedAt = entity.finishedAt?.toUtcOffset(),
+ createdAt = entity.createdAt,
+ startedAt = entity.startedAt,
+ finishedAt = entity.finishedAt,
durationMs = entity.durationMs,
errorMessage = entity.errorMessage,
calculatedModesCount = entity.calculatedModesCount,
@@ -75,8 +72,4 @@ class ComplexPlanRunMapper {
.mapNotNull { it.id }
.distinct()
.sorted()
-
- private fun LocalDateTime.toUtcOffset(): OffsetDateTime =
- atOffset(ZoneOffset.UTC)
-
}
diff --git a/services/pcp-complex-mission-service/src/test/kotlin/space/nstart/pcp/pcp_satellites_service/controller/ComplexPlanControllerTest.kt b/services/pcp-complex-mission-service/src/test/kotlin/space/nstart/pcp/pcp_satellites_service/controller/ComplexPlanControllerTest.kt
index 669a3db..b1b9915 100644
--- a/services/pcp-complex-mission-service/src/test/kotlin/space/nstart/pcp/pcp_satellites_service/controller/ComplexPlanControllerTest.kt
+++ b/services/pcp-complex-mission-service/src/test/kotlin/space/nstart/pcp/pcp_satellites_service/controller/ComplexPlanControllerTest.kt
@@ -50,9 +50,9 @@ class ComplexPlanControllerTest {
intervalEnd = LocalDateTime.of(2026, 3, 26, 12, 0),
satelliteIds = listOf(22L),
requestedBy = "tester",
- createdAt = LocalDateTime.of(2026, 3, 26, 9, 59).atOffset(ZoneOffset.UTC),
- startedAt = LocalDateTime.of(2026, 3, 26, 10, 0).atOffset(ZoneOffset.UTC),
- finishedAt = LocalDateTime.of(2026, 3, 26, 10, 1).atOffset(ZoneOffset.UTC),
+ createdAt = LocalDateTime.of(2026, 3, 26, 9, 59),
+ startedAt = LocalDateTime.of(2026, 3, 26, 10, 0),
+ finishedAt = LocalDateTime.of(2026, 3, 26, 10, 1),
durationMs = 60000,
errorMessage = null,
calculatedModesCount = 1,
diff --git a/services/pcp-complex-mission-service/src/test/kotlin/space/nstart/pcp/pcp_satellites_service/controller/ComplexPlanRunApiTest.kt b/services/pcp-complex-mission-service/src/test/kotlin/space/nstart/pcp/pcp_satellites_service/controller/ComplexPlanRunApiTest.kt
index 83a23c6..9fdbebc 100644
--- a/services/pcp-complex-mission-service/src/test/kotlin/space/nstart/pcp/pcp_satellites_service/controller/ComplexPlanRunApiTest.kt
+++ b/services/pcp-complex-mission-service/src/test/kotlin/space/nstart/pcp/pcp_satellites_service/controller/ComplexPlanRunApiTest.kt
@@ -22,9 +22,9 @@ import space.nstart.pcp.pcp_satellites_service.service.ComplexPlanRunPersistence
import space.nstart.pcp.pcp_satellites_service.service.SatelliteModePersistenceService
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.SatelliteModeResponseDTO
import java.time.LocalDateTime
-import java.time.ZoneOffset
import kotlin.test.assertEquals
import kotlin.test.assertFalse
+import kotlin.test.assertNotNull
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ComplexPlanRunApiTest {
@@ -81,7 +81,7 @@ class ComplexPlanRunApiTest {
assertEquals(10.0, response.totalRequestsArea)
assertEquals(4.0, response.coveredArea)
assertEquals(40.0, response.coveredAreaPercent)
- assertEquals(ZoneOffset.UTC, response.createdAt.offset)
+ assertNotNull(response.createdAt)
}
@Test
diff --git a/services/pcp-complex-mission-service/src/test/kotlin/space/nstart/pcp/pcp_satellites_service/service/ComplexPlanRunOrchestrationServiceTest.kt b/services/pcp-complex-mission-service/src/test/kotlin/space/nstart/pcp/pcp_satellites_service/service/ComplexPlanRunOrchestrationServiceTest.kt
index 7ad27df..0956d92 100644
--- a/services/pcp-complex-mission-service/src/test/kotlin/space/nstart/pcp/pcp_satellites_service/service/ComplexPlanRunOrchestrationServiceTest.kt
+++ b/services/pcp-complex-mission-service/src/test/kotlin/space/nstart/pcp/pcp_satellites_service/service/ComplexPlanRunOrchestrationServiceTest.kt
@@ -16,7 +16,6 @@ import space.nstart.pcp.pcp_types_lib.dto.requests.CoverageCalculationSource
import space.nstart.pcp.pcp_types_lib.dto.requests.ComplexPlanProcessRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageStrategy
import java.time.LocalDateTime
-import java.time.ZoneOffset
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
@@ -191,9 +190,9 @@ class ComplexPlanRunOrchestrationServiceTest {
intervalEnd = request.intervalEnd,
satelliteIds = listOf(22L),
requestedBy = "tester",
- createdAt = request.intervalStart.minusMinutes(1).atOffset(ZoneOffset.UTC),
- startedAt = request.intervalStart.atOffset(ZoneOffset.UTC),
- finishedAt = request.intervalStart.plusMinutes(1).atOffset(ZoneOffset.UTC),
+ createdAt = request.intervalStart.minusMinutes(1),
+ startedAt = request.intervalStart,
+ finishedAt = request.intervalStart.plusMinutes(1),
durationMs = 60_000,
errorMessage = null,
calculatedModesCount = 1,
@@ -301,9 +300,9 @@ class ComplexPlanRunOrchestrationServiceTest {
intervalEnd = request.intervalEnd,
satelliteIds = listOf(22L),
requestedBy = "tester",
- createdAt = request.intervalStart.minusMinutes(1).atOffset(ZoneOffset.UTC),
- startedAt = request.intervalStart.atOffset(ZoneOffset.UTC),
- finishedAt = request.intervalStart.plusMinutes(1).atOffset(ZoneOffset.UTC),
+ createdAt = request.intervalStart.minusMinutes(1),
+ startedAt = request.intervalStart,
+ finishedAt = request.intervalStart.plusMinutes(1),
durationMs = 60_000,
errorMessage = null,
calculatedModesCount = 0,
diff --git a/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/JacksonConfig.kt b/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/JacksonConfig.kt
index b7e6727..6c33e87 100644
--- a/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/JacksonConfig.kt
+++ b/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/JacksonConfig.kt
@@ -6,34 +6,15 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
-import space.nstart.pcp.pcp_types_lib.utils.json.PcpTimeModule
-
-
@Configuration
class JacksonConfig {
@Bean
fun objectMapper(): ObjectMapper {
val mapper = ObjectMapper()
-
- // Настраиваем формат для LocalDateTime
-// val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.zzz")
-
-// module.addSerializer(
-// LocalDateTime::class.java,
-// LocalDateTimeSerializer(formatter)
-// )
-// module.addDeserializer(
-// LocalDateTime::class.java,
-// LocalDateTimeDeserializer(formatter)
-// )
mapper.registerModule(JavaTimeModule())
.registerKotlinModule()
- // Слой толерантности времени (читать naive=UTC и Z) — см. docs/standards/DATETIME_UTC.md.
- // Этот маппер создаётся вручную и не подхватывает модуль ни через Boot-бин, ни через SPI.
- .registerModule(PcpTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
-
return mapper
}
}
diff --git a/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/apl/BasicPlanCalculator.kt b/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/apl/BasicPlanCalculator.kt
index 2bdcfff..78f7bff 100644
--- a/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/apl/BasicPlanCalculator.kt
+++ b/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/apl/BasicPlanCalculator.kt
@@ -48,6 +48,7 @@ import java.time.Duration
import java.time.Instant
import java.time.LocalDate
import java.time.LocalDateTime
+import java.time.ZoneOffset
import java.util.UUID
import java.util.concurrent.ExecutionException
import java.util.concurrent.Executors
@@ -841,7 +842,7 @@ class BasicPlanCalculator(
val resultFilePath = if (request.saveResultToFile && status == ComplexPlanCalculationStatus.COMPLETED) {
observationParametersDebugFileWriter.write(
ObservationParametersDebugFileDTO(
- createdAt = Instant.now(),
+ createdAt = LocalDateTime.now(ZoneOffset.UTC),
requestId = request.requestId,
satelliteIds = selectedSatelliteIds,
missingSatelliteIds = missingSatelliteIds,
diff --git a/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/service/RequestServiceClient.kt b/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/service/RequestServiceClient.kt
index da31900..98cccf2 100644
--- a/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/service/RequestServiceClient.kt
+++ b/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/service/RequestServiceClient.kt
@@ -7,8 +7,7 @@ import org.springframework.web.client.HttpClientErrorException
import org.springframework.web.client.RestClient
import org.springframework.web.util.UriComponentsBuilder
import space.nstart.pcp.complan.types.RequestItemDTO
-import java.time.OffsetDateTime
-import java.time.ZoneOffset
+import java.time.LocalDateTime
import java.util.UUID
/** Получение заявок из pcp-request-service /v1 API. */
@@ -44,10 +43,9 @@ class RequestServiceClient {
id = id,
name = name,
geometry = geometry,
- // Нормализуем к UTC: pcp-request-service шлёт `Z`, но голый toLocalDateTime() уехал бы
- // при любом ненулевом офсете. Все naive LocalDateTime в системе — UTC (docs/standards/DATETIME_UTC.md).
- intervalBegin = beginDateTime.withOffsetSameInstant(ZoneOffset.UTC).toLocalDateTime(),
- intervalEnd = endDateTime.withOffsetSameInstant(ZoneOffset.UTC).toLocalDateTime(),
+ // pcp-request-service шлёт naive LocalDateTime без зоны — берём как есть (настенные часы UTC).
+ intervalBegin = beginDateTime,
+ intervalEnd = endDateTime,
resolution = resolution(),
importance = importance,
)
@@ -66,16 +64,16 @@ data class RequestResponseDto(
val surveyType: String = "",
val geometry: String,
val importance: Double,
- val beginDateTime: OffsetDateTime,
- val endDateTime: OffsetDateTime,
+ val beginDateTime: LocalDateTime,
+ val endDateTime: LocalDateTime,
val kpp: List = emptyList(),
val highPriorityTransmit: Boolean = false,
val optics: SurveyParamsDto? = null,
val rsa: SurveyParamsDto? = null,
val coverage: CoverageStateDto = CoverageStateDto(),
- val createdAt: OffsetDateTime? = null,
- val updatedAt: OffsetDateTime? = null,
- val deletedAt: OffsetDateTime? = null,
+ val createdAt: LocalDateTime? = null,
+ val updatedAt: LocalDateTime? = null,
+ val deletedAt: LocalDateTime? = null,
)
@JsonIgnoreProperties(ignoreUnknown = true)
diff --git a/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/types/ObservationParametersDebugDTO.kt b/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/types/ObservationParametersDebugDTO.kt
index 6db84b0..76620ae 100644
--- a/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/types/ObservationParametersDebugDTO.kt
+++ b/services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/types/ObservationParametersDebugDTO.kt
@@ -3,7 +3,6 @@ package space.nstart.pcp.complan.types
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty
import io.swagger.v3.oas.annotations.media.Schema
-import java.time.Instant
import java.time.LocalDateTime
import java.util.UUID
@@ -116,7 +115,7 @@ data class ObservationParametersDebugFileDTO(
@get:JsonProperty("createdAt")
@param:JsonProperty("createdAt")
- val createdAt: Instant,
+ val createdAt: LocalDateTime,
@get:JsonProperty("requestId")
@param:JsonProperty("requestId")
diff --git a/services/pcp-dynamic-plan-service/src/test/kotlin/space/nstart/pcp/complan/apl/ObservationParametersDebugFileWriterTest.kt b/services/pcp-dynamic-plan-service/src/test/kotlin/space/nstart/pcp/complan/apl/ObservationParametersDebugFileWriterTest.kt
index 2f59ed7..b2217ee 100644
--- a/services/pcp-dynamic-plan-service/src/test/kotlin/space/nstart/pcp/complan/apl/ObservationParametersDebugFileWriterTest.kt
+++ b/services/pcp-dynamic-plan-service/src/test/kotlin/space/nstart/pcp/complan/apl/ObservationParametersDebugFileWriterTest.kt
@@ -9,7 +9,6 @@ import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamDTO
import java.nio.file.Files
import java.nio.file.Path
-import java.time.Instant
import java.time.LocalDateTime
import java.util.UUID
import kotlin.test.Test
@@ -29,7 +28,7 @@ class ObservationParametersDebugFileWriterTest {
outputDir = tempDir.resolve("observation-parameters").toString()
)
val artifact = ObservationParametersDebugFileDTO(
- createdAt = Instant.parse("2026-05-05T10:15:30Z"),
+ createdAt = LocalDateTime.parse("2026-05-05T10:15:30"),
requestId = requestId,
satelliteIds = listOf(1001L),
missingSatelliteIds = emptyList(),
diff --git a/services/pcp-dynamic-plan-service/src/test/kotlin/space/nstart/pcp/complan/service/RequestServiceClientTest.kt b/services/pcp-dynamic-plan-service/src/test/kotlin/space/nstart/pcp/complan/service/RequestServiceClientTest.kt
index ad1fafd..193a85c 100644
--- a/services/pcp-dynamic-plan-service/src/test/kotlin/space/nstart/pcp/complan/service/RequestServiceClientTest.kt
+++ b/services/pcp-dynamic-plan-service/src/test/kotlin/space/nstart/pcp/complan/service/RequestServiceClientTest.kt
@@ -62,29 +62,11 @@ class RequestServiceClientTest {
assertFalse(requestedUris.any { uri -> uri.path.startsWith("/api/v1/requests") })
}
- @Test
- fun `getRequest normalizes non-UTC offsets to UTC LocalDateTime`() {
- val requestId = UUID.fromString("00000000-0000-0000-0000-000000000002")
- // Источник прислал то же мгновение, но со смещением +03:00. Голый toLocalDateTime() оставил бы
- // локальную стенку (06:04:05); нормализация к UTC обязана дать 03:04:05.
- server = serverWithRequestResponse(
- mutableListOf(),
- requestId,
- beginDateTime = "2026-01-02T06:04:05+03:00",
- endDateTime = "2026-01-03T07:05:06+03:00",
- )
-
- val request = requestServiceClient().getRequest(requestId)!!
-
- assertEquals(LocalDateTime.of(2026, 1, 2, 3, 4, 5), request.intervalBegin)
- assertEquals(LocalDateTime.of(2026, 1, 3, 4, 5, 6), request.intervalEnd)
- }
-
private fun serverWithRequestResponse(
requestedUris: MutableList,
requestId: UUID,
- beginDateTime: String = "2026-01-02T03:04:05Z",
- endDateTime: String = "2026-01-03T04:05:06Z",
+ beginDateTime: String = "2026-01-02T03:04:05",
+ endDateTime: String = "2026-01-03T04:05:06",
): HttpServer =
HttpServer.create(InetSocketAddress(0), 0).apply {
createContext("/v1/requests") { exchange ->
@@ -110,8 +92,8 @@ class RequestServiceClientTest {
},
"rsa": null,
"coverage": {"currentPercent": 0.0},
- "createdAt": "2026-01-01T00:00:00Z",
- "updatedAt": "2026-01-01T00:00:00Z",
+ "createdAt": "2026-01-01T00:00:00",
+ "updatedAt": "2026-01-01T00:00:00",
"deletedAt": null
}
""".trimIndent()
diff --git a/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/entity/DropModeEntity.kt b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/entity/DropModeEntity.kt
index 8a614ea..0b065db 100644
--- a/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/entity/DropModeEntity.kt
+++ b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/entity/DropModeEntity.kt
@@ -4,7 +4,7 @@ import jakarta.persistence.Column
import jakarta.persistence.DiscriminatorValue
import jakarta.persistence.Entity
import jakarta.persistence.Table
-import java.time.OffsetDateTime
+import java.time.LocalDateTime
@Entity
@Table(name = "drop_modes")
@@ -12,7 +12,7 @@ import java.time.OffsetDateTime
class DropModeEntity(
id: Long? = null,
plan: PlanEntity? = null,
- timeStart: OffsetDateTime? = null,
+ timeStart: LocalDateTime? = null,
revolution: Long = 0,
@Column(name = "station")
diff --git a/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/entity/ModeEntity.kt b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/entity/ModeEntity.kt
index 75daeec..ab7e87d 100644
--- a/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/entity/ModeEntity.kt
+++ b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/entity/ModeEntity.kt
@@ -13,7 +13,7 @@ import jakarta.persistence.InheritanceType
import jakarta.persistence.JoinColumn
import jakarta.persistence.ManyToOne
import jakarta.persistence.Table
-import java.time.OffsetDateTime
+import java.time.LocalDateTime
@Entity
@Table(name = "modes")
@@ -29,7 +29,7 @@ abstract class ModeEntity(
open var plan: PlanEntity? = null,
@Column(name = "time_start")
- open var timeStart: OffsetDateTime? = null,
+ open var timeStart: LocalDateTime? = null,
@Column(name = "revolution", nullable = false)
open var revolution: Long = 0
diff --git a/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/entity/SurveyModeEntity.kt b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/entity/SurveyModeEntity.kt
index ee053fc..cdeaacc 100644
--- a/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/entity/SurveyModeEntity.kt
+++ b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/entity/SurveyModeEntity.kt
@@ -7,7 +7,7 @@ import jakarta.persistence.EnumType
import jakarta.persistence.Enumerated
import jakarta.persistence.Table
import space.nstart.pcp.pcp_types_lib.dto.routes.SurveyModeStatus
-import java.time.OffsetDateTime
+import java.time.LocalDateTime
@Entity
@Table(name = "survey_modes")
@@ -15,7 +15,7 @@ import java.time.OffsetDateTime
class SurveyModeEntity(
id: Long? = null,
plan: PlanEntity? = null,
- timeStart: OffsetDateTime? = null,
+ timeStart: LocalDateTime? = null,
revolution: Long = 0,
@Column(name = "lat", nullable = false)
diff --git a/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/repository/ModeRepository.kt b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/repository/ModeRepository.kt
index e75ccfc..f199ba3 100644
--- a/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/repository/ModeRepository.kt
+++ b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/repository/ModeRepository.kt
@@ -6,7 +6,7 @@ import org.springframework.data.jpa.repository.Modifying
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param
import space.nstart.pcp.pcp_mission_planing_service.entity.ModeEntity
-import java.time.OffsetDateTime
+import java.time.LocalDateTime
import java.util.UUID
interface ModeRepository : JpaRepository {
@@ -14,7 +14,7 @@ interface ModeRepository : JpaRepository {
fun findAllByPlan_Mission_MissionIdOrderByTimeStartAsc(missionId: UUID): List
fun findAllByPlan_Mission_SatelliteIdAndTimeStartIsNotNullAndTimeStartLessThanEqualOrderByTimeStartAsc(
satelliteId: Long,
- timeStop: OffsetDateTime
+ timeStop: LocalDateTime
): List
fun countByPlan_Mission_SatelliteId(satelliteId: Long): Long
diff --git a/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/repository/SurveyModeRepository.kt b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/repository/SurveyModeRepository.kt
index b264ef1..064a71b 100644
--- a/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/repository/SurveyModeRepository.kt
+++ b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/repository/SurveyModeRepository.kt
@@ -4,7 +4,7 @@ import org.springframework.data.jpa.repository.Query
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.repository.query.Param
import space.nstart.pcp.pcp_mission_planing_service.entity.SurveyModeEntity
-import java.time.OffsetDateTime
+import java.time.LocalDateTime
import java.util.UUID
interface SurveyModeRepository : JpaRepository {
@@ -26,7 +26,7 @@ interface SurveyModeRepository : JpaRepository {
fun findAllBySatelliteNumberAndRevolutionAndTimeStartBetween(
@Param("satelliteNumber") satelliteNumber: String,
@Param("revolution") revolution: Long,
- @Param("timeStartFrom") timeStartFrom: OffsetDateTime,
- @Param("timeStartTo") timeStartTo: OffsetDateTime
+ @Param("timeStartFrom") timeStartFrom: LocalDateTime,
+ @Param("timeStartTo") timeStartTo: LocalDateTime
): 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 18b29aa..d48476a 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
@@ -62,7 +62,6 @@ import space.nstart.pcp.pcp_types_lib.dto.requests.slots.BookedSlotStatus
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.SatelliteModeResponseDTO
import java.time.Duration
import java.time.LocalDateTime
-import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.util.UUID
import kotlin.math.PI
@@ -305,7 +304,7 @@ class MissionPlaningService(
val modes = modeRepository
.findAllByPlan_Mission_SatelliteIdAndTimeStartIsNotNullAndTimeStartLessThanEqualOrderByTimeStartAsc(
satelliteId,
- timeStop.atOffset(ZoneOffset.UTC)
+ timeStop
)
.filter { it.intersects(timeStart, timeStop) }
@@ -346,27 +345,18 @@ class MissionPlaningService(
.mapValues { (_, slotIds) -> slotIds.filter { it > 0 }.distinct() }
}
- /**
- * Нормализует `OffsetDateTime` к UTC и отбрасывает зону. Все naive `LocalDateTime` в сервисе — UTC
- * (см. docs/standards/DATETIME_UTC.md), поэтому сравнивать их можно только с UTC-стенкой времени мода,
- * а не с локальной стенкой произвольного офсета. Голый `toLocalDateTime()` был корректен лишь пока
- * источник слал офсет 0 (`Z`).
- */
- private fun OffsetDateTime.toUtcLocalDateTime(): LocalDateTime =
- withOffsetSameInstant(ZoneOffset.UTC).toLocalDateTime()
-
/**
* Пересечение интервала включения с окном `[intervalStart, intervalStop)` (правый конец открыт).
* Применяется при fork-forward-клоне: включения строго вне окна не клонируются.
*/
private fun ModeEntity.intersects(intervalStart: LocalDateTime, intervalStop: LocalDateTime): Boolean {
- val modeStart = timeStart?.toUtcLocalDateTime() ?: return false
+ val modeStart = timeStart ?: return false
val modeStop = modeStop() ?: return false
return modeStop.isAfter(intervalStart) && modeStart.isBefore(intervalStop)
}
private fun ModeEntity.modeStop(): LocalDateTime? {
- val modeStart = timeStart?.toUtcLocalDateTime() ?: return null
+ val modeStart = timeStart ?: return null
val durationSeconds = when (this) {
is SurveyModeEntity -> duration
is DropModeEntity -> duration
@@ -731,7 +721,7 @@ class MissionPlaningService(
val saved = surveyModeRepository.save(
SurveyModeEntity(
plan = plan,
- timeStart = built.timeStart.atOffset(ZoneOffset.UTC),
+ timeStart = built.timeStart,
revolution = built.revolution,
lat = built.lat,
long = built.longitude,
@@ -805,7 +795,7 @@ class MissionPlaningService(
}
val start = survey.timeStart
?: throw CustomErrorException("У маршрута съёмки ${survey.id} не задано время начала")
- val surveyEnd = start.toUtcLocalDateTime()
+ val surveyEnd = start
.plusNanos((survey.duration.coerceAtLeast(0.0) * 1_000_000_000L).toLong())
if (!surveyEnd.isBefore(req.zoneStart)) {
throw CustomErrorException(
@@ -826,7 +816,7 @@ class MissionPlaningService(
val dropMode = dropModeRepository.save(
DropModeEntity(
plan = plan,
- timeStart = req.zoneStart.atOffset(ZoneOffset.UTC),
+ timeStart = req.zoneStart,
revolution = req.revolution,
station = req.station,
duration = usedDuration
@@ -867,7 +857,7 @@ class MissionPlaningService(
}
val built = buildContour(req)
- survey.timeStart = built.timeStart.atOffset(ZoneOffset.UTC)
+ survey.timeStart = built.timeStart
survey.revolution = built.revolution
survey.lat = built.lat
survey.long = built.longitude
@@ -1164,12 +1154,11 @@ class MissionPlaningService(
val iterator = unassigned.iterator()
while (iterator.hasNext()) {
val survey = iterator.next()
- val surveyStartOffset = survey.timeStart
- if (surveyStartOffset == null) {
+ val surveyStart = survey.timeStart
+ if (surveyStart == null) {
iterator.remove()
continue
}
- val surveyStart = surveyStartOffset.toUtcLocalDateTime()
val surveyEnd = surveyStart.plusNanos((survey.duration.coerceAtLeast(0.0) * 1_000_000_000L).toLong())
if (!surveyEnd.isBefore(zoneStart)) {
@@ -1193,7 +1182,7 @@ class MissionPlaningService(
val dropMode = dropModeRepository.save(
DropModeEntity(
plan = plan,
- timeStart = zoneStart.atOffset(ZoneOffset.UTC),
+ timeStart = zoneStart,
revolution = zone.revolution,
station = zone.stationId.toString(),
duration = usedDuration
@@ -1294,8 +1283,8 @@ class MissionPlaningService(
@Transactional(readOnly = true)
fun findMode(routePassport: RoutePassportDto): SurveyModeEntity? {
- val searchStart = routePassport.intervalBegin.atOffset(ZoneOffset.UTC).minusSeconds(1)
- val searchEnd = routePassport.intervalBegin.atOffset(ZoneOffset.UTC).plusSeconds(1)
+ val searchStart = routePassport.intervalBegin.minusSeconds(1)
+ val searchEnd = routePassport.intervalBegin.plusSeconds(1)
val candidates = surveyModeRepository.findAllBySatelliteNumberAndRevolutionAndTimeStartBetween(
satelliteNumber = routePassport.kaShort.trim(),
revolution = routePassport.orbitNumber,
@@ -1305,7 +1294,7 @@ class MissionPlaningService(
return candidates.minByOrNull { candidate ->
val candidateStart = candidate.timeStart ?: return@minByOrNull Long.MAX_VALUE
- abs(Duration.between(routePassport.intervalBegin.atOffset(ZoneOffset.UTC), candidateStart).toMillis())
+ abs(Duration.between(routePassport.intervalBegin, candidateStart).toMillis())
}
}
diff --git a/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/satellite_type/LegacySatelliteMissionTypeSupport.kt b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/satellite_type/LegacySatelliteMissionTypeSupport.kt
index 4b02ce8..9f1868c 100644
--- a/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/satellite_type/LegacySatelliteMissionTypeSupport.kt
+++ b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/satellite_type/LegacySatelliteMissionTypeSupport.kt
@@ -6,7 +6,6 @@ import space.nstart.pcp.pcp_mission_planing_service.entity.SurveyModeSource
import space.nstart.pcp.pcp_types_lib.dto.routes.SurveyModeStatus
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.SatelliteModeResponseDTO
-import java.time.ZoneOffset
abstract class LegacySatelliteMissionTypeSupport : SatelliteMissionType {
override fun prepareSurvey(
@@ -15,7 +14,7 @@ abstract class LegacySatelliteMissionTypeSupport : SatelliteMissionType {
plan: PlanEntity
): SurveyModeEntity = SurveyModeEntity(
plan = plan,
- timeStart = surv.startTime.atOffset(ZoneOffset.UTC),
+ timeStart = surv.startTime,
revolution = surv.revolution,
lat = surv.lat,
long = surv.longitude,
diff --git a/services/pcp-mission-planing-service/src/test/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/MissionCloneTest.kt b/services/pcp-mission-planing-service/src/test/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/MissionCloneTest.kt
index e0fcb57..3500d55 100644
--- a/services/pcp-mission-planing-service/src/test/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/MissionCloneTest.kt
+++ b/services/pcp-mission-planing-service/src/test/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/MissionCloneTest.kt
@@ -27,8 +27,6 @@ import space.nstart.pcp.pcp_mission_planing_service.service.satellite_type.Refli
import space.nstart.pcp.pcp_mission_planing_service.service.satellite_type.SatelliteMissionTypeFactory
import space.nstart.pcp.pcp_types_lib.dto.routes.SurveyModeStatus
import java.time.LocalDateTime
-import java.time.OffsetDateTime
-import java.time.ZoneOffset
import java.util.Optional
import java.util.UUID
@@ -263,27 +261,24 @@ class MissionCloneTest {
val plan = PlanEntity(id = ++planAutoId, mission = mission, planNumber = 1)
planStore[plan.id!!] = plan
val duration = java.time.Duration.between(surveyStart, surveyStop).toMillis().toDouble() / 1000.0
- val s1 = survey(plan, surveyStart.atOffset(ZoneOffset.UTC), duration)
- val s2 = survey(plan, surveyStart.plusMinutes(1).atOffset(ZoneOffset.UTC), duration)
+ val s1 = survey(plan, surveyStart, duration)
+ val s2 = survey(plan, surveyStart.plusMinutes(1), duration)
val d = drop(plan, surveyStart.plusMinutes(5))
return SourceFixture(mission.missionId, plan, s1, s2, d)
}
- private fun survey(plan: PlanEntity, startOffset: OffsetDateTime, duration: Double = 300.0): SurveyModeEntity {
+ private fun survey(plan: PlanEntity, start: LocalDateTime, duration: Double = 300.0): SurveyModeEntity {
val id = ++modeAutoId
- val s = SurveyModeEntity(id = id, plan = plan, timeStart = startOffset, revolution = id,
+ val s = SurveyModeEntity(id = id, plan = plan, timeStart = start, revolution = id,
lat = 55.0, long = 37.0, duration = duration, roll = 0.0, source = SurveyModeSource.MANUAL,
status = SurveyModeStatus.PLANNED)
surveyStore[id] = s
return s
}
- private fun survey(plan: PlanEntity, start: LocalDateTime, duration: Double = 300.0) =
- survey(plan, start.atOffset(ZoneOffset.UTC), duration)
-
private fun drop(plan: PlanEntity, start: LocalDateTime): DropModeEntity {
val id = ++modeAutoId
- val d = DropModeEntity(id = id, plan = plan, timeStart = start.atOffset(ZoneOffset.UTC),
+ val d = DropModeEntity(id = id, plan = plan, timeStart = start,
revolution = id, station = "102", duration = 60.0)
dropStore[id] = d
return d
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 05c9ab0..639e5e7 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
@@ -32,7 +32,6 @@ import space.nstart.pcp.pcp_types_lib.dto.routes.SurveyModeStatus
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.DropRouteSaveRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.SurveyRouteBuildRequestDTO
import java.time.LocalDateTime
-import java.time.ZoneOffset
import java.util.UUID
class MissionEditModesTest {
@@ -262,7 +261,7 @@ class MissionEditModesTest {
private fun survey(): SurveyModeEntity {
val id = ++autoId
val s = SurveyModeEntity(
- id = id, plan = plan, timeStart = LocalDateTime.of(2026, 6, 10, 12, 0).atOffset(ZoneOffset.UTC),
+ id = id, plan = plan, timeStart = LocalDateTime.of(2026, 6, 10, 12, 0),
revolution = id, lat = 55.0, long = 37.0, duration = 60.0, roll = 0.0,
status = SurveyModeStatus.PLANNED, source = SurveyModeSource.MANUAL
)
@@ -273,7 +272,7 @@ class MissionEditModesTest {
private fun drop(): DropModeEntity {
val id = ++autoId
val d = DropModeEntity(
- id = id, plan = plan, timeStart = LocalDateTime.of(2026, 6, 10, 13, 0).atOffset(ZoneOffset.UTC),
+ id = id, plan = plan, timeStart = LocalDateTime.of(2026, 6, 10, 13, 0),
revolution = id, station = "102", duration = 60.0
)
dropStore[id] = d
diff --git a/services/pcp-mission-planing-service/src/test/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/MissionPlaningServiceTest.kt b/services/pcp-mission-planing-service/src/test/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/MissionPlaningServiceTest.kt
index 2991f1a..54f43f8 100644
--- a/services/pcp-mission-planing-service/src/test/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/MissionPlaningServiceTest.kt
+++ b/services/pcp-mission-planing-service/src/test/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/MissionPlaningServiceTest.kt
@@ -41,8 +41,6 @@ import space.nstart.pcp.pcp_mission_planing_service.service.satellite_type.Refli
import space.nstart.pcp.pcp_mission_planing_service.service.satellite_type.SatelliteMissionTypeFactory
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteDTO
import java.time.LocalDateTime
-import java.time.OffsetDateTime
-import java.time.ZoneOffset
import java.util.UUID
import java.math.BigDecimal
@@ -93,7 +91,7 @@ class MissionPlaningServiceTest {
val overlappingSurvey = SurveyModeEntity(
id = 1L,
plan = plan,
- timeStart = intervalStart.minusMinutes(5).atOffset(ZoneOffset.UTC),
+ timeStart = intervalStart.minusMinutes(5),
revolution = 10,
lat = 55.0,
long = 37.0,
@@ -105,7 +103,7 @@ class MissionPlaningServiceTest {
val inWindowSurvey = SurveyModeEntity(
id = 2L,
plan = plan,
- timeStart = intervalStart.plusMinutes(10).atOffset(ZoneOffset.UTC),
+ timeStart = intervalStart.plusMinutes(10),
revolution = 11,
lat = 56.0,
long = 38.0,
@@ -117,7 +115,7 @@ class MissionPlaningServiceTest {
val inWindowDrop = DropModeEntity(
id = 3L,
plan = plan,
- timeStart = intervalStart.plusMinutes(20).atOffset(ZoneOffset.UTC),
+ timeStart = intervalStart.plusMinutes(20),
revolution = 12,
station = "station-1",
duration = 120.0
@@ -126,7 +124,7 @@ class MissionPlaningServiceTest {
`when`(
modeRepository.findAllByPlan_Mission_SatelliteIdAndTimeStartIsNotNullAndTimeStartLessThanEqualOrderByTimeStartAsc(
77,
- intervalStop.atOffset(ZoneOffset.UTC)
+ intervalStop
)
).thenReturn(listOf(overlappingSurvey, inWindowSurvey, inWindowDrop))
`when`(modeBookingRepository.findAllByMode_IdIn(listOf(1L, 2L))).thenReturn(
@@ -153,41 +151,6 @@ class MissionPlaningServiceTest {
assertEquals(listOf(2L), (result[2] as DropModeResponseDTO).surveys)
}
- @Test
- fun `modesBySatelliteAndInterval normalizes non-UTC mode offsets to UTC before matching`() {
- val mission = MissionEntity(missionId = UUID.randomUUID(), satelliteId = 77)
- val plan = PlanEntity(id = 101L, mission = mission, planNumber = 1)
- val intervalStart = LocalDateTime.of(2026, 3, 16, 10, 0, 0)
- val intervalStop = LocalDateTime.of(2026, 3, 16, 10, 30, 0)
-
- // То же мгновение, что 10:10 UTC (внутри окна), но выражено со смещением +03:00 → 13:10.
- // Голый toLocalDateTime() дал бы 13:10 и выкинул мод из окна; нормализация к UTC оставляет его.
- val offsetSurvey = SurveyModeEntity(
- id = 1L,
- plan = plan,
- timeStart = OffsetDateTime.of(2026, 3, 16, 13, 10, 0, 0, ZoneOffset.ofHours(3)),
- revolution = 11,
- lat = 56.0,
- long = 38.0,
- duration = 300.0,
- contourWkt = "POLYGON((0 0,1 0,1 1,0 0))",
- roll = 1.0,
- source = SurveyModeSource.SLOT
- )
-
- `when`(
- modeRepository.findAllByPlan_Mission_SatelliteIdAndTimeStartIsNotNullAndTimeStartLessThanEqualOrderByTimeStartAsc(
- 77,
- intervalStop.atOffset(ZoneOffset.UTC)
- )
- ).thenReturn(listOf(offsetSurvey))
- `when`(modeBookingRepository.findAllByMode_IdIn(listOf(1L))).thenReturn(emptyList())
-
- val result = service.modesBySatelliteAndInterval(77, intervalStart, intervalStop)
-
- assertEquals(listOf(1L), result.map { it.id })
- }
-
@Test
fun `modesBySatelliteAndInterval rejects inverted interval`() {
val exception = assertThrows(ResponseStatusException::class.java) {
@@ -223,12 +186,12 @@ class MissionPlaningServiceTest {
)
val exactMode = SurveyModeEntity(
id = 10L,
- timeStart = routePassport.intervalBegin.atOffset(ZoneOffset.UTC),
+ timeStart = routePassport.intervalBegin,
revolution = 321
)
val nearMode = SurveyModeEntity(
id = 11L,
- timeStart = routePassport.intervalBegin.plusSeconds(1).atOffset(ZoneOffset.UTC),
+ timeStart = routePassport.intervalBegin.plusSeconds(1),
revolution = 321
)
@@ -236,8 +199,8 @@ class MissionPlaningServiceTest {
surveyModeRepository.findAllBySatelliteNumberAndRevolutionAndTimeStartBetween(
"22",
321,
- OffsetDateTime.of(routePassport.intervalBegin.minusSeconds(1), ZoneOffset.UTC),
- OffsetDateTime.of(routePassport.intervalBegin.plusSeconds(1), ZoneOffset.UTC)
+ routePassport.intervalBegin.minusSeconds(1),
+ routePassport.intervalBegin.plusSeconds(1)
)
).thenReturn(listOf(nearMode, exactMode))
diff --git a/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/controller/RequestController.kt b/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/controller/RequestController.kt
index c1d5367..dd56310 100644
--- a/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/controller/RequestController.kt
+++ b/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/controller/RequestController.kt
@@ -22,7 +22,7 @@ import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
-import java.time.OffsetDateTime
+import java.time.LocalDateTime
import java.util.UUID
@RestController
@@ -41,10 +41,10 @@ class RequestController(
@RequestParam(required = false) surveyType: SurveyTypeDto?,
@RequestParam(required = false) kpp: Int?,
@RequestParam(required = false) highPriorityTransmit: Boolean?,
- @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) beginFrom: OffsetDateTime?,
- @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) beginTo: OffsetDateTime?,
- @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) endFrom: OffsetDateTime?,
- @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) endTo: OffsetDateTime?,
+ @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) beginFrom: LocalDateTime?,
+ @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) beginTo: LocalDateTime?,
+ @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) endFrom: LocalDateTime?,
+ @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) endTo: LocalDateTime?,
@RequestParam(defaultValue = "false") includeDeleted: Boolean,
@RequestParam(defaultValue = "0") page: Int,
@RequestParam(defaultValue = "50") size: Int,
@@ -73,10 +73,10 @@ class RequestController(
@RequestParam(required = false) surveyType: SurveyTypeDto?,
@RequestParam(required = false) kpp: Int?,
@RequestParam(required = false) highPriorityTransmit: Boolean?,
- @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) beginFrom: OffsetDateTime?,
- @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) beginTo: OffsetDateTime?,
- @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) endFrom: OffsetDateTime?,
- @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) endTo: OffsetDateTime?,
+ @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) beginFrom: LocalDateTime?,
+ @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) beginTo: LocalDateTime?,
+ @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) endFrom: LocalDateTime?,
+ @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) endTo: LocalDateTime?,
@RequestParam(defaultValue = "false") includeDeleted: Boolean,
@RequestParam(defaultValue = "0") page: Int,
@RequestParam(defaultValue = "50") size: Int,
diff --git a/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/dto/GridManagementDtos.kt b/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/dto/GridManagementDtos.kt
index 669b84d..7c4cabb 100644
--- a/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/dto/GridManagementDtos.kt
+++ b/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/dto/GridManagementDtos.kt
@@ -3,7 +3,7 @@ package org.nstart.dep265.requestservice.dto
import com.fasterxml.jackson.annotation.JsonFormat
import jakarta.validation.constraints.DecimalMin
import jakarta.validation.constraints.NotNull
-import java.time.OffsetDateTime
+import java.time.LocalDateTime
/**
* Текущие настройки сетки.
@@ -38,5 +38,5 @@ data class GridRebuildResponseDto(
/** Дата-время завершения перестроения в UTC. */
@field:JsonFormat(shape = JsonFormat.Shape.STRING)
- val rebuiltAt: OffsetDateTime,
+ val rebuiltAt: LocalDateTime,
)
diff --git a/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/dto/RequestApiDtos.kt b/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/dto/RequestApiDtos.kt
index 2131f87..a67df9a 100644
--- a/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/dto/RequestApiDtos.kt
+++ b/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/dto/RequestApiDtos.kt
@@ -11,7 +11,7 @@ import jakarta.validation.constraints.Min
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.NotNull
import jakarta.validation.constraints.Size
-import java.time.OffsetDateTime
+import java.time.LocalDateTime
import java.util.UUID
/**
@@ -48,11 +48,11 @@ data class CreateRequestDto(
/** Начало временного окна выполнения заявки. */
@field:NotNull
- val beginDateTime: OffsetDateTime,
+ val beginDateTime: LocalDateTime,
/** Окончание временного окна выполнения заявки. */
@field:NotNull
- val endDateTime: OffsetDateTime,
+ val endDateTime: LocalDateTime,
/** Список номеров КПП для сброса. */
val kpp: List = emptyList(),
@@ -120,10 +120,10 @@ data class RequestResponseDto(
val importance: Double,
/** Начало временного окна выполнения заявки. */
- val beginDateTime: OffsetDateTime,
+ val beginDateTime: LocalDateTime,
/** Окончание временного окна выполнения заявки. */
- val endDateTime: OffsetDateTime,
+ val endDateTime: LocalDateTime,
/** Список номеров КПП для сброса. */
val kpp: List = emptyList(),
@@ -141,13 +141,13 @@ data class RequestResponseDto(
val coverage: CoverageStateDto,
/** Время создания заявки. */
- val createdAt: OffsetDateTime,
+ val createdAt: LocalDateTime,
/** Время последнего обновления заявки. */
- val updatedAt: OffsetDateTime,
+ val updatedAt: LocalDateTime,
/** Время soft delete, если заявка удалена. */
- val deletedAt: OffsetDateTime? = null,
+ val deletedAt: LocalDateTime? = null,
)
/**
@@ -170,10 +170,10 @@ data class RequestSummaryResponseDto(
val importance: Double,
/** Начало временного окна выполнения заявки. */
- val beginDateTime: OffsetDateTime,
+ val beginDateTime: LocalDateTime,
/** Окончание временного окна выполнения заявки. */
- val endDateTime: OffsetDateTime,
+ val endDateTime: LocalDateTime,
/** Список номеров КПП для сброса. */
val kpp: List = emptyList(),
@@ -185,13 +185,13 @@ data class RequestSummaryResponseDto(
val coveragePercent: Double,
/** Время создания заявки. */
- val createdAt: OffsetDateTime,
+ val createdAt: LocalDateTime,
/** Время последнего обновления заявки. */
- val updatedAt: OffsetDateTime,
+ val updatedAt: LocalDateTime,
/** Время soft delete, если заявка удалена. */
- val deletedAt: OffsetDateTime? = null,
+ val deletedAt: LocalDateTime? = null,
)
/**
@@ -237,10 +237,10 @@ data class RequestMapItemDto(
val importance: Double,
/** Начало временного окна выполнения заявки. */
- val beginDateTime: OffsetDateTime,
+ val beginDateTime: LocalDateTime,
/** Окончание временного окна выполнения заявки. */
- val endDateTime: OffsetDateTime,
+ val endDateTime: LocalDateTime,
/** Список номеров КПП для сброса. */
val kpp: List = emptyList(),
@@ -252,13 +252,13 @@ data class RequestMapItemDto(
val coveragePercent: Double,
/** Время создания заявки. */
- val createdAt: OffsetDateTime,
+ val createdAt: LocalDateTime,
/** Время последнего обновления заявки. */
- val updatedAt: OffsetDateTime,
+ val updatedAt: LocalDateTime,
/** Время soft delete, если заявка удалена. */
- val deletedAt: OffsetDateTime? = null,
+ val deletedAt: LocalDateTime? = null,
)
/**
@@ -317,7 +317,7 @@ data class DeleteRequestResponseDto(
val id: UUID,
/** Время soft delete. */
- val deletedAt: OffsetDateTime,
+ val deletedAt: LocalDateTime,
)
/**
@@ -421,16 +421,16 @@ data class RequestListFilter(
val highPriorityTransmit: Boolean? = null,
/** Нижняя граница beginDateTime, включительно. */
- val beginFrom: OffsetDateTime? = null,
+ val beginFrom: LocalDateTime? = null,
/** Верхняя граница beginDateTime, включительно. */
- val beginTo: OffsetDateTime? = null,
+ val beginTo: LocalDateTime? = null,
/** Нижняя граница endDateTime, включительно. */
- val endFrom: OffsetDateTime? = null,
+ val endFrom: LocalDateTime? = null,
/** Верхняя граница endDateTime, включительно. */
- val endTo: OffsetDateTime? = null,
+ val endTo: LocalDateTime? = null,
/** Включать soft-deleted заявки в результат. */
val includeDeleted: Boolean = false,
diff --git a/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/service/GridRebuildService.kt b/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/service/GridRebuildService.kt
index 7cf002b..eee1af5 100644
--- a/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/service/GridRebuildService.kt
+++ b/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/service/GridRebuildService.kt
@@ -7,7 +7,7 @@ import org.nstart.dep265.requestservice.repository.RequestCellRepository
import org.nstart.dep265.requestservice.repository.RequestRepository
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
-import java.time.OffsetDateTime
+import java.time.LocalDateTime
import java.time.ZoneOffset
class GridRebuildAlreadyRunningException : RuntimeException("Grid rebuild is already running")
@@ -41,7 +41,7 @@ class GridRebuildService(
return GridRebuildResponseDto(
cellsCount = earthCellRepository.count(),
rebuiltRequestProjectionsCount = activeRequests.size.toLong(),
- rebuiltAt = OffsetDateTime.now(ZoneOffset.UTC),
+ rebuiltAt = LocalDateTime.now(ZoneOffset.UTC),
)
}
diff --git a/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/service/RequestService.kt b/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/service/RequestService.kt
index c4f3d73..a59ead0 100644
--- a/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/service/RequestService.kt
+++ b/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/service/RequestService.kt
@@ -39,7 +39,6 @@ import org.springframework.data.domain.Sort
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime
-import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.util.UUID
@@ -82,8 +81,8 @@ class RequestService(
remainingArea = geometryArea,
coverageRequiredPercent = DEFAULT_COVERAGE_REQUIRED_PERCENT,
importance = importance,
- beginDateTime = request.beginDateTime.toUtcLocalDateTime(),
- endDateTime = request.endDateTime.toUtcLocalDateTime(),
+ beginDateTime = request.beginDateTime,
+ endDateTime = request.endDateTime,
kpp = request.kpp.toMutableList(),
highPriorityTransmit = request.highPriorityTransmit,
surveyType = surveyType,
@@ -162,7 +161,7 @@ class RequestService(
requestCellRepository.deleteByRequestId(request.id)
return DeleteRequestResponseDto(
id = request.id,
- deletedAt = deletedAt.toOffsetDateTime(),
+ deletedAt = deletedAt,
)
}
@@ -206,10 +205,10 @@ class RequestService(
surveyType = query.surveyType?.toEntity(),
kpp = query.kpp,
highPriorityTransmit = query.highPriorityTransmit,
- beginFrom = query.beginFrom?.toUtcLocalDateTime(),
- beginTo = query.beginTo?.toUtcLocalDateTime(),
- endFrom = query.endFrom?.toUtcLocalDateTime(),
- endTo = query.endTo?.toUtcLocalDateTime(),
+ beginFrom = query.beginFrom,
+ beginTo = query.beginTo,
+ endFrom = query.endFrom,
+ endTo = query.endTo,
pageable = PageRequest.of(query.page, query.size, query.sort.toRequestSort()),
)
@@ -223,16 +222,16 @@ class RequestService(
surveyType = surveyType.toDto(),
geometry = geometry,
importance = importance,
- beginDateTime = beginDateTime.toOffsetDateTime(),
- endDateTime = endDateTime.toOffsetDateTime(),
+ beginDateTime = beginDateTime,
+ endDateTime = endDateTime,
kpp = kpp,
highPriorityTransmit = highPriorityTransmit,
optics = optics,
rsa = rsa,
coverage = coverage(),
- createdAt = createdAt.toOffsetDateTime(),
- updatedAt = updatedAt.toOffsetDateTime(),
- deletedAt = deletedAt?.toOffsetDateTime(),
+ createdAt = createdAt,
+ updatedAt = updatedAt,
+ deletedAt = deletedAt,
)
}
@@ -252,14 +251,14 @@ class RequestService(
status = status.toDto(),
surveyType = surveyType.toDto(),
importance = importance,
- beginDateTime = beginDateTime.toOffsetDateTime(),
- endDateTime = endDateTime.toOffsetDateTime(),
+ beginDateTime = beginDateTime,
+ endDateTime = endDateTime,
kpp = kpp,
highPriorityTransmit = highPriorityTransmit,
coveragePercent = coverage().currentPercent,
- createdAt = createdAt.toOffsetDateTime(),
- updatedAt = updatedAt.toOffsetDateTime(),
- deletedAt = deletedAt?.toOffsetDateTime(),
+ createdAt = createdAt,
+ updatedAt = updatedAt,
+ deletedAt = deletedAt,
)
}
@@ -271,14 +270,14 @@ class RequestService(
surveyType = surveyType.toDto(),
geometry = geometry,
importance = importance,
- beginDateTime = beginDateTime.toOffsetDateTime(),
- endDateTime = endDateTime.toOffsetDateTime(),
+ beginDateTime = beginDateTime,
+ endDateTime = endDateTime,
kpp = kpp,
highPriorityTransmit = highPriorityTransmit,
coveragePercent = coverage().currentPercent,
- createdAt = createdAt.toOffsetDateTime(),
- updatedAt = updatedAt.toOffsetDateTime(),
- deletedAt = deletedAt?.toOffsetDateTime(),
+ createdAt = createdAt,
+ updatedAt = updatedAt,
+ deletedAt = deletedAt,
)
}
@@ -361,10 +360,6 @@ class RequestService(
return Sort.by(direction, property)
}
- private fun OffsetDateTime.toUtcLocalDateTime(): LocalDateTime = withOffsetSameInstant(ZoneOffset.UTC).toLocalDateTime()
-
- private fun LocalDateTime.toOffsetDateTime(): OffsetDateTime = atOffset(ZoneOffset.UTC)
-
private fun Throwable.isDuplicateRequestId(): Boolean {
return generateSequence(this) { throwable -> throwable.cause }
.filterIsInstance()
diff --git a/services/pcp-request-service/src/test/kotlin/org/nstart/dep265/requestservice/controller/GridManagementControllerTest.kt b/services/pcp-request-service/src/test/kotlin/org/nstart/dep265/requestservice/controller/GridManagementControllerTest.kt
index 71ef3cd..9a094e1 100644
--- a/services/pcp-request-service/src/test/kotlin/org/nstart/dep265/requestservice/controller/GridManagementControllerTest.kt
+++ b/services/pcp-request-service/src/test/kotlin/org/nstart/dep265/requestservice/controller/GridManagementControllerTest.kt
@@ -21,7 +21,7 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import org.springframework.test.web.servlet.setup.MockMvcBuilders
-import java.time.OffsetDateTime
+import java.time.LocalDateTime
import java.util.Optional
class GridManagementControllerTest {
@@ -97,7 +97,7 @@ class GridManagementControllerTest {
GridRebuildResponseDto(
cellsCount = 2,
rebuiltRequestProjectionsCount = 2,
- rebuiltAt = OffsetDateTime.parse("2026-05-20T10:15:30Z"),
+ rebuiltAt = LocalDateTime.of(2026, 5, 20, 10, 15, 30),
)
).`when`(gridRebuildService).rebuildGrid()
@@ -105,7 +105,7 @@ class GridManagementControllerTest {
.andExpect(status().isOk)
.andExpect(jsonPath("$.cellsCount").value(2))
.andExpect(jsonPath("$.rebuiltRequestProjectionsCount").value(2))
- .andExpect(jsonPath("$.rebuiltAt").value("2026-05-20T10:15:30Z"))
+ .andExpect(jsonPath("$.rebuiltAt").value("2026-05-20T10:15:30"))
}
@Test
diff --git a/services/pcp-request-service/src/test/kotlin/org/nstart/dep265/requestservice/dto/RequestApiDtoContractTest.kt b/services/pcp-request-service/src/test/kotlin/org/nstart/dep265/requestservice/dto/RequestApiDtoContractTest.kt
index fcec078..3996a45 100644
--- a/services/pcp-request-service/src/test/kotlin/org/nstart/dep265/requestservice/dto/RequestApiDtoContractTest.kt
+++ b/services/pcp-request-service/src/test/kotlin/org/nstart/dep265/requestservice/dto/RequestApiDtoContractTest.kt
@@ -4,7 +4,7 @@ import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
-import java.time.OffsetDateTime
+import java.time.LocalDateTime
import java.util.UUID
import kotlin.test.Test
import kotlin.test.assertEquals
@@ -25,8 +25,8 @@ class RequestApiDtoContractTest {
"name": "Тест1",
"geometry": "POLYGON ((37.5 55.5, 37.7 55.5, 37.7 55.7, 37.5 55.7, 37.5 55.5))",
"importance": 10.0,
- "beginDateTime": "2026-01-01T00:00:00Z",
- "endDateTime": "2026-01-31T23:59:59Z",
+ "beginDateTime": "2026-01-01T00:00:00",
+ "endDateTime": "2026-01-31T23:59:59",
"kpp": [1, 2, 3],
"highPriorityTransmit": false,
"optics": {
@@ -43,7 +43,7 @@ class RequestApiDtoContractTest {
assertEquals(UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6"), request.id)
assertEquals("Тест1", request.name)
assertEquals(10.0, request.importance)
- assertEquals(OffsetDateTime.parse("2026-01-01T00:00:00Z"), request.beginDateTime)
+ assertEquals(LocalDateTime.of(2026, 1, 1, 0, 0, 0), request.beginDateTime)
assertEquals(OpticsResultTypeDto.PANCHROMATIC, request.optics?.resultType)
}
@@ -55,8 +55,8 @@ class RequestApiDtoContractTest {
"name": "Тест1",
"geometry": "POLYGON ((37.5 55.5, 37.7 55.5, 37.7 55.7, 37.5 55.7, 37.5 55.5))",
"importance": 10.0,
- "beginDateTime": "2026-01-01T00:00:00Z",
- "endDateTime": "2026-01-31T23:59:59Z",
+ "beginDateTime": "2026-01-01T00:00:00",
+ "endDateTime": "2026-01-31T23:59:59",
"surveyType": "OPTICS",
"optics": {
"resultType": "PANCHROMATIC",
@@ -78,8 +78,8 @@ class RequestApiDtoContractTest {
name = "Тест1",
geometry = "POLYGON ((37.5 55.5, 37.7 55.5, 37.7 55.7, 37.5 55.7, 37.5 55.5))",
importance = 10.0,
- beginDateTime = OffsetDateTime.parse("2026-01-01T00:00:00Z"),
- endDateTime = OffsetDateTime.parse("2026-01-31T23:59:59Z"),
+ beginDateTime = LocalDateTime.of(2026, 1, 1, 0, 0, 0),
+ endDateTime = LocalDateTime.of(2026, 1, 31, 23, 59, 59),
optics = OpticsParamsDto(
resultType = OpticsResultTypeDto.PANCHROMATIC,
resolution = 1.0,
@@ -87,40 +87,7 @@ class RequestApiDtoContractTest {
),
)
- assertFalse(json.contains("surveyType"))
- assertFalse(json.contains("app_type"))
- assertFalse(json.contains("appType"))
- assertFalse(json.contains("AppType"))
- assertFalse(json.contains("requestType"))
- }
-
- @Test
- fun `cells list response dto matches openapi fields`() {
- val json = objectMapper.writeValueAsString(
- CellsListResponseDto(
- items = listOf(
- CellSummaryResponseDto(
- cellNum = 42,
- latitude = 55.0,
- longitude = 37.0,
- importance = 10.0,
- contour = "POLYGON ((36 55, 37 55, 37 56, 36 56, 36 55))",
- )
- ),
- page = 0,
- size = 50,
- totalItems = 1,
- totalPages = 1,
- )
- )
-
- assertEquals(
- setOf("items", "page", "size", "totalItems", "totalPages"),
- objectMapper.readTree(json).fieldNames().asSequence().toSet(),
- )
- assertEquals(
- setOf("cellNum", "latitude", "longitude", "importance", "contour"),
- objectMapper.readTree(json)["items"][0].fieldNames().asSequence().toSet(),
- )
+ val node = objectMapper.readTree(json)
+ assertFalse(node.has("surveyType"), "surveyType should not be serialized")
}
}
diff --git a/services/pcp-request-service/src/test/kotlin/org/nstart/dep265/requestservice/dto/RequestResponseDtoContractTest.kt b/services/pcp-request-service/src/test/kotlin/org/nstart/dep265/requestservice/dto/RequestResponseDtoContractTest.kt
index 86bb075..e65ddb2 100644
--- a/services/pcp-request-service/src/test/kotlin/org/nstart/dep265/requestservice/dto/RequestResponseDtoContractTest.kt
+++ b/services/pcp-request-service/src/test/kotlin/org/nstart/dep265/requestservice/dto/RequestResponseDtoContractTest.kt
@@ -3,18 +3,14 @@ package org.nstart.dep265.requestservice.dto
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.junit.jupiter.api.Assertions.assertEquals
-import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Test
-import java.time.OffsetDateTime
-import java.time.ZoneOffset
+import java.time.LocalDateTime
import java.util.UUID
/**
- * Контракт формата времени на проводе для [RequestResponseDto] — эталон группы: `OffsetDateTime`
- * сериализуется с явной зоной `Z` (см. docs/standards/DATETIME_UTC.md). Тест ловит молчаливый дрейф
- * (смену типа поля на naive `LocalDateTime` или Jackson-конфига).
- *
- * Mapper зеркалит конфиг Spring Boot: JavaTimeModule + disable(WRITE_DATES_AS_TIMESTAMPS).
+ * Контракт формата времени на проводе для [RequestResponseDto]: naive `LocalDateTime` → ISO без зоны
+ * (напр. `"2026-05-29T01:00:00"`). Тест ловит случайный возврат к OffsetDateTime/Z или числовым меткам.
*/
class RequestResponseDtoContractTest {
@@ -22,7 +18,7 @@ class RequestResponseDtoContractTest {
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
@Test
- fun `serializes request times as ISO with explicit Z zone`() {
+ fun `serializes request times as naive ISO without zone`() {
val dto = RequestResponseDto(
id = UUID.fromString("00000000-0000-0000-0000-000000000001"),
name = "request",
@@ -30,18 +26,18 @@ class RequestResponseDtoContractTest {
surveyType = SurveyTypeDto.OPTICS,
geometry = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
importance = 1.0,
- beginDateTime = OffsetDateTime.of(2026, 5, 29, 1, 0, 0, 0, ZoneOffset.UTC),
- endDateTime = OffsetDateTime.of(2026, 5, 29, 2, 0, 0, 0, ZoneOffset.UTC),
+ beginDateTime = LocalDateTime.of(2026, 5, 29, 1, 0, 0),
+ endDateTime = LocalDateTime.of(2026, 5, 29, 2, 0, 0),
highPriorityTransmit = false,
coverage = CoverageStateDto(currentPercent = 0.0),
- createdAt = OffsetDateTime.of(2026, 5, 29, 0, 0, 0, 0, ZoneOffset.UTC),
- updatedAt = OffsetDateTime.of(2026, 5, 29, 0, 0, 0, 0, ZoneOffset.UTC)
+ createdAt = LocalDateTime.of(2026, 5, 29, 0, 0, 0),
+ updatedAt = LocalDateTime.of(2026, 5, 29, 0, 0, 0)
)
val node = mapper.readTree(mapper.writeValueAsString(dto))
- assertEquals("2026-05-29T01:00:00Z", node.get("beginDateTime").asText())
- assertEquals("2026-05-29T02:00:00Z", node.get("endDateTime").asText())
- assertTrue(node.get("createdAt").asText().endsWith("Z"), "ожидалась явная зона Z")
+ assertEquals("2026-05-29T01:00:00", node.get("beginDateTime").asText())
+ assertEquals("2026-05-29T02:00:00", node.get("endDateTime").asText())
+ assertFalse(node.get("createdAt").asText().endsWith("Z"), "не ожидалась зона Z")
}
}
diff --git a/services/pcp-request-service/src/test/kotlin/org/nstart/dep265/requestservice/service/RequestServiceListRequestsJpaTest.kt b/services/pcp-request-service/src/test/kotlin/org/nstart/dep265/requestservice/service/RequestServiceListRequestsJpaTest.kt
index 50d8f8b..5603f62 100644
--- a/services/pcp-request-service/src/test/kotlin/org/nstart/dep265/requestservice/service/RequestServiceListRequestsJpaTest.kt
+++ b/services/pcp-request-service/src/test/kotlin/org/nstart/dep265/requestservice/service/RequestServiceListRequestsJpaTest.kt
@@ -20,7 +20,6 @@ import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime
-import java.time.OffsetDateTime
import java.util.UUID
import kotlin.test.assertEquals
@@ -137,8 +136,8 @@ class RequestServiceListRequestsJpaTest @Autowired constructor(
val response = service.listRequests(
RequestListFilter(
- beginFrom = OffsetDateTime.parse("2026-01-10T00:00:00Z"),
- beginTo = OffsetDateTime.parse("2026-01-31T23:59:59Z"),
+ beginFrom = LocalDateTime.of(2026, 1, 10, 0, 0, 0),
+ beginTo = LocalDateTime.of(2026, 1, 31, 23, 59, 59),
)
)
@@ -153,8 +152,8 @@ class RequestServiceListRequestsJpaTest @Autowired constructor(
val response = service.listRequests(
RequestListFilter(
- endFrom = OffsetDateTime.parse("2026-01-10T00:00:00Z"),
- endTo = OffsetDateTime.parse("2026-01-31T23:59:59Z"),
+ endFrom = LocalDateTime.of(2026, 1, 10, 0, 0, 0),
+ endTo = LocalDateTime.of(2026, 1, 31, 23, 59, 59),
)
)
diff --git a/services/pcp-request-service/src/test/kotlin/org/nstart/dep265/requestservice/service/RequestServiceTest.kt b/services/pcp-request-service/src/test/kotlin/org/nstart/dep265/requestservice/service/RequestServiceTest.kt
index d6e071e..3f02619 100644
--- a/services/pcp-request-service/src/test/kotlin/org/nstart/dep265/requestservice/service/RequestServiceTest.kt
+++ b/services/pcp-request-service/src/test/kotlin/org/nstart/dep265/requestservice/service/RequestServiceTest.kt
@@ -30,7 +30,6 @@ import org.springframework.data.domain.PageImpl
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Pageable
import java.time.LocalDateTime
-import java.time.OffsetDateTime
import java.util.Optional
import java.util.UUID
import kotlin.test.assertEquals
@@ -286,8 +285,8 @@ class RequestServiceTest {
name = REQUEST_NAME,
geometry = "POLYGON ((37.5 55.5, 37.7 55.5, 37.7 55.7, 37.5 55.7, 37.5 55.5))",
importance = REQUEST_IMPORTANCE,
- beginDateTime = OffsetDateTime.parse("2026-01-01T00:00:00Z"),
- endDateTime = OffsetDateTime.parse("2026-01-31T23:59:59Z"),
+ beginDateTime = LocalDateTime.of(2026, 1, 1, 0, 0, 0),
+ endDateTime = LocalDateTime.of(2026, 1, 31, 23, 59, 59),
optics = OpticsParamsDto(
resultType = OpticsResultTypeDto.PANCHROMATIC,
resolution = 1.0,
diff --git a/services/pcp-tgu-ops-ui/src/features/tgu-map-2d/Tgu2DMapTab.tsx b/services/pcp-tgu-ops-ui/src/features/tgu-map-2d/Tgu2DMapTab.tsx
index b9b5637..e897cae 100644
--- a/services/pcp-tgu-ops-ui/src/features/tgu-map-2d/Tgu2DMapTab.tsx
+++ b/services/pcp-tgu-ops-ui/src/features/tgu-map-2d/Tgu2DMapTab.tsx
@@ -86,8 +86,8 @@ export function Tgu2DMapTab({
try {
const result = await fetchRequestsForMap(
{
- endFrom: new Date(fromMs).toISOString(),
- beginTo: new Date(toMs).toISOString()
+ endFrom: new Date(fromMs).toISOString().slice(0, 19),
+ beginTo: new Date(toMs).toISOString().slice(0, 19)
},
(loaded, total) => {
setRequestsState((current) => ({ ...current, loadedCount: loaded, totalItems: total }));
diff --git a/services/pcp-tgu-ops-ui/src/features/tgu-requests/TguRequestsTab.tsx b/services/pcp-tgu-ops-ui/src/features/tgu-requests/TguRequestsTab.tsx
index c111700..5372fc3 100644
--- a/services/pcp-tgu-ops-ui/src/features/tgu-requests/TguRequestsTab.tsx
+++ b/services/pcp-tgu-ops-ui/src/features/tgu-requests/TguRequestsTab.tsx
@@ -82,7 +82,7 @@ export function TguRequestsTab({ range, invalidRange, platforms }: TguRequestsTa
setRequestsState((current) => ({ ...current, loading: true, error: undefined, loadedCount: 0 }));
try {
const result = await fetchRequestsForMap(
- { endFrom: new Date(fromMs).toISOString(), beginTo: new Date(toMs).toISOString() },
+ { endFrom: new Date(fromMs).toISOString().slice(0, 19), beginTo: new Date(toMs).toISOString().slice(0, 19) },
(loaded, total) => setRequestsState((current) => ({ ...current, loadedCount: loaded, totalItems: total }))
);
setRequestsState((current) => ({
diff --git a/services/pcp-tgu-ops-ui/src/utils/utcTime.ts b/services/pcp-tgu-ops-ui/src/utils/utcTime.ts
index 117c03e..351f904 100644
--- a/services/pcp-tgu-ops-ui/src/utils/utcTime.ts
+++ b/services/pcp-tgu-ops-ui/src/utils/utcTime.ts
@@ -42,10 +42,9 @@ export function toUtcInputValue(ms: number): string {
}
/**
- * Naive-строку (из `datetime-local`) трактует как UTC и возвращает ISO с `Z`.
- * Для бэкендов с `OffsetDateTime` на границе API (напр. pcp-request-service):
- * пользователь вводит время по UTC, отправляем его же как UTC-смещение.
+ * Naive-строку (из `datetime-local`) возвращает как naive ISO без зоны (`YYYY-MM-DDTHH:mm:ss`).
+ * Все бэкенды ожидают `LocalDateTime` без зоны.
*/
export function toUtcIso(value: string): string {
- return new Date(parseUtc(value)).toISOString();
+ return new Date(parseUtc(value)).toISOString().slice(0, 19);
}
diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/config/CamundaJacksonConfig.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/config/CamundaJacksonConfig.kt
index 0c04f21..ecc1537 100644
--- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/config/CamundaJacksonConfig.kt
+++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/config/CamundaJacksonConfig.kt
@@ -10,7 +10,6 @@ import io.camunda.client.api.JsonMapper as CamundaJsonMapper
import io.camunda.client.impl.CamundaObjectMapper
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
-import space.nstart.pcp.pcp_types_lib.utils.json.PcpTimeModule
@Configuration
class CamundaJacksonConfig {
@@ -25,10 +24,6 @@ class CamundaJacksonConfig {
return JsonMapper.builder()
.addModule(KotlinModule.Builder().build())
.addModule(JavaTimeModule())
- // Слой толерантности времени (читать naive=UTC и Z) — см. docs/standards/DATETIME_UTC.md.
- // Модуль из pcp-types-lib; покрывает Jackson-2 путь (этот ручной маппер для Camunda).
- // REST-границу (Jackson 3) покрывает PcpTimeModuleV3 (см. RestJacksonConfig).
- .addModule(PcpTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.build()
}
diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/config/PcpTimeModuleV3.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/config/PcpTimeModuleV3.kt
deleted file mode 100644
index bed6d43..0000000
--- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/config/PcpTimeModuleV3.kt
+++ /dev/null
@@ -1,94 +0,0 @@
-package space.nstart.pcp_tgu_service.config
-
-import tools.jackson.core.JsonParser
-import tools.jackson.core.JsonToken
-import tools.jackson.databind.DeserializationContext
-import tools.jackson.databind.ValueDeserializer
-import tools.jackson.databind.ext.javatime.deser.InstantDeserializer
-import tools.jackson.databind.ext.javatime.deser.LocalDateTimeDeserializer
-import tools.jackson.databind.module.SimpleModule
-import java.time.Instant
-import java.time.LocalDateTime
-import java.time.OffsetDateTime
-import java.time.ZoneOffset
-import java.time.format.DateTimeParseException
-
-/**
- * Слой толерантности времени для **Jackson 3** (`tools.jackson.*`).
- *
- * Близнец библиотечного `PcpTimeModule` (Jackson 2): то же поведение — читать время в обоих
- * форматах на проводе (naive=UTC и `Z`/офсет), нормализуя к UTC; трогает только десериализацию.
- * Нужен отдельно, потому что Boot 4 авто-конфигурирует Jackson 3 `JsonMapper` для REST HTTP message
- * converters (MVC), а он не видит Jackson-2 модули. Регистрируется как бин
- * `tools.jackson.databind.JacksonModule` ([RestJacksonConfig]) — `JacksonAutoConfiguration` собирает
- * такие бины в общий `JsonMapper`.
- *
- * Канонический источник поведения — `space.nstart.pcp.pcp_types_lib.utils.json.PcpTimeModule`;
- * правки синхронизировать. См. docs/standards/DATETIME_UTC.md.
- */
-class PcpTimeModuleV3 : SimpleModule(NAME) {
-
- init {
- addDeserializer(LocalDateTime::class.java, TolerantLocalDateTimeDeserializerV3())
- addDeserializer(OffsetDateTime::class.java, TolerantOffsetDateTimeDeserializerV3())
- addDeserializer(Instant::class.java, TolerantInstantDeserializerV3())
- }
-
- companion object {
- const val NAME = "PcpTimeModuleV3"
- }
-}
-
-private class TolerantLocalDateTimeDeserializerV3 : ValueDeserializer() {
-
- private val default: ValueDeserializer = LocalDateTimeDeserializer.INSTANCE
-
- override fun deserialize(p: JsonParser, ctxt: DeserializationContext): LocalDateTime? {
- if (p.currentToken() == JsonToken.VALUE_STRING) {
- val text = p.string.trim()
- if (text.isEmpty()) return null
- try {
- return OffsetDateTime.parse(text).withOffsetSameInstant(ZoneOffset.UTC).toLocalDateTime()
- } catch (_: DateTimeParseException) {
- // naive-строка без зоны — трактуем как UTC
- }
- }
- return default.deserialize(p, ctxt)
- }
-}
-
-private class TolerantOffsetDateTimeDeserializerV3 : ValueDeserializer() {
-
- private val default: ValueDeserializer = InstantDeserializer.OFFSET_DATE_TIME
-
- override fun deserialize(p: JsonParser, ctxt: DeserializationContext): OffsetDateTime? {
- if (p.currentToken() == JsonToken.VALUE_STRING) {
- val text = p.string.trim()
- if (text.isEmpty()) return null
- try {
- return OffsetDateTime.parse(text).withOffsetSameInstant(ZoneOffset.UTC)
- } catch (_: DateTimeParseException) {
- return LocalDateTime.parse(text).atOffset(ZoneOffset.UTC)
- }
- }
- return default.deserialize(p, ctxt)
- }
-}
-
-private class TolerantInstantDeserializerV3 : ValueDeserializer() {
-
- private val default: ValueDeserializer = InstantDeserializer.INSTANT
-
- override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Instant? {
- if (p.currentToken() == JsonToken.VALUE_STRING) {
- val text = p.string.trim()
- if (text.isEmpty()) return null
- try {
- return OffsetDateTime.parse(text).toInstant()
- } catch (_: DateTimeParseException) {
- return LocalDateTime.parse(text).toInstant(ZoneOffset.UTC)
- }
- }
- return default.deserialize(p, ctxt)
- }
-}
diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/config/RestJacksonConfig.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/config/RestJacksonConfig.kt
index 4dcb2e9..2bb3eaa 100644
--- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/config/RestJacksonConfig.kt
+++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/config/RestJacksonConfig.kt
@@ -1,21 +1,4 @@
package space.nstart.pcp_tgu_service.config
-import org.springframework.context.annotation.Bean
-import org.springframework.context.annotation.Configuration
-import tools.jackson.databind.JacksonModule
-
-/**
- * Регистрирует слой толерантности времени ([PcpTimeModuleV3]) в авто-конфигурируемом Jackson 3
- * `JsonMapper`, который Boot 4 использует для REST HTTP message converters (MVC).
- *
- * `JacksonAutoConfiguration` собирает все бины `tools.jackson.databind.JacksonModule` из контекста
- * и добавляет их в общий `JsonMapper` (`builder.addModules(...)`), поэтому достаточно объявить бин
- * этого типа. Входящий REST-JSON читается толерантно к обоим форматам времени (naive=UTC и `Z`),
- * как и Jackson-2 путь Camunda ([CamundaJacksonConfig]). См. docs/standards/DATETIME_UTC.md.
- */
-@Configuration
-class RestJacksonConfig {
-
- @Bean
- fun pcpTimeModuleV3(): JacksonModule = PcpTimeModuleV3()
-}
+// Пустой — Jackson 3 регистрирует JavaTimeModule/KotlinModule автоматически через Boot-автоконфиг.
+// LocalDateTime сериализуется как ISO-строка без зоны при disable(WRITE_DATES_AS_TIMESTAMPS).
diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/dto/NewChainDTOs.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/dto/NewChainDTOs.kt
index f313778..2fdbdcd 100644
--- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/dto/NewChainDTOs.kt
+++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/dto/NewChainDTOs.kt
@@ -2,24 +2,19 @@ package space.nstart.pcp_tgu_service.dto
import space.nstart.pcp_tgu_service.domain.CalculatedPlan
import java.time.LocalDateTime
-import java.time.OffsetDateTime
-import java.time.ZoneOffset
import java.util.UUID
-/**
- * «Сырое» окно ЗРВ КА для ручного набора плана новой цепочки. Времена — UTC ISO с `Z`
- * (как [PlanResponse]); внутри домена это naive `LocalDateTime` (= UTC).
- */
+/** «Сырое» окно ЗРВ КА для ручного набора плана новой цепочки. Времена — naive ISO без зоны. */
data class ZrvWindowResponse(
- val startTime: OffsetDateTime,
- val endTime: OffsetDateTime,
+ val startTime: LocalDateTime,
+ val endTime: LocalDateTime,
val kppId: String
) {
companion object {
fun from(window: CalculatedPlan): ZrvWindowResponse =
ZrvWindowResponse(
- startTime = window.startTime.atOffset(ZoneOffset.UTC),
- endTime = window.endTime.atOffset(ZoneOffset.UTC),
+ startTime = window.startTime,
+ endTime = window.endTime,
kppId = window.kppId
)
}
diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/dto/PlanResponse.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/dto/PlanResponse.kt
index 6d746f0..71b21a1 100644
--- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/dto/PlanResponse.kt
+++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/dto/PlanResponse.kt
@@ -4,17 +4,14 @@ import space.nstart.pcp_tgu_service.domain.CalculatedPlan
import space.nstart.pcp_tgu_service.entity.PlannedPlanEntity
import space.nstart.pcp_tgu_service.entity.PlannedPlanStatus
import java.nio.charset.StandardCharsets
-import java.time.OffsetDateTime
-import java.time.ZoneOffset
+import java.time.LocalDateTime
import java.util.UUID
/**
* DTO ответа REST API для плана.
*
- * Времена ([startTime]/[endTime]) — `OffsetDateTime` в UTC и сериализуются как ISO-строка с явной
- * зоной `Z` (напр. `"2026-05-29T01:00:00Z"`), см. `docs/standards/DATETIME_UTC.md`. Внутреннее
- * хранение и домен (`PlannedPlanEntity`/`CalculatedPlan`) остаются naive `LocalDateTime` (= UTC);
- * конвертация в `Z` происходит только на границе API.
+ * Времена ([startTime]/[endTime]) — naive `LocalDateTime`, сериализуются как ISO-строка без зоны
+ * (напр. `"2026-05-29T01:00:00"`). Хранение, домен и API работают на одних настенных часах.
*/
data class PlanResponse(
@@ -24,11 +21,11 @@ data class PlanResponse(
/** Идентификатор космического аппарата */
val spacecraftId: String,
- /** Время начала плана (UTC, ISO с `Z`). */
- val startTime: OffsetDateTime,
+ /** Время начала плана (ISO без зоны). */
+ val startTime: LocalDateTime,
- /** Время окончания плана (UTC, ISO с `Z`). */
- val endTime: OffsetDateTime,
+ /** Время окончания плана (ISO без зоны). */
+ val endTime: LocalDateTime,
/** Идентификатор пункта закладки */
val kppId: String,
@@ -46,9 +43,8 @@ data class PlanResponse(
PlanResponse(
planId = plan.planId,
spacecraftId = plan.spacecraftId,
- // naive LocalDateTime в БД = UTC → отдаём с явной зоной Z.
- startTime = plan.startTime.atOffset(ZoneOffset.UTC),
- endTime = plan.endTime.atOffset(ZoneOffset.UTC),
+ startTime = plan.startTime,
+ endTime = plan.endTime,
kppId = plan.kppId,
status = plan.status
)
@@ -63,9 +59,8 @@ data class PlanResponse(
PlanResponse(
planId = deterministicPlanId(plan),
spacecraftId = plan.spacecraftId,
- // naive LocalDateTime расчёта = UTC → отдаём с явной зоной Z.
- startTime = plan.startTime.atOffset(ZoneOffset.UTC),
- endTime = plan.endTime.atOffset(ZoneOffset.UTC),
+ startTime = plan.startTime,
+ endTime = plan.endTime,
kppId = plan.kppId,
status = PlannedPlanStatus.PLANNED
)
diff --git a/services/pcp-tgu-service/src/test/kotlin/space/nstart/pcp_tgu_service/config/PcpTimeModuleV3Test.kt b/services/pcp-tgu-service/src/test/kotlin/space/nstart/pcp_tgu_service/config/PcpTimeModuleV3Test.kt
deleted file mode 100644
index 4679474..0000000
--- a/services/pcp-tgu-service/src/test/kotlin/space/nstart/pcp_tgu_service/config/PcpTimeModuleV3Test.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-package space.nstart.pcp_tgu_service.config
-
-import org.junit.jupiter.api.Assertions.assertEquals
-import org.junit.jupiter.api.Test
-import tools.jackson.databind.json.JsonMapper
-import java.time.Instant
-import java.time.LocalDateTime
-import java.time.OffsetDateTime
-import java.time.ZoneOffset
-
-/**
- * Слой толерантности времени на Jackson-3 пути (REST-кодеки WebFlux): [PcpTimeModuleV3] читает
- * время в обоих форматах на проводе (naive=UTC и `Z`/офсет), нормализуя к UTC. Близнец
- * библиотечного PcpTimeModuleTest на Jackson 2. См. docs/standards/DATETIME_UTC.md.
- */
-class PcpTimeModuleV3Test {
-
- private val mapper = JsonMapper.builder().addModule(PcpTimeModuleV3()).build()
-
- @Test
- fun `LocalDateTime reads naive and Z as the same UTC wall-clock`() {
- val fromNaive = mapper.readValue("\"2026-05-29T01:00:00\"", LocalDateTime::class.java)
- val fromZ = mapper.readValue("\"2026-05-29T01:00:00Z\"", LocalDateTime::class.java)
-
- assertEquals(LocalDateTime.of(2026, 5, 29, 1, 0, 0), fromNaive)
- assertEquals(LocalDateTime.of(2026, 5, 29, 1, 0, 0), fromZ)
- }
-
- @Test
- fun `LocalDateTime normalizes an explicit offset to UTC`() {
- // 01:00+03:00 == 28-го 22:00 UTC
- val fromOffset = mapper.readValue("\"2026-05-29T01:00:00+03:00\"", LocalDateTime::class.java)
-
- assertEquals(LocalDateTime.of(2026, 5, 28, 22, 0, 0), fromOffset)
- }
-
- @Test
- fun `OffsetDateTime reads naive as UTC and normalizes offset to UTC`() {
- val fromNaive = mapper.readValue("\"2026-05-29T01:00:00\"", OffsetDateTime::class.java)
- val fromOffset = mapper.readValue("\"2026-05-29T01:00:00+03:00\"", OffsetDateTime::class.java)
-
- assertEquals(OffsetDateTime.of(2026, 5, 29, 1, 0, 0, 0, ZoneOffset.UTC), fromNaive)
- assertEquals(OffsetDateTime.of(2026, 5, 28, 22, 0, 0, 0, ZoneOffset.UTC), fromOffset)
- }
-
- @Test
- fun `Instant reads naive(=UTC) and Z to the same moment`() {
- val fromNaive = mapper.readValue("\"2026-05-29T01:00:00\"", Instant::class.java)
- val fromZ = mapper.readValue("\"2026-05-29T01:00:00Z\"", Instant::class.java)
-
- val expected = OffsetDateTime.of(2026, 5, 29, 1, 0, 0, 0, ZoneOffset.UTC).toInstant()
- assertEquals(expected, fromNaive)
- assertEquals(expected, fromZ)
- }
-}
diff --git a/services/pcp-tgu-service/src/test/kotlin/space/nstart/pcp_tgu_service/dto/PlanResponseContractTest.kt b/services/pcp-tgu-service/src/test/kotlin/space/nstart/pcp_tgu_service/dto/PlanResponseContractTest.kt
index cf2a44b..18e0dd8 100644
--- a/services/pcp-tgu-service/src/test/kotlin/space/nstart/pcp_tgu_service/dto/PlanResponseContractTest.kt
+++ b/services/pcp-tgu-service/src/test/kotlin/space/nstart/pcp_tgu_service/dto/PlanResponseContractTest.kt
@@ -3,19 +3,15 @@ package space.nstart.pcp_tgu_service.dto
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.junit.jupiter.api.Assertions.assertEquals
-import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Test
import space.nstart.pcp_tgu_service.entity.PlannedPlanStatus
-import java.time.OffsetDateTime
-import java.time.ZoneOffset
+import java.time.LocalDateTime
import java.util.UUID
/**
- * Контракт формата времени на проводе для [PlanResponse]: `OffsetDateTime` в UTC → ISO с явной зоной `Z`
- * (см. docs/standards/DATETIME_UTC.md). Тест ловит молчаливый дрейф контракта — смену типа поля
- * (назад на naive `LocalDateTime`) или Jackson-конфига, которые поломали бы потребителей.
- *
- * Mapper зеркалит конфиг Spring Boot: JavaTimeModule + disable(WRITE_DATES_AS_TIMESTAMPS).
+ * Контракт формата времени на проводе для [PlanResponse]: naive `LocalDateTime` → ISO без зоны
+ * (напр. `"2026-05-29T01:00:00"`). Тест ловит случайный возврат к OffsetDateTime/Z или числовым меткам.
*/
class PlanResponseContractTest {
@@ -23,20 +19,20 @@ class PlanResponseContractTest {
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
@Test
- fun `serializes plan times as ISO with explicit Z zone`() {
+ fun `serializes plan times as naive ISO without zone`() {
val response = PlanResponse(
planId = UUID.fromString("00000000-0000-0000-0000-000000000001"),
spacecraftId = "25544",
- startTime = OffsetDateTime.of(2026, 5, 29, 1, 0, 0, 0, ZoneOffset.UTC),
- endTime = OffsetDateTime.of(2026, 5, 29, 2, 0, 0, 0, ZoneOffset.UTC),
+ startTime = LocalDateTime.of(2026, 5, 29, 1, 0, 0),
+ endTime = LocalDateTime.of(2026, 5, 29, 2, 0, 0),
kppId = "KPP-1",
status = PlannedPlanStatus.PLANNED
)
val node = mapper.readTree(mapper.writeValueAsString(response))
- assertEquals("2026-05-29T01:00:00Z", node.get("startTime").asText())
- assertEquals("2026-05-29T02:00:00Z", node.get("endTime").asText())
- assertTrue(node.get("startTime").asText().endsWith("Z"), "ожидалась явная зона Z")
+ assertEquals("2026-05-29T01:00:00", node.get("startTime").asText())
+ assertEquals("2026-05-29T02:00:00", node.get("endTime").asText())
+ assertFalse(node.get("startTime").asText().endsWith("Z"), "не ожидалась зона Z")
}
}
diff --git a/services/pcp-tgu-service/src/test/kotlin/space/nstart/pcp_tgu_service/service/PlanQueryServiceTest.kt b/services/pcp-tgu-service/src/test/kotlin/space/nstart/pcp_tgu_service/service/PlanQueryServiceTest.kt
index 9adb4e7..345f0be 100644
--- a/services/pcp-tgu-service/src/test/kotlin/space/nstart/pcp_tgu_service/service/PlanQueryServiceTest.kt
+++ b/services/pcp-tgu-service/src/test/kotlin/space/nstart/pcp_tgu_service/service/PlanQueryServiceTest.kt
@@ -39,10 +39,9 @@ class PlanQueryServiceTest {
val result = service.getAllPlans(historyDays = 7)
assertEquals(2, result.size)
- // PlanResponse.startTime — OffsetDateTime(UTC); доменные времена naive LocalDateTime (= UTC).
- val persistedResponse = result.first { it.startTime == persisted.startTime.atOffset(ZoneOffset.UTC) }
+ val persistedResponse = result.first { it.startTime == persisted.startTime }
assertEquals(PlannedPlanStatus.AWAITING_LAYIN, persistedResponse.status)
- val projectedResponse = result.first { it.startTime == freshProjected.startTime.atOffset(ZoneOffset.UTC) }
+ val projectedResponse = result.first { it.startTime == freshProjected.startTime }
assertEquals(PlannedPlanStatus.PLANNED, projectedResponse.status)
// Отсортировано по startTime.
assertTrue(result[0].startTime <= result[1].startTime)
diff --git a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/dto/tgu/TguPlanningDTO.kt b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/dto/tgu/TguPlanningDTO.kt
index a0be157..ee32dcf 100644
--- a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/dto/tgu/TguPlanningDTO.kt
+++ b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/dto/tgu/TguPlanningDTO.kt
@@ -2,13 +2,11 @@ package space.nstart.pcp.slots_service.dto.tgu
import java.time.Instant
import java.time.LocalDateTime
-import java.time.OffsetDateTime
import java.util.UUID
/*
- * Времена в этом файле — UTC (см. docs/standards/DATETIME_UTC.md). Времена плана ([TguPlanResponse]) —
- * `OffsetDateTime` с явной зоной `Z` (как отдаёт pcp-tgu-service); `Instant` — тоже с `Z`. `LocalDateTime`
- * (напр. [TguPlanDecisionMessage.decisionTime]) — naive ISO без зоны (= UTC). Фронт парсит через utils/utcTime.
+ * Времена плана ([TguPlanResponse]) — naive `LocalDateTime` без зоны (как отдаёт pcp-tgu-service).
+ * `Instant` ([TguPlatformResponse.validFrom/validTo]) — с `Z` (как отдаёт НСИ-прокси).
*/
data class TguPlatformResponse(
@@ -25,8 +23,8 @@ data class TguPlatformResponse(
data class TguPlanResponse(
val planId: UUID,
val spacecraftId: String,
- val startTime: OffsetDateTime,
- val endTime: OffsetDateTime,
+ val startTime: LocalDateTime,
+ val endTime: LocalDateTime,
val kppId: String,
val status: String
)
diff --git a/services/pcp-ui-service/src/test/kotlin/space/nstart/pcp/slots_service/controller/CatalogControllerTest.kt b/services/pcp-ui-service/src/test/kotlin/space/nstart/pcp/slots_service/controller/CatalogControllerTest.kt
index 13d99ca..d219125 100644
--- a/services/pcp-ui-service/src/test/kotlin/space/nstart/pcp/slots_service/controller/CatalogControllerTest.kt
+++ b/services/pcp-ui-service/src/test/kotlin/space/nstart/pcp/slots_service/controller/CatalogControllerTest.kt
@@ -45,8 +45,6 @@ import space.nstart.pcp.slots_service.service.SlotService
import space.nstart.pcp.slots_service.service.StationService
import space.nstart.pcp.slots_service.service.TguPlanningService
import java.time.LocalDateTime
-import java.time.OffsetDateTime
-import java.time.ZoneOffset
import java.util.UUID
import kotlin.test.assertEquals
import kotlin.test.assertFalse
@@ -212,8 +210,8 @@ class CatalogControllerTest {
TguPlanResponse(
planId = planId,
spacecraftId = "56756",
- startTime = OffsetDateTime.of(2026, 5, 29, 10, 0, 0, 0, ZoneOffset.UTC),
- endTime = OffsetDateTime.of(2026, 5, 29, 10, 15, 0, 0, ZoneOffset.UTC),
+ startTime = LocalDateTime.of(2026, 5, 29, 10, 0, 0),
+ endTime = LocalDateTime.of(2026, 5, 29, 10, 15, 0),
kppId = "KPP-1",
status = "WAITING_DECISION"
)
diff --git a/services/pcp-ui-service/src/test/kotlin/space/nstart/pcp/slots_service/dto/tgu/TguPlanningDtoContractTest.kt b/services/pcp-ui-service/src/test/kotlin/space/nstart/pcp/slots_service/dto/tgu/TguPlanningDtoContractTest.kt
index 63225ec..d166025 100644
--- a/services/pcp-ui-service/src/test/kotlin/space/nstart/pcp/slots_service/dto/tgu/TguPlanningDtoContractTest.kt
+++ b/services/pcp-ui-service/src/test/kotlin/space/nstart/pcp/slots_service/dto/tgu/TguPlanningDtoContractTest.kt
@@ -3,20 +3,17 @@ package space.nstart.pcp.slots_service.dto.tgu
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import java.time.Instant
-import java.time.OffsetDateTime
-import java.time.ZoneOffset
+import java.time.LocalDateTime
import java.util.UUID
/**
* Контракт формата времени на проводе для DTO вкладки ТГУ в pcp-ui-service:
- * времена плана ([TguPlanResponse]) — `OffsetDateTime` с `Z` (как отдаёт pcp-tgu-service),
- * `Instant` ([TguPlatformResponse.validFrom]) — тоже с `Z` (см. docs/standards/DATETIME_UTC.md).
- * Тест ловит молчаливый дрейф контракта.
- *
- * Mapper зеркалит конфиг Spring Boot: JavaTimeModule + disable(WRITE_DATES_AS_TIMESTAMPS).
+ * времена плана ([TguPlanResponse]) — naive `LocalDateTime` без зоны (как отдаёт pcp-tgu-service),
+ * `Instant` ([TguPlatformResponse.validFrom]) — с `Z` (как отдаёт НСИ-прокси).
*/
class TguPlanningDtoContractTest {
@@ -24,21 +21,21 @@ class TguPlanningDtoContractTest {
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
@Test
- fun `serializes plan times as ISO with explicit Z zone`() {
+ fun `serializes plan times as naive ISO without zone`() {
val plan = TguPlanResponse(
planId = UUID.fromString("00000000-0000-0000-0000-000000000001"),
spacecraftId = "25544",
- startTime = OffsetDateTime.of(2026, 5, 29, 1, 0, 0, 0, ZoneOffset.UTC),
- endTime = OffsetDateTime.of(2026, 5, 29, 2, 0, 0, 0, ZoneOffset.UTC),
+ startTime = LocalDateTime.of(2026, 5, 29, 1, 0, 0),
+ endTime = LocalDateTime.of(2026, 5, 29, 2, 0, 0),
kppId = "KPP-1",
status = "PLANNED"
)
val node = mapper.readTree(mapper.writeValueAsString(plan))
- assertEquals("2026-05-29T01:00:00Z", node.get("startTime").asText())
- assertEquals("2026-05-29T02:00:00Z", node.get("endTime").asText())
- assertTrue(node.get("startTime").asText().endsWith("Z"), "ожидалась явная зона Z")
+ assertEquals("2026-05-29T01:00:00", node.get("startTime").asText())
+ assertEquals("2026-05-29T02:00:00", node.get("endTime").asText())
+ assertFalse(node.get("startTime").asText().endsWith("Z"), "не ожидалась зона Z")
}
@Test