diff --git a/ramls/acq-models b/ramls/acq-models index 0c45dae63..25083ed83 160000 --- a/ramls/acq-models +++ b/ramls/acq-models @@ -1 +1 @@ -Subproject commit 0c45dae633299528629968fa314049c3f755f558 +Subproject commit 25083ed834962a4b9423b33b4b307f86a2e4918b diff --git a/ramls/bind-pieces.raml b/ramls/bind-pieces.raml index a3fcded0a..823a0d870 100644 --- a/ramls/bind-pieces.raml +++ b/ramls/bind-pieces.raml @@ -36,3 +36,11 @@ resourceTypes: is: [validate] post: description: bind pieces to item and connect that item to title + /{id}: + uriParameters: + id: + description: The UUID of a piece record + type: UUID + is: [ validate ] + delete: + description: Remove binding for a piece with given {id} diff --git a/src/main/java/org/folio/helper/BindHelper.java b/src/main/java/org/folio/helper/BindHelper.java index 89ddf5411..3c0ccd303 100644 --- a/src/main/java/org/folio/helper/BindHelper.java +++ b/src/main/java/org/folio/helper/BindHelper.java @@ -25,6 +25,7 @@ import org.springframework.beans.factory.annotation.Autowired; import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; @@ -55,6 +56,10 @@ public BindHelper(BindPiecesCollection bindPiecesCollection, bindPiecesCollection.getPoLineId(), bindPiecesCollection.getBindPieceIds().size()); } + public BindHelper(Map okapiHeaders, Context ctx) { + super(okapiHeaders, ctx); + } + private Map> groupBindPieceByPoLineId(BindPiecesCollection bindPiecesCollection) { String poLineId = bindPiecesCollection.getPoLineId(); Map bindPieceMap = bindPiecesCollection.getBindPieceIds().stream() @@ -66,6 +71,44 @@ private Map> groupBindPieceByPoLineId( return Map.of(poLineId, bindPieceMap); } + public Future removeBinding(String pieceId, RequestContext requestContext) { + logger.debug("removeBinding:: Removing binding for piece: {}", pieceId); + return pieceStorageService.getPieceById(pieceId, requestContext) + .compose(piece -> { + var bindItemId = piece.getBindItemId(); + piece.withBindItemId(null).withIsBound(false); + return removeForbiddenEntities(piece, requestContext) + .compose(v -> getValidPieces(requestContext)) + .compose(piecesGroupedByPoLine -> storeUpdatedPieceRecords(piecesGroupedByPoLine, requestContext)) + .compose(piecesGroupedByPoLine -> clearTitleBindItemsIfNeeded(piece.getTitleId(), bindItemId, requestContext)); + }); + } + + private Future removeForbiddenEntities(Piece piece, RequestContext requestContext) { + // Populate piecesByLineId used by removeForbiddenEntities and parent helper methods + piecesByLineId = Map.of(piece.getPoLineId(), Collections.singletonMap(piece.getId(), null)); + return removeForbiddenEntities(requestContext); + } + + private Future clearTitleBindItemsIfNeeded(String titleId, String bindItemId, RequestContext requestContext) { + String query = String.format("titleId==%s and bindItemId==%s and isBound==true", titleId, bindItemId); + return pieceStorageService.getPieces(0, 0, query, requestContext) + .compose(pieceCollection -> { + var totalRecords = pieceCollection.getTotalRecords(); + if (totalRecords != 0) { + logger.info("clearTitleBindItemsIfNeeded:: Found '{}' piece(s) associated with bind item '{}'", totalRecords, bindItemId); + return Future.succeededFuture(); + } + logger.info("clearTitleBindItemsIfNeeded:: Removing bind item '{}' from title '{}' as no associated piece(s) to the item was found", bindItemId, titleId); + return titlesService.getTitleById(titleId, requestContext) + .compose(title -> { + List bindItemIds = new ArrayList<>(title.getBindItemIds()); + bindItemIds.remove(bindItemId); + return titlesService.saveTitle(title.withBindItemIds(bindItemIds), requestContext); + }); + }); + } + public Future bindPieces(BindPiecesCollection bindPiecesCollection, RequestContext requestContext) { return removeForbiddenEntities(requestContext) .compose(vVoid -> processBindPieces(bindPiecesCollection, requestContext)); @@ -188,7 +231,7 @@ private Future createItemForPieces(BindPiecesHolder holder, Re inventoryItemRequestService.transferItemRequests(itemIds, newItemId, requestContext); } // Set new item ids for pieces and holder - holder.getPieces().forEach(piece -> piece.setItemId(newItemId)); + holder.getPieces().forEach(piece -> piece.setBindItemId(newItemId)); return holder.withBindItemId(newItemId); }); } @@ -225,7 +268,7 @@ private Future storeUpdatedPieces(BindPiecesHolder holder, Req } private Future updateTitleWithBindItems(BindPiecesHolder holder, RequestContext requestContext) { - var itemIds = holder.getPieces().map(Piece::getItemId).distinct().toList(); + var itemIds = holder.getPieces().map(Piece::getBindItemId).distinct().toList(); return titlesService.getTitlesByQuery(String.format(TITLE_BY_POLINE_QUERY, holder.getPoLineId()), requestContext) .map(titles -> updateTitle(titles, itemIds, requestContext)) .map(v -> holder); diff --git a/src/main/java/org/folio/rest/impl/ReceivingAPI.java b/src/main/java/org/folio/rest/impl/ReceivingAPI.java index af06200bf..6d6b62e81 100644 --- a/src/main/java/org/folio/rest/impl/ReceivingAPI.java +++ b/src/main/java/org/folio/rest/impl/ReceivingAPI.java @@ -79,6 +79,15 @@ public void postOrdersBindPieces(BindPiecesCollection entity, Map handleErrorResponse(asyncResultHandler, helper, t)); } + @Override + public void deleteOrdersBindPiecesById(String id, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { + logger.info("Removing binding for piece: {}", id); + BindHelper helper = new BindHelper(okapiHeaders, vertxContext); + helper.removeBinding(id, new RequestContext(vertxContext, okapiHeaders)) + .onSuccess(s -> asyncResultHandler.handle(succeededFuture(helper.buildNoContentResponse()))) + .onFailure(t -> handleErrorResponse(asyncResultHandler, helper, t)); + } + @Override @Validate public void getOrdersReceivingHistory(String totalRecords, int offset, int limit, String query, Map okapiHeaders, diff --git a/src/test/java/org/folio/TestConstants.java b/src/test/java/org/folio/TestConstants.java index 92e445927..96007b3c9 100644 --- a/src/test/java/org/folio/TestConstants.java +++ b/src/test/java/org/folio/TestConstants.java @@ -21,6 +21,7 @@ private TestConstants() {} public static final String ORDERS_CHECKIN_ENDPOINT = "/orders/check-in"; public static final String ORDERS_EXPECT_ENDPOINT = "/orders/expect"; public static final String ORDERS_BIND_ENDPOINT = "/orders/bind-pieces"; + public static final String ORDERS_BIND_ID_ENDPOINT = "/orders/bind-pieces/%s"; public static final String PO_LINE_NUMBER_VALUE = "1"; public static final String BAD_QUERY = "unprocessableQuery"; diff --git a/src/test/java/org/folio/rest/impl/CheckinReceivingApiTest.java b/src/test/java/org/folio/rest/impl/CheckinReceivingApiTest.java index 28fa50434..e58908eb3 100644 --- a/src/test/java/org/folio/rest/impl/CheckinReceivingApiTest.java +++ b/src/test/java/org/folio/rest/impl/CheckinReceivingApiTest.java @@ -31,6 +31,7 @@ import org.folio.rest.jaxrs.model.ReceivingItemResult; import org.folio.rest.jaxrs.model.ReceivingResult; import org.folio.rest.jaxrs.model.ReceivingResults; +import org.folio.rest.jaxrs.model.Title; import org.folio.rest.jaxrs.model.ToBeCheckedIn; import org.folio.rest.jaxrs.model.ToBeExpected; import org.junit.jupiter.api.AfterAll; @@ -42,6 +43,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -55,12 +57,14 @@ import static javax.ws.rs.core.MediaType.APPLICATION_JSON; import static org.folio.RestTestUtils.prepareHeaders; +import static org.folio.RestTestUtils.verifyDeleteResponse; import static org.folio.RestTestUtils.verifyPostResponse; import static org.folio.TestConfig.clearServiceInteractions; import static org.folio.TestConfig.initSpringContext; import static org.folio.TestConfig.isVerticleNotDeployed; import static org.folio.TestConstants.EXIST_CONFIG_X_OKAPI_TENANT_LIMIT_10; import static org.folio.TestConstants.ORDERS_BIND_ENDPOINT; +import static org.folio.TestConstants.ORDERS_BIND_ID_ENDPOINT; import static org.folio.TestConstants.ORDERS_CHECKIN_ENDPOINT; import static org.folio.TestConstants.ORDERS_EXPECT_ENDPOINT; import static org.folio.TestConstants.ORDERS_RECEIVING_ENDPOINT; @@ -108,6 +112,7 @@ import static org.folio.rest.impl.MockServer.getPieceUpdates; import static org.folio.rest.impl.MockServer.getPoLineSearches; import static org.folio.rest.impl.MockServer.getPoLineUpdates; +import static org.folio.rest.impl.MockServer.getUpdatedTitles; import static org.folio.rest.jaxrs.model.ProcessingStatus.Type.SUCCESS; import static org.folio.rest.jaxrs.model.ReceivedItem.ItemStatus.ON_ORDER; import static org.folio.service.inventory.InventoryItemManager.COPY_NUMBER; @@ -1035,12 +1040,15 @@ void testBindPiecesToTitleWithItem() { var order = getMinimalContentCompositePurchaseOrder() .withWorkflowStatus(CompositePurchaseOrder.WorkflowStatus.OPEN); var poLine = getMinimalContentCompositePoLine(order.getId()); + var title = getTitle(poLine); var bindingPiece1 = getMinimalContentPiece(poLine.getId()) + .withTitleId(title.getId()) .withHoldingId(holdingId) .withReceivingStatus(receivingStatus) .withFormat(format); var bindingPiece2 = getMinimalContentPiece(poLine.getId()) .withId(UUID.randomUUID().toString()) + .withTitleId(title.getId()) .withHoldingId(holdingId) .withReceivingStatus(receivingStatus) .withFormat(format); @@ -1049,7 +1057,7 @@ void testBindPiecesToTitleWithItem() { addMockEntry(PO_LINES_STORAGE, poLine); addMockEntry(PIECES_STORAGE, bindingPiece1); addMockEntry(PIECES_STORAGE, bindingPiece2); - addMockEntry(TITLES, getTitle(poLine)); + addMockEntry(TITLES, title); var pieceIds = List.of(bindingPiece1.getId(), bindingPiece2.getId()); var bindPiecesCollection = new BindPiecesCollection() @@ -1072,13 +1080,28 @@ void testBindPiecesToTitleWithItem() { assertThat(pieceUpdates, notNullValue()); assertThat(pieceUpdates, hasSize(bindPiecesCollection.getBindPieceIds().size())); - var pieceList = pieceUpdates.stream().filter(pol -> { - Piece piece = pol.mapTo(Piece.class); - String pieceId = piece.getId(); - return Objects.equals(bindingPiece1.getId(), pieceId) || Objects.equals(bindingPiece2.getId(), pieceId); - }).toList(); + var pieceList = pieceUpdates.stream() + .map(json -> json.mapTo(Piece.class)) + .filter(piece -> pieceIds.contains(piece.getId())) + .filter(piece -> piece.getBindItemId().equals(newItemId)) + .toList(); assertThat(pieceList.size(), is(2)); + var titleUpdates = getUpdatedTitles(); + assertThat(titleUpdates, notNullValue()); + assertThat(titleUpdates, hasSize(1)); + + var updatedTitleOpt = titleUpdates.stream() + .map(json -> json.mapTo(Title.class)) + .filter(updatedTitle -> updatedTitle.getId().equals(pieceList.get(0).getTitleId())) + .filter(updatedTitle -> updatedTitle.getId().equals(pieceList.get(1).getTitleId())) + .findFirst(); + + assertThat(updatedTitleOpt.isPresent(), is(true)); + var titleBindItemIds = updatedTitleOpt.get().getBindItemIds(); + assertThat(titleBindItemIds, hasSize(1)); + assertThat(titleBindItemIds.get(0), is(newItemId)); + var createdHoldings = getCreatedHoldings(); assertThat(createdHoldings, nullValue()); @@ -1291,6 +1314,74 @@ void testBindExpectedPieces() { assertThat(errors.get(0).getMessage(), equalTo(PIECES_MUST_HAVE_RECEIVED_STATUS.getDescription())); } + @Test + void testRemovePieceBinding() { + logger.info("=== Test DELETE Remove binding"); + + var holdingId = "849241fa-4a14-4df5-b951-846dcd6cfc4d"; + var order = getMinimalContentCompositePurchaseOrder() + .withWorkflowStatus(CompositePurchaseOrder.WorkflowStatus.OPEN); + var poLine = getMinimalContentCompositePoLine(order.getId()); + var title = getTitle(poLine); + var bindPiece1 = getMinimalContentPiece(poLine.getId()) + .withTitleId(title.getId()) + .withHoldingId(holdingId) + .withReceivingStatus(Piece.ReceivingStatus.RECEIVED) + .withFormat(Piece.Format.PHYSICAL); + var bindPiece2 = getMinimalContentPiece(poLine.getId()) + .withId(UUID.randomUUID().toString()) + .withTitleId(title.getId()) + .withHoldingId(holdingId) + .withReceivingStatus(Piece.ReceivingStatus.RECEIVED) + .withFormat(Piece.Format.PHYSICAL); + var bindPieceIds = List.of(bindPiece1.getId(), bindPiece2.getId()); + + addMockEntry(PURCHASE_ORDER_STORAGE, order); + addMockEntry(PO_LINES_STORAGE, poLine); + addMockEntry(PIECES_STORAGE, bindPiece1); + addMockEntry(PIECES_STORAGE, bindPiece2); + addMockEntry(TITLES, title); + + var bindPiecesCollection = new BindPiecesCollection() + .withPoLineId(poLine.getId()) + .withBindItem(getMinimalContentBindItem() + .withLocationId(null) + .withHoldingId(holdingId)) + .withBindPieceIds(bindPieceIds); + + var bindResponse = verifyPostResponse(ORDERS_BIND_ENDPOINT, JsonObject.mapFrom(bindPiecesCollection).encode(), + prepareHeaders(EXIST_CONFIG_X_OKAPI_TENANT_LIMIT_10), APPLICATION_JSON, HttpStatus.HTTP_OK.toInt()) + .as(BindPiecesResult.class); + + assertThat(bindResponse.getPoLineId(), is(poLine.getId())); + assertThat(bindResponse.getBoundPieceIds(), hasSize(2)); + assertThat(bindResponse.getBoundPieceIds(), is(bindPieceIds)); + assertThat(bindResponse.getItemId(), notNullValue()); + + var bindItemId = bindResponse.getItemId(); + var url = String.format(ORDERS_BIND_ID_ENDPOINT, bindPiece1.getId()); + verifyDeleteResponse(url, "", HttpStatus.HTTP_NO_CONTENT.toInt()); + + var pieceUpdates = getPieceUpdates(); + assertThat(pieceUpdates, notNullValue()); + assertThat(pieceUpdates, hasSize(3)); + + var pieceList = pieceUpdates.stream() + .map(json -> json.mapTo(Piece.class)) + .filter(piece -> Objects.equals(bindPiece1.getId(), piece.getId())) + .sorted(Comparator.comparing(Piece::getIsBound)) + .toList(); + assertThat(pieceList.size(), is(2)); + + var pieceBefore = pieceList.get(1); + assertThat(pieceBefore.getIsBound(), is(true)); + assertThat(pieceBefore.getBindItemId(), notNullValue()); + + var pieceAfter = pieceList.get(0); + assertThat(pieceAfter.getIsBound(), is(false)); + assertThat(pieceAfter.getBindItemId(), nullValue()); + } + @Test void testPostReceivingWithErrorSearchingForPiece() { logger.info("=== Test POST Receiving - Receive resources with error searching for piece");