pcp: единообразие времени (UTC) по группе сервисов

Фронт (pcp-tgu-ops-ui):
- тест-страж utcTime.guard.test.ts: запрещает Date.parse / new Date(string) /
  toLocale* без timeZone вне utils/utcTime; тестовые фикстуры переведены на parseUtc.

Бэкенд:
- UTC-now: LocalDateTime.now() -> LocalDateTime.now(ZoneOffset.UTC) по всем сервисам
  (entity-дефолты, воркеры, use case'ы) и в тестовых фикстурах.
- Конвертации к UTC (баги на не-UTC JVM, каждая с регресс-тестом в не-UTC поясе):
  - tgu: VisibilityPayloadParser, SyncSpacecraftFromNsiUseCase (Instant -> UTC LocalDateTime);
  - mission-planing: MissionPlaningService (OffsetDateTime -> UTC через toUtcLocalDateTime);
  - dynamic-plan: RequestServiceClient (OffsetDateTime -> UTC);
  - slots: SlotRepositoryImpl (getObject(LocalDateTime) вместо getTimestamp().toLocalDateTime()).
- Контрактные сериализационные тесты формата на проводе (naive без зоны / Z):
  PlanResponse (tgu), RequestResponseDto (request, эталон), TguPlanning DTO (ui).
- KDoc-инвариант «=UTC» на UI-facing DTO (tgu PlanResponse, ui-service DTO вкладки ТГУ).

Документация: docs/standards/DATETIME_UTC.md (соглашение, таблица сервисов, под-задачи на миграцию к Z),
docs/standards/CHECKLIST.md (пункт про UTC на API).
This commit is contained in:
Дмитрий Соловьев
2026-06-02 21:51:38 +03:00
parent ac2220d063
commit 260583b134
84 changed files with 760 additions and 166 deletions
@@ -17,6 +17,7 @@ import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper as JpaJsonObjectMapper
import tools.jackson.databind.ObjectMapper
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.UUID
/**
@@ -38,7 +39,7 @@ class RoutePassportIngestionService(
rawPayload: String,
sourceRecord: RoutePassportSourceRecordMetadata,
) {
val now = LocalDateTime.now()
val now = LocalDateTime.now(ZoneOffset.UTC)
val routePassportDto = parsedMessage.routePassportDto
val existingRoutePassport = routePassportRepository.findById(routePassportDto.routeId)
val routeCreated = existingRoutePassport.isEmpty
@@ -9,6 +9,7 @@ import space.nstart.pcp.routepassportconsumer.entity.RoutePassportOutboxStatus
import space.nstart.pcp.routepassportconsumer.repository.RoutePassportOutboxRepository
import java.time.Duration
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.UUID
/**
@@ -32,7 +33,7 @@ class RoutePassportOutboxService(
return emptyList()
}
val now = LocalDateTime.now()
val now = LocalDateTime.now(ZoneOffset.UTC)
val lockedUntil = now.plus(Duration.ofMillis(outboxProperties.lockTimeoutMs))
val claimedRows = routePassportOutboxRepository.findDueForUpdateSkipLocked(outboxProperties.batchSize)
@@ -70,7 +71,7 @@ class RoutePassportOutboxService(
*/
@Transactional
fun markPublished(outboxId: UUID) {
val now = LocalDateTime.now()
val now = LocalDateTime.now(ZoneOffset.UTC)
val outbox = findOutbox(outboxId)
outbox.status = RoutePassportOutboxStatus.PUBLISHED
@@ -99,7 +100,7 @@ class RoutePassportOutboxService(
*/
@Transactional
fun markRetryOrFailed(outboxId: UUID, exception: Exception) {
val now = LocalDateTime.now()
val now = LocalDateTime.now(ZoneOffset.UTC)
val outbox = findOutbox(outboxId)
outbox.attempts += 1
@@ -27,6 +27,7 @@ import tools.jackson.databind.json.JsonMapper
import tools.jackson.module.kotlin.KotlinModule
import java.math.BigDecimal
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.Optional
import java.util.UUID
@@ -219,9 +220,9 @@ class RoutePassportIngestionServiceTest {
status = RoutePassportOutboxStatus.PENDING,
attempts = 0,
maxAttempts = 10,
nextAttemptAt = LocalDateTime.now(),
createdAt = LocalDateTime.now(),
updatedAt = LocalDateTime.now(),
nextAttemptAt = LocalDateTime.now(ZoneOffset.UTC),
createdAt = LocalDateTime.now(ZoneOffset.UTC),
updatedAt = LocalDateTime.now(ZoneOffset.UTC),
)
private fun fixture(): IngestionFixture {