diff --git a/src/main/kotlin/no/nb/mlt/wls/application/hostapi/item/ApiCreateItemPayload.kt b/src/main/kotlin/no/nb/mlt/wls/application/hostapi/item/ApiCreateItemPayload.kt new file mode 100644 index 0000000..35dbe30 --- /dev/null +++ b/src/main/kotlin/no/nb/mlt/wls/application/hostapi/item/ApiCreateItemPayload.kt @@ -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 + ) diff --git a/src/main/kotlin/no/nb/mlt/wls/application/hostapi/item/ApiItemPayload.kt b/src/main/kotlin/no/nb/mlt/wls/application/hostapi/item/ApiItemPayload.kt index 177f56c..1c00ddb 100644 --- a/src/main/kotlin/no/nb/mlt/wls/application/hostapi/item/ApiItemPayload.kt +++ b/src/main/kotlin/no/nb/mlt/wls/application/hostapi/item/ApiItemPayload.kt @@ -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 @@ -72,8 +71,7 @@ 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?, @@ -81,7 +79,6 @@ data class ApiItemPayload( 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? diff --git a/src/main/kotlin/no/nb/mlt/wls/application/hostapi/item/ItemController.kt b/src/main/kotlin/no/nb/mlt/wls/application/hostapi/item/ItemController.kt index 02596be..3cb5c3b 100644 --- a/src/main/kotlin/no/nb/mlt/wls/application/hostapi/item/ItemController.kt +++ b/src/main/kotlin/no/nb/mlt/wls/application/hostapi/item/ItemController.kt @@ -111,7 +111,7 @@ class ItemController( @PostMapping("/item") suspend fun createItem( @AuthenticationPrincipal jwt: JwtAuthenticationToken, - @RequestBody payload: ApiItemPayload + @RequestBody payload: ApiCreateItemPayload ): ResponseEntity { jwt.checkIfAuthorized(payload.hostName) payload.validate() diff --git a/src/main/kotlin/no/nb/mlt/wls/application/hostapi/order/ApiCreateOrderPayload.kt b/src/main/kotlin/no/nb/mlt/wls/application/hostapi/order/ApiCreateOrderPayload.kt new file mode 100644 index 0000000..fa366de --- /dev/null +++ b/src/main/kotlin/no/nb/mlt/wls/application/hostapi/order/ApiCreateOrderPayload.kt @@ -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, + @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 + ) diff --git a/src/main/kotlin/no/nb/mlt/wls/application/hostapi/order/OrderController.kt b/src/main/kotlin/no/nb/mlt/wls/application/hostapi/order/OrderController.kt index ff6fa26..0292b17 100644 --- a/src/main/kotlin/no/nb/mlt/wls/application/hostapi/order/OrderController.kt +++ b/src/main/kotlin/no/nb/mlt/wls/application/hostapi/order/OrderController.kt @@ -121,7 +121,7 @@ class OrderController( @PostMapping("/order") suspend fun createOrder( @AuthenticationPrincipal jwt: JwtAuthenticationToken, - @RequestBody payload: ApiOrderPayload + @RequestBody payload: ApiCreateOrderPayload ): ResponseEntity { jwt.checkIfAuthorized(payload.hostName) diff --git a/src/main/kotlin/no/nb/mlt/wls/infrastructure/callbacks/NotificationItemPayload.kt b/src/main/kotlin/no/nb/mlt/wls/infrastructure/callbacks/NotificationItemPayload.kt index 448130a..f81ba1b 100644 --- a/src/main/kotlin/no/nb/mlt/wls/infrastructure/callbacks/NotificationItemPayload.kt +++ b/src/main/kotlin/no/nb/mlt/wls/infrastructure/callbacks/NotificationItemPayload.kt @@ -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 @@ -71,8 +70,7 @@ 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?, @@ -80,7 +78,6 @@ data class NotificationItemPayload( 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?