refactor(pcp-request-service): rename REST endpoints and DTOs for clarity

- GET /v1/requests/with-geometry → /v1/requests/map
- GET /v1/requests/{id}/with-cells → /v1/requests/{id}/cells
- GET /v1/cells/with-requests → /v1/cells/priority-map
- CreateRequestRequestDto → CreateRequestDto
- ListRequestsQueryDto → RequestListFilter
- RequestWithGeometry* → RequestMap* (RequestMapItemDto, RequestMapPageDto)
- CellsWithRequestsResponseDto → CellPriorityMapResponseDto
- DeleteRequestStatusDto removed; status field removed from DeleteRequestResponseDto

Updated all consumers (pcp-complex-mission-service, pcp-ui-service, slots-service),
tests, and OpenAPI YAML.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Дмитрий Соловьев
2026-05-30 23:28:45 +03:00
parent 3d508b6be9
commit bb84844674
21 changed files with 545 additions and 136 deletions
@@ -50,7 +50,7 @@ class EarthGridService(webClientBuilderProvider: ObjectProvider<WebClient.Builde
val params = mutableListOf("minImportance=$REQUEST_GRID_MIN_IMPORTANCE") val params = mutableListOf("minImportance=$REQUEST_GRID_MIN_IMPORTANCE")
countLat?.let { params += "countLat=$it" } countLat?.let { params += "countLat=$it" }
countLong?.let { params += "countLong=$it" } countLong?.let { params += "countLong=$it" }
return "/v1/cells/with-requests?${params.joinToString("&")}" return "/v1/cells/priority-map?${params.joinToString("&")}"
} }
private fun CellWithRequestsResponseDto.toEarthCellWithRequestsDto(): EarthCellWithRequestsDTO = private fun CellWithRequestsResponseDto.toEarthCellWithRequestsDto(): EarthCellWithRequestsDTO =
@@ -33,7 +33,7 @@ class EarthGridServiceTest {
val cell = cells.single() val cell = cells.single()
val request = cell.requests.single() val request = cell.requests.single()
assertEquals("/v1/cells/with-requests", requestedUris.single().path) assertEquals("/v1/cells/priority-map", requestedUris.single().path)
assertEquals("minImportance=0.001", requestedUris.single().query) assertEquals("minImportance=0.001", requestedUris.single().query)
assertFalse(requestedUris.any { uri -> uri.path == "/api/earth-grid/with-requests" }) assertFalse(requestedUris.any { uri -> uri.path == "/api/earth-grid/with-requests" })
assertEquals(1L, cell.id) assertEquals(1L, cell.id)
@@ -55,7 +55,7 @@ class EarthGridServiceTest {
earthGridService().cells(countLat = 12, countLong = 24).toList() earthGridService().cells(countLat = 12, countLong = 24).toList()
assertEquals("/v1/cells/with-requests", requestedUris.single().path) assertEquals("/v1/cells/priority-map", requestedUris.single().path)
assertEquals("minImportance=0.001&countLat=12&countLong=24", requestedUris.single().query) assertEquals("minImportance=0.001&countLat=12&countLong=24", requestedUris.single().query)
assertFalse(requestedUris.any { uri -> uri.path == "/api/earth-grid/with-requests" }) assertFalse(requestedUris.any { uri -> uri.path == "/api/earth-grid/with-requests" })
} }
@@ -93,7 +93,7 @@ class EarthGridServiceTest {
}.cells().toList() }.cells().toList()
assertEquals(itemCount, cells.size) assertEquals(itemCount, cells.size)
assertEquals("/v1/cells/with-requests", requestedUris.single().path) assertEquals("/v1/cells/priority-map", requestedUris.single().path)
assertEquals("minImportance=0.001", requestedUris.single().query) assertEquals("minImportance=0.001", requestedUris.single().query)
} }
@@ -128,7 +128,7 @@ class EarthGridServiceTest {
responseBody: String responseBody: String
): HttpServer = ): HttpServer =
HttpServer.create(InetSocketAddress(0), 0).apply { HttpServer.create(InetSocketAddress(0), 0).apply {
createContext("/v1/cells/with-requests") { exchange -> createContext("/v1/cells/priority-map") { exchange ->
requestedUris += exchange.requestURI requestedUris += exchange.requestURI
exchange.responseHeaders.add("Content-Type", "application/json") exchange.responseHeaders.add("Content-Type", "application/json")
exchange.sendResponseHeaders(200, responseBody.toByteArray().size.toLong()) exchange.sendResponseHeaders(200, responseBody.toByteArray().size.toLong())
@@ -10,6 +10,7 @@ info:
- заявка создается через POST /v1/requests; - заявка создается через POST /v1/requests;
- чтение одной заявки выполняется через GET /v1/requests/{id}; - чтение одной заявки выполняется через GET /v1/requests/{id};
- чтение списка заявок выполняется через GET /v1/requests; - чтение списка заявок выполняется через GET /v1/requests;
- чтение списка заявок с geometry выполняется через GET /v1/requests/map;
- удаление заявки выполняется через DELETE /v1/requests/{id}; - удаление заявки выполняется через DELETE /v1/requests/{id};
- удаление является мягким: заявка получает status = DELETED и deletedAt; - удаление является мягким: заявка получает status = DELETED и deletedAt;
- заявка должна содержать хотя бы один payload-блок: optics или rsa; - заявка должна содержать хотя бы один payload-блок: optics или rsa;
@@ -106,14 +107,15 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/ErrorResponse' $ref: '#/components/schemas/ErrorResponse'
/v1/cells/with-requests: /v1/cells/priority-map:
get: get:
tags: tags:
- Cells - Cells
summary: Получить ячейки сетки с фрагментами заявок summary: Получить ячейки сетки с фрагментами заявок
description: | description: |
Возвращает список ячеек сетки вместе с фрагментами заявок, пересекающими каждую ячейку. Возвращает список ячеек сетки вместе с фрагментами заявок, пересекающими каждую ячейку.
operationId: listCellsWithRequests Используется для построения тепловой карты приоритетов.
operationId: listCellsPriorityMap
parameters: parameters:
- name: minImportance - name: minImportance
in: query in: query
@@ -143,7 +145,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/CellsWithRequestsResponse' $ref: '#/components/schemas/CellPriorityMapResponse'
'400': '400':
description: Некорректные query parameters description: Некорректные query parameters
content: content:
@@ -304,7 +306,8 @@ paths:
Возвращает постраничный список заявок. Возвращает постраничный список заявок.
По умолчанию удаленные заявки не возвращаются. Для технического поиска можно передать includeDeleted=true. По умолчанию удаленные заявки не возвращаются. Для технического поиска можно передать includeDeleted=true.
Полная geometry не возвращается в списке по умолчанию; для полной заявки используйте GET /v1/requests/{id}. Полная geometry не возвращается в списке по умолчанию; для списка с geometry используйте GET /v1/requests/map,
для полной заявки используйте GET /v1/requests/{id}.
operationId: listRequests operationId: listRequests
parameters: parameters:
- name: status - name: status
@@ -415,7 +418,127 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/ErrorResponse' $ref: '#/components/schemas/ErrorResponse'
/v1/requests/{requestId}/with-cells: /v1/requests/map:
get:
tags:
- Requests
summary: Получить список заявок с геометрией для карты
description: |
Возвращает постраничный список заявок с полной geometry в WKT.
Endpoint использует те же фильтры и сортировку, что GET /v1/requests, но возвращает расширенную read-модель.
Используйте его только там, где клиенту действительно нужна геометрия списка (например, для отображения на карте).
operationId: listRequestsForMap
parameters:
- name: status
in: query
required: false
description: Фильтр по статусу заявки
schema:
$ref: '#/components/schemas/RequestStatus'
- name: surveyType
in: query
required: false
description: Фильтр по вычисленному типу заявки
schema:
$ref: '#/components/schemas/SurveyType'
- name: kpp
in: query
required: false
description: Фильтр по номеру КПП. Возвращает заявки, содержащие указанный КПП.
schema:
type: integer
example: 1
- name: highPriorityTransmit
in: query
required: false
description: Фильтр по признаку высокоприоритетной передачи
schema:
type: boolean
- name: beginFrom
in: query
required: false
description: Нижняя граница beginDateTime, включительно
schema:
type: string
format: date-time
- name: beginTo
in: query
required: false
description: Верхняя граница beginDateTime, включительно
schema:
type: string
format: date-time
- name: endFrom
in: query
required: false
description: Нижняя граница endDateTime, включительно
schema:
type: string
format: date-time
- name: endTo
in: query
required: false
description: Верхняя граница endDateTime, включительно
schema:
type: string
format: date-time
- name: includeDeleted
in: query
required: false
description: Включать soft-deleted заявки в результат
schema:
type: boolean
default: false
- name: page
in: query
required: false
description: Номер страницы, начиная с 0
schema:
type: integer
minimum: 0
default: 0
- name: size
in: query
required: false
description: Размер страницы
schema:
type: integer
minimum: 1
maximum: 500
default: 50
- name: sort
in: query
required: false
description: Сортировка в формате field,direction. Разрешенные поля зависят от реализации.
schema:
type: string
default: createdAt,desc
examples:
createdDesc:
value: createdAt,desc
beginAsc:
value: beginDateTime,asc
responses:
'200':
description: Список заявок с геометрией
content:
application/json:
schema:
$ref: '#/components/schemas/RequestMapPageResponse'
'400':
description: Некорректные query parameters
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'500':
description: Внутренняя ошибка сервера
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/v1/requests/{id}/cells:
get: get:
tags: tags:
- Cells - Cells
@@ -425,7 +548,7 @@ paths:
Возвращает заявку и проекцию ее пересечений с ячейками сетки. Возвращает заявку и проекцию ее пересечений с ячейками сетки.
operationId: getRequestWithCells operationId: getRequestWithCells
parameters: parameters:
- $ref: '#/components/parameters/RequestIdWithCellsPathParam' - $ref: '#/components/parameters/RequestIdPathParam'
responses: responses:
'200': '200':
description: Заявка с привязанными ячейками description: Заявка с привязанными ячейками
@@ -500,7 +623,6 @@ paths:
$ref: '#/components/schemas/DeleteRequestResponse' $ref: '#/components/schemas/DeleteRequestResponse'
example: example:
id: 3fa85f64-5717-4562-b3fc-2c963f66afa6 id: 3fa85f64-5717-4562-b3fc-2c963f66afa6
status: DELETED
deletedAt: '2026-05-19T13:00:00Z' deletedAt: '2026-05-19T13:00:00Z'
'404': '404':
description: Заявка не найдена description: Заявка не найдена
@@ -609,15 +731,6 @@ components:
type: string type: string
format: uuid format: uuid
example: 3fa85f64-5717-4562-b3fc-2c963f66afa6 example: 3fa85f64-5717-4562-b3fc-2c963f66afa6
RequestIdWithCellsPathParam:
name: requestId
in: path
required: true
description: Идентификатор заявки
schema:
type: string
format: uuid
example: 3fa85f64-5717-4562-b3fc-2c963f66afa6
schemas: schemas:
CreateRequestRequest: CreateRequestRequest:
type: object type: object
@@ -921,12 +1034,125 @@ components:
minimum: 0 minimum: 0
example: 1 example: 1
additionalProperties: false additionalProperties: false
RequestMapItemResponse:
type: object
description: Краткая карточка заявки с полной геометрией для GET /v1/requests/map
required:
- id
- name
- status
- surveyType
- geometry
- importance
- beginDateTime
- endDateTime
- highPriorityTransmit
- coveragePercent
- createdAt
- updatedAt
properties:
id:
type: string
format: uuid
description: Идентификатор заявки
example: 3fa85f64-5717-4562-b3fc-2c963f66afa6
name:
type: string
minLength: 1
maxLength: 255
description: Наименование задания.
example: Тест1
status:
$ref: '#/components/schemas/RequestStatus'
surveyType:
$ref: '#/components/schemas/SurveyType'
geometry:
type: string
format: wkt
description: Полная геометрия заявки в формате WKT
example: POLYGON ((37.5 55.5, 37.7 55.5, 37.7 55.7, 37.5 55.7, 37.5 55.5))
importance:
type: number
format: double
minimum: 0
description: Важность заявки, используемая при расчете важности ячеек сетки.
example: 10.0
beginDateTime:
type: string
format: date-time
example: '2026-01-01T00:00:00Z'
endDateTime:
type: string
format: date-time
example: '2026-01-31T23:59:59Z'
kpp:
type: array
items:
type: integer
description: Список номеров КПП для сброса
example: [1, 2, 3]
highPriorityTransmit:
type: boolean
example: false
coveragePercent:
type: number
format: double
minimum: 0
maximum: 100
description: Текущий процент покрытия заявки
example: 35.5
createdAt:
type: string
format: date-time
example: '2026-05-19T12:00:00Z'
updatedAt:
type: string
format: date-time
example: '2026-05-19T12:30:00Z'
deletedAt:
type: string
format: date-time
nullable: true
description: Дата-время soft delete. Обычно отсутствует/null при includeDeleted=false.
example: null
additionalProperties: false
RequestMapPageResponse:
type: object
description: Постраничный список заявок с полной геометрией для GET /v1/requests/map
required:
- items
- page
- size
- totalItems
- totalPages
properties:
items:
type: array
items:
$ref: '#/components/schemas/RequestMapItemResponse'
page:
type: integer
minimum: 0
example: 0
size:
type: integer
minimum: 1
example: 50
totalItems:
type: integer
format: int64
minimum: 0
example: 1
totalPages:
type: integer
minimum: 0
example: 1
additionalProperties: false
DeleteRequestResponse: DeleteRequestResponse:
type: object type: object
description: Ответ на soft delete заявки description: Ответ на soft delete заявки
required: required:
- id - id
- status
- deletedAt - deletedAt
properties: properties:
id: id:
@@ -934,12 +1160,6 @@ components:
format: uuid format: uuid
description: Идентификатор заявки description: Идентификатор заявки
example: 3fa85f64-5717-4562-b3fc-2c963f66afa6 example: 3fa85f64-5717-4562-b3fc-2c963f66afa6
status:
type: string
enum:
- DELETED
description: Итоговый статус после soft delete
example: DELETED
deletedAt: deletedAt:
type: string type: string
format: date-time format: date-time
@@ -1091,9 +1311,9 @@ components:
description: Важность заявки для расчета важности ячейки description: Важность заявки для расчета важности ячейки
example: 10.0 example: 10.0
additionalProperties: false additionalProperties: false
CellsWithRequestsResponse: CellPriorityMapResponse:
type: object type: object
description: Список ячеек сетки с фрагментами заявок description: Список ячеек сетки с фрагментами заявок для GET /v1/cells/priority-map
required: required:
- items - items
properties: properties:
@@ -1,7 +1,7 @@
package org.nstart.dep265.requestservice.controller package org.nstart.dep265.requestservice.controller
import org.nstart.dep265.requestservice.dto.CellPriorityMapResponseDto
import org.nstart.dep265.requestservice.dto.CellsListResponseDto import org.nstart.dep265.requestservice.dto.CellsListResponseDto
import org.nstart.dep265.requestservice.dto.CellsWithRequestsResponseDto
import org.nstart.dep265.requestservice.service.CellsQueryService import org.nstart.dep265.requestservice.service.CellsQueryService
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMapping
@@ -32,13 +32,13 @@ class CellsController(
) )
} }
@GetMapping("/with-requests") @GetMapping("/priority-map")
fun listCellsWithRequests( fun listCellsPriorityMap(
@RequestParam(required = false) minImportance: Double?, @RequestParam(required = false) minImportance: Double?,
@RequestParam(required = false) countLat: Int?, @RequestParam(required = false) countLat: Int?,
@RequestParam(required = false) countLong: Int?, @RequestParam(required = false) countLong: Int?,
): CellsWithRequestsResponseDto { ): CellPriorityMapResponseDto {
return cellsQueryService.listCellsWithRequests( return cellsQueryService.listCellsPriorityMap(
minImportance = minImportance, minImportance = minImportance,
countLat = countLat, countLat = countLat,
countLong = countLong, countLong = countLong,
@@ -1,11 +1,12 @@
package org.nstart.dep265.requestservice.controller package org.nstart.dep265.requestservice.controller
import jakarta.validation.Valid import jakarta.validation.Valid
import org.nstart.dep265.requestservice.dto.CreateRequestRequestDto import org.nstart.dep265.requestservice.dto.CreateRequestDto
import org.nstart.dep265.requestservice.dto.CreateRequestResponseDto import org.nstart.dep265.requestservice.dto.CreateRequestResponseDto
import org.nstart.dep265.requestservice.dto.DeleteRequestResponseDto import org.nstart.dep265.requestservice.dto.DeleteRequestResponseDto
import org.nstart.dep265.requestservice.dto.ListRequestsQueryDto import org.nstart.dep265.requestservice.dto.RequestListFilter
import org.nstart.dep265.requestservice.dto.RequestListResponseDto import org.nstart.dep265.requestservice.dto.RequestListResponseDto
import org.nstart.dep265.requestservice.dto.RequestMapPageDto
import org.nstart.dep265.requestservice.dto.RequestResponseDto import org.nstart.dep265.requestservice.dto.RequestResponseDto
import org.nstart.dep265.requestservice.dto.RequestStatusDto import org.nstart.dep265.requestservice.dto.RequestStatusDto
import org.nstart.dep265.requestservice.dto.RequestWithCellsResponseDto import org.nstart.dep265.requestservice.dto.RequestWithCellsResponseDto
@@ -30,7 +31,7 @@ class RequestController(
private val requestService: RequestService, private val requestService: RequestService,
) { ) {
@PostMapping @PostMapping
fun createRequest(@Valid @RequestBody request: CreateRequestRequestDto): ResponseEntity<CreateRequestResponseDto> { fun createRequest(@Valid @RequestBody request: CreateRequestDto): ResponseEntity<CreateRequestResponseDto> {
return ResponseEntity.status(201).body(requestService.createRequest(request)) return ResponseEntity.status(201).body(requestService.createRequest(request))
} }
@@ -49,7 +50,7 @@ class RequestController(
@RequestParam(defaultValue = "50") size: Int, @RequestParam(defaultValue = "50") size: Int,
@RequestParam(defaultValue = "createdAt,desc") sort: String, @RequestParam(defaultValue = "createdAt,desc") sort: String,
): RequestListResponseDto { ): RequestListResponseDto {
val query = ListRequestsQueryDto( val query = RequestListFilter(
status = status, status = status,
surveyType = surveyType, surveyType = surveyType,
kpp = kpp, kpp = kpp,
@@ -66,6 +67,38 @@ class RequestController(
return requestService.listRequests(query) return requestService.listRequests(query)
} }
@GetMapping("/map")
fun listRequestsForMap(
@RequestParam(required = false) status: RequestStatusDto?,
@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(defaultValue = "false") includeDeleted: Boolean,
@RequestParam(defaultValue = "0") page: Int,
@RequestParam(defaultValue = "50") size: Int,
@RequestParam(defaultValue = "createdAt,desc") sort: String,
): RequestMapPageDto {
val query = RequestListFilter(
status = status,
surveyType = surveyType,
kpp = kpp,
highPriorityTransmit = highPriorityTransmit,
beginFrom = beginFrom,
beginTo = beginTo,
endFrom = endFrom,
endTo = endTo,
includeDeleted = includeDeleted,
page = page,
size = size,
sort = sort,
)
return requestService.listRequestsForMap(query)
}
@GetMapping("/{id}") @GetMapping("/{id}")
fun getRequestById(@PathVariable id: UUID): ResponseEntity<RequestResponseDto> { fun getRequestById(@PathVariable id: UUID): ResponseEntity<RequestResponseDto> {
return requestService.getRequestById(id) return requestService.getRequestById(id)
@@ -73,9 +106,9 @@ class RequestController(
?: ResponseEntity.notFound().build() ?: ResponseEntity.notFound().build()
} }
@GetMapping("/{requestId}/with-cells") @GetMapping("/{id}/cells")
fun getRequestWithCells(@PathVariable requestId: UUID): ResponseEntity<RequestWithCellsResponseDto> { fun getRequestWithCells(@PathVariable id: UUID): ResponseEntity<RequestWithCellsResponseDto> {
return requestService.getRequestWithCells(requestId) return requestService.getRequestWithCells(id)
?.let { response -> ResponseEntity.ok(response) } ?.let { response -> ResponseEntity.ok(response) }
?: ResponseEntity.notFound().build() ?: ResponseEntity.notFound().build()
} }
@@ -43,7 +43,7 @@ data class CellsListResponseDto(
) )
/** /**
* Фрагмент заявки внутри ячейки сетки для GET /v1/cells/with-requests. * Фрагмент заявки внутри ячейки сетки для GET /v1/cells/priority-map.
*/ */
data class CellRequestFragmentResponseDto( data class CellRequestFragmentResponseDto(
/** Идентификатор заявки. */ /** Идентификатор заявки. */
@@ -60,7 +60,7 @@ data class CellRequestFragmentResponseDto(
) )
/** /**
* Ячейка сетки с фрагментами заявок для GET /v1/cells/with-requests. * Ячейка сетки с фрагментами заявок для GET /v1/cells/priority-map.
*/ */
data class CellWithRequestsResponseDto( data class CellWithRequestsResponseDto(
/** Номер ячейки сетки. */ /** Номер ячейки сетки. */
@@ -83,9 +83,9 @@ data class CellWithRequestsResponseDto(
) )
/** /**
* Список ячеек сетки с фрагментами заявок. * Список ячеек сетки с фрагментами заявок для GET /v1/cells/priority-map.
*/ */
data class CellsWithRequestsResponseDto( data class CellPriorityMapResponseDto(
/** Ячейки сетки с привязанными request_cells fragments. */ /** Ячейки сетки с привязанными request_cells fragments. */
val items: List<CellWithRequestsResponseDto>, val items: List<CellWithRequestsResponseDto>,
) )
@@ -27,7 +27,7 @@ abstract class StrictRequestApiDto {
/** /**
* Тело запроса на создание заявки по OpenAPI. * Тело запроса на создание заявки по OpenAPI.
*/ */
data class CreateRequestRequestDto( data class CreateRequestDto(
/** Внешний идентификатор заявки. */ /** Внешний идентификатор заявки. */
@field:NotNull @field:NotNull
val id: UUID, val id: UUID,
@@ -68,7 +68,6 @@ data class CreateRequestRequestDto(
@field:Valid @field:Valid
val rsa: RsaParamsDto? = null, val rsa: RsaParamsDto? = null,
) : StrictRequestApiDto() { ) : StrictRequestApiDto() {
/** В create-запросе должен быть указан хотя бы один payload-блок съемки. */
@get:AssertTrue(message = "Должен быть указан хотя бы один блок съемки: optics или rsa") @get:AssertTrue(message = "Должен быть указан хотя бы один блок съемки: optics или rsa")
@get:JsonIgnore @get:JsonIgnore
val hasSurveyPayload: Boolean val hasSurveyPayload: Boolean
@@ -215,6 +214,73 @@ data class RequestListResponseDto(
val totalPages: Int, val totalPages: Int,
) )
/**
* Краткая карточка заявки с геометрией для GET /v1/requests/map.
*/
data class RequestMapItemDto(
/** Идентификатор заявки. */
val id: UUID,
/** Наименование задания. */
val name: String,
/** Текущий статус заявки. */
val status: RequestStatusDto,
/** Вычисленный тип заявки. */
val surveyType: SurveyTypeDto,
/** Полная геометрия заявки в WKT. */
val geometry: String,
/** Важность заявки для расчета важности ячеек сетки. */
val importance: Double,
/** Начало временного окна выполнения заявки. */
val beginDateTime: OffsetDateTime,
/** Окончание временного окна выполнения заявки. */
val endDateTime: OffsetDateTime,
/** Список номеров КПП для сброса. */
val kpp: List<Int> = emptyList(),
/** Признак высокоприоритетной передачи. */
val highPriorityTransmit: Boolean,
/** Текущий процент покрытия заявки. */
val coveragePercent: Double,
/** Время создания заявки. */
val createdAt: OffsetDateTime,
/** Время последнего обновления заявки. */
val updatedAt: OffsetDateTime,
/** Время soft delete, если заявка удалена. */
val deletedAt: OffsetDateTime? = null,
)
/**
* Постраничный список заявок с полной геометрией для GET /v1/requests/map.
*/
data class RequestMapPageDto(
/** Элементы текущей страницы. */
val items: List<RequestMapItemDto>,
/** Номер страницы, начиная с 0. */
val page: Int,
/** Размер страницы. */
val size: Int,
/** Общее количество элементов. */
val totalItems: Long,
/** Общее количество страниц. */
val totalPages: Int,
)
/** /**
* Ячейка, связанная с заявкой через request_cells projection. * Ячейка, связанная с заявкой через request_cells projection.
*/ */
@@ -250,9 +316,6 @@ data class DeleteRequestResponseDto(
/** Идентификатор заявки. */ /** Идентификатор заявки. */
val id: UUID, val id: UUID,
/** Итоговый статус после удаления. */
val status: DeleteRequestStatusDto,
/** Время soft delete. */ /** Время soft delete. */
val deletedAt: OffsetDateTime, val deletedAt: OffsetDateTime,
) )
@@ -342,9 +405,9 @@ data class ErrorDetailDto(
) )
/** /**
* Query parameters list endpoint. * Query parameters для list endpoint.
*/ */
data class ListRequestsQueryDto( data class RequestListFilter(
/** Фильтр по статусу заявки. */ /** Фильтр по статусу заявки. */
val status: RequestStatusDto? = null, val status: RequestStatusDto? = null,
@@ -405,14 +468,6 @@ enum class RequestStatusDto {
DELETED, DELETED,
} }
/**
* Статус ответа delete endpoint.
*/
enum class DeleteRequestStatusDto {
/** Заявка удалена или уже была удалена ранее. */
DELETED,
}
/** /**
* Вычисленный тип заявки по наличию optics/rsa. * Вычисленный тип заявки по наличию optics/rsa.
*/ */
@@ -4,7 +4,7 @@ import org.nstart.dep265.requestservice.dto.CellRequestFragmentResponseDto
import org.nstart.dep265.requestservice.dto.CellSummaryResponseDto import org.nstart.dep265.requestservice.dto.CellSummaryResponseDto
import org.nstart.dep265.requestservice.dto.CellWithRequestsResponseDto import org.nstart.dep265.requestservice.dto.CellWithRequestsResponseDto
import org.nstart.dep265.requestservice.dto.CellsListResponseDto import org.nstart.dep265.requestservice.dto.CellsListResponseDto
import org.nstart.dep265.requestservice.dto.CellsWithRequestsResponseDto import org.nstart.dep265.requestservice.dto.CellPriorityMapResponseDto
import org.nstart.dep265.requestservice.entity.EarthCellEntity import org.nstart.dep265.requestservice.entity.EarthCellEntity
import org.nstart.dep265.requestservice.entity.RequestCellEntity import org.nstart.dep265.requestservice.entity.RequestCellEntity
import org.nstart.dep265.requestservice.repository.EarthCellRepository import org.nstart.dep265.requestservice.repository.EarthCellRepository
@@ -49,11 +49,11 @@ class CellsQueryService(
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun listCellsWithRequests( fun listCellsPriorityMap(
minImportance: Double?, minImportance: Double?,
countLat: Int?, countLat: Int?,
countLong: Int?, countLong: Int?,
): CellsWithRequestsResponseDto { ): CellPriorityMapResponseDto {
validateWithRequestsQuery(minImportance, countLat, countLong) validateWithRequestsQuery(minImportance, countLat, countLong)
val items = if (countLat == null && countLong == null) { val items = if (countLat == null && countLong == null) {
@@ -68,7 +68,7 @@ class CellsQueryService(
.map { earthCell -> earthCell.toWithRequestsResponse() } .map { earthCell -> earthCell.toWithRequestsResponse() }
} }
return CellsWithRequestsResponseDto(items = items) return CellPriorityMapResponseDto(items = items)
} }
private fun validateQuery( private fun validateQuery(
@@ -4,18 +4,19 @@ import jakarta.persistence.EntityExistsException
import jakarta.persistence.EntityManager import jakarta.persistence.EntityManager
import jakarta.persistence.PersistenceException import jakarta.persistence.PersistenceException
import org.hibernate.exception.ConstraintViolationException import org.hibernate.exception.ConstraintViolationException
import org.nstart.dep265.requestservice.dto.CreateRequestRequestDto import org.nstart.dep265.requestservice.dto.CreateRequestDto
import org.nstart.dep265.requestservice.dto.CreateRequestResponseDto import org.nstart.dep265.requestservice.dto.CreateRequestResponseDto
import org.nstart.dep265.requestservice.dto.CoverageStateDto import org.nstart.dep265.requestservice.dto.CoverageStateDto
import org.nstart.dep265.requestservice.dto.DeleteRequestResponseDto import org.nstart.dep265.requestservice.dto.DeleteRequestResponseDto
import org.nstart.dep265.requestservice.dto.DeleteRequestStatusDto import org.nstart.dep265.requestservice.dto.RequestListFilter
import org.nstart.dep265.requestservice.dto.ListRequestsQueryDto
import org.nstart.dep265.requestservice.dto.OpticsParamsDto import org.nstart.dep265.requestservice.dto.OpticsParamsDto
import org.nstart.dep265.requestservice.dto.RequestListResponseDto import org.nstart.dep265.requestservice.dto.RequestListResponseDto
import org.nstart.dep265.requestservice.dto.RequestCellResponseDto import org.nstart.dep265.requestservice.dto.RequestCellResponseDto
import org.nstart.dep265.requestservice.dto.RequestResponseDto import org.nstart.dep265.requestservice.dto.RequestResponseDto
import org.nstart.dep265.requestservice.dto.RequestSummaryResponseDto import org.nstart.dep265.requestservice.dto.RequestSummaryResponseDto
import org.nstart.dep265.requestservice.dto.RequestWithCellsResponseDto import org.nstart.dep265.requestservice.dto.RequestWithCellsResponseDto
import org.nstart.dep265.requestservice.dto.RequestMapPageDto
import org.nstart.dep265.requestservice.dto.RequestMapItemDto
import org.nstart.dep265.requestservice.dto.RequestStatusDto import org.nstart.dep265.requestservice.dto.RequestStatusDto
import org.nstart.dep265.requestservice.dto.RsaParamsDto import org.nstart.dep265.requestservice.dto.RsaParamsDto
import org.nstart.dep265.requestservice.dto.SurveyTypeDto import org.nstart.dep265.requestservice.dto.SurveyTypeDto
@@ -32,6 +33,7 @@ import org.nstart.dep265.requestservice.repository.RequestCellRepository
import org.nstart.dep265.requestservice.repository.RequestOpticsParamsRepository import org.nstart.dep265.requestservice.repository.RequestOpticsParamsRepository
import org.nstart.dep265.requestservice.repository.RequestRepository import org.nstart.dep265.requestservice.repository.RequestRepository
import org.nstart.dep265.requestservice.repository.RequestRsaParamsRepository import org.nstart.dep265.requestservice.repository.RequestRsaParamsRepository
import org.springframework.data.domain.Page
import org.springframework.data.domain.PageRequest import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Sort import org.springframework.data.domain.Sort
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@@ -54,7 +56,7 @@ class RequestService(
private val entityManager: EntityManager, private val entityManager: EntityManager,
) { ) {
@Transactional @Transactional
fun createRequest(request: CreateRequestRequestDto): CreateRequestResponseDto { fun createRequest(request: CreateRequestDto): CreateRequestResponseDto {
val requestId = request.id val requestId = request.id
if (requestRepository.existsById(requestId)) { if (requestRepository.existsById(requestId)) {
throw RequestAlreadyExistsException(requestId) throw RequestAlreadyExistsException(requestId)
@@ -105,20 +107,8 @@ class RequestService(
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun listRequests(query: ListRequestsQueryDto): RequestListResponseDto { fun listRequests(query: RequestListFilter): RequestListResponseDto {
val requests = requestRepository.findRequests( val requests = findRequests(query)
includeDeleted = query.includeDeleted,
deletedStatus = RequestStatus.DELETED,
status = query.status?.toEntity(),
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(),
pageable = PageRequest.of(query.page, query.size, query.sort.toRequestSort()),
)
return RequestListResponseDto( return RequestListResponseDto(
items = requests.content.map { request -> request.toSummaryResponse() }, items = requests.content.map { request -> request.toSummaryResponse() },
page = query.page, page = query.page,
@@ -128,6 +118,18 @@ class RequestService(
) )
} }
@Transactional(readOnly = true)
fun listRequestsForMap(query: RequestListFilter): RequestMapPageDto {
val requests = findRequests(query)
return RequestMapPageDto(
items = requests.content.map { request -> request.toMapItemResponse() },
page = query.page,
size = query.size,
totalItems = requests.totalElements,
totalPages = requests.totalPages,
)
}
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun getRequestById(id: UUID): RequestResponseDto? { fun getRequestById(id: UUID): RequestResponseDto? {
val request = requestRepository.findById(id).orElse(null) ?: return null val request = requestRepository.findById(id).orElse(null) ?: return null
@@ -160,7 +162,6 @@ class RequestService(
requestCellRepository.deleteByRequestId(request.id) requestCellRepository.deleteByRequestId(request.id)
return DeleteRequestResponseDto( return DeleteRequestResponseDto(
id = request.id, id = request.id,
status = DeleteRequestStatusDto.DELETED,
deletedAt = deletedAt.toOffsetDateTime(), deletedAt = deletedAt.toOffsetDateTime(),
) )
} }
@@ -197,6 +198,21 @@ class RequestService(
} }
} }
private fun findRequests(query: RequestListFilter): Page<RequestEntity> =
requestRepository.findRequests(
includeDeleted = query.includeDeleted,
deletedStatus = RequestStatus.DELETED,
status = query.status?.toEntity(),
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(),
pageable = PageRequest.of(query.page, query.size, query.sort.toRequestSort()),
)
private fun RequestEntity.toResponse(): RequestResponseDto { private fun RequestEntity.toResponse(): RequestResponseDto {
val optics = requestOpticsParamsRepository.findById(id).orElse(null)?.toDto() val optics = requestOpticsParamsRepository.findById(id).orElse(null)?.toDto()
val rsa = requestRsaParamsRepository.findById(id).orElse(null)?.toDto() val rsa = requestRsaParamsRepository.findById(id).orElse(null)?.toDto()
@@ -247,6 +263,25 @@ class RequestService(
) )
} }
private fun RequestEntity.toMapItemResponse(): RequestMapItemDto {
return RequestMapItemDto(
id = id,
name = name,
status = status.toDto(),
surveyType = surveyType.toDto(),
geometry = geometry,
importance = importance,
beginDateTime = beginDateTime.toOffsetDateTime(),
endDateTime = endDateTime.toOffsetDateTime(),
kpp = kpp,
highPriorityTransmit = highPriorityTransmit,
coveragePercent = coverage().currentPercent,
createdAt = createdAt.toOffsetDateTime(),
updatedAt = updatedAt.toOffsetDateTime(),
deletedAt = deletedAt?.toOffsetDateTime(),
)
}
private fun RequestEntity.coverage(): CoverageStateDto { private fun RequestEntity.coverage(): CoverageStateDto {
return CoverageStateDto( return CoverageStateDto(
requiredPercent = coverageRequiredPercent, requiredPercent = coverageRequiredPercent,
@@ -254,7 +289,7 @@ class RequestService(
) )
} }
private fun CreateRequestRequestDto.surveyType(): RequestSurveyType { private fun CreateRequestDto.surveyType(): RequestSurveyType {
return when { return when {
optics != null && rsa != null -> RequestSurveyType.COMBINED optics != null && rsa != null -> RequestSurveyType.COMBINED
optics != null -> RequestSurveyType.OPTICS optics != null -> RequestSurveyType.OPTICS
@@ -133,8 +133,8 @@ class CellsControllerTest {
} }
@Test @Test
fun `get v1 cells with requests returns request fragments`() { fun `get v1 cells priority map returns request fragments`() {
mockMvc.perform(get("/v1/cells/with-requests").accept(MediaType.APPLICATION_JSON)) mockMvc.perform(get("/v1/cells/priority-map").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk) .andExpect(status().isOk)
.andExpect(jsonPath("$.items").isArray) .andExpect(jsonPath("$.items").isArray)
.andExpect(jsonPath("$.items[0].cellNum").value(42)) .andExpect(jsonPath("$.items[0].cellNum").value(42))
@@ -150,9 +150,9 @@ class CellsControllerTest {
} }
@Test @Test
fun `get v1 cells with requests rejects partial aggregation parameters`() { fun `get v1 cells priority map rejects partial aggregation parameters`() {
mockMvc.perform( mockMvc.perform(
get("/v1/cells/with-requests") get("/v1/cells/priority-map")
.queryParam("countLat", "2") .queryParam("countLat", "2")
.accept(MediaType.APPLICATION_JSON), .accept(MediaType.APPLICATION_JSON),
) )
@@ -162,9 +162,9 @@ class CellsControllerTest {
} }
@Test @Test
fun `get v1 cells with requests rejects invalid numeric parameters`() { fun `get v1 cells priority map rejects invalid numeric parameters`() {
mockMvc.perform( mockMvc.perform(
get("/v1/cells/with-requests") get("/v1/cells/priority-map")
.queryParam("minImportance", "-1") .queryParam("minImportance", "-1")
.accept(MediaType.APPLICATION_JSON), .accept(MediaType.APPLICATION_JSON),
) )
@@ -173,7 +173,7 @@ class CellsControllerTest {
.andExpect(jsonPath("$.message").value("minImportance must be greater than or equal to 0")) .andExpect(jsonPath("$.message").value("minImportance must be greater than or equal to 0"))
mockMvc.perform( mockMvc.perform(
get("/v1/cells/with-requests") get("/v1/cells/priority-map")
.queryParam("countLat", "0") .queryParam("countLat", "0")
.queryParam("countLong", "1") .queryParam("countLong", "1")
.accept(MediaType.APPLICATION_JSON), .accept(MediaType.APPLICATION_JSON),
@@ -184,9 +184,9 @@ class CellsControllerTest {
} }
@Test @Test
fun `get v1 cells with requests accepts complete aggregation parameters`() { fun `get v1 cells priority map accepts complete aggregation parameters`() {
mockMvc.perform( mockMvc.perform(
get("/v1/cells/with-requests") get("/v1/cells/priority-map")
.queryParam("countLat", "1") .queryParam("countLat", "1")
.queryParam("countLong", "1") .queryParam("countLong", "1")
.accept(MediaType.APPLICATION_JSON), .accept(MediaType.APPLICATION_JSON),
@@ -11,6 +11,7 @@ import org.mockito.ArgumentMatchers.isNull
import org.mockito.Mockito.doAnswer import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.doReturn import org.mockito.Mockito.doReturn
import org.mockito.Mockito.mock import org.mockito.Mockito.mock
import org.nstart.dep265.requestservice.dto.CreateRequestDto
import org.nstart.dep265.requestservice.entity.EarthCellEntity import org.nstart.dep265.requestservice.entity.EarthCellEntity
import org.nstart.dep265.requestservice.entity.RequestEntity import org.nstart.dep265.requestservice.entity.RequestEntity
import org.nstart.dep265.requestservice.entity.RequestCellEntity import org.nstart.dep265.requestservice.entity.RequestCellEntity
@@ -345,6 +346,32 @@ class RequestControllerOpenApiSkeletonTest {
.andExpect(jsonPath("$.items[0].importance").value(10.0)) .andExpect(jsonPath("$.items[0].importance").value(10.0))
} }
@Test
fun `get v1 requests map returns map items with geometry`() {
doReturn(PageImpl(listOf(existingRequest())))
.`when`(requestRepository)
.findRequests(
anyBoolean(),
deletedStatusMatcher(),
isNull(),
isNull(),
isNull(),
isNull(),
isNull(),
isNull(),
isNull(),
isNull(),
pageableMatcher(),
)
mockMvc.perform(get("/v1/requests/map").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk)
.andExpect(jsonPath("$.items[0].name").value("Тест1"))
.andExpect(jsonPath("$.items[0].geometry").value("POLYGON ((37.5 55.5, 37.7 55.5, 37.7 55.7, 37.5 55.7, 37.5 55.5))"))
.andExpect(jsonPath("$.items[0].importance").value(10.0))
.andExpect(jsonPath("$.items[0].coveragePercent").value(50.0))
}
@Test @Test
fun `get v1 requests rejects unsupported sort field`() { fun `get v1 requests rejects unsupported sort field`() {
mockMvc.perform( mockMvc.perform(
@@ -370,7 +397,7 @@ class RequestControllerOpenApiSkeletonTest {
} }
@Test @Test
fun `get v1 request with cells returns request and cell projection fields`() { fun `get v1 request cells returns request and cell projection fields`() {
val id = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6") val id = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6")
val earthCell = EarthCellEntity( val earthCell = EarthCellEntity(
cellId = 1, cellId = 1,
@@ -397,7 +424,7 @@ class RequestControllerOpenApiSkeletonTest {
.`when`(requestCellRepository) .`when`(requestCellRepository)
.findWithCellByRequestId(id) .findWithCellByRequestId(id)
mockMvc.perform(get("/v1/requests/$id/with-cells").accept(MediaType.APPLICATION_JSON)) mockMvc.perform(get("/v1/requests/$id/cells").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk) .andExpect(status().isOk)
.andExpect(jsonPath("$.request.id").value(id.toString())) .andExpect(jsonPath("$.request.id").value(id.toString()))
.andExpect(jsonPath("$.request.name").value("Тест1")) .andExpect(jsonPath("$.request.name").value("Тест1"))
@@ -411,7 +438,7 @@ class RequestControllerOpenApiSkeletonTest {
} }
@Test @Test
fun `get v1 request with cells returns empty cells for request without projections`() { fun `get v1 request cells returns empty cells for request without projections`() {
val id = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6") val id = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6")
doReturn(Optional.of(existingRequest())) doReturn(Optional.of(existingRequest()))
.`when`(requestRepository) .`when`(requestRepository)
@@ -420,14 +447,14 @@ class RequestControllerOpenApiSkeletonTest {
.`when`(requestCellRepository) .`when`(requestCellRepository)
.findWithCellByRequestId(id) .findWithCellByRequestId(id)
mockMvc.perform(get("/v1/requests/$id/with-cells").accept(MediaType.APPLICATION_JSON)) mockMvc.perform(get("/v1/requests/$id/cells").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk) .andExpect(status().isOk)
.andExpect(jsonPath("$.request.id").value(id.toString())) .andExpect(jsonPath("$.request.id").value(id.toString()))
.andExpect(jsonPath("$.cells").isEmpty) .andExpect(jsonPath("$.cells").isEmpty)
} }
@Test @Test
fun `get v1 request with cells returns deleted request with empty cells`() { fun `get v1 request cells returns deleted request with empty cells`() {
val id = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6") val id = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6")
val request = existingRequest().also { existingRequest -> val request = existingRequest().also { existingRequest ->
existingRequest.status = RequestStatus.DELETED existingRequest.status = RequestStatus.DELETED
@@ -440,7 +467,7 @@ class RequestControllerOpenApiSkeletonTest {
.`when`(requestCellRepository) .`when`(requestCellRepository)
.findWithCellByRequestId(id) .findWithCellByRequestId(id)
mockMvc.perform(get("/v1/requests/$id/with-cells").accept(MediaType.APPLICATION_JSON)) mockMvc.perform(get("/v1/requests/$id/cells").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk) .andExpect(status().isOk)
.andExpect(jsonPath("$.request.status").value("DELETED")) .andExpect(jsonPath("$.request.status").value("DELETED"))
.andExpect(jsonPath("$.request.deletedAt").exists()) .andExpect(jsonPath("$.request.deletedAt").exists())
@@ -454,7 +481,7 @@ class RequestControllerOpenApiSkeletonTest {
mockMvc.perform(get("/v1/requests/$id").accept(MediaType.APPLICATION_JSON)) mockMvc.perform(get("/v1/requests/$id").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isNotFound) .andExpect(status().isNotFound)
mockMvc.perform(get("/v1/requests/$id/with-cells").accept(MediaType.APPLICATION_JSON)) mockMvc.perform(get("/v1/requests/$id/cells").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isNotFound) .andExpect(status().isNotFound)
mockMvc.perform(delete("/v1/requests/$id").accept(MediaType.APPLICATION_JSON)) mockMvc.perform(delete("/v1/requests/$id").accept(MediaType.APPLICATION_JSON))
@@ -18,7 +18,7 @@ class RequestApiDtoContractTest {
@Test @Test
fun `create request dto matches openapi input fields`() { fun `create request dto matches openapi input fields`() {
val request = objectMapper.readValue<CreateRequestRequestDto>( val request = objectMapper.readValue<CreateRequestDto>(
""" """
{ {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
@@ -66,14 +66,14 @@ class RequestApiDtoContractTest {
""".trimIndent() """.trimIndent()
assertFailsWith<Exception> { assertFailsWith<Exception> {
objectMapper.readValue<CreateRequestRequestDto>(payloadWithSurveyType) objectMapper.readValue<CreateRequestDto>(payloadWithSurveyType)
} }
} }
@Test @Test
fun `create request serialization does not expose survey type`() { fun `create request serialization does not expose survey type`() {
val json = objectMapper.writeValueAsString( val json = objectMapper.writeValueAsString(
CreateRequestRequestDto( CreateRequestDto(
id = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6"), id = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6"),
name = "Тест1", name = "Тест1",
geometry = "POLYGON ((37.5 55.5, 37.7 55.5, 37.7 55.7, 37.5 55.7, 37.5 55.5))", geometry = "POLYGON ((37.5 55.5, 37.7 55.5, 37.7 55.7, 37.5 55.7, 37.5 55.5))",
@@ -212,7 +212,7 @@ class CellsQueryServiceJpaTest @Autowired constructor(
) )
entityManager.clear() entityManager.clear()
val response = service.listCellsWithRequests( val response = service.listCellsPriorityMap(
minImportance = null, minImportance = null,
countLat = null, countLat = null,
countLong = null, countLong = null,
@@ -234,7 +234,7 @@ class CellsQueryServiceJpaTest @Autowired constructor(
saveCell(cellNum = 11, importance = 2.0) saveCell(cellNum = 11, importance = 2.0)
entityManager.clear() entityManager.clear()
val response = service.listCellsWithRequests( val response = service.listCellsPriorityMap(
minImportance = 2.0, minImportance = 2.0,
countLat = null, countLat = null,
countLong = null, countLong = null,
@@ -249,7 +249,7 @@ class CellsQueryServiceJpaTest @Autowired constructor(
saveCell(cellNum = 11, importance = 1.0) saveCell(cellNum = 11, importance = 1.0)
entityManager.clear() entityManager.clear()
val response = service.listCellsWithRequests( val response = service.listCellsPriorityMap(
minImportance = 0.0, minImportance = 0.0,
countLat = null, countLat = null,
countLong = null, countLong = null,
@@ -264,7 +264,7 @@ class CellsQueryServiceJpaTest @Autowired constructor(
saveCell(cellNum = 11, importance = 0.002) saveCell(cellNum = 11, importance = 0.002)
entityManager.clear() entityManager.clear()
val response = service.listCellsWithRequests( val response = service.listCellsPriorityMap(
minImportance = 0.001, minImportance = 0.001,
countLat = null, countLat = null,
countLong = null, countLong = null,
@@ -121,7 +121,7 @@ class CellsQueryServiceTest {
} }
val service = service(earthCellRepository) val service = service(earthCellRepository)
val response = service.listCellsWithRequests( val response = service.listCellsPriorityMap(
minImportance = null, minImportance = null,
countLat = null, countLat = null,
countLong = null, countLong = null,
@@ -149,7 +149,7 @@ class CellsQueryServiceTest {
} }
val service = service(earthCellRepository) val service = service(earthCellRepository)
val response = service.listCellsWithRequests( val response = service.listCellsPriorityMap(
minImportance = 2.0, minImportance = 2.0,
countLat = null, countLat = null,
countLong = null, countLong = null,
@@ -164,7 +164,7 @@ class CellsQueryServiceTest {
val service = service(emptyCellsRepository()) val service = service(emptyCellsRepository())
assertThrows<InvalidCellsQueryException> { assertThrows<InvalidCellsQueryException> {
service.listCellsWithRequests( service.listCellsPriorityMap(
minImportance = null, minImportance = null,
countLat = 2, countLat = 2,
countLong = null, countLong = null,
@@ -172,7 +172,7 @@ class CellsQueryServiceTest {
} }
assertThrows<InvalidCellsQueryException> { assertThrows<InvalidCellsQueryException> {
service.listCellsWithRequests( service.listCellsPriorityMap(
minImportance = null, minImportance = null,
countLat = null, countLat = null,
countLong = 2, countLong = 2,
@@ -194,7 +194,7 @@ class CellsQueryServiceTest {
} }
val service = service(earthCellRepository) val service = service(earthCellRepository)
val response = service.listCellsWithRequests( val response = service.listCellsPriorityMap(
minImportance = null, minImportance = null,
countLat = 1, countLat = 1,
countLong = 2, countLong = 2,
@@ -2,7 +2,7 @@ package org.nstart.dep265.requestservice.service
import jakarta.persistence.EntityManager import jakarta.persistence.EntityManager
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.nstart.dep265.requestservice.dto.ListRequestsQueryDto import org.nstart.dep265.requestservice.dto.RequestListFilter
import org.nstart.dep265.requestservice.dto.RequestStatusDto import org.nstart.dep265.requestservice.dto.RequestStatusDto
import org.nstart.dep265.requestservice.dto.SurveyTypeDto import org.nstart.dep265.requestservice.dto.SurveyTypeDto
import org.nstart.dep265.requestservice.PcpRequestServiceApplication import org.nstart.dep265.requestservice.PcpRequestServiceApplication
@@ -57,7 +57,7 @@ class RequestServiceListRequestsJpaTest @Autowired constructor(
val activeId = saveRequest(status = RequestStatus.ACTIVE, createdAt = "2026-01-02T00:00:00") val activeId = saveRequest(status = RequestStatus.ACTIVE, createdAt = "2026-01-02T00:00:00")
saveRequest(status = RequestStatus.DELETED, createdAt = "2026-01-03T00:00:00") saveRequest(status = RequestStatus.DELETED, createdAt = "2026-01-03T00:00:00")
val response = service.listRequests(ListRequestsQueryDto()) val response = service.listRequests(RequestListFilter())
assertEquals(listOf(activeId), response.items.map { request -> request.id }) assertEquals(listOf(activeId), response.items.map { request -> request.id })
assertEquals("Тест1", response.items.single().name) assertEquals("Тест1", response.items.single().name)
@@ -65,12 +65,25 @@ class RequestServiceListRequestsJpaTest @Autowired constructor(
assertEquals(1, response.totalPages) assertEquals(1, response.totalPages)
} }
@Test
fun `list requests with geometry returns geometry and keeps list filters`() {
val activeId = saveRequest(status = RequestStatus.ACTIVE, createdAt = "2026-01-02T00:00:00")
saveRequest(status = RequestStatus.DELETED, createdAt = "2026-01-03T00:00:00")
val response = service.listRequestsForMap(RequestListFilter())
assertEquals(listOf(activeId), response.items.map { request -> request.id })
assertEquals("POLYGON ((37.5 55.5, 37.7 55.5, 37.7 55.7, 37.5 55.7, 37.5 55.5))", response.items.single().geometry)
assertEquals(1, response.totalItems)
assertEquals(1, response.totalPages)
}
@Test @Test
fun `list requests includes deleted when includeDeleted is true`() { fun `list requests includes deleted when includeDeleted is true`() {
val activeId = saveRequest(status = RequestStatus.ACTIVE, createdAt = "2026-01-02T00:00:00") val activeId = saveRequest(status = RequestStatus.ACTIVE, createdAt = "2026-01-02T00:00:00")
val deletedId = saveRequest(status = RequestStatus.DELETED, createdAt = "2026-01-03T00:00:00") val deletedId = saveRequest(status = RequestStatus.DELETED, createdAt = "2026-01-03T00:00:00")
val response = service.listRequests(ListRequestsQueryDto(includeDeleted = true)) val response = service.listRequests(RequestListFilter(includeDeleted = true))
assertEquals(listOf(deletedId, activeId), response.items.map { request -> request.id }) assertEquals(listOf(deletedId, activeId), response.items.map { request -> request.id })
assertEquals(2, response.totalItems) assertEquals(2, response.totalItems)
@@ -81,7 +94,7 @@ class RequestServiceListRequestsJpaTest @Autowired constructor(
val activeId = saveRequest(status = RequestStatus.ACTIVE) val activeId = saveRequest(status = RequestStatus.ACTIVE)
saveRequest(status = RequestStatus.ACCEPTED) saveRequest(status = RequestStatus.ACCEPTED)
val response = service.listRequests(ListRequestsQueryDto(status = RequestStatusDto.ACTIVE)) val response = service.listRequests(RequestListFilter(status = RequestStatusDto.ACTIVE))
assertEquals(listOf(activeId), response.items.map { request -> request.id }) assertEquals(listOf(activeId), response.items.map { request -> request.id })
} }
@@ -91,7 +104,7 @@ class RequestServiceListRequestsJpaTest @Autowired constructor(
val rsaId = saveRequest(surveyType = RequestSurveyType.RSA) val rsaId = saveRequest(surveyType = RequestSurveyType.RSA)
saveRequest(surveyType = RequestSurveyType.OPTICS) saveRequest(surveyType = RequestSurveyType.OPTICS)
val response = service.listRequests(ListRequestsQueryDto(surveyType = SurveyTypeDto.RSA)) val response = service.listRequests(RequestListFilter(surveyType = SurveyTypeDto.RSA))
assertEquals(listOf(rsaId), response.items.map { request -> request.id }) assertEquals(listOf(rsaId), response.items.map { request -> request.id })
} }
@@ -101,7 +114,7 @@ class RequestServiceListRequestsJpaTest @Autowired constructor(
val matchedId = saveRequest(kpp = mutableListOf(1, 2)) val matchedId = saveRequest(kpp = mutableListOf(1, 2))
saveRequest(kpp = mutableListOf(3, 4)) saveRequest(kpp = mutableListOf(3, 4))
val response = service.listRequests(ListRequestsQueryDto(kpp = 2)) val response = service.listRequests(RequestListFilter(kpp = 2))
assertEquals(listOf(matchedId), response.items.map { request -> request.id }) assertEquals(listOf(matchedId), response.items.map { request -> request.id })
} }
@@ -111,7 +124,7 @@ class RequestServiceListRequestsJpaTest @Autowired constructor(
val highPriorityId = saveRequest(highPriorityTransmit = true) val highPriorityId = saveRequest(highPriorityTransmit = true)
saveRequest(highPriorityTransmit = false) saveRequest(highPriorityTransmit = false)
val response = service.listRequests(ListRequestsQueryDto(highPriorityTransmit = true)) val response = service.listRequests(RequestListFilter(highPriorityTransmit = true))
assertEquals(listOf(highPriorityId), response.items.map { request -> request.id }) assertEquals(listOf(highPriorityId), response.items.map { request -> request.id })
} }
@@ -123,7 +136,7 @@ class RequestServiceListRequestsJpaTest @Autowired constructor(
saveRequest(beginDateTime = "2026-02-01T00:00:00", endDateTime = "2026-02-28T00:00:00") saveRequest(beginDateTime = "2026-02-01T00:00:00", endDateTime = "2026-02-28T00:00:00")
val response = service.listRequests( val response = service.listRequests(
ListRequestsQueryDto( RequestListFilter(
beginFrom = OffsetDateTime.parse("2026-01-10T00:00:00Z"), beginFrom = OffsetDateTime.parse("2026-01-10T00:00:00Z"),
beginTo = OffsetDateTime.parse("2026-01-31T23:59:59Z"), beginTo = OffsetDateTime.parse("2026-01-31T23:59:59Z"),
) )
@@ -139,7 +152,7 @@ class RequestServiceListRequestsJpaTest @Autowired constructor(
saveRequest(endDateTime = "2026-02-01T00:00:00") saveRequest(endDateTime = "2026-02-01T00:00:00")
val response = service.listRequests( val response = service.listRequests(
ListRequestsQueryDto( RequestListFilter(
endFrom = OffsetDateTime.parse("2026-01-10T00:00:00Z"), endFrom = OffsetDateTime.parse("2026-01-10T00:00:00Z"),
endTo = OffsetDateTime.parse("2026-01-31T23:59:59Z"), endTo = OffsetDateTime.parse("2026-01-31T23:59:59Z"),
) )
@@ -154,7 +167,7 @@ class RequestServiceListRequestsJpaTest @Autowired constructor(
saveRequest(createdAt = "2026-01-02T00:00:00") saveRequest(createdAt = "2026-01-02T00:00:00")
val oldestId = saveRequest(createdAt = "2026-01-01T00:00:00") val oldestId = saveRequest(createdAt = "2026-01-01T00:00:00")
val response = service.listRequests(ListRequestsQueryDto(page = 1, size = 2)) val response = service.listRequests(RequestListFilter(page = 1, size = 2))
assertEquals(listOf(oldestId), response.items.map { request -> request.id }) assertEquals(listOf(oldestId), response.items.map { request -> request.id })
assertEquals(1, response.page) assertEquals(1, response.page)
@@ -169,7 +182,7 @@ class RequestServiceListRequestsJpaTest @Autowired constructor(
val newestId = saveRequest(createdAt = "2026-01-03T00:00:00") val newestId = saveRequest(createdAt = "2026-01-03T00:00:00")
val middleId = saveRequest(createdAt = "2026-01-02T00:00:00") val middleId = saveRequest(createdAt = "2026-01-02T00:00:00")
val response = service.listRequests(ListRequestsQueryDto()) val response = service.listRequests(RequestListFilter())
assertEquals(listOf(newestId, middleId, oldestId), response.items.map { request -> request.id }) assertEquals(listOf(newestId, middleId, oldestId), response.items.map { request -> request.id })
} }
@@ -179,7 +192,7 @@ class RequestServiceListRequestsJpaTest @Autowired constructor(
val latestBeginId = saveRequest(beginDateTime = "2026-01-03T00:00:00") val latestBeginId = saveRequest(beginDateTime = "2026-01-03T00:00:00")
val earliestBeginId = saveRequest(beginDateTime = "2026-01-01T00:00:00") val earliestBeginId = saveRequest(beginDateTime = "2026-01-01T00:00:00")
val response = service.listRequests(ListRequestsQueryDto(sort = "beginDateTime,asc")) val response = service.listRequests(RequestListFilter(sort = "beginDateTime,asc"))
assertEquals(listOf(earliestBeginId, latestBeginId), response.items.map { request -> request.id }) assertEquals(listOf(earliestBeginId, latestBeginId), response.items.map { request -> request.id })
} }
@@ -11,8 +11,8 @@ import org.mockito.Mockito.mock
import org.mockito.Mockito.never import org.mockito.Mockito.never
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoInteractions import org.mockito.Mockito.verifyNoInteractions
import org.nstart.dep265.requestservice.dto.CreateRequestRequestDto import org.nstart.dep265.requestservice.dto.CreateRequestDto
import org.nstart.dep265.requestservice.dto.ListRequestsQueryDto import org.nstart.dep265.requestservice.dto.RequestListFilter
import org.nstart.dep265.requestservice.dto.OpticsParamsDto import org.nstart.dep265.requestservice.dto.OpticsParamsDto
import org.nstart.dep265.requestservice.dto.OpticsResultTypeDto import org.nstart.dep265.requestservice.dto.OpticsResultTypeDto
import org.nstart.dep265.requestservice.entity.EarthCellEntity import org.nstart.dep265.requestservice.entity.EarthCellEntity
@@ -215,12 +215,38 @@ class RequestServiceTest {
pageableMatcher(), pageableMatcher(),
) )
val response = service.listRequests(ListRequestsQueryDto()) val response = service.listRequests(RequestListFilter())
assertEquals(REQUEST_NAME, response.items.single().name) assertEquals(REQUEST_NAME, response.items.single().name)
assertEquals(REQUEST_IMPORTANCE, response.items.single().importance) assertEquals(REQUEST_IMPORTANCE, response.items.single().importance)
} }
@Test
fun `list requests with geometry returns expanded summary`() {
doReturn(PageImpl(listOf(existingRequest())))
.`when`(requestRepository)
.findRequests(
anyBoolean(),
deletedStatusMatcher(),
isNull(),
isNull(),
isNull(),
isNull(),
isNull(),
isNull(),
isNull(),
isNull(),
pageableMatcher(),
)
val response = service.listRequestsForMap(RequestListFilter())
assertEquals(REQUEST_NAME, response.items.single().name)
assertEquals("POLYGON ((37.5 55.5, 37.7 55.5, 37.7 55.7, 37.5 55.7, 37.5 55.5))", response.items.single().geometry)
assertEquals(REQUEST_IMPORTANCE, response.items.single().importance)
assertEquals(50.0, response.items.single().coveragePercent)
}
@Test @Test
fun `create request rejects duplicate id without touching existing request state`() { fun `create request rejects duplicate id without touching existing request state`() {
doReturn(true) doReturn(true)
@@ -254,8 +280,8 @@ class RequestServiceTest {
verify(requestCellRepository).deleteByRequestId(REQUEST_ID) verify(requestCellRepository).deleteByRequestId(REQUEST_ID)
} }
private fun createRequestDto(): CreateRequestRequestDto { private fun createRequestDto(): CreateRequestDto {
return CreateRequestRequestDto( return CreateRequestDto(
id = REQUEST_ID, id = REQUEST_ID,
name = REQUEST_NAME, 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))", geometry = "POLYGON ((37.5 55.5, 37.7 55.5, 37.7 55.7, 37.5 55.7, 37.5 55.5))",
@@ -52,7 +52,7 @@ class EarthService(
fun reqcells(id : String): RequestWithCellsDTO? = fun reqcells(id : String): RequestWithCellsDTO? =
webClientBuilder.build() webClientBuilder.build()
.get() .get()
.uri("$url/v1/requests/$id/with-cells") .uri("$url/v1/requests/$id/cells")
.retrieve() .retrieve()
.bodyToMono(RequestWithCellsResponseDto::class.java) .bodyToMono(RequestWithCellsResponseDto::class.java)
.block() .block()
@@ -38,7 +38,7 @@ class CoverageSchemeServiceTest {
val timeStop = LocalDateTime.of(2026, 4, 14, 12, 0) val timeStop = LocalDateTime.of(2026, 4, 14, 12, 0)
val serverUrl = startServer( val serverUrl = startServer(
mapOf( mapOf(
"/v1/requests/req-1/with-cells" to stringHandler(200, requestWithCellsJson()), "/v1/requests/req-1/cells" to stringHandler(200, requestWithCellsJson()),
"/api/coverage-schemes/calculate" to jsonHandler( "/api/coverage-schemes/calculate" to jsonHandler(
CoverageSchemeResponseDTO( CoverageSchemeResponseDTO(
targetPolygonWkt = "POLYGON ((30 10, 40 40, 20 40, 30 10))", targetPolygonWkt = "POLYGON ((30 10, 40 40, 20 40, 30 10))",
@@ -111,7 +111,7 @@ class CoverageSchemeServiceTest {
fun `requestCoverageScheme converts backend error to CustomErrorException`() { fun `requestCoverageScheme converts backend error to CustomErrorException`() {
val serverUrl = startServer( val serverUrl = startServer(
mapOf( mapOf(
"/v1/requests/req-1/with-cells" to stringHandler(200, requestWithCellsJson()), "/v1/requests/req-1/cells" to stringHandler(200, requestWithCellsJson()),
"/api/coverage-schemes/calculate" to stringHandler(400, """{"message":"coverage failed"}""") "/api/coverage-schemes/calculate" to stringHandler(400, """{"message":"coverage failed"}""")
) )
) )
@@ -377,7 +377,7 @@ class EarthServiceTest {
service.reqcells(requestId.toString()) service.reqcells(requestId.toString())
assertEquals("/v1/requests/$requestId/with-cells", requestedUris.single().path) assertEquals("/v1/requests/$requestId/cells", requestedUris.single().path)
assertFalse(requestedUris.any { uri -> uri.path.startsWith("/api/") }) assertFalse(requestedUris.any { uri -> uri.path.startsWith("/api/") })
} }
@@ -49,7 +49,7 @@ class EarthService(
fun reqcells(id : String): RequestWithCellsDTO? = fun reqcells(id : String): RequestWithCellsDTO? =
webClientBuilder.build() webClientBuilder.build()
.get() .get()
.uri("$url/v1/requests/$id/with-cells") .uri("$url/v1/requests/$id/cells")
.retrieve() .retrieve()
.bodyToMono(RequestWithCellsResponseDto::class.java) .bodyToMono(RequestWithCellsResponseDto::class.java)
.block() .block()
@@ -115,7 +115,7 @@ class EarthServiceTest {
val response = service.reqcells(requestId.toString())!! val response = service.reqcells(requestId.toString())!!
assertEquals("/v1/requests/$requestId/with-cells", requestedUris.single().path) assertEquals("/v1/requests/$requestId/cells", requestedUris.single().path)
assertFalse(requestedUris.any { uri -> uri.path.contains("/api/requests/by-number") }) assertFalse(requestedUris.any { uri -> uri.path.contains("/api/requests/by-number") })
assertEquals(requestId, response.request.requestId) assertEquals(requestId, response.request.requestId)
assertEquals("Request with cells", response.request.name) assertEquals("Request with cells", response.request.name)