diff --git a/ramls/check-in.raml b/ramls/check-in.raml index 70337a156..2aaa91381 100644 --- a/ramls/check-in.raml +++ b/ramls/check-in.raml @@ -34,3 +34,11 @@ resourceTypes: is: [validate] post: description: Check-in items spanning one or more po_lines in this order + queryParameters: + deleteHoldings: + displayName: The associated holdings should be removed + description: The associated holdings should be removed + type: boolean + required: false + example: true + default: false diff --git a/src/main/java/org/folio/helper/CheckinHelper.java b/src/main/java/org/folio/helper/CheckinHelper.java index 3af376393..79767a94b 100644 --- a/src/main/java/org/folio/helper/CheckinHelper.java +++ b/src/main/java/org/folio/helper/CheckinHelper.java @@ -12,6 +12,7 @@ import org.apache.commons.lang3.tuple.Pair; import org.folio.models.pieces.PiecesHolder; import org.folio.orders.events.handlers.MessageAddress; +import org.folio.orders.utils.StreamUtils; import org.folio.rest.core.models.RequestContext; import org.folio.rest.jaxrs.model.CheckInPiece; import org.folio.rest.jaxrs.model.CheckinCollection; @@ -22,6 +23,8 @@ import org.folio.rest.jaxrs.model.ReceivingResults; import org.folio.rest.jaxrs.model.ToBeCheckedIn; import org.folio.service.inventory.InventoryUtils; +import org.folio.service.pieces.PieceUpdateInventoryService; +import org.springframework.beans.factory.annotation.Autowired; import java.util.ArrayList; import java.util.Collection; @@ -40,8 +43,10 @@ public class CheckinHelper extends CheckinReceivePiecesHelper { public static final String IS_ITEM_ORDER_CLOSED_PRESENT = "isItemOrderClosedPresent"; - public CheckinHelper(CheckinCollection checkinCollection, Map okapiHeaders, - Context ctx) { + @Autowired + private PieceUpdateInventoryService pieceUpdateInventoryService; + + public CheckinHelper(CheckinCollection checkinCollection, Map okapiHeaders, Context ctx) { super(okapiHeaders, ctx); // Convert request to map representation CheckinCollection checkinCollectionClone = JsonObject.mapFrom(checkinCollection).mapTo(CheckinCollection.class); @@ -57,12 +62,12 @@ public CheckinHelper(CheckinCollection checkinCollection, Map ok } } - public Future checkinPieces(CheckinCollection checkinCollection, RequestContext requestContext) { + public Future checkinPieces(CheckinCollection checkinCollection, boolean deleteHoldings, RequestContext requestContext) { return removeForbiddenEntities(requestContext) - .compose(voidResult -> processCheckInPieces(checkinCollection, requestContext)); + .compose(voidResult -> processCheckInPieces(checkinCollection, deleteHoldings, requestContext)); } - private Future processCheckInPieces(CheckinCollection checkinCollection, RequestContext requestContext) { + private Future processCheckInPieces(CheckinCollection checkinCollection, boolean deleteHoldings, RequestContext requestContext) { PiecesHolder holder = new PiecesHolder(); // 1. Get purchase order and poLine from storage return findAndSetPurchaseOrderPoLinePair(extractPoLineId(checkinCollection), holder, requestContext) @@ -72,14 +77,15 @@ private Future processCheckInPieces(CheckinCollection checkinC .compose(piecesByPoLineIds -> filterMissingLocations(piecesByPoLineIds, requestContext)) // 4. Update items in the Inventory if required .compose(pieces -> updateInventoryItemsAndHoldings(pieces, holder, requestContext)) - // 5. Update piece records with checkIn details which do not have - // associated item + // 5. Delete abandoned holdings if deleteHoldings flag is set to true + .compose(pieces -> deleteHoldings(pieces, deleteHoldings, requestContext)) + // 6. Update piece records with checkIn details which do not have an associated item .map(this::updatePieceRecordsWithoutItems) - // 6. Update received piece records in the storage + // 7. Update received piece records in the storage .compose(piecesGroupedByPoLine -> storeUpdatedPieceRecords(piecesGroupedByPoLine, requestContext)) - // 7. Update PO Line status + // 8. Update PO Line status .compose(piecesGroupedByPoLine -> updateOrderAndPoLinesStatus(holder.getPiecesFromStorage(), piecesGroupedByPoLine, checkinCollection, requestContext)) - // 8. Return results to the client + // 9. Return results to the client .map(piecesGroupedByPoLine -> prepareResponseBody(checkinCollection, piecesGroupedByPoLine)); } @@ -205,6 +211,13 @@ protected Future receiveInventoryItemAndUpdatePiece(PiecesHolder holder return promise.future(); } + private Future>> deleteHoldings(Map> piecesGroupedByPoLine, boolean deleteHoldings, RequestContext requestContext) { + return deleteHoldings + ? pieceUpdateInventoryService.deleteHoldingsConnectedToPieces(StreamUtils.flatten(piecesGroupedByPoLine.values()), requestContext).map(piecesGroupedByPoLine) + : Future.succeededFuture(piecesGroupedByPoLine); + + } + private void updatePieceWithCheckinInfo(Piece piece) { CheckInPiece checkinPiece = getByPiece(piece); diff --git a/src/main/java/org/folio/orders/utils/StreamUtils.java b/src/main/java/org/folio/orders/utils/StreamUtils.java index c57633283..f6db650f0 100644 --- a/src/main/java/org/folio/orders/utils/StreamUtils.java +++ b/src/main/java/org/folio/orders/utils/StreamUtils.java @@ -43,6 +43,10 @@ public static Set mapToSet(Collection collection, Function ma return collection.stream().map(mapper).collect(Collectors.toSet()); } + public static > List flatten(Collection collection) { + return collection.stream().flatMap(Collection::stream).toList(); + } + public static Map listToMap(Collection collection, Function toKey) { return listToMap(collection, toKey, Function.identity()); } diff --git a/src/main/java/org/folio/rest/impl/ReceivingAPI.java b/src/main/java/org/folio/rest/impl/ReceivingAPI.java index e74063542..c444422da 100644 --- a/src/main/java/org/folio/rest/impl/ReceivingAPI.java +++ b/src/main/java/org/folio/rest/impl/ReceivingAPI.java @@ -50,11 +50,11 @@ public void postOrdersReceive(ReceivingCollection entity, Map ok @Override @Validate - public void postOrdersCheckIn(CheckinCollection entity, Map okapiHeaders, + public void postOrdersCheckIn(boolean deleteHoldings, CheckinCollection entity, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { logger.debug("CheckIn {} items", entity.getTotalRecords()); CheckinHelper helper = new CheckinHelper(entity, okapiHeaders, vertxContext); - helper.checkinPieces(entity, new RequestContext(vertxContext, okapiHeaders)) + helper.checkinPieces(entity, deleteHoldings, new RequestContext(vertxContext, okapiHeaders)) .onSuccess(result -> asyncResultHandler.handle(succeededFuture(helper.buildOkResponse(result)))) .onFailure(t -> handleErrorResponse(asyncResultHandler, helper, t)); } diff --git a/src/main/java/org/folio/service/pieces/PieceUpdateInventoryService.java b/src/main/java/org/folio/service/pieces/PieceUpdateInventoryService.java index 0534819f8..40227fa83 100644 --- a/src/main/java/org/folio/service/pieces/PieceUpdateInventoryService.java +++ b/src/main/java/org/folio/service/pieces/PieceUpdateInventoryService.java @@ -1,15 +1,19 @@ package org.folio.service.pieces; +import static org.folio.orders.utils.HelperUtils.collectResultsOnSuccess; import static org.folio.orders.utils.RequestContextUtil.createContextWithNewTenantId; import static org.folio.service.inventory.InventoryHoldingManager.HOLDING_PERMANENT_LOCATION_ID; import java.util.List; +import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import one.util.streamex.StreamEx; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.tuple.Pair; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.folio.rest.core.models.RequestContext; import org.folio.rest.jaxrs.model.PoLine; import org.folio.rest.jaxrs.model.CompositePurchaseOrder; @@ -21,68 +25,75 @@ import io.vertx.core.Future; import io.vertx.core.json.JsonObject; +@Log4j2 +@RequiredArgsConstructor public class PieceUpdateInventoryService { - private static final Logger logger = LogManager.getLogger(PieceUpdateInventoryService.class); + + private static final int ITEM_QUANTITY = 1; private final InventoryItemManager inventoryItemManager; private final InventoryHoldingManager inventoryHoldingManager; private final PieceStorageService pieceStorageService; - public PieceUpdateInventoryService(InventoryItemManager inventoryItemManager, - InventoryHoldingManager inventoryHoldingManager, - PieceStorageService pieceStorageService) { - this.inventoryItemManager = inventoryItemManager; - this.inventoryHoldingManager = inventoryHoldingManager; - this.pieceStorageService = pieceStorageService; - } - /** * Return id of created Item */ public Future manualPieceFlowCreateItemRecord(Piece piece, CompositePurchaseOrder compPO, PoLine poLine, RequestContext requestContext) { - final int ITEM_QUANTITY = 1; - logger.debug("manualPieceFlowCreateItemRecord:: Handling {} items for PO Line and holdings with id={}, receivingTenantId={}", + log.debug("manualPieceFlowCreateItemRecord:: Handling {} items for PO Line and holdings with id={}, receivingTenantId={}", ITEM_QUANTITY, piece.getHoldingId(), piece.getReceivingTenantId()); if (piece.getFormat() == Piece.Format.ELECTRONIC && DefaultPieceFlowsValidator.isCreateItemForElectronicPiecePossible(piece, poLine)) { return inventoryItemManager.createMissingElectronicItems(compPO, poLine, piece, ITEM_QUANTITY, requestContext) - .map(idS -> idS.get(0)); + .map(List::getFirst); } else if (DefaultPieceFlowsValidator.isCreateItemForNonElectronicPiecePossible(piece, poLine)) { return inventoryItemManager.createMissingPhysicalItems(compPO, poLine, piece, ITEM_QUANTITY, requestContext) - .map(idS -> idS.get(0)); + .map(List::getFirst); } else { - logger.warn("manualPieceFlowCreateItemRecord:: Creating Item is not possible for piece: {}, poLine: {}", + log.warn("manualPieceFlowCreateItemRecord:: Creating Item is not possible for piece: {}, poLine: {}", piece.getId(), poLine.getId()); return Future.succeededFuture(); } } public Future> deleteHoldingConnectedToPiece(Piece piece, RequestContext requestContext) { - if (piece == null || piece.getHoldingId() == null) { - return Future.succeededFuture(); - } - var locationContext = createContextWithNewTenantId(requestContext, piece.getReceivingTenantId()); - String holdingId = piece.getHoldingId(); - return inventoryHoldingManager.getHoldingById(holdingId, true, locationContext) - .compose(holding -> getUpdatePossibleForHolding(holding, holdingId, piece, locationContext, requestContext)) - .compose(isUpdatePossibleVsHolding -> deleteHoldingIfPossible(isUpdatePossibleVsHolding, holdingId, locationContext)); + return deleteHoldingsConnectedToPieces(Optional.ofNullable(piece).map(List::of).orElseGet(List::of), requestContext) + .compose(result -> Optional.ofNullable(result).map(List::getFirst).map(Future::succeededFuture).orElseGet(Future::succeededFuture)); + } + + public Future>> deleteHoldingsConnectedToPieces(List pieces, RequestContext requestContext) { + var viablePieces = StreamEx.of(pieces) + .filter(piece -> piece != null && piece.getHoldingId() != null) + .toList(); + return viablePieces.isEmpty() + ? Future.succeededFuture() + : collectResultsOnSuccess(doDeleteHolding(viablePieces, requestContext)); + } + + private List>> doDeleteHolding(List pieces, RequestContext requestContext) { + return StreamEx.of(pieces) + .groupingBy(piece -> Pair.of(piece.getHoldingId(), piece.getReceivingTenantId()), Collectors.mapping(Piece::getId, Collectors.toSet())) + .entrySet().stream() + .map(entry -> { + var holdingId = entry.getKey().getKey(); + var receivingTenantId = entry.getKey().getValue(); + var pieceIds = entry.getValue(); + var locationContext = createContextWithNewTenantId(requestContext, receivingTenantId); + return inventoryHoldingManager.getHoldingById(holdingId, true, locationContext) + .compose(holding -> getUpdatePossibleForHolding(holding, holdingId, pieceIds, locationContext, requestContext)) + .compose(isUpdatePossibleVsHolding -> deleteHoldingIfPossible(isUpdatePossibleVsHolding, holdingId, locationContext)); + }).toList(); } - private Future> getUpdatePossibleForHolding(JsonObject holding, String holdingId, Piece piece, + private Future> getUpdatePossibleForHolding(JsonObject holding, String holdingId, Set pieceIds, RequestContext locationContext, RequestContext requestContext) { if (holding == null || holding.isEmpty()) { return Future.succeededFuture(Pair.of(false, new JsonObject())); } return pieceStorageService.getPiecesByHoldingId(holdingId, requestContext) - .map(pieces -> skipPieceToProcess(piece, pieces)) - .compose(existingPieces -> inventoryItemManager.getItemsByHoldingId(holdingId, locationContext) - .map(existingItems-> { - List remainingPieces = skipPieceToProcess(piece, existingPieces); - if (CollectionUtils.isEmpty(remainingPieces) && CollectionUtils.isEmpty(existingItems)) { - return Pair.of(true, holding); - } - return Pair.of(false, new JsonObject()); - }) + .compose(pieces -> inventoryItemManager.getItemsByHoldingId(holdingId, locationContext) + .map(items -> CollectionUtils.isEmpty(filterPiecesToProcess(pieceIds, pieces)) && CollectionUtils.isEmpty(items) + ? Pair.of(true, holding) + : Pair.of(false, new JsonObject())) ); } @@ -91,15 +102,16 @@ private Future> deleteHoldingIfPossible(Pair Pair.of(holdingId, permanentLocationId)); + .map(v -> Pair.of(holdingId, holding.getString(HOLDING_PERMANENT_LOCATION_ID))); } return Future.succeededFuture(); } - private List skipPieceToProcess(Piece piece, List pieces) { - return pieces.stream().filter(aPiece -> !aPiece.getId().equals(piece.getId())).collect( - Collectors.toList()); + private List filterPiecesToProcess(Set pieceIdsToSkip, List pieces) { + return pieces.stream() + .filter(piece -> !pieceIdsToSkip.contains(piece.getId())) + .toList(); } + } diff --git a/src/test/java/org/folio/helper/CheckinHelperTest.java b/src/test/java/org/folio/helper/CheckinHelperTest.java index ab45674a5..0d406d8b5 100644 --- a/src/test/java/org/folio/helper/CheckinHelperTest.java +++ b/src/test/java/org/folio/helper/CheckinHelperTest.java @@ -49,6 +49,7 @@ import org.folio.rest.jaxrs.model.Piece; import org.folio.rest.jaxrs.model.ToBeCheckedIn; import org.folio.service.ProtectionService; +import org.folio.service.pieces.PieceUpdateInventoryService; import org.folio.service.settings.CommonSettingsRetriever; import org.folio.service.inventory.InventoryHoldingManager; import org.folio.service.inventory.InventoryInstanceManager; @@ -66,11 +67,11 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; -@ExtendWith(VertxExtension.class) +@ExtendWith({VertxExtension.class, MockitoExtension.class}) public class CheckinHelperTest { @Autowired @@ -80,6 +81,8 @@ public class CheckinHelperTest { @Autowired PieceUpdateFlowPoLineService pieceUpdateFlowPoLineService; @Autowired + PieceUpdateInventoryService pieceUpdateInventoryService; + @Autowired ItemRecreateInventoryService itemRecreateInventoryService; @Autowired PurchaseOrderStorageService purchaseOrderStorageService; @@ -108,7 +111,6 @@ public static void after() { @BeforeEach void beforeEach() { - MockitoAnnotations.openMocks(this); autowireDependencies(this); var ctxMock = getFirstContextFromVertx(getVertx()); okapiHeadersMock = new HashMap<>(); @@ -327,17 +329,18 @@ private static class ContextConfiguration { PieceCreateFlowInventoryManager pieceCreateFlowInventoryManager() { return mock(PieceCreateFlowInventoryManager.class); } - @Bean PieceUpdateFlowPoLineService pieceUpdateFlowPoLineService() { return mock(PieceUpdateFlowPoLineService.class); } - + @Bean + PieceUpdateInventoryService pieceUpdateInventoryService() { + return mock(PieceUpdateInventoryService.class); + } @Bean CommonSettingsRetriever configurationEntriesService() { return mock(CommonSettingsRetriever.class); } - @Bean ProtectionService protectionService() { return mock(ProtectionService.class); diff --git a/src/test/java/org/folio/service/pieces/PieceUpdateInventoryServiceTest.java b/src/test/java/org/folio/service/pieces/PieceUpdateInventoryServiceTest.java index de07c6880..12d4f3d37 100644 --- a/src/test/java/org/folio/service/pieces/PieceUpdateInventoryServiceTest.java +++ b/src/test/java/org/folio/service/pieces/PieceUpdateInventoryServiceTest.java @@ -35,15 +35,18 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import io.vertx.core.Context; import io.vertx.core.json.JsonObject; +@ExtendWith(MockitoExtension.class) public class PieceUpdateInventoryServiceTest { + @Autowired private PieceUpdateInventoryService pieceUpdateInventoryService; @Autowired @@ -54,14 +57,13 @@ public class PieceUpdateInventoryServiceTest { private PieceStorageService pieceStorageService; @Mock private Map okapiHeadersMock; - private final Context ctx = getFirstContextFromVertx(getVertx()); + private final Context ctx = getFirstContextFromVertx(getVertx()); private RequestContext requestContext; private static boolean runningOnOwn; @BeforeEach void initMocks(){ - MockitoAnnotations.openMocks(this); autowireDependencies(this); requestContext = new RequestContext(ctx, okapiHeadersMock); }