diff --git a/ramls/pieces.raml b/ramls/pieces.raml index 6cdb65a4c..eb9607d1d 100644 --- a/ramls/pieces.raml +++ b/ramls/pieces.raml @@ -6,7 +6,7 @@ protocols: [ HTTP, HTTPS ] documentation: - title: Orders Business Logic API - content: API for managing pieces + content: API for managing pieces including batch operations for deletion. types: piece: !include acq-models/mod-orders-storage/schemas/piece.json @@ -15,6 +15,19 @@ types: UUID: type: string pattern: ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$ + BatchDeletePayload: + type: object + properties: + ids: + type: array + items: + type: string + description: List of IDs to be deleted + deleteHoldings: + type: boolean + description: Specifies whether associated holdings should also be deleted + required: false + default: false traits: pageable: !include raml-util/traits/pageable.raml @@ -47,6 +60,42 @@ resourceTypes: example: true required: false default: false + /batch: + delete: + description: Deletes multiple pieces optionally including related holdings. + body: + application/json: + type: string + example: | + { + "ids": ["123", "456", "789"], + "deleteHoldings": false + } + responses: + 204: + description: "Pieces records successfully deleted" + 400: + description: "Bad request due to invalid data format or validation error" + body: + application/json: + example: + strict: false + value: !include examples/errors_400.sample + text/plain: + example: "unable to delete Pieces -- Bad request" + 404: + description: "One or more specified IDs do not exist" + body: + application/json: + 500: + description: "Internal server error due to a misconfiguration or server fault" + body: + application/json: + example: + strict: false + value: !include examples/errors_500.sample + text/plain: + example: "unable to delete Pieces -- Internal server error" /{id}: uriParameters: id: diff --git a/src/main/java/org/folio/rest/impl/PiecesAPI.java b/src/main/java/org/folio/rest/impl/PiecesAPI.java index 968aa9c8b..ee807c9f3 100644 --- a/src/main/java/org/folio/rest/impl/PiecesAPI.java +++ b/src/main/java/org/folio/rest/impl/PiecesAPI.java @@ -2,6 +2,7 @@ import static io.vertx.core.Future.succeededFuture; +import java.util.List; import java.util.Map; import javax.ws.rs.core.Response; @@ -11,6 +12,7 @@ import org.apache.logging.log4j.Logger; import org.folio.rest.annotations.Validate; import org.folio.rest.core.models.RequestContext; +import org.folio.rest.jaxrs.model.BatchDeletePayload; import org.folio.rest.jaxrs.model.Piece; import org.folio.rest.jaxrs.resource.OrdersPieces; import org.folio.service.pieces.PieceStorageService; @@ -66,6 +68,14 @@ public void postOrdersPieces(boolean createItem, Piece entity, Map handleErrorResponse(asyncResultHandler, t)); } + @Override + public void deleteOrdersPiecesBatch(String entity, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { + JsonObject json = new JsonObject(entity); + List ids = json.getJsonArray("ids").getList(); + boolean deleteHoldings = json.getBoolean("deleteHoldings", false); + pieceDeleteFlowManager.batchDeletePiece(ids, deleteHoldings, new RequestContext(vertxContext, okapiHeaders)); + } + @Override @Validate public void getOrdersPiecesById(String id, Map okapiHeaders, @@ -96,4 +106,5 @@ public void deleteOrdersPiecesById(String pieceId, boolean deleteHolding, Map asyncResultHandler.handle(succeededFuture(buildNoContentResponse()))) .onFailure(fail -> handleErrorResponse(asyncResultHandler, fail)); } + } diff --git a/src/main/java/org/folio/service/pieces/flows/delete/PieceDeleteFlowManager.java b/src/main/java/org/folio/service/pieces/flows/delete/PieceDeleteFlowManager.java index a3ec3f1e3..55b09edab 100644 --- a/src/main/java/org/folio/service/pieces/flows/delete/PieceDeleteFlowManager.java +++ b/src/main/java/org/folio/service/pieces/flows/delete/PieceDeleteFlowManager.java @@ -2,6 +2,10 @@ import static org.folio.orders.utils.ProtectedOperationType.DELETE; +import java.util.List; +import java.util.stream.Collectors; + +import io.vertx.core.CompositeFuture; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.folio.models.pieces.PieceDeletionHolder; @@ -79,4 +83,12 @@ protected Future updatePoLine(PieceDeletionHolder holder, RequestContext r : pieceDeleteFlowPoLineService.updatePoLine(holder, requestContext); } + public Future> batchDeletePiece (List ids, boolean deleteHolding ,RequestContext requestContext) { + List deleteFutures = ids.stream() + .map(id -> deletePiece(id, deleteHolding, requestContext)) + .collect(Collectors.toList()); + return CompositeFuture.all(deleteFutures) + .map(empty -> null); + } + } diff --git a/src/test/java/org/folio/RestTestUtils.java b/src/test/java/org/folio/RestTestUtils.java index 002952b6f..c579d25c4 100644 --- a/src/test/java/org/folio/RestTestUtils.java +++ b/src/test/java/org/folio/RestTestUtils.java @@ -59,6 +59,22 @@ public static Response verifyPostResponse(String url, String body, Headers heade return response; } + public static Response verifyDeleteResponse(String url, String body, Headers headers, String expectedContentType, int expectedCode) { + return RestAssured + .with() + .header(X_OKAPI_URL) + .header(X_OKAPI_TOKEN) + .headers(headers) + .contentType(APPLICATION_JSON) + .body(body) + .when() + .delete(url) + .then() + .statusCode(expectedCode) + .contentType(expectedContentType) + .extract() + .response(); + } public static Response verifyPut(String url, JsonObject body, String expectedContentType, int expectedCode) { return verifyPut(url, body.encodePrettily(), expectedContentType, expectedCode); diff --git a/src/test/java/org/folio/rest/impl/PieceApiTest.java b/src/test/java/org/folio/rest/impl/PieceApiTest.java index aa33fefe8..f637eda91 100644 --- a/src/test/java/org/folio/rest/impl/PieceApiTest.java +++ b/src/test/java/org/folio/rest/impl/PieceApiTest.java @@ -33,6 +33,8 @@ import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.assertNull; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.UUID; @@ -40,6 +42,7 @@ import java.util.concurrent.TimeoutException; import io.restassured.http.Header; +import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -55,6 +58,7 @@ import org.folio.rest.jaxrs.model.Location; import org.folio.rest.jaxrs.model.Physical; import org.folio.rest.jaxrs.model.Piece; +import org.folio.rest.jaxrs.model.PieceCollection; import org.folio.rest.jaxrs.model.PurchaseOrder; import org.folio.rest.jaxrs.model.Title; import org.junit.jupiter.api.AfterAll; @@ -70,6 +74,8 @@ public class PieceApiTest { private static final String PIECES_ID_PATH = PIECES_ENDPOINT + "/%s"; static final String CONSISTENT_RECEIVED_STATUS_PIECE_UUID = "7d0aa803-a659-49f0-8a95-968f277c87d7"; private JsonObject pieceJsonReqData = getMockAsJson(PIECE_RECORDS_MOCK_DATA_PATH + "pieceRecord.json"); + public static final String PIECES_BATCH_DELETE_ENDPOINT = "orders/pieces/batch"; + private static boolean runningOnOwn; @@ -235,8 +241,8 @@ void shouldNotDeletePieceAndItemIfGetItemByIdTrowInternalServerErrorTest() { CompositePoLine poLine = new CompositePoLine().withId(UUID.randomUUID().toString()).withPurchaseOrderId(order.getId()) .withPhysical(new Physical().withCreateInventory(Physical.CreateInventory.INSTANCE_HOLDING_ITEM)); Piece piece = new Piece().withId(UUID.randomUUID().toString()) - .withFormat(Piece.Format.PHYSICAL) - .withItemId(ID_FOR_INTERNAL_SERVER_ERROR).withPoLineId(poLine.getId()); + .withFormat(Piece.Format.PHYSICAL) + .withItemId(ID_FOR_INTERNAL_SERVER_ERROR).withPoLineId(poLine.getId()); order.setCompositePoLines(Collections.singletonList(poLine)); MockServer.addMockEntry(PIECES_STORAGE, JsonObject.mapFrom(piece)); MockServer.addMockEntry(PO_LINES_STORAGE, JsonObject.mapFrom(poLine)); @@ -356,4 +362,62 @@ void deletePieceInternalErrorOnStorageTest() { logger.info("=== Test delete piece by id - internal error from storage 500 ==="); verifyDeleteResponse(String.format(PIECES_ID_PATH, ID_FOR_INTERNAL_SERVER_ERROR), APPLICATION_JSON, 500); } + + @Test + public void deletePiecesByIdsTest() { + logger.info("=== Test delete pieces by ids - item deleted ==="); + + // Create unique IDs for the components + String itemId = UUID.randomUUID().toString(); + String lineId = UUID.randomUUID().toString(); + String orderId = UUID.randomUUID().toString(); + String holdingId = UUID.randomUUID().toString(); + String titleId = UUID.randomUUID().toString(); + + CompositePurchaseOrder order = new CompositePurchaseOrder().withId(orderId); + Location loc = new Location().withHoldingId(holdingId).withQuantityElectronic(1).withQuantity(1); + Cost cost = new Cost().withQuantityElectronic(1); + + // Setup the PO Line + CompositePoLine poLine = new CompositePoLine().withId(lineId) + .withOrderFormat(CompositePoLine.OrderFormat.PHYSICAL_RESOURCE) + .withLocations(Collections.singletonList(loc)) + .withCost(cost) + .withPhysical(new Physical().withCreateInventory(Physical.CreateInventory.INSTANCE_HOLDING_ITEM)); + + order.setCompositePoLines(Collections.singletonList(poLine)); + + // Create a title + Title title = new Title().withId(titleId).withTitle("title name"); + + // Setup the piece + Piece piece = new Piece().withId(UUID.randomUUID().toString()) + .withFormat(Piece.Format.PHYSICAL) + .withHoldingId(holdingId) + .withItemId(itemId) + .withPoLineId(poLine.getId()) + .withTitleId(titleId); + + // Prepare request data as JSON Array + JsonArray jsonArray = new JsonArray().add(piece.getId()); + JsonObject jsonObject = new JsonObject() + .put("ids", jsonArray) + .put("deleteHoldings", false); + // Mock the server responses + MockServer.addMockEntry(PIECES_STORAGE, JsonObject.mapFrom(piece)); + MockServer.addMockEntry(PO_LINES_STORAGE, JsonObject.mapFrom(poLine)); + //MockServer.addMockEntry(PURCHASE_ORDER_STORAGE, JsonObject.mapFrom(order)); + // MockServer.addMockEntry(TITLES, JsonObject.mapFrom(title)); + //MockServer.addMockEntry(ITEM_RECORDS, new JsonObject().put(ID, itemId)); + + // Perform the delete operation and verify the response + verifyDeleteResponse(PIECES_BATCH_DELETE_ENDPOINT, jsonObject.encode(), + prepareHeaders(EXIST_CONFIG_X_OKAPI_TENANT_LIMIT_10, X_OKAPI_USER_ID), APPLICATION_JSON, 400); + + // Assert no items were deleted + assertNull(MockServer.getItemDeletions()); + + // Assert that exactly one piece was deleted + assertThat(MockServer.getPieceDeletions(), hasSize(1)); + } } diff --git a/src/test/java/org/folio/service/pieces/flows/delete/PieceDeleteFlowManagerTest.java b/src/test/java/org/folio/service/pieces/flows/delete/PieceDeleteFlowManagerTest.java index 07848cb01..103b9a815 100644 --- a/src/test/java/org/folio/service/pieces/flows/delete/PieceDeleteFlowManagerTest.java +++ b/src/test/java/org/folio/service/pieces/flows/delete/PieceDeleteFlowManagerTest.java @@ -1,5 +1,6 @@ package org.folio.service.pieces.flows.delete; +import static io.vertx.core.Future.*; import static io.vertx.core.Future.succeededFuture; import static org.folio.TestConfig.autowireDependencies; import static org.folio.TestConfig.clearServiceInteractions; @@ -42,6 +43,7 @@ import org.folio.rest.jaxrs.model.Eresource; import org.folio.rest.jaxrs.model.Location; import org.folio.rest.jaxrs.model.Piece; +import org.folio.rest.jaxrs.model.PieceCollection; import org.folio.rest.jaxrs.model.PoLine; import org.folio.rest.jaxrs.model.PurchaseOrder; import org.folio.rest.jaxrs.model.Title; @@ -368,6 +370,67 @@ void shouldUpdateLineQuantityIfPoLineIsNotPackageAndManualPieceCreateFalseAndInv verify(basePieceFlowHolderBuilder).updateHolderWithOrderInformation(holder, requestContext); } + @Test + void shouldDeletePiecesInBatch() { + String orderId = UUID.randomUUID().toString(); + String holdingId = UUID.randomUUID().toString(); + String lineId = UUID.randomUUID().toString(); + String itemId = UUID.randomUUID().toString(); + String locationId = UUID.randomUUID().toString(); + String titleId = UUID.randomUUID().toString(); + JsonObject holding = new JsonObject(); + holding.put(ID, holdingId); + holding.put(HOLDING_PERMANENT_LOCATION_ID, locationId); + JsonObject item = new JsonObject().put(ID, itemId); + item.put(ITEM_STATUS, new JsonObject().put(ITEM_STATUS_NAME, ItemStatus.ON_ORDER.value())); + Piece piece = new Piece().withId(UUID.randomUUID().toString()).withPoLineId(lineId) + .withHoldingId(holdingId).withFormat(Piece.Format.ELECTRONIC); + Location loc = new Location().withHoldingId(holdingId).withQuantityElectronic(1).withQuantity(1); + Cost cost = new Cost().withQuantityElectronic(1) + .withListUnitPriceElectronic(1d).withExchangeRate(1d).withCurrency("USD") + .withPoLineEstimatedPrice(1d); + PoLine poLine = new PoLine().withIsPackage(false).withCheckinItems(false).withOrderFormat(PoLine.OrderFormat.ELECTRONIC_RESOURCE) + .withEresource(new Eresource().withCreateInventory(Eresource.CreateInventory.INSTANCE_HOLDING)) + .withPurchaseOrderId(orderId).withId(lineId).withLocations(List.of(loc)).withCost(cost); + PurchaseOrder purchaseOrder = new PurchaseOrder().withId(orderId).withWorkflowStatus(PurchaseOrder.WorkflowStatus.OPEN); + Title title = new Title().withId(titleId); + List pieces = new ArrayList<>(); + pieces.add(piece); + List ids = new ArrayList<>(); + ids.add(piece.getId()); + doReturn(succeededFuture(piece)).when(pieceStorageService).getPieceById(piece.getId(), requestContext); + doReturn(succeededFuture(null)).when(protectionService).isOperationRestricted(any(), any(ProtectedOperationType.class), eq(requestContext)); + doReturn(succeededFuture(null)).when(pieceStorageService).deletePiece(eq(piece.getId()), eq(true), eq(requestContext)); + doReturn(succeededFuture(null)).when(circulationRequestsRetriever).getNumberOfRequestsByItemId(eq(piece.getItemId()), eq(requestContext)); + doReturn(succeededFuture(holding)).when(inventoryHoldingManager).getHoldingById(holdingId, false, requestContext); + doReturn(succeededFuture(null)).when(inventoryItemManager).getItemsByHoldingId(holdingId, requestContext); + doReturn(succeededFuture(null)).when(inventoryHoldingManager).deleteHoldingById(piece.getHoldingId(), true, requestContext); + doReturn(succeededFuture(null)).when(inventoryItemManager).getItemRecordById(itemId, true, requestContext); + doReturn(succeededFuture(null)).when(inventoryItemManager).deleteItem(itemId, true, requestContext); + doReturn(succeededFuture(holding)).when(inventoryHoldingManager).getHoldingById(holdingId, true, requestContext); + doReturn(succeededFuture(null)).when(pieceUpdateInventoryService).deleteHoldingConnectedToPiece(piece, requestContext); + doReturn(succeededFuture(new ArrayList())).when(inventoryItemManager).getItemsByHoldingId(holdingId, requestContext); + final ArgumentCaptor PieceDeletionHolderCapture = ArgumentCaptor.forClass(PieceDeletionHolder.class); + doAnswer((Answer>) invocation -> { + PieceDeletionHolder answerHolder = invocation.getArgument(0); + answerHolder.withOrderInformation(purchaseOrder, poLine); + return succeededFuture(null); + }).when(basePieceFlowHolderBuilder).updateHolderWithOrderInformation(PieceDeletionHolderCapture.capture(), eq(requestContext)); + doAnswer((Answer>) invocation -> { + PieceDeletionHolder answerHolder = invocation.getArgument(0); + answerHolder.withTitleInformation(title); + return succeededFuture(null); + }).when(basePieceFlowHolderBuilder).updateHolderWithTitleInformation(PieceDeletionHolderCapture.capture(), eq(requestContext)); + + final ArgumentCaptor pieceDeletionHolderCapture = ArgumentCaptor.forClass(PieceDeletionHolder.class); + doReturn(succeededFuture(null)).when(pieceDeleteFlowPoLineService).updatePoLine(pieceDeletionHolderCapture.capture(), eq(requestContext)); + //When + pieceDeleteFlowManager.batchDeletePiece(ids,false ,requestContext).result(); + verify(pieceStorageService).deletePiece(eq(piece.getId()), eq(true), eq(requestContext)); + verify(inventoryItemManager, times(0)).deleteItem(itemId, true, requestContext); + verify(pieceStorageService, times(1)).deletePiece(eq(piece.getId()), eq(true), eq(requestContext)); + } + private static class ContextConfiguration { @Bean