Skip to content

Commit

Permalink
MLT-0039 Deleting orders (#32)
Browse files Browse the repository at this point in the history
Fixed duplicate order creation, removed Enum values where not needed, added endpoint to remove orders
  • Loading branch information
MormonJesus69420 authored Sep 5, 2024
1 parent 1f82bce commit 1f43459
Show file tree
Hide file tree
Showing 9 changed files with 201 additions and 85 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,12 @@

<properties>
<java.version>21</java.version>
<mockk.version>3.0.1</mockk.version>
<kotlin.version>2.0.0</kotlin.version>
<openapi.version>2.6.0</openapi.version>
<spotless.version>2.43.0</spotless.version>
<spring.cloud.version>2023.0.1</spring.cloud.version>
<testcontainers.version>1.19.8</testcontainers.version>
<mockk.version>3.0.1</mockk.version>

<kotlin.code.style>official</kotlin.code.style>
<kotlin.compiler.jvmTarget>21</kotlin.compiler.jvmTarget>
Expand Down
11 changes: 7 additions & 4 deletions src/main/kotlin/no/nb/mlt/wls/core/data/synq/SynqError.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@ data class SynqError(val errorCode: Int, val errorText: String) {
val errorBody = error.getResponseBodyAs(SynqError::class.java)

return ServerErrorException(
"Failed to create product in SynQ, the storage system responded with error code: " +
"'${errorBody?.errorCode ?: "NO ERROR CODE FOUND"}' " +
"and error text: " +
"'${errorBody?.errorText ?: "NO ERROR TEXT FOUND"}'",
"""
While communicating with SynQ API, an error occurred with code:
'${errorBody?.errorCode ?: "NO ERROR CODE FOUND"}'
and error text:
'${errorBody?.errorText ?: "NO ERROR TEXT FOUND"}'.
A copy of the original exception is attached to this error.
""".trimIndent(),
error
)
}
Expand Down
49 changes: 42 additions & 7 deletions src/main/kotlin/no/nb/mlt/wls/order/controller/OrderController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import no.nb.mlt.wls.order.service.OrderService
import org.springframework.http.ResponseEntity
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
Expand All @@ -29,8 +30,7 @@ class OrderController(val orderService: OrderService) {
@Operation(
summary = "Creates an order for products from the storage system",
description = """Creates an order for specified products to appropriate storage systems via Hermes WLS.
Orders are automatically sent to the systems that own the respective product(s).
"""
Orders are automatically sent to the systems that own the respective product(s)."""
)
@ApiResponses(
ApiResponse(
Expand Down Expand Up @@ -67,7 +67,7 @@ class OrderController(val orderService: OrderService) {
),
ApiResponse(
responseCode = "401",
description = "Client sending the request is not authorized order products.",
description = "Client sending the request is not authorized to order products.",
content = [Content(schema = Schema())]
),
ApiResponse(
Expand All @@ -76,11 +76,11 @@ class OrderController(val orderService: OrderService) {
content = [Content(schema = Schema())]
)
)
@PostMapping("/order/batch/create")
@PostMapping("/order")
suspend fun createOrder(
@AuthenticationPrincipal jwt: JwtAuthenticationToken,
@RequestBody payload: ApiOrderPayload
): ResponseEntity<ApiOrderPayload> = orderService.createOrder(jwt.name, payload)
): ResponseEntity<ApiOrderPayload> = orderService.createOrder(payload, jwt.name)

@Operation(
summary = "Gets an order from the storage system",
Expand All @@ -104,7 +104,7 @@ class OrderController(val orderService: OrderService) {
),
ApiResponse(
responseCode = "401",
description = "Client sending the request is not authorized to request orders.",
description = "Client sending the request is not authorized to request orders, or this order does not belong to them.",
content = [Content(schema = Schema())]
),
ApiResponse(
Expand Down Expand Up @@ -145,7 +145,7 @@ class OrderController(val orderService: OrderService) {
),
ApiResponse(
responseCode = "401",
description = "Client sending the request is not authorized order products.",
description = "Client sending the request is not authorized to update orders, or this order does not belong to them.",
content = [Content(schema = Schema())]
),
ApiResponse(
Expand All @@ -164,4 +164,39 @@ class OrderController(val orderService: OrderService) {
@AuthenticationPrincipal jwt: JwtAuthenticationToken,
@RequestBody payload: ApiUpdateOrderPayload
): ResponseEntity<ApiOrderPayload> = orderService.updateOrder(payload, jwt.name)

@Operation(
summary = "Deletes an order from the storage system",
description = """Deletes an order from the appropriate storage systems via Hermes WLS.
To delete an order it must have a status of "NOT_STARTED".
Additionally the caller must "own", e.g. be the creator of the order."""
)
@ApiResponses(
ApiResponse(
responseCode = "200",
description = "Order with given 'hostName' and 'hostOrderId' was deleted from the system.",
content = [Content(schema = Schema())]
),
ApiResponse(
responseCode = "401",
description = "Client sending the request is not authorized to delete orders, or this order does not belong to them."
),
ApiResponse(
responseCode = "403",
description = """A valid 'Authorization' header is missing from the request,
or the caller is not authorized to delete the order."""
),
ApiResponse(
responseCode = "404",
description = "Order with given 'hostName' and 'hostOrderId' does not exist in the system."
)
)
@DeleteMapping("/order/{hostName}/{hostOrderId}")
suspend fun deleteOrder(
@PathVariable hostName: HostName,
@PathVariable hostOrderId: String,
@AuthenticationPrincipal caller: JwtAuthenticationToken
): ResponseEntity<String> {
return orderService.deleteOrder(hostName, hostOrderId, caller.name.uppercase())
}
}
2 changes: 1 addition & 1 deletion src/main/kotlin/no/nb/mlt/wls/order/model/Order.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import org.springframework.data.mongodb.core.mapping.Document
data class Order(
@Id
private val id: ObjectId = ObjectId(),
val hostOrderId: String,
val hostName: HostName,
val hostOrderId: String,
val status: OrderStatus,
val productLine: List<ProductLine>,
val orderType: OrderType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,9 @@ interface OrderRepository : ReactiveMongoRepository<Order, String> {
hostName: HostName,
hostOrderId: String
): Mono<Order>

fun deleteByHostNameAndHostOrderId(
hostName: HostName,
hostOrderId: String
): Mono<Void>
}
81 changes: 60 additions & 21 deletions src/main/kotlin/no/nb/mlt/wls/order/service/OrderService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,12 @@ class OrderService(val db: OrderRepository, val synqService: SynqOrderService) {
* Creates an order within the WLS database, and sends it to the appropriate storage systems
*/
suspend fun createOrder(
clientName: String,
payload: ApiOrderPayload
payload: ApiOrderPayload,
clientName: String
): ResponseEntity<ApiOrderPayload> {
throwIfInvalidClientName(clientName, payload.hostName)
throwIfInvalidPayload(payload)

val existingOrder = findOrderInDb(payload.hostName, payload.hostOrderId)

if (existingOrder != null) {
Expand Down Expand Up @@ -97,42 +98,80 @@ class OrderService(val db: OrderRepository, val synqService: SynqOrderService) {
return ResponseEntity.ok(updatedOrder.toApiOrderPayload())
}

/**
* Gets an order from the WLS database
*/
suspend fun getOrder(
clientName: String,
hostName: HostName,
hostOrderId: String
): ResponseEntity<Order> {
throwIfInvalidClientName(clientName, hostName)

val order =
findOrderInDb(hostName, hostOrderId) ?: throw ResponseStatusException(
HttpStatus.NOT_FOUND,
"Order with id $hostOrderId from $hostName was not found"
)

return ResponseEntity.ok(order)
}

/**
* Query the WLS database if an order exists
*/
private suspend fun findOrderInDb(
suspend fun deleteOrder(
hostName: HostName,
hostOrderId: String
): Order? {
return db.findByHostNameAndHostOrderId(hostName, hostOrderId)
.timeout(Duration.ofSeconds(8))
hostOrderId: String,
clientName: String
): ResponseEntity<String> {
throwIfInvalidClientName(clientName, hostName)

if (hostOrderId.isBlank()) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body("The order's hostOrderId is required, and can not be blank")
}

val order =
findOrderInDb(hostName, hostOrderId)
?: return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body("Cannot find order with hostName: $hostName and hostOrderId: $hostOrderId")

if (order.status != OrderStatus.NOT_STARTED) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body("Order with hostName: $hostName and hostOrderId: $hostOrderId has status: ${order.status}, and can not be deleted")
}

val synqResponse = synqService.deleteOrder(hostName, hostOrderId)

if (!synqResponse.statusCode.isSameCodeAs(HttpStatus.OK)) {
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Failed to delete order in SynQ, error from synq: ${synqResponse.body}")
}

db.deleteByHostNameAndHostOrderId(hostName, hostOrderId)
.timeout(Duration.ofSeconds(6))
.onErrorMap {
if (it is TimeoutException) {
logger.error(it) {
"Timed out while fetching order $hostOrderId from WLS database. Owner: $hostName"
}
} else {
logger.error(it) { "Unexpected error for getting $hostOrderId from $hostName" }
}
ServerErrorException("Failed while checking if order already exists in the database", it)
logger.error(it) { "Failed to delete order with hostName: $hostName and hostOrderId: $hostOrderId" }
ServerErrorException("Failed to delete order in the database", it)
}
.awaitSingleOrNull()

return ResponseEntity.ok().build()
}

private suspend fun findOrderInDb(
hostName: HostName,
hostOrderId: String
) = db.findByHostNameAndHostOrderId(hostName, hostOrderId)
.timeout(Duration.ofSeconds(8))
.onErrorMap {
if (it is TimeoutException) {
logger.error(it) {
"Timed out while fetching order from WLS database. HostName: $hostName, hostOrderId: $hostOrderId"
}
} else {
logger.error(it) { "Unexpected error while fetching order with HostName: $hostName, hostOrderId: $hostOrderId" }
}
ServerErrorException("Failed while checking if order already exists in the database", it)
}
.awaitSingleOrNull()
}
30 changes: 24 additions & 6 deletions src/main/kotlin/no/nb/mlt/wls/order/service/SynqOrderService.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package no.nb.mlt.wls.order.service

import kotlinx.coroutines.reactor.awaitSingle
import no.nb.mlt.wls.core.data.HostName
import no.nb.mlt.wls.core.data.synq.SynqError
import no.nb.mlt.wls.core.data.synq.SynqError.Companion.createServerError
import no.nb.mlt.wls.order.payloads.ApiUpdateOrderPayload
Expand All @@ -16,6 +17,7 @@ import org.springframework.stereotype.Service
import org.springframework.web.reactive.function.BodyInserters
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.WebClientResponseException
import org.springframework.web.reactive.function.client.toEntity
import org.springframework.web.server.ResponseStatusException
import org.springframework.web.server.ServerErrorException
import reactor.core.publisher.Mono
Expand All @@ -29,14 +31,12 @@ class SynqOrderService(
lateinit var baseUrl: String

suspend fun createOrder(payload: SynqOrderPayload): ResponseEntity<SynqError> {
val uri = URI.create("$baseUrl/orders/batch")

// Wrap the order in the way SynQ likes it
val orders = SynqOrder(listOf(payload))

return webClient
.post()
.uri(uri)
.uri(URI.create("$baseUrl/orders/batch"))
.body(BodyInserters.fromValue(orders))
.retrieve()
.toEntity(SynqError::class.java)
Expand All @@ -57,20 +57,38 @@ class SynqOrderService(
}

suspend fun updateOrder(payload: ApiUpdateOrderPayload): ResponseEntity<SynqError> {
val uri = URI.create("$baseUrl/orders/batch")

// TODO - Should maybe just get a regular SynqOrderPayload here instead of converting it?
// If anything the regular order service should do the conversion either at call site or somewhere else
val orders = SynqOrder(listOf(payload.toOrder().toSynqPayload()))

// TODO - Extend error handling
return webClient
.put()
.uri(uri)
.uri(URI.create("$baseUrl/orders/batch"))
.bodyValue(orders)
.retrieve()
.toEntity(SynqError::class.java)
.onErrorMap(WebClientResponseException::class.java) { createServerError(it) }
.awaitSingle()
}

suspend fun deleteOrder(
hostName: HostName,
hostOrderId: String
): ResponseEntity<SynqError> {
return webClient
.delete()
.uri(URI.create("$baseUrl/orders/$hostName/$hostOrderId"))
.exchangeToMono { response ->
if (response.statusCode().isSameCodeAs(HttpStatus.OK)) {
response.toEntity<SynqError>()
} else {
response.createError()
}
}
.onErrorMap(WebClientResponseException::class.java) { createServerError(it) }
.awaitSingle()
}
}

class DuplicateOrderException(override val cause: Throwable) : ServerErrorException("Order already exists in SynQ", cause)
Loading

0 comments on commit 1f43459

Please sign in to comment.