fix(slots-service): #48 by-request отдаёт бронь в базовом cycle-фрейме
GET booking/by-request/{id} возвращал tn/tk/revolution из базового
cycle-фрейма без сдвига (BookedSlotEntity.toViewDTO), тогда как все
остальные пути (toDetails, allByInterval→shiftedToCycle,
resolveBookedSlotStartTime) сдвигают на cycle*durationCalc и
cycle*cycleRevs. Для броней с cycle > 0 дата уходила на cycle*durationCalc
суток раньше расчётной (cycle=0 совпадал, поэтому баг прятался; #42
починил только ключ sat:slotNum:cycle, не время).
Сдвиг перенесён в SlotService.bookedByReqId через новый
toShiftedViewDTO(satellite); КА резолвится из каталога, нет в каталоге —
базовый фрейм без падения списка. Небезопасный базовый билдер в сущности
удалён. Контур/координаты не сдвигаются (наземный след повторяется
каждый цикл), симметрично shiftedToCycle.
Регрессия в SlotServiceCancelTest: сдвиг tn/tk/витка на cycle брони и
fallback без КА. Бэклог #48 помечен сделанным.
This commit is contained in:
+3
-23
@@ -13,11 +13,8 @@ import jakarta.persistence.OneToMany
|
||||
import jakarta.persistence.Table
|
||||
import org.hibernate.annotations.Fetch
|
||||
import org.hibernate.annotations.FetchMode
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.BookedSlotDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.BookedSlotStatus
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.BookedSlotViewDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotStatus
|
||||
|
||||
|
||||
@Entity
|
||||
@@ -55,24 +52,7 @@ class BookedSlotEntity(
|
||||
cycle = cycle
|
||||
)
|
||||
|
||||
/**
|
||||
* Представление брони для фронта: поля слота (контур, координаты) + bookedSlotId + cycle БРОНИ.
|
||||
* cycle берём из самой брони (this.cycle), а не из slot.cycle — иначе ключ sat:slotNum:cycle
|
||||
* не совпадёт с тем, что вернул POST booking/request (рассинхрон cycle).
|
||||
*/
|
||||
fun toViewDTO() = BookedSlotViewDTO(
|
||||
bookedSlotId = bookedSlotId ?: 0,
|
||||
cycle = cycle,
|
||||
satelliteId = slot.satelliteId,
|
||||
tn = slot.tn,
|
||||
tk = slot.tk,
|
||||
roll = slot.roll,
|
||||
contour = slot.contour,
|
||||
revolution = slot.revolution,
|
||||
revolutionSign = RevolutionSign.valueOf(slot.revolutionSign),
|
||||
slotNumber = slot.slotNum,
|
||||
state = SlotStatus.BOOKED,
|
||||
latitude = slot.latitude,
|
||||
longitude = slot.longitude
|
||||
)
|
||||
// Представление брони для фронта (контур/координаты + bookedSlotId + cycle брони) строится в
|
||||
// SlotService.toShiftedViewDTO — там доступен КА для сдвига tn/tk/витка на cycle брони. В сущности
|
||||
// строить нельзя: без сдвига получался базовый cycle-фрейм и дата брони расходилась с расчётом.
|
||||
}
|
||||
|
||||
+33
-3
@@ -2459,9 +2459,16 @@ class SlotService {
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun bookedByReqId(id : String) : List<BookedSlotViewDTO> {
|
||||
// toViewDTO() несёт bookedSlotId (для снятия по id) и cycle БРОНИ (а не slot.cycle),
|
||||
// чтобы ключ слота на фронте совпал с ответом POST booking/request.
|
||||
return requestRepository.findByRequestId(id).map { req -> req.bookedSlot.toViewDTO() }
|
||||
// Время/виток брони сдвигаем на cycle БРОНИ (как resolveBookedSlotStartTime / shiftedToCycle /
|
||||
// toDetails). Иначе by-request отдаёт слот в БАЗОВОМ cycle-фрейме, а request-cover перечисляет
|
||||
// слоты уже сдвинутыми — отсюда баг «слоты бронируются на не те даты»: дата брони, восстановленная
|
||||
// из by-request, на cycle*durationCalc суток раньше расчётной. cycle в DTO остаётся циклом брони —
|
||||
// чтобы ключ sat:slotNum:cycle на фронте совпал с ответом POST booking/request.
|
||||
return requestRepository.findByRequestId(id).map { req ->
|
||||
val booked = req.bookedSlot
|
||||
val satellite = runCatching { satelliteById(booked.slot.satelliteId) }.getOrNull()
|
||||
booked.toShiftedViewDTO(satellite)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2629,6 +2636,29 @@ class SlotService {
|
||||
f81 = f81 ?: InitialConditionsDTO().f81
|
||||
)
|
||||
|
||||
// Бронь для фронта со сдвигом на cycle брони (виток + tn/tk). satellite == null (КА не в каталоге) —
|
||||
// отдаём базовый фрейм без сдвига: лучше показать слот, чем уронить весь список by-request. Контур и
|
||||
// координаты не сдвигаем — наземный след повторяется каждый цикл (симметрично shiftedToCycle).
|
||||
private fun BookedSlotEntity.toShiftedViewDTO(satellite: AbstractSatellite?): BookedSlotViewDTO {
|
||||
val shiftDays = satellite?.let { cycle * it.durationCalc } ?: 0L
|
||||
val revShift = satellite?.let { cycle * it.cycleRevs } ?: 0L
|
||||
return BookedSlotViewDTO(
|
||||
bookedSlotId = bookedSlotId ?: 0,
|
||||
cycle = cycle,
|
||||
satelliteId = slot.satelliteId,
|
||||
tn = slot.tn.plusDays(shiftDays),
|
||||
tk = slot.tk.plusDays(shiftDays),
|
||||
roll = slot.roll,
|
||||
contour = slot.contour,
|
||||
revolution = slot.revolution + revShift,
|
||||
revolutionSign = RevolutionSign.valueOf(slot.revolutionSign),
|
||||
slotNumber = slot.slotNum,
|
||||
state = SlotStatus.BOOKED,
|
||||
latitude = slot.latitude,
|
||||
longitude = slot.longitude
|
||||
)
|
||||
}
|
||||
|
||||
private fun SlotEntity.shiftedToCycle(cycle: Long, satellite: AbstractSatellite): SlotDTO =
|
||||
SlotDTO(
|
||||
cycle = cycle,
|
||||
|
||||
+37
@@ -9,6 +9,7 @@ import org.springframework.test.util.ReflectionTestUtils
|
||||
import space.nstart.pcp.slots_service.entity.BookedRequestEntity
|
||||
import space.nstart.pcp.slots_service.entity.BookedSlotEntity
|
||||
import space.nstart.pcp.slots_service.entity.SlotEntity
|
||||
import space.nstart.pcp.slots_service.model.satellite.TestSatelliteImpl
|
||||
import space.nstart.pcp.slots_service.repository.BookedRequestRepository
|
||||
import space.nstart.pcp.slots_service.repository.BookedSlotsRepository
|
||||
import java.time.LocalDateTime
|
||||
@@ -22,11 +23,13 @@ class SlotServiceCancelTest {
|
||||
|
||||
private val bookedSlotsRepository = mock(BookedSlotsRepository::class.java)
|
||||
private val requestRepository = mock(BookedRequestRepository::class.java)
|
||||
private val satelliteCatalogClient = mock(SatelliteCatalogClient::class.java)
|
||||
|
||||
private fun service(): SlotService {
|
||||
val service = SlotService()
|
||||
ReflectionTestUtils.setField(service, "bookedSlotsRepository", bookedSlotsRepository)
|
||||
ReflectionTestUtils.setField(service, "requestRepository", requestRepository)
|
||||
ReflectionTestUtils.setField(service, "satelliteCatalogClient", satelliteCatalogClient)
|
||||
return service
|
||||
}
|
||||
|
||||
@@ -96,6 +99,40 @@ class SlotServiceCancelTest {
|
||||
assertEquals("POLYGON ((0 0, 1 0, 1 1, 0 0))", view[0].contour)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `bookedByReqId shifts tn-tk and revolution by booking cycle`() {
|
||||
// Регрессия на «слоты бронируются на не те даты»: by-request обязан отдавать слот, сдвинутый
|
||||
// на cycle брони (как request-cover), а не в базовом cycle-фрейме. cycle=2, durationCalc=16,
|
||||
// cycleRevs=243 -> сдвиг +32 суток и +486 витков относительно базового slot.tn/revolution.
|
||||
val link = soleLink(bookedSlotId = 10L, linkId = 100L, requestId = "R1", bookingCycle = 2L)
|
||||
`when`(requestRepository.findByRequestId("R1")).thenReturn(listOf(link))
|
||||
`when`(satelliteCatalogClient.satellite(77L))
|
||||
.thenReturn(TestSatelliteImpl(id = 77L, cycleRevs = 243L, durationCalc = 16L))
|
||||
|
||||
val view = service().bookedByReqId("R1")
|
||||
|
||||
assertEquals(1, view.size)
|
||||
assertEquals(2L, view[0].cycle)
|
||||
// базовый slot.tn/tk из builder slot(): 2026-03-31 10:00 / 10:10, базовый revolution = 0.
|
||||
assertEquals(LocalDateTime.of(2026, 3, 31, 10, 0).plusDays(32), view[0].tn)
|
||||
assertEquals(LocalDateTime.of(2026, 3, 31, 10, 10).plusDays(32), view[0].tk)
|
||||
assertEquals(486L, view[0].revolution)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `bookedByReqId without satellite in catalog falls back to base frame`() {
|
||||
// КА нет в каталоге -> сдвиг невозможен, отдаём базовый фрейм без падения (лучше слот, чем 500).
|
||||
val link = soleLink(bookedSlotId = 10L, linkId = 100L, requestId = "R1", bookingCycle = 2L)
|
||||
`when`(requestRepository.findByRequestId("R1")).thenReturn(listOf(link))
|
||||
`when`(satelliteCatalogClient.satellite(77L)).thenThrow(RuntimeException("нет в каталоге"))
|
||||
|
||||
val view = service().bookedByReqId("R1")
|
||||
|
||||
assertEquals(1, view.size)
|
||||
assertEquals(LocalDateTime.of(2026, 3, 31, 10, 0), view[0].tn) // без сдвига
|
||||
assertEquals(2L, view[0].cycle) // cycle брони сохранён даже без КА
|
||||
}
|
||||
|
||||
// --- builders ---
|
||||
|
||||
private fun slot(
|
||||
|
||||
Reference in New Issue
Block a user