Skip to content

Commit

Permalink
MLT-0030 Order Controller (#20)
Browse files Browse the repository at this point in the history
Started implementing order controller
  • Loading branch information
anotheroneofthese authored Aug 1, 2024
1 parent e7742ca commit 7883e6c
Show file tree
Hide file tree
Showing 12 changed files with 260 additions and 96 deletions.
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mongodb</artifactId>
Expand Down
3 changes: 3 additions & 0 deletions src/main/kotlin/no/nb/mlt/wls/core/data/synq/SynqError.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package no.nb.mlt.wls.core.data.synq

data class SynqError(val errorCode: Int, val errorText: String)
54 changes: 54 additions & 0 deletions src/main/kotlin/no/nb/mlt/wls/order/controller/OrderController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package no.nb.mlt.wls.order.controller

import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.media.Content
import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses
import io.swagger.v3.oas.annotations.tags.Tag
import no.nb.mlt.wls.order.payloads.ApiOrderPayload
import no.nb.mlt.wls.order.service.OrderService
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping(path = ["", "/v1"])
@Tag(name = "Order Controller", description = "API for ordering products via Hermes WLS")
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).
"""
)
@ApiResponses(
ApiResponse(
responseCode = "201",
description = "Created order for specified products to appropriate systems"
),
ApiResponse(
responseCode = "400",
description =
"""Order payload is invalid and was not created.
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())]
)
)
@PostMapping("/order/batch/create")
fun createOrder(
@RequestBody payload: ApiOrderPayload
): ResponseEntity<ApiOrderPayload> = orderService.createOrder(payload)
}
23 changes: 18 additions & 5 deletions src/main/kotlin/no/nb/mlt/wls/order/payloads/SynqOrderPayload.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
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
import no.nb.mlt.wls.core.data.synq.toOwner
Expand All @@ -14,23 +17,25 @@ import java.time.LocalDateTime
data class SynqOrderPayload(
val orderId: String,
val orderType: SynqOrderType,
// "yyyy-MM-ddTHH:mm:ssZ"
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'")
val dispatchDate: LocalDateTime,
// "yyyy-MM-ddTHH:mm:ssZ"
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'")
val orderDate: LocalDateTime,
val priority: Int,
val owner: SynqOwner,
val orderLine: List<OrderLine>
) {
data class OrderLine(
@Min(1)
val orderLineNumber: Int,
val productId: String,
val quantityOrdered: Double
)

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

@JsonValue
override fun toString(): String {
return type
}
Expand Down Expand Up @@ -73,15 +78,15 @@ fun Order.toSynqPayload() =
orderType = orderType.toSynqOrderType(),
// When order should be dispatched, AFAIK it's not used by us as we don't receive orders in future
dispatchDate = LocalDateTime.now(),
// When order was made in SynQ, if we want to we can ommit it and SynQ will set it to current date itself
// When order was made in SynQ, if we want to we can omit it and SynQ will set it to current date itself
orderDate = LocalDateTime.now(),
// TODO: we don't get it from API so we set it to 1, is other value more appropriate?
priority = 1,
owner = owner?.toSynqOwner() ?: SynqOwner.NB,
orderLine =
productLine.mapIndexed { index, it ->
SynqOrderPayload.OrderLine(
orderLineNumber = index,
orderLineNumber = index + 1,
productId = it.hostId,
quantityOrdered = 1.0
)
Expand All @@ -98,3 +103,11 @@ fun OrderType.toSynqOrderType(): SynqOrderPayload.SynqOrderType =
OrderType.LOAN -> SynqOrderPayload.SynqOrderType.STANDARD // TODO: Since mock api defined more types than Synq has we map both to standard
OrderType.DIGITIZATION -> SynqOrderPayload.SynqOrderType.STANDARD
}

// TODO - Improve this

/**
* Utility classed used to wrap the payload.
* This is required for SynQ's specification of handling orders
*/
data class SynqOrder(val order: List<SynqOrderPayload>)
14 changes: 14 additions & 0 deletions src/main/kotlin/no/nb/mlt/wls/order/repository/OrderRepository.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package no.nb.mlt.wls.order.repository

import no.nb.mlt.wls.core.data.HostName
import no.nb.mlt.wls.order.model.Order
import org.springframework.data.mongodb.repository.MongoRepository
import org.springframework.stereotype.Repository

@Repository
interface OrderRepository : MongoRepository<Order, String> {
fun getByHostNameAndHostOrderId(
hostName: HostName,
hostOrderId: String
): Order?
}
36 changes: 36 additions & 0 deletions src/main/kotlin/no/nb/mlt/wls/order/service/OrderService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package no.nb.mlt.wls.order.service

import no.nb.mlt.wls.order.payloads.ApiOrderPayload
import no.nb.mlt.wls.order.payloads.toApiOrderPayload
import no.nb.mlt.wls.order.payloads.toOrder
import no.nb.mlt.wls.order.payloads.toSynqPayload
import no.nb.mlt.wls.order.repository.OrderRepository
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Service
import org.springframework.web.server.ServerErrorException

@Service
class OrderService(val db: OrderRepository, val synqService: SynqOrderService) {
fun createOrder(payload: ApiOrderPayload): ResponseEntity<ApiOrderPayload> {
// TODO - Order validation?

val existingOrder = db.getByHostNameAndHostOrderId(payload.hostName, payload.orderId)
if (existingOrder != null) {
return ResponseEntity.ok(existingOrder.toApiOrderPayload())
}

val synqResponse = synqService.createOrder(payload.toOrder().toSynqPayload())
if (!synqResponse.statusCode.is2xxSuccessful) {
return ResponseEntity.internalServerError().build()
}

try {
db.save(payload.toOrder())
} catch (e: Exception) {
throw ServerErrorException("Failed to save product in database, but created in storage system", e)
}

return ResponseEntity.status(HttpStatus.CREATED).build()
}
}
43 changes: 43 additions & 0 deletions src/main/kotlin/no/nb/mlt/wls/order/service/SynqOrderService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package no.nb.mlt.wls.order.service

import no.nb.mlt.wls.core.data.synq.SynqError
import no.nb.mlt.wls.order.payloads.SynqOrder
import no.nb.mlt.wls.order.payloads.SynqOrderPayload
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.HttpEntity
import org.springframework.http.HttpMethod
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Service
import org.springframework.web.client.HttpClientErrorException
import org.springframework.web.client.RestTemplate
import org.springframework.web.server.ServerErrorException
import java.net.URI

@Service
class SynqOrderService {
val restTemplate: RestTemplate = RestTemplate()

@Value("\${synq.path.base}")
lateinit var baseUrl: String

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))

try {
return restTemplate.exchange(uri, HttpMethod.POST, HttpEntity(orders), SynqError::class.java)
} catch (e: HttpClientErrorException) {
val errorBody = e.getResponseBodyAs(SynqError::class.java)

throw 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"}'",
e
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package no.nb.mlt.wls.product.repository
import no.nb.mlt.wls.core.data.HostName
import no.nb.mlt.wls.product.model.Product
import org.springframework.data.mongodb.repository.MongoRepository
import org.springframework.stereotype.Repository

@Repository
interface ProductRepository : MongoRepository<Product, String> {
fun findByHostNameAndHostId(
hostName: HostName,
id: String
hostId: String
): Product?
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import kotlin.math.ceil
import kotlin.math.floor

@Service
class ProductService(val db: ProductRepository, val synqService: SynqService) {
class ProductService(val db: ProductRepository, val synqProductService: SynqProductService) {
fun save(payload: ApiProductPayload): ResponseEntity<ApiProductPayload> {
// Check if the payload is valid and throw an exception if it is not
throwIfInvalidPayload(payload)
Expand All @@ -34,7 +34,7 @@ class ProductService(val db: ProductRepository, val synqService: SynqService) {
val product = payload.toProduct().copy(quantity = 0.0, location = null)

// Product service should create the product in the storage system, and return error message if it fails
if (synqService.createProduct(product.toSynqPayload()).statusCode.isSameCodeAs(HttpStatus.OK)) {
if (synqProductService.createProduct(product.toSynqPayload()).statusCode.isSameCodeAs(HttpStatus.OK)) {
return ResponseEntity.ok().build()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package no.nb.mlt.wls.product.service

import no.nb.mlt.wls.core.data.synq.SynqError
import no.nb.mlt.wls.product.payloads.SynqProductPayload
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.HttpEntity
Expand All @@ -12,7 +13,7 @@ import org.springframework.web.server.ServerErrorException
import java.net.URI

@Service
class SynqService {
class SynqProductService {
val restTemplate: RestTemplate = RestTemplate()

@Value("\${synq.path.base}")
Expand Down Expand Up @@ -47,6 +48,4 @@ class SynqService {
)
}
}

data class SynqError(val errorCode: Int, val errorText: String)
}
85 changes: 0 additions & 85 deletions src/test/kotlin/no/nb/mlt/wls/controller/ProductControllerTest.kt

This file was deleted.

Loading

0 comments on commit 7883e6c

Please sign in to comment.