Skip to content

Commit

Permalink
Host API Controller changes (#55)
Browse files Browse the repository at this point in the history
Update HostAPI models to reflect newest changes to the system
  • Loading branch information
anotheroneofthese authored Dec 19, 2024
1 parent addd3db commit 69787f9
Show file tree
Hide file tree
Showing 6 changed files with 269 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package no.nb.mlt.wls.application.hostapi.item

import io.swagger.v3.oas.annotations.media.Schema
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.ItemCategory
import no.nb.mlt.wls.domain.model.Packaging
import no.nb.mlt.wls.domain.ports.inbound.ItemMetadata
import no.nb.mlt.wls.domain.ports.inbound.ValidationException
import org.apache.commons.validator.routines.UrlValidator

@Schema(
description = """Payload for registering an item in Hermes WLS, and appropriate storage systems.""",
example = """
{
"hostId": "mlt-12345",
"hostName": "AXIELL",
"description": "Tyven, tyven skal du hete",
"itemCategory": "PAPER",
"preferredEnvironment": "NONE",
"packaging": "NONE",
"callbackUrl": "https://callback-wls.no/item"
}
"""
)
data class ApiCreateItemPayload(
@Schema(
description = """The item ID from the host system, usually a barcode or an equivalent ID.""",
example = "mlt-12345"
)
val hostId: String,
@Schema(
description = """Name of the host system which the item originates from.
Host system is usually the catalogue that the item is registered in.""",
examples = [ "AXIELL", "ALMA", "ASTA", "BIBLIOFIL" ]
)
val hostName: HostName,
@Schema(
description = """Description of the item for easy identification in the warehouse system.
Usually an item title/name, e.g. book title, film name, etc. or contents description.""",
examples = ["Tyven, tyven skal du hete", "Avisa Hemnes", "Kill Buljo"]
)
val description: String,
@Schema(
description = """Item's category, same category indicates that the items can be stored together without any preservation issues.
For example: books, magazines, newspapers, etc. are of type PAPER, and can be stored together without damaging each other.""",
examples = ["PAPER", "DISC", "FILM", "PHOTO", "EQUIPMENT", "BULK_ITEMS", "MAGNETIC_TAPE"]
)
val itemCategory: ItemCategory,
@Schema(
description = """What kind of environment the item should be stored in.
"NONE" is for normal storage for the item category, "FRYS" is for frozen storage, etc.
NOTE: This is not a guarantee that the item will be stored in the preferred environment.
In cases where storage space is limited, the item may be stored in a different environment.""",
examples = ["NONE", "FRYS"]
)
val preferredEnvironment: Environment,
@Schema(
description = """Whether the item is a single object or a container with other items inside.
"NONE" is for single objects, "ABOX" is for archival boxes, etc.
NOTE: It is up to the catalogue to keep track of the items inside a container.""",
examples = ["NONE", "BOX", "ABOX", "CRATE"]
)
val packaging: Packaging,
@Schema(
description = """Callback URL to use for sending item updates to the host system.
For example when item moves or changes quantity in storage.""",
example = "https://callback-wls.no/item"
)
val callbackUrl: String?
) {
fun toItem(): Item =
Item(
hostId = hostId,
hostName = hostName,
description = description,
itemCategory = itemCategory,
preferredEnvironment = preferredEnvironment,
packaging = packaging,
callbackUrl = callbackUrl,
location = "UNKNOWN",
quantity = 0
)

fun toItemMetadata(): ItemMetadata =
ItemMetadata(
hostId = hostId,
hostName = hostName,
description = description,
itemCategory = itemCategory,
preferredEnvironment = preferredEnvironment,
packaging = packaging,
callbackUrl = callbackUrl
)

@Throws(ValidationException::class)
fun validate() {
if (hostId.isBlank()) {
throw ValidationException("The item's 'hostId' is required, and it cannot be blank")
}

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

if (callbackUrl != null && !isValidUrl(callbackUrl)) {
throw ValidationException("The item's 'callback URL' must be valid if set")
}
}

private fun isValidUrl(url: String): Boolean {
// Yes I am aware that this function is duplicated in three places
// But I prefer readability to DRY in cases like this

val validator = UrlValidator(arrayOf("http", "https")) // Allow only HTTP/HTTPS
return validator.isValid(url)
}
}

fun Item.toCreateApiPayload() =
ApiCreateItemPayload(
hostId = hostId,
hostName = hostName,
description = description,
itemCategory = itemCategory,
preferredEnvironment = preferredEnvironment,
packaging = packaging,
callbackUrl = callbackUrl
)
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package no.nb.mlt.wls.application.hostapi.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.domain.model.Environment
import no.nb.mlt.wls.domain.model.HostName
import no.nb.mlt.wls.domain.model.Item
Expand Down Expand Up @@ -72,16 +71,14 @@ data class ApiItemPayload(
val callbackUrl: String?,
@Schema(
description = """Where the item is located, can be used for tracking item movement through storage systems.""",
examples = ["SYNQ_WAREHOUSE", "AUTOSTORE", "KARDEX"],
accessMode = READ_ONLY,
examples = ["UNKNOWN", "WITH_LENDER", "SYNQ_WAREHOUSE", "AUTOSTORE", "KARDEX"],
required = false
)
val location: String?,
@Schema(
description = """Quantity on hand of the item, this easily denotes if the item is in the storage or not.
If the item is in storage then quantity is 1, if it's not in storage then quantity is 0.""",
examples = [ "0", "1"],
accessMode = READ_ONLY,
required = false
)
val quantity: Int?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ class ItemController(
@PostMapping("/item")
suspend fun createItem(
@AuthenticationPrincipal jwt: JwtAuthenticationToken,
@RequestBody payload: ApiItemPayload
@RequestBody payload: ApiCreateItemPayload
): ResponseEntity<ApiItemPayload> {
jwt.checkIfAuthorized(payload.hostName)
payload.validate()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package no.nb.mlt.wls.application.hostapi.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.domain.model.HostName
import no.nb.mlt.wls.domain.model.Order
import no.nb.mlt.wls.domain.ports.inbound.CreateOrderDTO
import no.nb.mlt.wls.domain.ports.inbound.ValidationException
import org.apache.commons.validator.routines.UrlValidator

@Schema(
description = """Payload for creating orders in Hermes WLS, and appropriate storage system(s).""",
example = """
{
"hostName": "AXIELL",
"hostOrderId": "mlt-12345-order",
"orderLine": [
{
"hostId": "mlt-12345",
"status": "NOT_STARTED"
}
],
"orderType": "LOAN",
"contactPerson": "Dr. Heinz Doofenshmirtz",
"address": {
"recipient": "Doug Dimmadome",
"addressLine1": "Dimmsdale Dimmadome",
"addressLine2": "21st Texan Ave.",
"city": "Dimmsdale",
"country": "United States",
"region": "California",
"postcode": "CA-55415"
},
"note": "Handle with care",
"callbackUrl": "https://callback-wls.no/order"
}
"""
)
data class ApiCreateOrderPayload(
@Schema(
description = """Name of the host system which made the order.""",
examples = ["AXIELL", "ALMA", "ASTA", "BIBLIOFIL"]
)
val hostName: HostName,
@Schema(
description = """ID for the order, preferably the same ID as the one in the host system.""",
example = "mlt-12345-order"
)
val hostOrderId: String,
@Schema(
description = """List of items in the order, also called order lines.""",
accessMode = READ_ONLY
)
val orderLine: List<OrderLine>,
@Schema(
description = """Describes what type of order this is.
"LOAN" means that the order is for borrowing items to external or internal users,
usually meaning the items will be viewed, inspected, etc.
"DIGITIZATION" means that the order is specifically for digitizing items,
usually meaning that the order will be delivered to digitization workstation.""",
examples = ["LOAN", "DIGITIZATION"]
)
val orderType: Order.Type,
@Schema(
description = """Who to contact in relation to the order if case of any problems/issues/questions.""",
example = "Dr. Heinz Doofenshmirtz"
)
val contactPerson: String,
@Schema(
description = """Address for the order, used in cases where storage operator sends out the order directly.""",
example = "{...}"
)
val address: Order.Address?,
@Schema(
description = """Notes regarding the order, such as delivery instructions, special requests, etc.""",
example = "I need this order in four weeks, not right now."
)
val note: String?,
@Schema(
description = """Callback URL to use for sending order updates to the host system.
For example when order items get picked or the order is cancelled.""",
example = "https://callback-wls.no/order"
)
val callbackUrl: String
) {
fun toCreateOrderDTO() =
CreateOrderDTO(
hostName = hostName,
hostOrderId = hostOrderId,
orderLine = orderLine.map { it.toCreateOrderItem() },
orderType = orderType,
address = address,
contactPerson = contactPerson,
note = note,
callbackUrl = callbackUrl
)

@Throws(ValidationException::class)
fun validate() {
if (hostOrderId.isBlank()) {
throw ValidationException("The order's hostOrderId is required, and can not be blank")
}

if (orderLine.isEmpty()) {
throw ValidationException("The order must have at least one order line")
}

if (!isValidUrl(callbackUrl)) {
throw ValidationException("The order's callback URL is required, and must be a valid URL")
}

orderLine.forEach(OrderLine::validate)
address?.validate()
}

private fun isValidUrl(url: String): Boolean {
// Yes I am aware that this function is duplicated in three places
// But I prefer readability over DRY in cases like this

val validator = UrlValidator(arrayOf("http", "https")) // Allow only HTTP/HTTPS
return validator.isValid(url)
}
}

fun Order.toCreateApiOrderPayload() =
ApiCreateOrderPayload(
hostName = hostName,
hostOrderId = hostOrderId,
orderLine = orderLine.map { it.toApiOrderLine() },
orderType = orderType,
contactPerson = contactPerson,
address = address,
note = note,
callbackUrl = callbackUrl
)
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ class OrderController(
@PostMapping("/order")
suspend fun createOrder(
@AuthenticationPrincipal jwt: JwtAuthenticationToken,
@RequestBody payload: ApiOrderPayload
@RequestBody payload: ApiCreateOrderPayload
): ResponseEntity<ApiOrderPayload> {
jwt.checkIfAuthorized(payload.hostName)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package no.nb.mlt.wls.infrastructure.callbacks

import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY
import no.nb.mlt.wls.domain.model.Environment
import no.nb.mlt.wls.domain.model.HostName
import no.nb.mlt.wls.domain.model.Item
Expand Down Expand Up @@ -71,16 +70,14 @@ data class NotificationItemPayload(
val callbackUrl: String?,
@Schema(
description = """Where the item is located, can be used for tracking item movement through storage systems.""",
examples = ["SYNQ_WAREHOUSE", "AUTOSTORE", "KARDEX"],
accessMode = READ_ONLY,
examples = ["UNKNOWN", "WITH_LENDER", "SYNQ_WAREHOUSE", "AUTOSTORE", "KARDEX"],
required = false
)
val location: String?,
@Schema(
description = """Quantity on hand of the item, this easily denotes if the item is in the storage or not.
If the item is in storage then quantity is 1, if it's not in storage then quantity is 0.""",
examples = [ "0.0", "1.0"],
accessMode = READ_ONLY,
required = false
)
val quantity: Int?
Expand Down

0 comments on commit 69787f9

Please sign in to comment.