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