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-0048 Architecture refactoring #35

Merged
merged 23 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b65f4c6
Refactored product controller, service etc
tomolse Sep 9, 2024
0d88cec
Architecture refactoring
anotheroneofthese Sep 9, 2024
940edd0
spotless
anotheroneofthese Sep 9, 2024
813f7ab
Move ports to new packages (outbound and inbound)
tomolse Sep 9, 2024
60c9451
Move core enums and classes to domain package
tomolse Sep 9, 2024
2759d57
Remove unnecessary comment
tomolse Sep 9, 2024
04dde0f
Refactored most of the order code into new architecture
tomolse Sep 10, 2024
36d8eee
Update test comment
anotheroneofthese Sep 10, 2024
ad950a5
Fix getting orders, and refactor order creation
anotheroneofthese Sep 10, 2024
05ea704
Refactor OrderRepository, and refactor order deletion
anotheroneofthese Sep 11, 2024
b2bc1a3
Reimplement ItemController tests
anotheroneofthese Sep 11, 2024
64413d9
Reimplement OrderController tests
anotheroneofthese Sep 11, 2024
84043fa
Fix some tests
anotheroneofthese Sep 13, 2024
9586c28
Remove old service tests
anotheroneofthese Sep 13, 2024
8caf989
Refactor order update, exception handling in controller and run spotl…
tomolse Sep 13, 2024
ed1f277
Make rest api return 200 when item exists
tomolse Sep 13, 2024
6cef33c
Disable test that should be refactored
tomolse Sep 13, 2024
8768ccb
Rename Product to Item
anotheroneofthese Sep 13, 2024
7fd19dd
Merge remote-tracking branch 'origin/architecture-refactoring' into a…
anotheroneofthese Sep 13, 2024
e58959f
Add architecture tests to make sure the project does not break the st…
tomolse Sep 14, 2024
33bd330
Spotless fix
tomolse Sep 15, 2024
317d154
Test WLSService functions
tomolse Sep 17, 2024
056f3f8
Implement update and delete order tests
anotheroneofthese Sep 17, 2024
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
6 changes: 3 additions & 3 deletions docker/mongo/mongo-init.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ db.createUser(
roles: [{ role: 'readWrite', db: 'wls' }],
},
);
db.createCollection('products');
db.createCollection('items');

db.products.insertOne({
db.items.insertOne({
"hostName": "AXIELL",
"hostId": "product-12345",
"category": "BOOK",
Expand All @@ -24,7 +24,7 @@ db.products.insertOne({
"quantity": 1,
"preferredEnvironment": "NONE",
"owner": "NB",
"_class": "no.nb.mlt.wls.product.model.Product"
"_class": "no.nb.mlt.wls.domain.model.Item"
})

print('END #################################################################');
7 changes: 6 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,12 @@
<artifactId>kotlinx-coroutines-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>1.3.0</version>
<scope>test</scope>
</dependency>

</dependencies>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package no.nb.mlt.wls.application.restapi

import no.nb.mlt.wls.domain.ports.inbound.IllegalOrderStateException
import no.nb.mlt.wls.domain.ports.inbound.OrderNotFoundException
import no.nb.mlt.wls.domain.ports.inbound.ServerException
import no.nb.mlt.wls.domain.ports.inbound.ValidationException
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice

@RestControllerAdvice
class ExceptionHandler {
@ExceptionHandler(ValidationException::class)
fun handleValidationException(e: ValidationException): ResponseEntity<ErrorMessage> {
return ResponseEntity
.badRequest()
.body(ErrorMessage(e.message))
}

@ExceptionHandler(IllegalOrderStateException::class)
fun handleIllegalOrderStateException(e: IllegalOrderStateException): ResponseEntity<ErrorMessage> {
return ResponseEntity
.status(HttpStatus.CONFLICT)
.body(ErrorMessage(e.message))
}

@ExceptionHandler(ServerException::class)
fun handleServerException(e: ServerException): ResponseEntity<ErrorMessage> {
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ErrorMessage(e.message))
}

@ExceptionHandler(OrderNotFoundException::class)
fun handleOrderNotFoundException(e: OrderNotFoundException): ResponseEntity<ErrorMessage> {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(ErrorMessage(e.message))
}
}

data class ErrorMessage(val message: String)
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package no.nb.mlt.wls.core.config
package no.nb.mlt.wls.application.restapi.config

import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package no.nb.mlt.wls.core.config
package no.nb.mlt.wls.application.restapi.config

import io.swagger.v3.oas.annotations.enums.SecuritySchemeType
import io.swagger.v3.oas.annotations.security.OAuthFlow
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package no.nb.mlt.wls.core.config
package no.nb.mlt.wls.application.restapi.config

import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package no.nb.mlt.wls.product.payloads
package no.nb.mlt.wls.application.restapi.item

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.Environment
import no.nb.mlt.wls.core.data.HostName
import no.nb.mlt.wls.core.data.Owner
import no.nb.mlt.wls.core.data.Packaging
import no.nb.mlt.wls.product.model.Product
import no.nb.mlt.wls.domain.model.Environment
import no.nb.mlt.wls.domain.model.HostName
import no.nb.mlt.wls.domain.model.Item
import no.nb.mlt.wls.domain.model.Owner
import no.nb.mlt.wls.domain.model.Packaging
import no.nb.mlt.wls.domain.ports.inbound.ItemMetadata

@Schema(
description = "Payload for registering a product in Hermes WLS, and appropriate storage system for the product.",
Expand All @@ -22,7 +23,7 @@ import no.nb.mlt.wls.product.model.Product
}
"""
)
data class ApiProductPayload(
data class ApiItemPayload(
@Schema(
description = "The product ID from the host system, usually a barcode or an equivalent ID.",
example = "mlt-12345"
Expand Down Expand Up @@ -82,8 +83,8 @@ data class ApiProductPayload(
val quantity: Double?
)

fun ApiProductPayload.toProduct() =
Product(
fun ApiItemPayload.toItem() =
Item(
hostId = hostId,
hostName = hostName,
description = description,
Expand All @@ -95,8 +96,8 @@ fun ApiProductPayload.toProduct() =
quantity = quantity
)

fun Product.toApiPayload() =
ApiProductPayload(
fun Item.toApiPayload() =
ApiItemPayload(
hostId = hostId,
hostName = hostName,
description = description,
Expand All @@ -107,3 +108,14 @@ fun Product.toApiPayload() =
location = location,
quantity = quantity
)

fun ApiItemPayload.toItemMetadata(): ItemMetadata =
ItemMetadata(
hostId = hostId,
hostName = hostName,
description = description,
productCategory = productCategory,
preferredEnvironment = preferredEnvironment,
packaging = packaging,
owner = owner
)
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
package no.nb.mlt.wls.product.controller
package no.nb.mlt.wls.application.restapi.item

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.product.payloads.ApiProductPayload
import no.nb.mlt.wls.product.service.ProductService
import no.nb.mlt.wls.domain.ports.inbound.AddNewItem
import no.nb.mlt.wls.domain.ports.inbound.GetItem
import org.springframework.http.HttpStatus
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
import org.springframework.web.server.ServerWebInputException

@RestController
@RequestMapping(path = ["", "/v1"])
@Tag(name = "Product Controller", description = "API for managing products in Hermes WLS")
class ProductController(val productService: ProductService) {
class ProductController(
private val addNewItem: AddNewItem,
private val getItem: GetItem
) {
@Operation(
summary = "Register a product in the storage system",
description = """Register data about the product in Hermes WLS and appropriate storage system,
Expand All @@ -38,7 +43,7 @@ class ProductController(val productService: ProductService) {
content = [
Content(
mediaType = "application/json",
schema = Schema(implementation = ApiProductPayload::class)
schema = Schema(implementation = ApiItemPayload::class)
)
]
),
Expand All @@ -50,7 +55,7 @@ class ProductController(val productService: ProductService) {
content = [
Content(
mediaType = "application/json",
schema = Schema(implementation = ApiProductPayload::class)
schema = Schema(implementation = ApiItemPayload::class)
)
]
),
Expand All @@ -74,6 +79,28 @@ class ProductController(val productService: ProductService) {
)
@PostMapping("/product")
suspend fun createProduct(
@RequestBody payload: ApiProductPayload
): ResponseEntity<ApiProductPayload> = productService.save(payload)
@RequestBody payload: ApiItemPayload
): ResponseEntity<ApiItemPayload> {
throwIfInvalidPayload(payload)
getItem.getItem(payload.hostName, payload.hostId)?.let {
return ResponseEntity.ok(it.toApiPayload())
}

val item = addNewItem.addItem(payload.toItemMetadata())
return ResponseEntity(item.toApiPayload(), HttpStatus.CREATED)
}

private fun throwIfInvalidPayload(payload: ApiItemPayload) {
if (payload.hostId.isBlank()) {
throw ServerWebInputException("The product's hostId is required, and it cannot be blank")
}

if (payload.description.isBlank()) {
throw ServerWebInputException("The product's description is required, and it cannot be blank")
}

if (payload.productCategory.isBlank()) {
throw ServerWebInputException("The product's category is required, and it cannot be blank")
}
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
package no.nb.mlt.wls.order.payloads
package no.nb.mlt.wls.application.restapi.order

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 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 no.nb.mlt.wls.domain.model.HostName
import no.nb.mlt.wls.domain.model.Order
import no.nb.mlt.wls.domain.model.Owner
import org.springframework.web.server.ServerWebInputException

@Schema(
Expand Down Expand Up @@ -49,7 +45,7 @@ data class ApiOrderPayload(
val orderId: String,
@Schema(
description = "Name of the host system which made the order.",
examples = [ "AXIELL", "ALMA", "ASTA", "BIBLIOFIL" ]
examples = ["AXIELL", "ALMA", "ASTA", "BIBLIOFIL"]
)
val hostName: HostName,
@Schema(
Expand All @@ -59,62 +55,65 @@ data class ApiOrderPayload(
val hostOrderId: String,
@Schema(
description = "Current status of the order as a whole.",
examples = [ "NOT_STARTED", "IN_PROGRESS", "COMPLETED", "DELETED" ]
examples = ["NOT_STARTED", "IN_PROGRESS", "COMPLETED", "DELETED"]
)
val status: OrderStatus?,
val status: Order.Status?,
@Schema(
description = "List of products in the order, also called order lines, or product lines.",
accessMode = READ_ONLY
)
val productLine: List<ProductLine>,
val productLine: List<Order.OrderItem>,
@Schema(
description = "Describes what type of order this is",
examples = [ "LOAN", "DIGITIZATION" ]
examples = ["LOAN", "DIGITIZATION"]
)
val orderType: OrderType,
val orderType: Order.Type,
@Schema(
description = "Who's the owner of the material in the order.",
examples = [ "NB", "ARKIVVERKET" ],
examples = ["NB", "ARKIVVERKET"],
accessMode = READ_ONLY
)
val owner: Owner?,
@Schema(
description = "Who's the receiver of the material in the order."
)
val receiver: OrderReceiver,
val receiver: Order.Receiver,
@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.toOrder() =
Order(
fun Order.toApiOrderPayload() =
ApiOrderPayload(
orderId = hostOrderId,
hostName = hostName,
hostOrderId = hostOrderId,
status = status ?: OrderStatus.NOT_STARTED,
productLine = productLine,
status = status,
productLine = this.productLine,
orderType = orderType,
owner = owner,
receiver = receiver,
callbackUrl = callbackUrl
)

fun Order.toApiOrderPayload() =
ApiOrderPayload(
orderId = hostOrderId,
fun ApiOrderPayload.toOrder(): Order =
Order(
hostName = hostName,
hostOrderId = hostOrderId,
status = status,
productLine = productLine,
status = status ?: Order.Status.NOT_STARTED,
productLine =
productLine.map {
Order.OrderItem(it.hostId, Order.OrderItem.Status.NOT_STARTED)
},
orderType = orderType,
owner = owner,
receiver = receiver,
callbackUrl = callbackUrl
)

fun throwIfInvalidPayload(payload: ApiOrderPayload) {
fun throwIfInvalid(payload: ApiOrderPayload) {
if (payload.orderId.isBlank()) {
throw ServerWebInputException("The order's orderId is required, and can not be blank")
}
Expand Down
Loading