diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json
index c3a2678f4..a96d500b2 100644
--- a/descriptors/ModuleDescriptor-template.json
+++ b/descriptors/ModuleDescriptor-template.json
@@ -351,6 +351,17 @@
"users.item.get"
]
},
+ {
+ "methods": ["PATCH"],
+ "pathPattern": "/inventory/items/{id}",
+ "permissionsRequired": ["inventory.items.item.patch.execute"],
+ "modulePermissions": [
+ "inventory-storage.items.item.get",
+ "inventory-storage.items.collection.get",
+ "inventory-storage.items.item.patch.execute",
+ "users.item.get"
+ ]
+ },
{
"methods": ["DELETE"],
"pathPattern": "/inventory/items/{id}",
@@ -473,9 +484,9 @@
{
"methods": ["PATCH"],
"pathPattern": "/inventory/instances/{id}",
- "permissionsRequired": ["inventory.instances.item.patch"],
+ "permissionsRequired": ["inventory.instances.item.patch.execute"],
"modulePermissions": [
- "inventory-storage.instances.item.patch",
+ "inventory-storage.instances.item.patch.execute",
"inventory-storage.instances.item.get",
"inventory-storage.instances.item.post",
"inventory-storage.instances.item.delete",
@@ -679,7 +690,7 @@
},
{
"id": "item-storage",
- "version": "11.0"
+ "version": "11.2"
},
{
"id": "instance-storage",
@@ -877,6 +888,11 @@
"displayName": "Inventory - modify item",
"description": "Modify item"
},
+ {
+ "permissionName": "inventory.items.item.patch.execute",
+ "displayName": "Inventory - modify item partially",
+ "description": "Modify item partially"
+ },
{
"permissionName": "inventory.items.item.delete",
"displayName": "Inventory - delete individual item",
@@ -923,7 +939,7 @@
"description": "Modify instance"
},
{
- "permissionName": "inventory.instances.item.patch",
+ "permissionName": "inventory.instances.item.patch.execute",
"displayName": "Inventory - partially modify instance",
"description": "Partially modify instance"
},
@@ -953,6 +969,7 @@
"inventory.items.item.get",
"inventory.items.item.post",
"inventory.items.item.put",
+ "inventory.items.item.patch.execute",
"inventory.items.item.delete",
"inventory.items.collection.delete",
"inventory.instances.collection.get",
@@ -961,7 +978,7 @@
"inventory.instances.item.post",
"inventory.instances.batch.post",
"inventory.instances.item.put",
- "inventory.instances.item.patch",
+ "inventory.instances.item.patch.execute",
"inventory.instances.item.delete",
"inventory.instances.collection.delete",
"inventory.config.instances.blocked-fields.get",
diff --git a/pom.xml b/pom.xml
index 62f253ace..ed8d63ded 100644
--- a/pom.xml
+++ b/pom.xml
@@ -479,7 +479,7 @@
${basedir}/ramls/tenantItemPair.json
${basedir}/ramls/tenantItemPairCollection.json
${basedir}/ramls/tenantItemResponse.json
- ${basedir}/ramls/instance_patch.json
+ ${basedir}/ramls/patch_request.json
org.folio
true
diff --git a/ramls/examples/instance-patch.json b/ramls/examples/patch-request.json
similarity index 100%
rename from ramls/examples/instance-patch.json
rename to ramls/examples/patch-request.json
diff --git a/ramls/inventory.raml b/ramls/inventory.raml
index 19c2a7387..394a547d3 100644
--- a/ramls/inventory.raml
+++ b/ramls/inventory.raml
@@ -17,7 +17,7 @@ types:
instance: !include instance.json
instances: !include instances.json
tenantItemPairCollection: !include tenantItemPairCollection.json
- instancePatchRequest: !include instance_patch.json
+ patchRequest: !include patch_request.json
traits:
language: !include raml-util/traits/language.raml
@@ -76,6 +76,35 @@ resourceTypes:
exampleItem: !include examples/item_get.json
schema: item
get:
+ patch:
+ description: Partial update of item with given ID
+ body:
+ application/json:
+ type: patchRequest
+ example: !include examples/patch-request.json
+ responses:
+ 204:
+ description: "Item successfully updated"
+ 404:
+ description: "Item with a given ID not found"
+ body:
+ text/plain:
+ example: Not found
+ 400:
+ description: "Bad request, e.g. malformed request body or query parameter."
+ body:
+ text/plain:
+ example: "unable to update instance - malformed JSON"
+ 409:
+ description: "Optimistic locking version conflict"
+ body:
+ text/plain:
+ example: "version conflict"
+ 500:
+ description: "Internal server error, e.g. due to misconfiguration"
+ body:
+ text/plain:
+ example: "internal server error, contact administrator"
/mark-withdrawn:
post:
responses:
@@ -398,8 +427,8 @@ resourceTypes:
description: Partial update of instance with given ID
body:
application/json:
- type: instancePatchRequest
- example: !include examples/instance-patch.json
+ type: patchRequest
+ example: !include examples/patch-request.json
responses:
204:
description: "Instance successfully updated"
diff --git a/ramls/instance_patch.json b/ramls/patch_request.json
similarity index 87%
rename from ramls/instance_patch.json
rename to ramls/patch_request.json
index bc3bc6f2c..015aaca79 100644
--- a/ramls/instance_patch.json
+++ b/ramls/patch_request.json
@@ -1,7 +1,7 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "An instance record patch request",
- "javaType": "org.folio.rest.jaxrs.model.InstancePatchRequest",
+ "javaType": "org.folio.rest.jaxrs.model.PatchRequest",
"type": "object",
"properties": {
"id": {
diff --git a/src/main/java/org/folio/inventory/domain/items/LastCheckIn.java b/src/main/java/org/folio/inventory/domain/items/LastCheckIn.java
index 9032da0ff..151cc286f 100644
--- a/src/main/java/org/folio/inventory/domain/items/LastCheckIn.java
+++ b/src/main/java/org/folio/inventory/domain/items/LastCheckIn.java
@@ -1,10 +1,12 @@
package org.folio.inventory.domain.items;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import io.vertx.core.json.JsonObject;
import org.joda.time.DateTime;
import org.joda.time.format.ISODateTimeFormat;
+@JsonIgnoreProperties(ignoreUnknown = true)
public class LastCheckIn {
private final DateTime dateTime;
diff --git a/src/main/java/org/folio/inventory/resources/Items.java b/src/main/java/org/folio/inventory/resources/Items.java
index 58a8826d8..6616ab045 100644
--- a/src/main/java/org/folio/inventory/resources/Items.java
+++ b/src/main/java/org/folio/inventory/resources/Items.java
@@ -1,13 +1,17 @@
package org.folio.inventory.resources;
+import static java.util.concurrent.CompletableFuture.completedFuture;
import static org.folio.HttpStatus.HTTP_OK;
import static org.folio.inventory.common.FutureAssistance.allOf;
+import static org.folio.inventory.support.CompletableFutures.failedFuture;
import static org.folio.inventory.support.CqlHelper.multipleRecordsCqlQuery;
import static org.folio.inventory.support.EndpointFailureHandler.doExceptionally;
import static org.folio.inventory.support.EndpointFailureHandler.handleFailure;
import static org.folio.inventory.support.ItemUtil.HOLDINGS_RECORD_ID;
import static org.folio.inventory.support.http.server.JsonResponse.unprocessableEntity;
+import static org.folio.inventory.validation.ItemStatusValidator.checkStatusIfPresent;
import static org.folio.inventory.validation.ItemStatusValidator.itemHasCorrectStatus;
+import static org.folio.inventory.validation.ItemsValidator.barcodeChanged;
import static org.folio.inventory.validation.ItemsValidator.claimedReturnedMarkedAsMissing;
import static org.folio.inventory.validation.ItemsValidator.hridChanged;
@@ -99,6 +103,7 @@ public Items(final Storage storage, final HttpClient client) {
public void register(Router router) {
router.post(RELATIVE_ITEMS_PATH + "*").handler(BodyHandler.create());
router.put(RELATIVE_ITEMS_PATH + "*").handler(BodyHandler.create());
+ router.patch(RELATIVE_ITEMS_PATH + "*").handler(BodyHandler.create());
router.get(RELATIVE_ITEMS_PATH).handler(this::getAll);
router.post(RELATIVE_ITEMS_PATH + "/retrieve").handler(this::retrieveAllByCQLBody);
@@ -107,6 +112,7 @@ public void register(Router router) {
router.get(RELATIVE_ITEMS_PATH_ID).handler(this::getById);
router.put(RELATIVE_ITEMS_PATH_ID).handler(this::update);
+ router.patch(RELATIVE_ITEMS_PATH_ID).handler(this::patch);
router.delete(RELATIVE_ITEMS_PATH_ID).handler(this::deleteById);
Arrays.stream(ItemStatusName.values())
@@ -293,6 +299,53 @@ private void update(RoutingContext routingContext) {
}).exceptionally(doExceptionally(routingContext));
}
+ private void patch(RoutingContext routingContext) {
+ WebContext context = new WebContext(routingContext);
+
+ var patchRequest = routingContext.body().asJsonObject();
+
+ Optional validationError = checkStatusIfPresent(patchRequest);
+ if (validationError.isPresent()) {
+ unprocessableEntity(routingContext.response(), validationError.get());
+ return;
+ }
+
+ ItemCollection itemCollection = storage.getItemCollection(context);
+ UserCollection userCollection = storage.getUserCollection(context);
+
+ final String itemId = routingContext.request().getParam("id");
+ final CompletableFuture> getItemFuture = new CompletableFuture<>();
+
+ itemCollection.findById(itemId, getItemFuture::complete,
+ FailureResponseConsumer.serverError(routingContext.response()));
+
+ getItemFuture
+ .thenApply(Success::getResult)
+ .thenCompose(ItemsValidator::refuseWhenItemNotFound)
+ .thenCompose(oldItem ->
+ applyPatch(oldItem, patchRequest)
+ .thenCompose(patchedItem -> hridChanged(oldItem, patchedItem).thenApply(x -> patchedItem))
+ .thenCompose(patchedItem -> barcodeChanged(oldItem, patchedItem).thenApply(x -> patchedItem))
+ .thenCompose(patchedItem -> claimedReturnedMarkedAsMissing(oldItem, patchedItem))
+ .thenCompose(patchedItem -> {
+ findUserAndPatchItem(routingContext, patchRequest, oldItem, userCollection, itemCollection);
+ return completedFuture(null);
+ }
+ )
+ )
+ .exceptionally(doExceptionally(routingContext));
+ }
+
+ private CompletableFuture- applyPatch(Item existingItem, JsonObject patchJson) {
+ try {
+ JsonObject mergedJson = JsonObject.mapFrom(existingItem);
+ mergedJson.mergeIn(patchJson, false);
+ return completedFuture(ItemUtil.jsonToItem(mergedJson));
+ } catch (Exception e) {
+ return failedFuture(e);
+ }
+ }
+
private void deleteById(RoutingContext routingContext) {
WebContext context = new WebContext(routingContext);
CollectionResourceClient itemsStorageClient;
@@ -842,6 +895,19 @@ private void findUserAndUpdateItem(
failure -> updateItem(routingContext, newItem, oldItem, null, itemCollection));
}
+ private void findUserAndPatchItem(
+ RoutingContext routingContext,
+ JsonObject patchJson,
+ Item oldItem,
+ UserCollection userCollection,
+ ItemCollection itemCollection) {
+
+ String userId = routingContext.request().getHeader("X-Okapi-User-Id");
+ userCollection.findById(userId,
+ success -> patchItem(routingContext, patchJson, oldItem, success.getResult(), itemCollection),
+ failure -> patchItem(routingContext, patchJson, oldItem, null, itemCollection));
+ }
+
private void updateItem(
RoutingContext routingContext,
Item newItem,
@@ -849,18 +915,47 @@ private void updateItem(
User user,
ItemCollection itemCollection) {
- Map oldNotes = oldItem.getCirculationNotes()
+ List updatedNotes = updateCirculationNotes(oldItem.getCirculationNotes(),
+ newItem.getCirculationNotes(), user);
+
+ itemCollection.update(newItem.withCirculationNotes(updatedNotes),
+ v -> SuccessResponse.noContent(routingContext.response()),
+ failure -> ForwardResponse.forward(routingContext.response(), failure));
+ }
+
+ private void patchItem(
+ RoutingContext routingContext,
+ JsonObject patchJson,
+ Item oldItem,
+ User user,
+ ItemCollection itemCollection) {
+
+ if (patchJson.containsKey("circulationNotes")) {
+ var newItem = ItemUtil.jsonToItem(patchJson.mergeIn(JsonObject.mapFrom(oldItem), false));
+ List updatedNotes = updateCirculationNotes(oldItem.getCirculationNotes(),
+ newItem.getCirculationNotes(), user);
+ var notesJson = new JsonArray(
+ updatedNotes.stream()
+ .map(JsonObject::mapFrom)
+ .toList());
+ patchJson.put("circulationNotes", notesJson);
+ }
+
+ itemCollection.patch(patchJson.getString("id"), patchJson,
+ v -> SuccessResponse.noContent(routingContext.response()),
+ failure -> ForwardResponse.forward(routingContext.response(), failure));
+ }
+
+ List updateCirculationNotes(List oldNotes,
+ List newNotes, User user) {
+ Map oldNoteList = oldNotes
.stream()
.collect(Collectors.toMap(CirculationNote::getId, Function.identity()));
- List updatedNotes = newItem.getCirculationNotes()
+ return newNotes
.stream()
- .map(note -> updateCirculationNoteIfChanged(note, user, oldNotes))
+ .map(note -> updateCirculationNoteIfChanged(note, user, oldNoteList))
.collect(Collectors.toList());
-
- itemCollection.update(newItem.withCirculationNotes(updatedNotes),
- v -> SuccessResponse.noContent(routingContext.response()),
- failure -> ForwardResponse.forward(routingContext.response(), failure));
}
private void checkForNonUniqueBarcode(
diff --git a/src/main/java/org/folio/inventory/validation/ItemStatusValidator.java b/src/main/java/org/folio/inventory/validation/ItemStatusValidator.java
index 323563a8a..e44d524c2 100644
--- a/src/main/java/org/folio/inventory/validation/ItemStatusValidator.java
+++ b/src/main/java/org/folio/inventory/validation/ItemStatusValidator.java
@@ -28,4 +28,10 @@ public static Optional itemHasCorrectStatus(JsonObject itemRequ
return Optional.empty();
}
+
+ public static Optional checkStatusIfPresent(JsonObject patchRequest) {
+ return patchRequest.containsKey("status")
+ ? itemHasCorrectStatus(patchRequest)
+ : Optional.empty();
+ }
}
diff --git a/src/main/java/org/folio/inventory/validation/ItemsValidator.java b/src/main/java/org/folio/inventory/validation/ItemsValidator.java
index 9958e40ee..b5a508f2f 100644
--- a/src/main/java/org/folio/inventory/validation/ItemsValidator.java
+++ b/src/main/java/org/folio/inventory/validation/ItemsValidator.java
@@ -45,6 +45,17 @@ public static CompletableFuture
- hridChanged(Item oldItem, Item newItem) {
return completedFuture(oldItem);
}
+ public static CompletableFuture
- barcodeChanged(Item oldItem, Item newItem) {
+ if (!Objects.equals(newItem.getBarcode(), oldItem.getBarcode())) {
+ final ValidationError validationError = new ValidationError(
+ "Barcode can not be patched", "barcode", newItem.getBarcode());
+
+ return failedFuture(new UnprocessableEntityException(validationError));
+ }
+
+ return completedFuture(oldItem);
+ }
+
private static boolean isClaimedReturnedItemMarkedMissing(Item oldItem, Item newItem) {
return oldItem.getStatus().getName() == ItemStatusName.CLAIMED_RETURNED
&& newItem.getStatus().getName() == ItemStatusName.MISSING;
diff --git a/src/test/java/api/items/ItemApiExamples.java b/src/test/java/api/items/ItemApiExamples.java
index ecb3d555e..6db672256 100644
--- a/src/test/java/api/items/ItemApiExamples.java
+++ b/src/test/java/api/items/ItemApiExamples.java
@@ -1815,6 +1815,222 @@ public void canDeleteAdditionalCallNumbers() throws InterruptedException,
assertThat(response.getStatusCode(), is(204));
}
+ @Test
+ public void canPatchExistingItem()
+ throws InterruptedException,
+ MalformedURLException,
+ TimeoutException,
+ ExecutionException {
+
+ UUID TRANSIT_DESTINATION_SERVICE_POINT_ID_FOR_CREATE = UUID.randomUUID();
+ UUID TRANSIT_DESTINATION_SERVICE_POINT_ID_FOR_UPDATE = UUID.randomUUID();
+ UUID holdingId = createInstanceAndHolding();
+ UUID itemId = UUID.randomUUID();
+
+ JsonObject lastCheckIn = new JsonObject()
+ .put("servicePointId", "7c5abc9f-f3d7-4856-b8d7-6712462ca007")
+ .put("staffMemberId", "12115707-d7c8-54e7-8287-22e97f7250a4")
+ .put("dateTime", "2020-01-02T13:02:46.000Z");
+
+ JsonObject newItemRequest = new ItemRequestBuilder()
+ .withId(itemId)
+ .forHolding(holdingId)
+ .withInTransitDestinationServicePointId(TRANSIT_DESTINATION_SERVICE_POINT_ID_FOR_CREATE)
+ .withBarcode("645398607547")
+ .canCirculate()
+ .temporarilyInReadingRoom()
+ .withTagList(new JsonObject().put(Item.TAG_LIST_KEY, new JsonArray().add("test-tag")))
+ .withLastCheckIn(lastCheckIn)
+ .withCopyNumber("cp")
+ .create();
+
+ newItemRequest = itemsClient.create(newItemRequest).getJson();
+
+ assertThat(newItemRequest.getString("copyNumber"), is("cp"));
+ assertThat(newItemRequest.getString(Item.TRANSIT_DESTINATION_SERVICE_POINT_ID_KEY),
+ is(TRANSIT_DESTINATION_SERVICE_POINT_ID_FOR_CREATE.toString()));
+
+ var patchRequest = new JsonObject()
+ .put("id", itemId)
+ .put("barcode", newItemRequest.getString("barcode"))
+ .put("status", new JsonObject().put("name", "Checked out"))
+ .put("copyNumber", "updatedCp")
+ .put(Item.TRANSIT_DESTINATION_SERVICE_POINT_ID_KEY,
+ TRANSIT_DESTINATION_SERVICE_POINT_ID_FOR_UPDATE)
+ .put("tags", new JsonObject().put("tagList", new JsonArray().add("")));
+
+ itemsClient.patch(itemId, patchRequest);
+
+ Response getResponse = itemsClient.getById(itemId);
+
+ assertThat(getResponse.getStatusCode(), is(200));
+ JsonObject updatedItem = getResponse.getJson();
+
+ assertThat(getTags(updatedItem), hasItem(""));
+ assertThat(updatedItem.containsKey("id"), is(true));
+ assertThat(updatedItem.getString("title"), is("Long Way to a Small Angry Planet"));
+ assertThat(updatedItem.getString("barcode"), is("645398607547"));
+ assertThat(updatedItem.getJsonObject("status").getString("name"), is("Checked out"));
+ assertThat(updatedItem.getJsonObject(Item.LAST_CHECK_IN).getString("servicePointId"),
+ is("7c5abc9f-f3d7-4856-b8d7-6712462ca007"));
+ assertThat(updatedItem.getJsonObject(Item.LAST_CHECK_IN).getString("staffMemberId"),
+ is("12115707-d7c8-54e7-8287-22e97f7250a4"));
+ assertThat(updatedItem.getJsonObject(Item.LAST_CHECK_IN).getString("dateTime"),
+ is("2020-01-02T13:02:46.000Z"));
+ assertThat(updatedItem.getJsonObject("status").getString("name"), is("Checked out"));
+
+ JsonObject materialType = updatedItem.getJsonObject("materialType");
+
+ assertThat(materialType.getString("id"), is(ApiTestSuite.getBookMaterialType()));
+ assertThat(materialType.getString("name"), is("Book"));
+
+ JsonObject permanentLoanType = updatedItem.getJsonObject("permanentLoanType");
+
+ assertThat(permanentLoanType.getString("id"), is(ApiTestSuite.getCanCirculateLoanType()));
+ assertThat(permanentLoanType.getString("name"), is("Can Circulate"));
+
+ assertThat("Item should not have permanent location",
+ updatedItem.containsKey("permanentLocation"), is(false));
+
+ assertThat(updatedItem.getJsonObject("temporaryLocation").getString("name"), is("Reading Room"));
+
+ assertThat(updatedItem.getString("copyNumber"), is("updatedCp"));
+ assertThat(updatedItem.getString(Item.TRANSIT_DESTINATION_SERVICE_POINT_ID_KEY),
+ is(TRANSIT_DESTINATION_SERVICE_POINT_ID_FOR_UPDATE.toString()));
+ }
+
+ @Test
+ public void cannotPatchItemThatDoesNotExist()
+ throws InterruptedException,
+ MalformedURLException,
+ TimeoutException,
+ ExecutionException {
+
+ UUID holdingId = createInstanceAndHolding();
+ UUID itemId = UUID.randomUUID();
+
+ JsonObject patchItemRequest = new ItemRequestBuilder()
+ .withId(itemId)
+ .forHolding(holdingId)
+ .canCirculate()
+ .temporarilyInReadingRoom()
+ .create();
+
+ final var patchCompleted = okapiClient.patch(
+ String.format("%s/%s", ApiRoot.items(), patchItemRequest.getString("id")),
+ patchItemRequest);
+
+ Response patchResponse = patchCompleted.toCompletableFuture().get(5, SECONDS);
+
+ assertThat(patchResponse.getStatusCode(), is(404));
+ }
+
+ @Test
+ public void cannotPatchItemIfBarcodeWasChanged()
+ throws InterruptedException,
+ MalformedURLException,
+ TimeoutException,
+ ExecutionException {
+
+ UUID TRANSIT_DESTINATION_SERVICE_POINT_ID_FOR_CREATE = UUID.randomUUID();
+ UUID TRANSIT_DESTINATION_SERVICE_POINT_ID_FOR_UPDATE = UUID.randomUUID();
+ UUID holdingId = createInstanceAndHolding();
+ UUID itemId = UUID.randomUUID();
+
+ JsonObject lastCheckIn = new JsonObject()
+ .put("servicePointId", "7c5abc9f-f3d7-4856-b8d7-6712462ca007")
+ .put("staffMemberId", "12115707-d7c8-54e7-8287-22e97f7250a4")
+ .put("dateTime", "2020-01-02T13:02:46.000Z");
+
+ JsonObject newItemRequest = new ItemRequestBuilder()
+ .withId(itemId)
+ .forHolding(holdingId)
+ .withInTransitDestinationServicePointId(TRANSIT_DESTINATION_SERVICE_POINT_ID_FOR_CREATE)
+ .withBarcode("645398607547")
+ .canCirculate()
+ .temporarilyInReadingRoom()
+ .withTagList(new JsonObject().put(Item.TAG_LIST_KEY, new JsonArray().add("test-tag")))
+ .withLastCheckIn(lastCheckIn)
+ .withCopyNumber("cp")
+ .create();
+
+ newItemRequest = itemsClient.create(newItemRequest).getJson();
+
+ assertThat(newItemRequest.getString("copyNumber"), is("cp"));
+ assertThat(newItemRequest.getString(Item.TRANSIT_DESTINATION_SERVICE_POINT_ID_KEY),
+ is(TRANSIT_DESTINATION_SERVICE_POINT_ID_FOR_CREATE.toString()));
+
+ var patchRequest = new JsonObject()
+ .put("id", itemId)
+ .put("barcode", "new_barcode")
+ .put("status", new JsonObject().put("name", "Checked out"))
+ .put("copyNumber", "updatedCp")
+ .put(Item.TRANSIT_DESTINATION_SERVICE_POINT_ID_KEY,
+ TRANSIT_DESTINATION_SERVICE_POINT_ID_FOR_UPDATE)
+ .put("tags", new JsonObject().put("tagList", new JsonArray().add("")));
+
+ final var patchCompleted = okapiClient.patch(
+ String.format("%s/%s", ApiRoot.items(), patchRequest.getString("id")),
+ patchRequest);
+
+ Response patchResponse = patchCompleted.toCompletableFuture().get(5, SECONDS);
+
+ assertThat(patchResponse.getStatusCode(), is(422));
+ }
+
+ @Test
+ public void cannotPatchItemWithIncorrectStatus()
+ throws InterruptedException,
+ MalformedURLException,
+ TimeoutException,
+ ExecutionException {
+
+ UUID TRANSIT_DESTINATION_SERVICE_POINT_ID_FOR_CREATE = UUID.randomUUID();
+ UUID TRANSIT_DESTINATION_SERVICE_POINT_ID_FOR_UPDATE = UUID.randomUUID();
+ UUID holdingId = createInstanceAndHolding();
+ UUID itemId = UUID.randomUUID();
+
+ JsonObject lastCheckIn = new JsonObject()
+ .put("servicePointId", "7c5abc9f-f3d7-4856-b8d7-6712462ca007")
+ .put("staffMemberId", "12115707-d7c8-54e7-8287-22e97f7250a4")
+ .put("dateTime", "2020-01-02T13:02:46.000Z");
+
+ JsonObject newItemRequest = new ItemRequestBuilder()
+ .withId(itemId)
+ .forHolding(holdingId)
+ .withInTransitDestinationServicePointId(TRANSIT_DESTINATION_SERVICE_POINT_ID_FOR_CREATE)
+ .withBarcode("645398607547")
+ .canCirculate()
+ .temporarilyInReadingRoom()
+ .withTagList(new JsonObject().put(Item.TAG_LIST_KEY, new JsonArray().add("test-tag")))
+ .withLastCheckIn(lastCheckIn)
+ .withCopyNumber("cp")
+ .create();
+
+ newItemRequest = itemsClient.create(newItemRequest).getJson();
+
+ assertThat(newItemRequest.getString("copyNumber"), is("cp"));
+ assertThat(newItemRequest.getString(Item.TRANSIT_DESTINATION_SERVICE_POINT_ID_KEY),
+ is(TRANSIT_DESTINATION_SERVICE_POINT_ID_FOR_CREATE.toString()));
+
+ var patchRequest = new JsonObject()
+ .put("id", itemId)
+ .put("barcode", "new_barcode")
+ .put("status", new JsonObject().put("name", "Invalid status"))
+ .put("copyNumber", "updatedCp")
+ .put(Item.TRANSIT_DESTINATION_SERVICE_POINT_ID_KEY,
+ TRANSIT_DESTINATION_SERVICE_POINT_ID_FOR_UPDATE)
+ .put("tags", new JsonObject().put("tagList", new JsonArray().add("")));
+
+ final var patchCompleted = okapiClient.patch(
+ String.format("%s/%s", ApiRoot.items(), patchRequest.getString("id")),
+ patchRequest);
+
+ Response patchResponse = patchCompleted.toCompletableFuture().get(5, SECONDS);
+
+ assertThat(patchResponse.getStatusCode(), is(422));
+ }
+
private Response updateItem(JsonObject item) throws MalformedURLException,
InterruptedException, ExecutionException, TimeoutException {
diff --git a/src/test/java/api/support/http/ResourceClient.java b/src/test/java/api/support/http/ResourceClient.java
index 52c80e9ce..e1fc16301 100644
--- a/src/test/java/api/support/http/ResourceClient.java
+++ b/src/test/java/api/support/http/ResourceClient.java
@@ -235,6 +235,29 @@ public Response attemptToReplace(UUID id, JsonObject request)
return putCompleted.toCompletableFuture().get(5, SECONDS);
}
+ public void patch(UUID id, JsonObject request)
+ throws MalformedURLException,
+ InterruptedException,
+ ExecutionException,
+ TimeoutException {
+
+ Response patchResponse = attemptToPatch(id, request);
+
+ assertThat(
+ String.format("Failed to update %s %s: %s", resourceName, id, patchResponse.getBody()),
+ patchResponse.getStatusCode(), is(HttpURLConnection.HTTP_NO_CONTENT));
+ }
+
+ public Response attemptToPatch(UUID id, JsonObject request)
+ throws MalformedURLException, InterruptedException, ExecutionException,
+ TimeoutException {
+
+ final var patchCompleted = client.patch(
+ urlMaker.combine(String.format("/%s", id)).toString(), request);
+
+ return patchCompleted.toCompletableFuture().get(5, SECONDS);
+ }
+
@SneakyThrows
public Response getById(UUID id) {
final var getCompleted = client.get(urlMaker.combine(String.format("/%s", id)));