From f285901bceb4fc52961753727a9f8559f75a1f68 Mon Sep 17 00:00:00 2001 From: Saba-Zedginidze-EPAM <148070844+Saba-Zedginidze-EPAM@users.noreply.github.com> Date: Thu, 16 May 2024 15:41:19 +0400 Subject: [PATCH] [MODORDERS-1114] Add transferRequests flag to validate binding (#936) * [MODORDERS-1114] Add transferRequests flag to validate binding * [MODORDERS-1114] Add transferRequests flag to validate binding * [MODORDERS-1114] Add new unit test for throwing error code * [MODORDERS-1114] Update transfer logic * [MODORDERS-1114] Replace transferRequests flag with an enum * [MODORDERS-1114] Update acq-models * [MODORDERS-1114] Update permissions * [MODORDERS-1114] Fix comment numbering * [MODORDERS-1114] Address comments * [MODORDERS-1114] Modify leftover comment * [MODORDERS-1114] Validate receivingTenantId if requests exist * [MODORDERS-1114] Add support for pieces in different tenants * [MODORDERS-1114] Minor refactoring and simplifications * [MODORDERS-1114] Filter out pieces with invalid itemIds when mapping by tenants --- descriptors/ModuleDescriptor-template.json | 4 +- ramls/acq-models | 2 +- .../java/org/folio/helper/BindHelper.java | 130 ++++++++++++------ .../java/org/folio/helper/CheckinHelper.java | 16 +-- .../helper/CheckinReceivePiecesHelper.java | 72 ++++++---- .../java/org/folio/helper/ExpectHelper.java | 16 +-- .../org/folio/helper/ReceivingHelper.java | 17 +-- .../orders/utils/RequestContextUtil.java | 4 + .../rest/core/exceptions/ErrorCodes.java | 2 + .../service/CirculationRequestsRetriever.java | 4 +- .../rest/impl/CheckinReceivingApiTest.java | 41 +++++- .../java/org/folio/rest/impl/MockServer.java | 3 + .../mockdata/itemRequests/itemRequests.json | 10 +- 13 files changed, 210 insertions(+), 111 deletions(-) diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 4f7751083..45ccd1c03 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -1100,7 +1100,9 @@ "orders-storage.titles.item.put", "inventory-storage.items.item.post", "acquisitions-units-storage.units.collection.get", - "acquisitions-units-storage.memberships.collection.get" + "acquisitions-units-storage.memberships.collection.get", + "circulation.requests.collection.get", + "circulation.requests.item.move.post" ] } ] diff --git a/ramls/acq-models b/ramls/acq-models index d02dc6be3..1c870fb69 160000 --- a/ramls/acq-models +++ b/ramls/acq-models @@ -1 +1 @@ -Subproject commit d02dc6be3cc208593dc13f80665a445ec12f930d +Subproject commit 1c870fb699cc59017d2492c20107c5fbe3dd101c diff --git a/src/main/java/org/folio/helper/BindHelper.java b/src/main/java/org/folio/helper/BindHelper.java index 11c171d7d..e77e88835 100644 --- a/src/main/java/org/folio/helper/BindHelper.java +++ b/src/main/java/org/folio/helper/BindHelper.java @@ -3,9 +3,12 @@ import io.vertx.core.Context; import io.vertx.core.Future; import io.vertx.core.json.JsonObject; -import one.util.streamex.StreamEx; import org.folio.models.ItemFields; +import org.folio.okapi.common.GenericCompositeFuture; import org.folio.orders.utils.PoLineCommonUtil; +import org.folio.orders.utils.RequestContextUtil; +import org.folio.rest.RestConstants; +import org.folio.rest.core.exceptions.ErrorCodes; import org.folio.rest.core.exceptions.HttpException; import org.folio.rest.core.models.RequestContext; import org.folio.rest.jaxrs.model.BindPiecesCollection; @@ -17,19 +20,26 @@ import org.folio.rest.jaxrs.model.ReceivingResult; import org.folio.rest.jaxrs.model.ReceivingResults; import org.folio.rest.jaxrs.model.Title; +import org.folio.service.inventory.InventoryItemRequestService; +import org.springframework.beans.factory.annotation.Autowired; import java.util.ArrayList; -import java.util.Collections; import java.util.Date; -import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; +import static org.folio.rest.jaxrs.model.BindPiecesCollection.RequestsAction.TRANSFER; + public class BindHelper extends CheckinReceivePiecesHelper { private static final String TITLE_BY_POLINE_QUERY = "poLineId==%s"; + + @Autowired + private InventoryItemRequestService inventoryItemRequestService; + public BindHelper(BindPiecesCollection bindPiecesCollection, Map okapiHeaders, Context ctx) { super(okapiHeaders, ctx); @@ -55,48 +65,83 @@ public Future bindPieces(BindPiecesCollection bindPiecesCollec } private Future processBindPieces(BindPiecesCollection bindPiecesCollection, RequestContext requestContext) { - // 1. Get piece records from storage + // 1. Get piece records from storage return retrievePieceRecords(requestContext) - // 2. Update piece isBound flag + // 2. Check if there are any outstanding requests for items + .compose(piecesGroupedByPoLine -> checkRequestsForPieceItems(piecesGroupedByPoLine, bindPiecesCollection, requestContext)) + // 3. Update piece isBound flag .map(this::updatePieceRecords) - // 3. Update currently associated items - .map(piecesGroupedByPoLine -> updateItemStatus(piecesGroupedByPoLine, requestContext)) - // 4. Crate item for pieces with specific fields + // 4. Update currently associated items + .compose(piecesGroupedByPoLine -> updateItemStatus(piecesGroupedByPoLine, requestContext)) + // 5. Crate item for pieces with specific fields .compose(piecesGroupedByPoLine -> createItemForPiece(piecesGroupedByPoLine, bindPiecesCollection, requestContext)) - // 5. Update received piece records in the storage + // 6. Update received piece records in the storage .compose(piecesGroupedByPoLine -> storeUpdatedPieceRecords(piecesGroupedByPoLine, requestContext)) - // 6. Update Title with new bind items + // 7. Update Title with new bind items .map(piecesGroupedByPoLine -> updateTitleWithBindItems(piecesGroupedByPoLine, requestContext)) - // 7. Return results to the client + // 8. Return results to the client .map(piecesGroupedByPoLine -> prepareResponseBody(piecesGroupedByPoLine, bindPiecesCollection)); } + private Future>> checkRequestsForPieceItems(Map> piecesGroupedByPoLine, + BindPiecesCollection bindPiecesCollection, + RequestContext requestContext) { + var tenantToItem = mapTenantIdsToItemIds(piecesGroupedByPoLine, requestContext); + return GenericCompositeFuture.all( + tenantToItem.entrySet().stream() + .map(entry -> { + var locationContext = RequestContextUtil.createContextWithNewTenantId(requestContext, entry.getKey()); + return inventoryItemRequestService.getItemsWithActiveRequests(entry.getValue(), locationContext) + .compose(items -> { + if (items.isEmpty()) { + return Future.succeededFuture(); + } + + // requestsAction is required to handle outstanding requests + if (Objects.isNull(bindPiecesCollection.getRequestsAction())) { + logger.warn("checkRequestsForPieceItems:: Found outstanding requests on items with ids: {}", items); + throw new HttpException(RestConstants.VALIDATION_ERROR, ErrorCodes.REQUESTS_ACTION_REQUIRED); + } + + // When requests are to be transferred check if pieces contain different receivingTenantIds + // Transferring requests between tenants is not a requirement for R + if (bindPiecesCollection.getRequestsAction().equals(TRANSFER) && !tenantToItem.keySet().isEmpty()) { + logger.warn("checkRequestsForPieceItems:: All pieces do not have the same receivingTenantId: {}", tenantToItem.keySet()); + throw new HttpException(RestConstants.VALIDATION_ERROR, ErrorCodes.PIECES_HAVE_DIFFERENT_RECEIVING_TENANT_IDS); + } + return Future.succeededFuture(); + }); + }) + .toList()) + .map(f -> piecesGroupedByPoLine); + } + private Map> updatePieceRecords(Map> piecesGroupedByPoLine) { logger.debug("updatePieceRecords:: Updating the piece records to set isBound flag as TRUE"); - piecesGroupedByPoLine.values().stream() - .flatMap(List::stream) + extractAllPieces(piecesGroupedByPoLine) .forEach(piece -> piece.setIsBound(true)); return piecesGroupedByPoLine; } - private Map> updateItemStatus(Map> piecesGroupedByPoLine, - RequestContext requestContext) { + private Future>> updateItemStatus(Map> piecesGroupedByPoLine, RequestContext requestContext) { logger.debug("updateItemStatus:: Updating previous item status to 'Unavailable'"); - var itemIds = piecesGroupedByPoLine.values() - .stream().flatMap(List::stream) - .map(Piece::getItemId).toList(); - inventoryItemManager.getItemRecordsByIds(itemIds, requestContext) - .compose(items -> { - items.forEach(item -> { - logger.info("updateItemStatus:: '{}' item status set to 'Unavailable'", item.getString(ItemFields.ID.value())); - item.put(ItemFields.STATUS.value(), new JsonObject() - .put(ItemFields.STATUS_DATE.value(), new Date()) - .put(ItemFields.STATUS_NAME.value(), ReceivedItem.ItemStatus.UNAVAILABLE)); - } - ); - return inventoryItemManager.updateItemRecords(items, requestContext); - }); - return piecesGroupedByPoLine; + return GenericCompositeFuture.all( + mapTenantIdsToItemIds(piecesGroupedByPoLine, requestContext).entrySet().stream() + .map(entry -> { + var locationContext = RequestContextUtil.createContextWithNewTenantId(requestContext, entry.getKey()); + return inventoryItemManager.getItemRecordsByIds(entry.getValue(), locationContext) + .compose(items -> { + items.forEach(item -> { + logger.info("updateItemStatus:: '{}' item status set to 'Unavailable'", item.getString(ItemFields.ID.value())); + item.put(ItemFields.STATUS.value(), new JsonObject() + .put(ItemFields.STATUS_DATE.value(), new Date()) + .put(ItemFields.STATUS_NAME.value(), ReceivedItem.ItemStatus.UNAVAILABLE)); + }); + return inventoryItemManager.updateItemRecords(items, locationContext); + }); + }) + .toList() + ).map(f -> piecesGroupedByPoLine); } private Future>> createItemForPiece(Map> piecesGroupedByPoLine, @@ -112,11 +157,16 @@ private Future>> createItemForPiece(Map inventoryItemManager.createBindItem(compPOL, holdingIds.get(0), bindPiecesCollection.getBindItem(), requestContext)) - .map(itemId -> { - piecesGroupedByPoLine.get(poLineId).forEach(piece -> piece.setItemId(itemId)); - return piecesGroupedByPoLine; + .map(newItemId -> { + // Move requests if requestsAction is TRANSFER, otherwise do nothing + if (TRANSFER.equals(bindPiecesCollection.getRequestsAction())) { + var itemIds = extractAllPieces(piecesGroupedByPoLine).map(Piece::getItemId).toList(); + inventoryItemRequestService.transferItemsRequests(itemIds, newItemId, requestContext); } - ); + // Set new item ids for pieces + piecesGroupedByPoLine.get(poLineId).forEach(piece -> piece.setItemId(newItemId)); + return piecesGroupedByPoLine; + }); } private void validateHoldingIds(List holdingIds, BindPiecesCollection bindPiecesCollection) { @@ -155,21 +205,17 @@ private ReceivingResults prepareResponseBody(Map> piecesGrou String poLineId = bindPiecesCollection.getPoLineId(); // Get all processed piece records for PO Line - Map processedPiecesForPoLine = StreamEx - .of(piecesGroupedByPoLine.getOrDefault(poLineId, Collections.emptyList())) - .toMap(Piece::getId, piece -> piece); + Map processedPiecesForPoLine = getProcessedPiecesForPoLine(poLineId, piecesGroupedByPoLine); - Map resultCounts = new HashMap<>(); - resultCounts.put(ProcessingStatus.Type.SUCCESS.toString(), 0); - resultCounts.put(ProcessingStatus.Type.FAILURE.toString(), 0); + var resultCounts = getEmptyResultCounts(); ReceivingResult result = new ReceivingResult(); for (String pieceId : bindPiecesCollection.getBindPieceIds()) { calculateProcessingErrors(poLineId, result, processedPiecesForPoLine, resultCounts, pieceId); } result.withPoLineId(poLineId) - .withProcessedSuccessfully(resultCounts.get(ProcessingStatus.Type.SUCCESS.toString())) - .withProcessedWithError(resultCounts.get(ProcessingStatus.Type.FAILURE.toString())); + .withProcessedSuccessfully(resultCounts.get(ProcessingStatus.Type.SUCCESS)) + .withProcessedWithError(resultCounts.get(ProcessingStatus.Type.FAILURE)); return new ReceivingResults() .withTotalRecords(1) .withReceivingResults(List.of(result)); diff --git a/src/main/java/org/folio/helper/CheckinHelper.java b/src/main/java/org/folio/helper/CheckinHelper.java index dfc88c2d5..7a165b1f8 100644 --- a/src/main/java/org/folio/helper/CheckinHelper.java +++ b/src/main/java/org/folio/helper/CheckinHelper.java @@ -23,7 +23,6 @@ import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -124,13 +123,9 @@ private ReceivingResults prepareResponseBody(CheckinCollection checkinCollection results.getReceivingResults().add(result); // Get all processed piece records for PO Line - Map processedPiecesForPoLine = StreamEx - .of(piecesGroupedByPoLine.getOrDefault(poLineId, Collections.emptyList())) - .toMap(Piece::getId, piece -> piece); + Map processedPiecesForPoLine = getProcessedPiecesForPoLine(poLineId, piecesGroupedByPoLine); - Map resultCounts = new HashMap<>(); - resultCounts.put(ProcessingStatus.Type.SUCCESS.toString(), 0); - resultCounts.put(ProcessingStatus.Type.FAILURE.toString(), 0); + Map resultCounts = getEmptyResultCounts(); for (CheckInPiece checkinPiece : toBeCheckedIn.getCheckInPieces()) { String pieceId = checkinPiece.getId(); @@ -138,8 +133,8 @@ private ReceivingResults prepareResponseBody(CheckinCollection checkinCollection } result.withPoLineId(poLineId) - .withProcessedSuccessfully(resultCounts.get(ProcessingStatus.Type.SUCCESS.toString())) - .withProcessedWithError(resultCounts.get(ProcessingStatus.Type.FAILURE.toString())); + .withProcessedSuccessfully(resultCounts.get(ProcessingStatus.Type.SUCCESS)) + .withProcessedWithError(resultCounts.get(ProcessingStatus.Type.FAILURE)); } return results; @@ -231,8 +226,7 @@ private void updatePieceWithCheckinInfo(Piece piece) { @Override protected Map> updatePieceRecordsWithoutItems(Map> piecesGroupedByPoLine) { - StreamEx.ofValues(piecesGroupedByPoLine) - .flatMap(List::stream) + extractAllPieces(piecesGroupedByPoLine) .filter(piece -> StringUtils.isEmpty(piece.getItemId())) .forEach(this::updatePieceWithCheckinInfo); diff --git a/src/main/java/org/folio/helper/CheckinReceivePiecesHelper.java b/src/main/java/org/folio/helper/CheckinReceivePiecesHelper.java index e1c9d9184..1262e068b 100644 --- a/src/main/java/org/folio/helper/CheckinReceivePiecesHelper.java +++ b/src/main/java/org/folio/helper/CheckinReceivePiecesHelper.java @@ -55,6 +55,8 @@ import java.util.function.Function; import java.util.stream.Collectors; +import static java.util.stream.Collectors.mapping; +import static java.util.stream.Collectors.toList; import static org.apache.commons.lang3.StringUtils.EMPTY; import static org.folio.orders.utils.HelperUtils.collectResultsOnSuccess; import static org.folio.orders.utils.ResourcePathResolver.PIECES_STORAGE; @@ -465,8 +467,7 @@ private Future processHoldingsUpdate(Map> piecesGroupe PoLineAndTitleById poLinesAndTitlesById, RequestContext requestContext) { List> futuresForHoldingsUpdates = new ArrayList<>(); - StreamEx.ofValues(piecesGroupedByPoLine) - .flatMap(List::stream) + extractAllPieces(piecesGroupedByPoLine) .forEach(piece -> { CompositePoLine poLine = poLinesAndTitlesById.poLineById.get(piece.getPoLineId()); if (poLine == null) { @@ -539,30 +540,18 @@ private boolean ifHoldingNotProcessed(String key) { private Future> getItemRecords(Map> piecesGroupedByPoLine, Map piecesByItemId, RequestContext requestContext) { - - Map> itemIdsByTenantId = new HashMap<>(); - for (List pieceList : piecesGroupedByPoLine.values()) { - for (Piece piece : pieceList) { - if (StringUtils.isNotEmpty(piece.getItemId())) { - itemIdsByTenantId.computeIfAbsent(piece.getReceivingTenantId(), k -> new ArrayList<>()).add(piece.getItemId()); - } - } - } - - // split all id lists by maximum number of id's for get query - List>> futures = itemIdsByTenantId.entrySet() - .stream() - .flatMap( - entry -> StreamEx.ofSubLists(entry.getValue(), MAX_IDS_FOR_GET_RQ_15) - .map(ids -> { - var locationContext = RequestContextUtil.createContextWithNewTenantId(requestContext, entry.getKey()); - return getItemRecordsByIds(ids, piecesByItemId, locationContext); - }) - ) - .toList(); - - return collectResultsOnSuccess(futures) - .map(lists -> StreamEx.of(lists).toFlatList(jsonObjects -> jsonObjects)); + // Split all id lists by maximum number of id's for get query + return collectResultsOnSuccess( + mapTenantIdsToItemIds(piecesGroupedByPoLine, requestContext).entrySet().stream() + .flatMap(entry -> + StreamEx.ofSubLists(entry.getValue(), MAX_IDS_FOR_GET_RQ_15) + .map(ids -> { + var locationContext = RequestContextUtil.createContextWithNewTenantId(requestContext, entry.getKey()); + return getItemRecordsByIds(ids, piecesByItemId, locationContext); + }) + ) + .toList()) + .map(lists -> StreamEx.of(lists).toFlatList(items -> items)); } /** @@ -767,6 +756,31 @@ private List getPieceIds() { .toFlatList(ids -> ids); } + protected Map getProcessedPiecesForPoLine(String poLineId, Map> piecesGroupedByPoLine) { + return StreamEx + .of(piecesGroupedByPoLine.getOrDefault(poLineId, Collections.emptyList())) + .toMap(Piece::getId, piece -> piece); + } + + protected Map getEmptyResultCounts() { + Map resultCounts = new HashMap<>(); + resultCounts.put(ProcessingStatus.Type.SUCCESS, 0); + resultCounts.put(ProcessingStatus.Type.FAILURE, 0); + return resultCounts; + } + + protected StreamEx extractAllPieces(Map> piecesGroupedByPoLine) { + return StreamEx.ofValues(piecesGroupedByPoLine).flatMap(List::stream); + } + + protected Map> mapTenantIdsToItemIds(Map> piecesGroupedByPoLine, RequestContext requestContext) { + return extractAllPieces(piecesGroupedByPoLine) + .filter(piece -> StringUtils.isNotEmpty(piece.getItemId())) + .groupingBy(piece -> Optional.ofNullable(piece.getReceivingTenantId()) + .orElse(RequestContextUtil.getContextTenantId(requestContext)), + mapping(Piece::getItemId, toList())); + } + //------------------------------------------------------------------------------------- /* Errors @@ -813,17 +827,17 @@ private void addError(String polId, String pieceId, Error error) { public void calculateProcessingErrors(String poLineId, ReceivingResult result, Map processedPiecesForPoLine, - Map resultCounts, String pieceId) { + Map resultCounts, String pieceId) { // Calculate processing status ProcessingStatus status = new ProcessingStatus(); Error error = getError(poLineId, pieceId); if (processedPiecesForPoLine.get(pieceId) != null && error == null) { status.setType(ProcessingStatus.Type.SUCCESS); - resultCounts.merge(ProcessingStatus.Type.SUCCESS.toString(), 1, Integer::sum); + resultCounts.merge(ProcessingStatus.Type.SUCCESS, 1, Integer::sum); } else { status.setType(ProcessingStatus.Type.FAILURE); status.setError(error); - resultCounts.merge(ProcessingStatus.Type.FAILURE.toString(), 1, Integer::sum); + resultCounts.merge(ProcessingStatus.Type.FAILURE, 1, Integer::sum); } ReceivingItemResult itemResult = new ReceivingItemResult(); diff --git a/src/main/java/org/folio/helper/ExpectHelper.java b/src/main/java/org/folio/helper/ExpectHelper.java index 01099098c..5ec8f40c2 100644 --- a/src/main/java/org/folio/helper/ExpectHelper.java +++ b/src/main/java/org/folio/helper/ExpectHelper.java @@ -15,7 +15,6 @@ import org.folio.rest.jaxrs.model.ToBeExpected; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; @@ -88,13 +87,9 @@ private ReceivingResults prepareResponseBody(ExpectCollection expectCollection, results.getReceivingResults().add(result); // Get all processed piece records for PO Line - Map processedPiecesForPoLine = StreamEx - .of(piecesGroupedByPoLine.getOrDefault(poLineId, Collections.emptyList())) - .toMap(Piece::getId, piece -> piece); + Map processedPiecesForPoLine = getProcessedPiecesForPoLine(poLineId, piecesGroupedByPoLine); - Map resultCounts = new HashMap<>(); - resultCounts.put(ProcessingStatus.Type.SUCCESS.toString(), 0); - resultCounts.put(ProcessingStatus.Type.FAILURE.toString(), 0); + Map resultCounts = getEmptyResultCounts(); for (ExpectPiece expectPiece : toBeExpected.getExpectPieces()) { String pieceId = expectPiece.getId(); @@ -102,16 +97,15 @@ private ReceivingResults prepareResponseBody(ExpectCollection expectCollection, } result.withPoLineId(poLineId) - .withProcessedSuccessfully(resultCounts.get(ProcessingStatus.Type.SUCCESS.toString())) - .withProcessedWithError(resultCounts.get(ProcessingStatus.Type.FAILURE.toString())); + .withProcessedSuccessfully(resultCounts.get(ProcessingStatus.Type.SUCCESS)) + .withProcessedWithError(resultCounts.get(ProcessingStatus.Type.FAILURE)); } return results; } private Map> updatePieceRecords(Map> piecesGroupedByPoLine) { - StreamEx.ofValues(piecesGroupedByPoLine) - .flatMap(List::stream) + extractAllPieces(piecesGroupedByPoLine) .forEach(this::updatePieceWithExpectInfo); return piecesGroupedByPoLine; diff --git a/src/main/java/org/folio/helper/ReceivingHelper.java b/src/main/java/org/folio/helper/ReceivingHelper.java index db44d3770..4fe9b6068 100644 --- a/src/main/java/org/folio/helper/ReceivingHelper.java +++ b/src/main/java/org/folio/helper/ReceivingHelper.java @@ -23,9 +23,7 @@ import org.folio.service.ProtectionService; import org.springframework.beans.factory.annotation.Autowired; -import java.util.Collections; import java.util.Date; -import java.util.HashMap; import java.util.List; import java.util.Map; @@ -142,13 +140,9 @@ private ReceivingResults prepareResponseBody(ReceivingCollection receivingCollec results.getReceivingResults().add(result); // Get all processed piece records for PO Line - Map processedPiecesForPoLine = StreamEx - .of(piecesGroupedByPoLine.getOrDefault(poLineId, Collections.emptyList())) - .toMap(Piece::getId, piece -> piece); + Map processedPiecesForPoLine = getProcessedPiecesForPoLine(poLineId, piecesGroupedByPoLine); - Map resultCounts = new HashMap<>(); - resultCounts.put(ProcessingStatus.Type.SUCCESS.toString(), 0); - resultCounts.put(ProcessingStatus.Type.FAILURE.toString(), 0); + Map resultCounts = getEmptyResultCounts(); for (ReceivedItem receivedItem : toBeReceived.getReceivedItems()) { String pieceId = receivedItem.getPieceId(); @@ -156,8 +150,8 @@ private ReceivingResults prepareResponseBody(ReceivingCollection receivingCollec } result.withPoLineId(poLineId) - .withProcessedSuccessfully(resultCounts.get(ProcessingStatus.Type.SUCCESS.toString())) - .withProcessedWithError(resultCounts.get(ProcessingStatus.Type.FAILURE.toString())); + .withProcessedSuccessfully(resultCounts.get(ProcessingStatus.Type.SUCCESS)) + .withProcessedWithError(resultCounts.get(ProcessingStatus.Type.FAILURE)); } return results; @@ -201,8 +195,7 @@ protected Future receiveInventoryItemAndUpdatePiece(JsonObject item, Pi @Override protected Map> updatePieceRecordsWithoutItems(Map> piecesGroupedByPoLine) { - StreamEx.ofValues(piecesGroupedByPoLine) - .flatMap(List::stream) + extractAllPieces(piecesGroupedByPoLine) .filter(piece -> StringUtils.isEmpty(piece.getItemId())) .forEach(this::updatePieceWithReceivingInfo); diff --git a/src/main/java/org/folio/orders/utils/RequestContextUtil.java b/src/main/java/org/folio/orders/utils/RequestContextUtil.java index dbd6ac27c..b48189ddb 100644 --- a/src/main/java/org/folio/orders/utils/RequestContextUtil.java +++ b/src/main/java/org/folio/orders/utils/RequestContextUtil.java @@ -22,4 +22,8 @@ public static RequestContext createContextWithNewTenantId(RequestContext request return new RequestContext(requestContext.getContext(), modifiedHeaders); } + public static String getContextTenantId(RequestContext requestContext) { + return requestContext.getHeaders().get(XOkapiHeaders.TENANT); + } + } diff --git a/src/main/java/org/folio/rest/core/exceptions/ErrorCodes.java b/src/main/java/org/folio/rest/core/exceptions/ErrorCodes.java index 53b5a89e9..6c9e84343 100644 --- a/src/main/java/org/folio/rest/core/exceptions/ErrorCodes.java +++ b/src/main/java/org/folio/rest/core/exceptions/ErrorCodes.java @@ -83,6 +83,8 @@ public enum ErrorCodes { PIECES_TO_BE_CREATED("piecesNeedToBeCreated", "Pieces need to be created"), LOCATION_CAN_NOT_BE_MODIFIER_AFTER_OPEN("locationCannotBeModifiedAfterOpen", "Please use the receiving App to update pieces and locations"), REQUEST_FOUND("thereAreRequestsOnItem", "There are requests on item"), + REQUESTS_ACTION_REQUIRED("requestsActionRequired", "There are circulation requests on items and requestsAction should be populated"), + PIECES_HAVE_DIFFERENT_RECEIVING_TENANT_IDS("piecesHaveDifferentReceivingTenantIds", "All pieces do not have the same receivingTenantId"), BUDGET_EXPENSE_CLASS_NOT_FOUND("budgetExpenseClassNotFound", "Budget expense class not found"), INACTIVE_EXPENSE_CLASS("inactiveExpenseClass", "Expense class is Inactive"), HOLDINGS_BY_INSTANCE_AND_LOCATION_NOT_FOUND("holdingsByInstanceAndLocationNotFoundError", "Holdings by instance id %s and location id %s not found"), diff --git a/src/main/java/org/folio/service/CirculationRequestsRetriever.java b/src/main/java/org/folio/service/CirculationRequestsRetriever.java index 4035d56d2..a61a18a93 100644 --- a/src/main/java/org/folio/service/CirculationRequestsRetriever.java +++ b/src/main/java/org/folio/service/CirculationRequestsRetriever.java @@ -33,7 +33,7 @@ public CirculationRequestsRetriever(RestClient restClient) { } public Future getNumberOfRequestsByItemId(String itemId, RequestContext requestContext) { - String query = String.format("(itemId==%s and status=\"%s\")", itemId, OUTSTANDING_REQUEST_STATUS_PREFIX); + String query = String.format("(itemId==%s and status=\"%s*\")", itemId, OUTSTANDING_REQUEST_STATUS_PREFIX); RequestEntry requestEntry = new RequestEntry(INVENTORY_LOOKUP_ENDPOINTS.get(REQUESTS)) .withQuery(query).withOffset(0).withLimit(0); // limit = 0 means payload will include only totalRecords value return restClient.getAsJsonObject(requestEntry, requestContext) @@ -57,7 +57,7 @@ public Future> getRequestIdsByItemIds(List itemIds, Request private Future> getRequestsByIds(List itemIds, RequestContext requestContext) { var futures = StreamEx.ofSubLists(itemIds, MAX_IDS_FOR_GET_RQ_15) - .map(ids -> String.format("(%s and status=\"*\")", convertIdsToCqlQuery(ids, ITEM_ID_KEY.getValue()))) + .map(ids -> String.format("(%s and status=\"%s*\")", convertIdsToCqlQuery(ids, ITEM_ID_KEY.getValue()), OUTSTANDING_REQUEST_STATUS_PREFIX)) .map(query -> new RequestEntry(INVENTORY_LOOKUP_ENDPOINTS.get(REQUESTS)) .withQuery(query).withOffset(0).withLimit(Integer.MAX_VALUE)) .map(entry -> restClient.getAsJsonObject(entry, requestContext)) diff --git a/src/test/java/org/folio/rest/impl/CheckinReceivingApiTest.java b/src/test/java/org/folio/rest/impl/CheckinReceivingApiTest.java index 68a6a1127..5ffe57f6c 100644 --- a/src/test/java/org/folio/rest/impl/CheckinReceivingApiTest.java +++ b/src/test/java/org/folio/rest/impl/CheckinReceivingApiTest.java @@ -19,6 +19,7 @@ import org.folio.rest.jaxrs.model.Errors; import org.folio.rest.jaxrs.model.ExpectCollection; import org.folio.rest.jaxrs.model.ExpectPiece; +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.PoLine; @@ -79,6 +80,7 @@ import static org.folio.orders.utils.ResourcePathResolver.PURCHASE_ORDER_STORAGE; import static org.folio.orders.utils.ResourcePathResolver.TITLES; import static org.folio.rest.RestConstants.MAX_IDS_FOR_GET_RQ_15; +import static org.folio.rest.RestConstants.VALIDATION_ERROR; import static org.folio.rest.core.exceptions.ErrorCodes.ITEM_NOT_RETRIEVED; import static org.folio.rest.core.exceptions.ErrorCodes.ITEM_UPDATE_FAILED; import static org.folio.rest.core.exceptions.ErrorCodes.LOC_NOT_PROVIDED; @@ -88,6 +90,7 @@ import static org.folio.rest.core.exceptions.ErrorCodes.PIECE_NOT_RETRIEVED; import static org.folio.rest.core.exceptions.ErrorCodes.PIECE_POL_MISMATCH; import static org.folio.rest.core.exceptions.ErrorCodes.PIECE_UPDATE_FAILED; +import static org.folio.rest.core.exceptions.ErrorCodes.REQUESTS_ACTION_REQUIRED; import static org.folio.rest.core.exceptions.ErrorCodes.TITLE_NOT_FOUND; import static org.folio.rest.impl.MockServer.BASE_MOCK_DATA_PATH; import static org.folio.rest.impl.MockServer.PIECE_RECORDS_MOCK_DATA_PATH; @@ -133,6 +136,7 @@ public class CheckinReceivingApiTest { private static final String ITEM_STATUS = "status"; private static final String COMPOSITE_POLINE_ONGOING_ID = "6e2b169a-ebeb-4c3c-a2f2-6233ff9c59ae"; private static final String COMPOSITE_POLINE_CANCELED_ID = "1196fcd9-7607-447d-ae85-6e91883d7e4f"; + private static final String OUTSTANDING_REQUEST_ITEM_ID = "f972fa0e-5e84-4a47-a27b-137724a73fee"; private static boolean runningOnOwn; @@ -968,7 +972,6 @@ private boolean isHoldingsUpdateRequired(org.folio.rest.acq.model.Piece piece, C } } - @Test void testBindPiecesToTitleWithItem() { logger.info("=== Test POST Bind to Title With Item"); @@ -1059,6 +1062,42 @@ void testBindPiecesWithDifferentHoldingIdAndThrowError() { assertEquals(errors.getErrors().get(0).getMessage(), "Holding Id must not be null or different for pieces"); } + @Test + void testBindPiecesToTitleWithItemWithOutstandingRequest() { + logger.info("=== Test POST Bind to Title with Item with Outstanding Request"); + + var order = getMinimalContentCompositePurchaseOrder() + .withId(UUID.randomUUID().toString()) + .withWorkflowStatus(CompositePurchaseOrder.WorkflowStatus.OPEN); + var poLine = getMinimalContentCompositePoLine(order.getId()) + .withId(UUID.randomUUID().toString()) + .withLocations(List.of(new Location().withHoldingId(UUID.randomUUID().toString()) + .withQuantityPhysical(1).withQuantity(1))); + var title = getTitle(poLine); + var bindingPiece = getMinimalContentPiece(poLine.getId()) + .withId(UUID.randomUUID().toString()) + .withItemId(OUTSTANDING_REQUEST_ITEM_ID) + .withReceivingStatus(Piece.ReceivingStatus.UNRECEIVABLE) + .withFormat(org.folio.rest.jaxrs.model.Piece.Format.ELECTRONIC); + var bindItem = getMinimalContentBindItem(); + var bindPiecesCollection = new BindPiecesCollection() + .withPoLineId(poLine.getId()) + .withBindItem(bindItem) + .withBindPieceIds(List.of(bindingPiece.getId())); + + addMockEntry(PURCHASE_ORDER_STORAGE, order); + addMockEntry(PO_LINES_STORAGE, poLine); + addMockEntry(PIECES_STORAGE, bindingPiece); + addMockEntry(TITLES, title); + + var errors = verifyPostResponse(ORDERS_BIND_ENDPOINT, JsonObject.mapFrom(bindPiecesCollection).encode(), + prepareHeaders(EXIST_CONFIG_X_OKAPI_TENANT_LIMIT_10), APPLICATION_JSON, VALIDATION_ERROR) + .as(Errors.class) + .getErrors(); + + assertThat(errors.get(0).getMessage(), equalTo(REQUESTS_ACTION_REQUIRED.getDescription())); + } + @Test void testPostReceivingWithErrorSearchingForPiece() { logger.info("=== Test POST Receiving - Receive resources with error searching for piece"); diff --git a/src/test/java/org/folio/rest/impl/MockServer.java b/src/test/java/org/folio/rest/impl/MockServer.java index e0d1f18c3..09d0567cc 100644 --- a/src/test/java/org/folio/rest/impl/MockServer.java +++ b/src/test/java/org/folio/rest/impl/MockServer.java @@ -1287,6 +1287,9 @@ private void handleGetItemRequests(RoutingContext ctx) { logger.info("handleGetItemRequests got: " + ctx.request().path()); try { String itemId = ctx.request().getParam("query").split("and")[0].split("==")[1].trim(); + if (itemId.startsWith("(") && itemId.endsWith(")")) { + itemId = itemId.substring(1, itemId.length() - 1); + } int limit = Integer.parseInt(ctx.request().getParam("limit")); JsonObject entries = new JsonObject(getMockData(ITEM_REQUESTS_MOCK_DATA_PATH + "itemRequests.json")); filterByKeyValue("itemId", itemId, entries.getJsonArray(REQUESTS)); diff --git a/src/test/resources/mockdata/itemRequests/itemRequests.json b/src/test/resources/mockdata/itemRequests/itemRequests.json index b99888c64..25102075a 100644 --- a/src/test/resources/mockdata/itemRequests/itemRequests.json +++ b/src/test/resources/mockdata/itemRequests/itemRequests.json @@ -37,7 +37,15 @@ "important" ] } + }, + { + "id": "f972fa0e-5e84-4a47-a27b-137724a73fee", + "requestType": "Recall", + "requestDate": "2023-03-23T11:04:25.000+00:00", + "requesterId": "b4cee18d-f862-4ef1-95a5-879fdd619603", + "itemId": "f972fa0e-5e84-4a47-a27b-137724a73fee", + "fulfillmentPreference": "Delivery" } ], - "totalRecords": 2 + "totalRecords": 3 }