Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MLT-0043 Update Order (PUT) endpoint #31

Merged
merged 15 commits into from
Aug 29, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions src/main/kotlin/no/nb/mlt/wls/order/controller/OrderController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import io.swagger.v3.oas.annotations.tags.Tag
import no.nb.mlt.wls.core.data.HostName
import no.nb.mlt.wls.order.model.Order
import no.nb.mlt.wls.order.payloads.ApiOrderPayload
import no.nb.mlt.wls.order.payloads.ApiUpdateOrderPayload
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.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
Expand Down Expand Up @@ -117,4 +119,49 @@ class OrderController(val orderService: OrderService) {
@PathVariable("hostName") hostName: HostName,
@PathVariable("hostOrderId") hostOrderId: String
): ResponseEntity<Order> = orderService.getOrder(jwt.name, hostName, hostOrderId)

@Operation(
summary = "Updates an existing order in the storage system(s)",
description = """Updates a specified order to the various storage systems via Hermes WLS.
"""
)
@ApiResponses(
ApiResponse(
responseCode = "200",
description = "The order was updated with the new products, and sent to appropriate systems",
content = [
Content(
mediaType = "application/json",
schema = Schema(implementation = ApiOrderPayload::class)
)
]
),
ApiResponse(
responseCode = "400",
description = """Order payload is invalid and the order was not updated.
This error is also produced if the order specified does not exist.
Otherwise, the error message contains information about the invalid fields.""",
content = [Content(schema = Schema())]
),
ApiResponse(
responseCode = "401",
description = "Client sending the request is not authorized order products.",
content = [Content(schema = Schema())]
),
ApiResponse(
responseCode = "403",
description = "A valid 'Authorization' header is missing from the request.",
content = [Content(schema = Schema())]
),
ApiResponse(
responseCode = "409",
description = "The order is already being processed, and can not be edited at this point.",
content = [Content(schema = Schema())]
)
)
@PutMapping("/order")
suspend fun updateOrder(
@AuthenticationPrincipal jwt: JwtAuthenticationToken,
@RequestBody payload: ApiUpdateOrderPayload
): ResponseEntity<ApiOrderPayload> = orderService.updateOrder(payload, jwt.name)
}
10 changes: 8 additions & 2 deletions src/main/kotlin/no/nb/mlt/wls/order/model/Order.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,18 @@ import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY
import no.nb.mlt.wls.core.data.HostName
import no.nb.mlt.wls.core.data.Owner
import org.bson.types.ObjectId
import org.springframework.data.annotation.Id
import org.springframework.data.mongodb.core.index.CompoundIndex
import org.springframework.data.mongodb.core.mapping.Document

@CompoundIndex(unique = true, def = "{'hostName':1,'hostOrderId':1}")
MormonJesus69420 marked this conversation as resolved.
Show resolved Hide resolved
@Document(collection = "orders")
data class Order(
val hostName: HostName,
@Id
private val id: ObjectId = ObjectId(),
val hostOrderId: String,
val hostName: HostName,
val status: OrderStatus,
val productLine: List<ProductLine>,
val orderType: OrderType,
Expand All @@ -35,7 +41,7 @@ data class ProductLine(
val hostId: String,
@Schema(
description = "Current status of the order line.",
examples = [ "NOT_STARTED", "PICKED", "FAILED" ],
examples = ["NOT_STARTED", "PICKED", "FAILED"],
defaultValue = "NOT_STARTED",
accessMode = READ_ONLY
)
Expand Down
13 changes: 13 additions & 0 deletions src/main/kotlin/no/nb/mlt/wls/order/payloads/ApiOrderPayload.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import no.nb.mlt.wls.order.model.OrderReceiver
import no.nb.mlt.wls.order.model.OrderStatus
import no.nb.mlt.wls.order.model.OrderType
import no.nb.mlt.wls.order.model.ProductLine
import org.springframework.web.server.ServerWebInputException

@Schema(
description = "Payload for creating and editing an order in Hermes WLS, and appropriate storage system(s).",
Expand Down Expand Up @@ -112,3 +113,15 @@ fun Order.toApiOrderPayload() =
receiver = receiver,
callbackUrl = callbackUrl
)

fun throwIfInvalidPayload(payload: ApiOrderPayload) {
if (payload.orderId.isBlank()) {
throw ServerWebInputException("The order's orderId is required, and can not be blank")
}
if (payload.hostOrderId.isBlank()) {
throw ServerWebInputException("The order's hostOrderId is required, and can not be blank")
}
if (payload.productLine.isEmpty()) {
throw ServerWebInputException("The order must contain product lines")
}
}
110 changes: 110 additions & 0 deletions src/main/kotlin/no/nb/mlt/wls/order/payloads/ApiUpdateOrderPayload.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package no.nb.mlt.wls.order.payloads

import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY
import no.nb.mlt.wls.core.data.HostName
import no.nb.mlt.wls.order.model.Order
import no.nb.mlt.wls.order.model.OrderReceiver
import no.nb.mlt.wls.order.model.OrderStatus
import no.nb.mlt.wls.order.model.OrderType
import no.nb.mlt.wls.order.model.ProductLine
import org.springframework.web.server.ServerWebInputException

@Schema(
description = "Payload for creating and editing an order in Hermes WLS, and appropriate storage system(s).",
example = """
{
"hostName": "AXIELL",
"hostOrderId": "mlt-12345-order",
"productLine": [
{
"hostId": "mlt-12345",
"status": "NOT_STARTED"
}
],
"orderType": "LOAN",
"receiver": {
"name": "Doug Dimmadome",
"location": "Doug Dimmadome's office in the Dimmsdale Dimmadome",
"address": "Dimmsdale Dimmadome",
"city": "Dimmsdale",
"postalCode": "69-420",
"phoneNum": "+47 666 69 420"
},
"callbackUrl": "https://example.com/send/callback/here"
}
"""
)
data class ApiUpdateOrderPayload(
@Schema(
description = "Name of the host system which made the order.",
examples = [ "AXIELL", "ALMA", "ASTA", "BIBLIOFIL" ]
)
val hostName: HostName,
@Schema(
description = "Order ID from the host system which made the order.",
example = "mlt-12345-order"
)
val hostOrderId: String,
@Schema(
description = "List of products in the order, also called order lines, or product lines.",
accessMode = READ_ONLY
)
val productLine: List<ProductLine>,
@Schema(
description = "Describes what type of order this is",
examples = [ "LOAN", "DIGITIZATION" ]
)
val orderType: OrderType,
@Schema(
description = "Who's the receiver of the material in the order."
)
val receiver: OrderReceiver,
@Schema(
description = "URL to send a callback to when the order is completed.",
example = "https://example.com/send/callback/here"
)
val callbackUrl: String
)

fun ApiOrderPayload.toUpdateOrderPayload() =
ApiUpdateOrderPayload(
hostName = hostName,
hostOrderId = hostOrderId,
productLine = productLine,
orderType = orderType,
receiver = receiver,
callbackUrl = callbackUrl
)

fun ApiUpdateOrderPayload.toOrder() =
Order(
hostOrderId = hostOrderId,
hostName = hostName,
status = OrderStatus.NOT_STARTED,
productLine = productLine,
orderType = orderType,
// TODO - validate if this is ok to do
owner = null,
receiver = receiver,
callbackUrl = callbackUrl
)

fun Order.toUpdateOrderPayload() =
ApiUpdateOrderPayload(
hostName = hostName,
hostOrderId = hostOrderId,
productLine = productLine,
orderType = orderType,
receiver = receiver,
callbackUrl = callbackUrl
)

fun throwIfInvalidPayload(payload: ApiUpdateOrderPayload) {
if (payload.hostOrderId.isBlank()) {
throw ServerWebInputException("The order's hostOrderId is required, and can not be blank")
}
if (payload.productLine.isEmpty()) {
throw ServerWebInputException("The order must contain product lines")
}
}
10 changes: 8 additions & 2 deletions src/main/kotlin/no/nb/mlt/wls/order/payloads/SynqOrderPayload.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package no.nb.mlt.wls.order.payloads

import com.fasterxml.jackson.annotation.JsonFormat
import com.fasterxml.jackson.annotation.JsonValue
import jakarta.validation.constraints.Min
import no.nb.mlt.wls.core.data.HostName
import no.nb.mlt.wls.core.data.synq.SynqOwner
Expand Down Expand Up @@ -31,8 +32,13 @@ data class SynqOrderPayload(
val quantityOrdered: Double
)

enum class SynqOrderType {
STANDARD
enum class SynqOrderType(private val type: String) {
STANDARD("Standard");

@JsonValue
override fun toString(): String {
return type
}
}
}

Expand Down
96 changes: 64 additions & 32 deletions src/main/kotlin/no/nb/mlt/wls/order/service/OrderService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import kotlinx.coroutines.reactor.awaitSingleOrNull
import no.nb.mlt.wls.core.data.HostName
import no.nb.mlt.wls.core.data.throwIfInvalidClientName
import no.nb.mlt.wls.order.model.Order
import no.nb.mlt.wls.order.model.OrderStatus
import no.nb.mlt.wls.order.payloads.ApiOrderPayload
import no.nb.mlt.wls.order.payloads.ApiUpdateOrderPayload
import no.nb.mlt.wls.order.payloads.throwIfInvalidPayload
import no.nb.mlt.wls.order.payloads.toApiOrderPayload
import no.nb.mlt.wls.order.payloads.toOrder
import no.nb.mlt.wls.order.payloads.toSynqPayload
Expand All @@ -16,7 +19,6 @@ import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Service
import org.springframework.web.server.ResponseStatusException
import org.springframework.web.server.ServerErrorException
import org.springframework.web.server.ServerWebInputException
import java.time.Duration
import java.util.concurrent.TimeoutException

Expand All @@ -33,21 +35,7 @@ class OrderService(val db: OrderRepository, val synqService: SynqOrderService) {
): ResponseEntity<ApiOrderPayload> {
throwIfInvalidClientName(clientName, payload.hostName)
throwIfInvalidPayload(payload)

val existingOrder =
db.findByHostNameAndHostOrderId(payload.hostName, payload.hostOrderId)
.timeout(Duration.ofSeconds(8))
.onErrorMap {
if (it is TimeoutException) {
logger.error(it) {
"Timed out while fetching from WLS database. Relevant payload: $payload"
}
} else {
logger.error(it) { "Unexpected error for $payload" }
}
ServerErrorException("Failed while checking if order already exists in the database", it)
}
.awaitSingleOrNull()
val existingOrder = findOrderInDb(payload.hostName, payload.hostOrderId)

if (existingOrder != null) {
return ResponseEntity.ok(existingOrder.toApiOrderPayload())
Expand Down Expand Up @@ -76,6 +64,41 @@ class OrderService(val db: OrderRepository, val synqService: SynqOrderService) {
return ResponseEntity.status(HttpStatus.CREATED).body(order.toApiOrderPayload())
}

suspend fun updateOrder(
payload: ApiUpdateOrderPayload,
clientName: String
): ResponseEntity<ApiOrderPayload> {
throwIfInvalidClientName(clientName, payload.hostName)
throwIfInvalidPayload(payload)

val existingOrder =
findOrderInDb(payload.hostName, payload.hostOrderId)
?: throw ResponseStatusException(
HttpStatus.BAD_REQUEST,
"Order with id $payload.hostOrderId from $payload.hostName does not exist in the database"
)

if (existingOrder.status != OrderStatus.NOT_STARTED) {
throw ResponseStatusException(HttpStatus.CONFLICT, "Order is already being processed, and can not be edited")
}
synqService.updateOrder(payload)

// Saving here will override the existing order, as the id's match
val updatedOrder =
db.save(
existingOrder.copy(
hostOrderId = payload.hostOrderId,
hostName = payload.hostName,
MormonJesus69420 marked this conversation as resolved.
Show resolved Hide resolved
productLine = payload.productLine,
orderType = payload.orderType,
receiver = payload.receiver,
callbackUrl = payload.callbackUrl
)
).awaitSingle()

return ResponseEntity.ok(updatedOrder.toApiOrderPayload())
}

/**
* Gets an order from the WLS database
*/
Expand All @@ -86,23 +109,32 @@ class OrderService(val db: OrderRepository, val synqService: SynqOrderService) {
): ResponseEntity<Order> {
throwIfInvalidClientName(clientName, hostName)
val order =
db.findByHostNameAndHostOrderId(hostName, hostOrderId)
.awaitSingleOrNull()
if (order != null) {
return ResponseEntity.ok(order)
}
throw ResponseStatusException(HttpStatus.NOT_FOUND, "Order with id $hostOrderId from $hostName was not found")
findOrderInDb(hostName, hostOrderId) ?: throw ResponseStatusException(
HttpStatus.NOT_FOUND,
"Order with id $hostOrderId from $hostName was not found"
)
return ResponseEntity.ok(order)
}

private fun throwIfInvalidPayload(payload: ApiOrderPayload) {
if (payload.orderId.isBlank()) {
throw ServerWebInputException("The order's orderId is required, and can not be blank")
}
if (payload.hostOrderId.isBlank()) {
throw ServerWebInputException("The order's hostOrderId is required, and can not be blank")
}
if (payload.productLine.isEmpty()) {
throw ServerWebInputException("The order must contain product lines")
}
/**
* Query the WLS database if an order exists
*/
private suspend fun findOrderInDb(
hostName: HostName,
hostOrderId: String
): Order? {
return db.findByHostNameAndHostOrderId(hostName, hostOrderId)
.timeout(Duration.ofSeconds(8))
.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)
}
.awaitSingleOrNull()
}
}
Loading