diff --git a/src/main/java/org/folio/config/ApplicationConfig.java b/src/main/java/org/folio/config/ApplicationConfig.java index eb87acf47..2b054729a 100644 --- a/src/main/java/org/folio/config/ApplicationConfig.java +++ b/src/main/java/org/folio/config/ApplicationConfig.java @@ -746,14 +746,15 @@ PurchaseOrderHelper purchaseOrderHelper(PurchaseOrderLineHelper purchaseOrderLin ProtectionService protectionService, InventoryItemStatusSyncService itemStatusSyncService, OpenCompositeOrderManager openCompositeOrderManager, PurchaseOrderStorageService purchaseOrderStorageService, CommonSettingsCache commonSettingsCache, PoNumberHelper poNumberHelper, - OpenCompositeOrderFlowValidator openCompositeOrderFlowValidator, ReOpenCompositeOrderManager reOpenCompositeOrderManager, - OrderValidationService orderValidationService, PoLineValidationService poLineValidationService) { + ReOpenCompositeOrderManager reOpenCompositeOrderManager, + OrderValidationService orderValidationService, PoLineValidationService poLineValidationService, + UnOpenCompositeOrderManager unOpenCompositeOrderManager) { return new PurchaseOrderHelper(purchaseOrderLineHelper, orderLinesSummaryPopulateService, encumbranceService, combinedPopulateService, encumbranceWorkflowStrategyFactory, orderInvoiceRelationService, tagService, purchaseOrderLineService, titlesService, protectionService, itemStatusSyncService, openCompositeOrderManager, purchaseOrderStorageService, commonSettingsCache, - poNumberHelper, openCompositeOrderFlowValidator, reOpenCompositeOrderManager, - orderValidationService, poLineValidationService); + poNumberHelper, reOpenCompositeOrderManager, + orderValidationService, poLineValidationService, unOpenCompositeOrderManager); } @Bean diff --git a/src/main/java/org/folio/helper/PurchaseOrderHelper.java b/src/main/java/org/folio/helper/PurchaseOrderHelper.java index 780fe3a3d..8f4b4bf8b 100644 --- a/src/main/java/org/folio/helper/PurchaseOrderHelper.java +++ b/src/main/java/org/folio/helper/PurchaseOrderHelper.java @@ -11,6 +11,7 @@ import static org.folio.orders.utils.OrderStatusTransitionUtil.isOrderReopening; import static org.folio.orders.utils.OrderStatusTransitionUtil.isTransitionToClosed; import static org.folio.orders.utils.OrderStatusTransitionUtil.isTransitionToOpen; +import static org.folio.orders.utils.OrderStatusTransitionUtil.isTransitionToPending; import static org.folio.orders.utils.OrderStatusTransitionUtil.isTransitionToReopen; import static org.folio.orders.utils.POProtectedFields.getFieldNames; import static org.folio.orders.utils.PermissionsUtil.*; @@ -67,9 +68,9 @@ import org.folio.service.orders.OrderWorkflowType; import org.folio.service.orders.PurchaseOrderLineService; import org.folio.service.orders.PurchaseOrderStorageService; -import org.folio.service.orders.flows.update.open.OpenCompositeOrderFlowValidator; import org.folio.service.orders.flows.update.open.OpenCompositeOrderManager; import org.folio.service.orders.flows.update.reopen.ReOpenCompositeOrderManager; +import org.folio.service.orders.flows.update.unopen.UnOpenCompositeOrderManager; import org.folio.service.titles.TitlesService; import io.vertx.core.Future; @@ -92,13 +93,13 @@ public class PurchaseOrderHelper { private final ProtectionService protectionService; private final InventoryItemStatusSyncService itemStatusSyncService; private final OpenCompositeOrderManager openCompositeOrderManager; - private final OpenCompositeOrderFlowValidator openCompositeOrderFlowValidator; private final PurchaseOrderStorageService purchaseOrderStorageService; private final CommonSettingsCache commonSettingsCache; private final PoNumberHelper poNumberHelper; private final ReOpenCompositeOrderManager reOpenCompositeOrderManager; private final OrderValidationService orderValidationService; private final PoLineValidationService poLineValidationService; + private final UnOpenCompositeOrderManager unOpenCompositeOrderManager; public PurchaseOrderHelper(PurchaseOrderLineHelper purchaseOrderLineHelper, CompositeOrderDynamicDataPopulateService orderLinesSummaryPopulateService, EncumbranceService encumbranceService, @@ -109,9 +110,8 @@ public PurchaseOrderHelper(PurchaseOrderLineHelper purchaseOrderLineHelper, ProtectionService protectionService, InventoryItemStatusSyncService itemStatusSyncService, OpenCompositeOrderManager openCompositeOrderManager, PurchaseOrderStorageService purchaseOrderStorageService, CommonSettingsCache commonSettingsCache, PoNumberHelper poNumberHelper, - OpenCompositeOrderFlowValidator openCompositeOrderFlowValidator, ReOpenCompositeOrderManager reOpenCompositeOrderManager, OrderValidationService orderValidationService, - PoLineValidationService poLineValidationService) { + PoLineValidationService poLineValidationService, UnOpenCompositeOrderManager unOpenCompositeOrderManager) { this.purchaseOrderLineHelper = purchaseOrderLineHelper; this.orderLinesSummaryPopulateService = orderLinesSummaryPopulateService; this.encumbranceService = encumbranceService; @@ -127,10 +127,10 @@ public PurchaseOrderHelper(PurchaseOrderLineHelper purchaseOrderLineHelper, this.purchaseOrderStorageService = purchaseOrderStorageService; this.commonSettingsCache = commonSettingsCache; this.poNumberHelper = poNumberHelper; - this.openCompositeOrderFlowValidator = openCompositeOrderFlowValidator; this.reOpenCompositeOrderManager = reOpenCompositeOrderManager; this.orderValidationService = orderValidationService; this.poLineValidationService = poLineValidationService; + this.unOpenCompositeOrderManager = unOpenCompositeOrderManager; } /** @@ -226,7 +226,13 @@ public Future updateOrder(CompositePurchaseOrder compPO, boolean deleteHol CompositePurchaseOrder clonedPoFromStorage = JsonObject.mapFrom(poFromStorage).mapTo(CompositePurchaseOrder.class); boolean isTransitionToOpen = isTransitionToOpen(poFromStorage, compPO); return validateUserUnaffiliatedPoLineLocations(clonedPoFromStorage.getPoLines(), requestContext) - .compose(v -> orderValidationService.validateOrderForUpdate(compPO, poFromStorage, deleteHoldings, requestContext)) + .compose(v -> orderValidationService.validateOrderForUpdate(compPO, poFromStorage, requestContext)) + .compose(ok -> { + if (isTransitionToPending(poFromStorage, compPO)) { + return unOpenCompositeOrderManager.process(compPO, poFromStorage, deleteHoldings, requestContext); + } + return Future.succeededFuture(); + }) .compose(v -> { if (isTransitionToOpen && CollectionUtils.isEmpty(compPO.getPoLines())) { compPO.setPoLines(clonedPoFromStorage.getPoLines()); diff --git a/src/main/java/org/folio/service/orders/OrderValidationService.java b/src/main/java/org/folio/service/orders/OrderValidationService.java index 5a8eda3eb..9103aab11 100644 --- a/src/main/java/org/folio/service/orders/OrderValidationService.java +++ b/src/main/java/org/folio/service/orders/OrderValidationService.java @@ -180,7 +180,7 @@ public Future> validateOrderForPut(String orderId, CompositePurchase * This validation is used for both the PUT endpoint and data import. */ public Future validateOrderForUpdate(CompositePurchaseOrder compPO, CompositePurchaseOrder poFromStorage, - boolean deleteHoldings, RequestContext requestContext) { + RequestContext requestContext) { logger.info("validateOrderForUpdate :: orderId: {}", compPO.getId()); return validateAcqUnitsOnUpdate(compPO, poFromStorage, requestContext) .compose(ok -> prefixService.validatePrefixAvailability(compPO.getPoNumberPrefix(), requestContext)) @@ -196,7 +196,7 @@ public Future validateOrderForUpdate(CompositePurchaseOrder compPO, Compos .compose(ok -> { if (isTransitionToPending(poFromStorage, compPO)) { checkOrderUnopenPermissions(requestContext); - return unOpenCompositeOrderManager.process(compPO, poFromStorage, deleteHoldings, requestContext); + return unOpenCompositeOrderManager.checkRequests(compPO, requestContext); } return Future.succeededFuture(); }); diff --git a/src/main/java/org/folio/service/orders/flows/update/unopen/UnOpenCompositeOrderManager.java b/src/main/java/org/folio/service/orders/flows/update/unopen/UnOpenCompositeOrderManager.java index f2a1b7b78..cd5818a89 100644 --- a/src/main/java/org/folio/service/orders/flows/update/unopen/UnOpenCompositeOrderManager.java +++ b/src/main/java/org/folio/service/orders/flows/update/unopen/UnOpenCompositeOrderManager.java @@ -17,6 +17,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.EnumSet; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -99,6 +100,48 @@ public Future process(CompositePurchaseOrder compPO, CompositePurchaseOrde } + public Future rollbackInventory(CompositePurchaseOrder compPO, RequestContext requestContext) { + return processPoLines(compPO.getPoLines(), poLine -> processInventory(poLine, true, requestContext)); + } + + public Future checkRequests(CompositePurchaseOrder compPO, RequestContext requestContext) { + List poLines = compPO.getPoLines().stream() + .filter(poLine -> !Boolean.TRUE.equals(poLine.getIsPackage())) + .toList(); + HashMap> tenantIdToPoLineIds = createTenantIdToPoLineIdsMap(poLines); + List> futures = tenantIdToPoLineIds.keySet().stream() + .map(tenantId -> { + List poLineIds = tenantIdToPoLineIds.get(tenantId); + RequestContext tenantRequestContext = createContextWithNewTenantId(requestContext, tenantId); + return inventoryItemManager.getItemsByPoLineIdsAndStatus(poLineIds, ItemStatus.ON_ORDER.value(), tenantRequestContext) + .compose(onOrderItems -> { + if (CollectionUtils.isEmpty(onOrderItems)) { + return Future.succeededFuture(); + } + List itemIds = onOrderItems.stream().map(item -> item.getString(ID)).toList(); + return checkRequestsForItems(itemIds, tenantRequestContext); + }); + }) + .toList(); + return HelperUtils.executeAllFailFast(futures); + } + + private HashMap> createTenantIdToPoLineIdsMap(List poLines) { + HashMap> tenantIdToPoLineIds = new HashMap<>(); + poLines.forEach(poLine -> { + List tenantIds = PoLineCommonUtil.getTenantsFromLocations(poLine); + tenantIds.forEach(tenantId -> { + List poLineIds = tenantIdToPoLineIds.get(tenantId); + if (poLineIds == null) { + poLineIds = new ArrayList<>(); + } + poLineIds.add(poLine.getId()); + tenantIdToPoLineIds.put(tenantId, poLineIds); + }); + }); + return tenantIdToPoLineIds; + } + private Future updateAndGetOrderWithLines(CompositePurchaseOrder compPO, RequestContext requestContext) { if (CollectionUtils.isNotEmpty(compPO.getPoLines())) { return Future.succeededFuture(compPO); @@ -121,10 +164,6 @@ private Future updatePoLinesSummary(List poLines, RequestContext r return processPoLines(poLines, poLine -> purchaseOrderLineService.saveOrderLine(poLine, requestContext)); } - public Future rollbackInventory(CompositePurchaseOrder compPO, RequestContext requestContext) { - return processPoLines(compPO.getPoLines(), poLine -> processInventory(poLine, true, requestContext)); - } - private Future processInventory(List poLines, boolean deleteHoldings, RequestContext requestContext) { return processPoLines(poLines, poLine -> processInventory(poLine, deleteHoldings, requestContext)); } @@ -366,7 +405,6 @@ private Future deletePieceWithItem(String pieceId, RequestContext requestC .compose(poLine -> purchaseOrderStorageService.getPurchaseOrderById(poLine.getPurchaseOrderId(), requestContext) .map(purchaseOrder -> holder.withOrderInformation(purchaseOrder, poLine))) .compose(aHolder -> protectionService.isOperationRestricted(holder.getOriginPurchaseOrder().getAcqUnitIds(), DELETE, requestContext)) - .compose(vVoid -> canDeletePieceWithItem(holder.getPieceToDelete(), requestContext)) .compose(aVoid -> pieceStorageService.deletePiece(pieceId, requestContext)) .compose(aVoid -> deletePieceConnectedItem(holder.getPieceToDelete(), itemContext)); } @@ -390,9 +428,9 @@ private Future deletePieceConnectedItem(Piece piece, RequestContext reques }); } - private Future canDeletePieceWithItem(Piece piece, RequestContext requestContext) { - return circulationRequestsRetriever.getNumberOfRequestsByItemId(piece.getItemId(), requestContext) - .compose(numOfRequests -> numOfRequests != null && numOfRequests > 0 + private Future checkRequestsForItems(List itemIds, RequestContext requestContext) { + return circulationRequestsRetriever.getNumbersOfRequestsByItemIds(itemIds, requestContext) + .compose(idsAndCounts -> idsAndCounts.values().stream().anyMatch(count -> count > 0) ? Future.failedFuture(new HttpException(422, ErrorCodes.REQUEST_FOUND.toError())) : Future.succeededFuture()); } diff --git a/src/test/java/org/folio/ApiTestSuite.java b/src/test/java/org/folio/ApiTestSuite.java index e5a5338dd..236aefe4e 100644 --- a/src/test/java/org/folio/ApiTestSuite.java +++ b/src/test/java/org/folio/ApiTestSuite.java @@ -96,6 +96,7 @@ import org.folio.service.orders.flows.update.open.OpenCompositeOrderManagerTest; import org.folio.service.orders.flows.update.open.OpenCompositeOrderPieceServiceTest; import org.folio.service.orders.flows.update.reopen.ReOpenCompositeOrderManagerTest; +import org.folio.service.orders.flows.update.unopen.UnOpenCompositeOrderManagerTest; import org.folio.service.orders.lines.update.OrderLineUpdateInstanceHandlerTest; import org.folio.service.orders.lines.update.instance.WithHoldingOrderLineUpdateInstanceStrategyTest; import org.folio.service.orders.lines.update.instance.WithoutHoldingOrderLineUpdateInstanceStrategyTest; @@ -497,6 +498,10 @@ class InvoiceLineServiceTestNested extends InvoiceLineServiceTest { class ReOpenCompositeOrderManagerTestNested extends ReOpenCompositeOrderManagerTest { } + @Nested + class UnOpenCompositeOrderManagerTestNested extends UnOpenCompositeOrderManagerTest { + } + @Nested class OrderLineUpdateInstanceHandlerTestNested extends OrderLineUpdateInstanceHandlerTest { } diff --git a/src/test/java/org/folio/helper/PurchaseOrderHelperTest.java b/src/test/java/org/folio/helper/PurchaseOrderHelperTest.java index c194f875b..ec6df2837 100644 --- a/src/test/java/org/folio/helper/PurchaseOrderHelperTest.java +++ b/src/test/java/org/folio/helper/PurchaseOrderHelperTest.java @@ -196,7 +196,7 @@ void testPutPendingCompositeOrder() throws IOException { .when(purchaseOrderLineService).populateOrderLines(any(CompositePurchaseOrder.class), eq(requestContext)); doReturn(succeededFuture(null)) .when(orderValidationService).validateOrderForUpdate(any(CompositePurchaseOrder.class), any(CompositePurchaseOrder.class), - eq(deleteHoldings), eq(requestContext)); + eq(requestContext)); doReturn(succeededFuture(null)) .when(purchaseOrderLineHelper).updatePoLines(any(CompositePurchaseOrder.class), any(CompositePurchaseOrder.class), eq(requestContext)); @@ -244,7 +244,7 @@ void testPutUnOpenOrderValidationThrow() throws IOException { // Then assertTrue(future.failed()); - verify(orderValidationService, times(0)).validateOrderForUpdate(any(), any(), anyBoolean(), any()); + verify(orderValidationService, times(0)).validateOrderForUpdate(any(), any(), any()); } @Test diff --git a/src/test/java/org/folio/service/orders/flows/update/unopen/UnOpenCompositeOrderManagerTest.java b/src/test/java/org/folio/service/orders/flows/update/unopen/UnOpenCompositeOrderManagerTest.java index b02153aa9..82f93ebb7 100644 --- a/src/test/java/org/folio/service/orders/flows/update/unopen/UnOpenCompositeOrderManagerTest.java +++ b/src/test/java/org/folio/service/orders/flows/update/unopen/UnOpenCompositeOrderManagerTest.java @@ -9,6 +9,7 @@ import static org.folio.TestUtils.getMockAsJson; import static org.folio.rest.impl.MockServer.BASE_MOCK_DATA_PATH; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyBoolean; import static org.mockito.Mockito.anyList; @@ -30,11 +31,13 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; +import io.vertx.core.Future; import io.vertx.core.json.JsonObject; import io.vertx.junit5.VertxExtension; import org.folio.ApiTestSuite; import org.folio.models.ItemStatus; import org.folio.orders.utils.ProtectedOperationType; +import org.folio.rest.core.exceptions.HttpException; import org.folio.rest.core.models.RequestContext; import org.folio.rest.jaxrs.model.PoLine; import org.folio.rest.jaxrs.model.CompositePurchaseOrder; @@ -99,6 +102,7 @@ public class UnOpenCompositeOrderManagerTest { private OpenToPendingEncumbranceStrategy openToPendingEncumbranceStrategy; @Mock private Map okapiHeadersMock; + private AutoCloseable mockitoMocks; private Context ctxMock; private RequestContext requestContext; @@ -123,15 +127,16 @@ public static void after() { @BeforeEach void beforeEach() { - MockitoAnnotations.openMocks(this); + mockitoMocks = MockitoAnnotations.openMocks(this); autowireDependencies(this); requestContext = new RequestContext(ctxMock, okapiHeadersMock); } @AfterEach - void resetMocks() { + void resetMocks() throws Exception { clearServiceInteractions(); reset(encumbranceWorkflowStrategyFactory, pieceStorageService, inventoryItemManager, inventoryHoldingManager); + mockitoMocks.close(); } @Test @@ -365,7 +370,7 @@ void testDeleteHoldingForIndependentWorkflowWhenDeleteHoldingIsFalse() { verify(inventoryItemManager, never()).deleteItems(anyList(), anyBoolean(), any(RequestContext.class)); } - private void prepareInitialSetup(CompositePurchaseOrder order, CompositePurchaseOrder orderFromStorage, PoLine poLine) { + private void prepareInitialSetup(CompositePurchaseOrder order, CompositePurchaseOrder orderFromStorage, PoLine poLine, long numberOfRequests) { doReturn(openToPendingEncumbranceStrategy).when(encumbranceWorkflowStrategyFactory).getStrategy(eq(OrderWorkflowType.OPEN_TO_PENDING)); doReturn(succeededFuture(null)).when(openToPendingEncumbranceStrategy).processEncumbrances(eq(order), eq(orderFromStorage), any()); JsonObject item = getItem(); @@ -383,7 +388,8 @@ private void prepareInitialSetup(CompositePurchaseOrder order, CompositePurchase PurchaseOrder simpleOrder = new PurchaseOrder().withId(order.getId()); doReturn(succeededFuture(simpleOrder)).when(purchaseOrderStorageService).getPurchaseOrderById(order.getId(), requestContext); doReturn(succeededFuture()).when(protectionService).isOperationRestricted(anyList(), any(ProtectedOperationType.class), any()); - doReturn(succeededFuture(0)).when(circulationRequestsRetriever).getNumberOfRequestsByItemId(ITEM_ID, requestContext); + doReturn(succeededFuture(Map.of(ITEM_ID, numberOfRequests))) + .when(circulationRequestsRetriever).getNumbersOfRequestsByItemIds(List.of(ITEM_ID), requestContext); doReturn(succeededFuture()).when(pieceStorageService).deletePiece(PIECE_ID, requestContext); doReturn(succeededFuture()).when(inventoryItemManager).deleteItem(piece.getItemId(), true, requestContext); doReturn(succeededFuture(Collections.emptyList())).when(inventoryItemManager).getItemsByHoldingId(eq(HOLDING_ID), RequestContextMatcher.matchCentralTenant()); @@ -394,6 +400,10 @@ private void prepareInitialSetup(CompositePurchaseOrder order, CompositePurchase doReturn(succeededFuture(Collections.emptyList())).when(pieceStorageService).getPiecesByHoldingIds(anyList(), any(RequestContext.class)); } + private void prepareInitialSetup(CompositePurchaseOrder order, CompositePurchaseOrder orderFromStorage, PoLine poLine) { + prepareInitialSetup(order, orderFromStorage, poLine, 0); + } + private JsonObject getItem() { return new JsonObject() .put("id", ITEM_ID) @@ -510,6 +520,21 @@ void testRollbackInventory_NoHoldingsMapEntry_ShouldSucceed() throws Exception { verify(inventoryHoldingManager, never()).deleteHoldingById(anyString(), anyBoolean(), any(RequestContext.class)); } + @Test + void testErrorWhenItemHasRequest() { + //given + CompositePurchaseOrder order = getMockAsJson(ORDER_PATH).mapTo(CompositePurchaseOrder.class); + CompositePurchaseOrder orderFromStorage = getMockAsJson(ORDER_PATH).mapTo(CompositePurchaseOrder.class); + PoLine poLine = getPoLine(order); + prepareInitialSetup(order, orderFromStorage, poLine, 1); + //When + Future future = unOpenCompositeOrderManager.checkRequests(order, requestContext); + //Then + assertTrue(future.failed()); + HttpException exception = (HttpException) future.cause(); + assertEquals(422, exception.getCode()); + } + /** * Define unit test specific beans to override actual ones */