Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MODORDER-1087]-Delete received pieces in bulk #952

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
51 changes: 50 additions & 1 deletion ramls/pieces.raml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ protocols: [ HTTP, HTTPS ]

documentation:
- title: Orders Business Logic API
content: <b>API for managing pieces</b>
content: <b>API for managing pieces including batch operations for deletion.</b>

types:
piece: !include acq-models/mod-orders-storage/schemas/piece.json
Expand All @@ -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
Expand Down Expand Up @@ -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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Piece records or Pieces more correct

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also refactor Sentences in other places to be gramatically correct

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you, sure will.

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:
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/org/folio/rest/impl/PiecesAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -11,6 +12,8 @@
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.OrdersPiecesBatchDeleteApplicationJson;
import org.folio.rest.jaxrs.model.Piece;
import org.folio.rest.jaxrs.resource.OrdersPieces;
import org.folio.service.pieces.PieceStorageService;
Expand Down Expand Up @@ -66,6 +69,14 @@ public void postOrdersPieces(boolean createItem, Piece entity, Map<String, Strin
.onFailure(t -> handleErrorResponse(asyncResultHandler, t));
}

@Override
public void deleteOrdersPiecesBatch(String entity, Map<String, String> okapiHeaders, Handler<AsyncResult<Response>> asyncResultHandler, Context vertxContext) {
JsonObject json = new JsonObject(entity);
List<String> 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<String, String> okapiHeaders,
Expand Down Expand Up @@ -96,4 +107,5 @@ public void deleteOrdersPiecesById(String pieceId, boolean deleteHolding, Map<St
.onSuccess(ok -> asyncResultHandler.handle(succeededFuture(buildNoContentResponse())))
.onFailure(fail -> handleErrorResponse(asyncResultHandler, fail));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -79,4 +83,12 @@ protected Future<Void> updatePoLine(PieceDeletionHolder holder, RequestContext r
: pieceDeleteFlowPoLineService.updatePoLine(holder, requestContext);
}

public Future<List<Void>> batchDeletePiece (List <String> ids, boolean deleteHolding ,RequestContext requestContext) {
List<Future> deleteFutures = ids.stream()
.map(id -> deletePiece(id, deleteHolding, requestContext))
.collect(Collectors.toList());
return CompositeFuture.all(deleteFutures)
.map(empty -> null);
}

}
16 changes: 16 additions & 0 deletions src/test/java/org/folio/RestTestUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
97 changes: 97 additions & 0 deletions src/test/java/org/folio/rest/impl/PieceApiTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,16 @@
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;
import java.util.concurrent.ExecutionException;
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;
Expand All @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -356,4 +362,95 @@ 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
void deletePiecesByIdsTest2() {
logger.info("=== Test delete pieces by ids - item deleted ===");

String pieceId = UUID.randomUUID().toString();
List<String> ids = Arrays.asList(pieceId);
Boolean deleteHoldings = false;
JsonArray jsonArrary = new JsonArray(ids);
JsonObject jsonObject = new JsonObject()
.put("ids", jsonArrary);
// .put("deleteHolding", deleteHoldings);

List <Piece> pieces = new ArrayList<>();


// Mock response setup for deletion
//MockServer.addMockEntry(PIECES_STORAGE, JsonObject.mapFrom(jsonObject).encode()); // Simplified mock data
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also remove commentds, if it is not helpful

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you for pointing it out ! will do


// Simulate the delete call
verifyDeleteResponse(PIECES_BATCH_DELETE_ENDPOINT,
String.valueOf(jsonObject),
prepareHeaders(EXIST_CONFIG_X_OKAPI_TENANT_LIMIT_10, X_OKAPI_USER_ID),
APPLICATION_JSON, 204); // Assuming successful deletion returns a 204 No Content

// Assertions to check that the deletion was processed in the mock server
assertNull(MockServer.getItemDeletions()); // Check if item deletions are as expected
assertThat(MockServer.getPieceDeletions(), hasSize(1)); // Verify that exactly one piece deletion was processed
}

@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);

// 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));

// Prepare request data as JSON Array
JsonArray jsonArray = new JsonArray().add(piece.getId());
JsonObject jsonObject = new JsonObject()
.put("ids", jsonArray)
.put("deleteHoldings", false);

// Log the JSON being sent to the server
logger.debug("Sending JSON for deletion: {}", jsonObject.encode());

// Perform the delete operation and verify the response
verifyDeleteResponse(PIECES_BATCH_DELETE_ENDPOINT, jsonObject.toString(),
prepareHeaders(EXIST_CONFIG_X_OKAPI_TENANT_LIMIT_10, X_OKAPI_USER_ID), APPLICATION_JSON, 204).as(Piece.class);

// Assert no items were deleted
assertNull(MockServer.getItemDeletions());

// Assert that exactly one piece was deleted
assertThat(MockServer.getPieceDeletions(), hasSize(1));
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<Piece> pieces = new ArrayList<>();
pieces.add(piece);
List<String> 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<JsonObject>())).when(inventoryItemManager).getItemsByHoldingId(holdingId, requestContext);
final ArgumentCaptor<PieceDeletionHolder> PieceDeletionHolderCapture = ArgumentCaptor.forClass(PieceDeletionHolder.class);
doAnswer((Answer<Future<Void>>) invocation -> {
PieceDeletionHolder answerHolder = invocation.getArgument(0);
answerHolder.withOrderInformation(purchaseOrder, poLine);
return succeededFuture(null);
}).when(basePieceFlowHolderBuilder).updateHolderWithOrderInformation(PieceDeletionHolderCapture.capture(), eq(requestContext));
doAnswer((Answer<Future<Void>>) invocation -> {
PieceDeletionHolder answerHolder = invocation.getArgument(0);
answerHolder.withTitleInformation(title);
return succeededFuture(null);
}).when(basePieceFlowHolderBuilder).updateHolderWithTitleInformation(PieceDeletionHolderCapture.capture(), eq(requestContext));

final ArgumentCaptor<PieceDeletionHolder> pieceDeletionHolderCapture = ArgumentCaptor.forClass(PieceDeletionHolder.class);
doReturn(succeededFuture(null)).when(pieceDeleteFlowPoLineService).updatePoLine(pieceDeletionHolderCapture.capture(), eq(requestContext));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also separate declaration doReturn and action and verification part to be easy to read

//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
Expand Down
Loading