diff --git a/src/main/java/org/folio/rest/core/RestClient.java b/src/main/java/org/folio/rest/core/RestClient.java index 6877f965e..90df9d878 100644 --- a/src/main/java/org/folio/rest/core/RestClient.java +++ b/src/main/java/org/folio/rest/core/RestClient.java @@ -103,13 +103,13 @@ public Future put(String endpoint, T dataObject, RequestContext reques } public Future patch(RequestEntry requestEntry, T dataObject, RequestContext requestContext) { - String endpoint = requestEntry.buildEndpoint(); + var endpoint = requestEntry.buildEndpoint(); return patch(endpoint, dataObject, requestContext); } public Future patch(String endpoint, T dataObject, RequestContext requestContext) { var caseInsensitiveHeader = convertToCaseInsensitiveMap(requestContext.getHeaders()); - Promise promise = Promise.promise(); + var promise = Promise.promise(); return getVertxWebClient(requestContext.getContext()) .patchAbs(buildAbsEndpoint(caseInsensitiveHeader, endpoint)) .putHeaders(caseInsensitiveHeader) diff --git a/src/main/java/org/folio/rest/impl/PoLineAPI.java b/src/main/java/org/folio/rest/impl/PoLineAPI.java index c0d06d6f5..e8aeb5508 100644 --- a/src/main/java/org/folio/rest/impl/PoLineAPI.java +++ b/src/main/java/org/folio/rest/impl/PoLineAPI.java @@ -40,6 +40,7 @@ import io.vertx.core.Vertx; public class PoLineAPI extends BaseApi implements OrdersOrderLines { + private static final Logger logger = LogManager.getLogger(); @Autowired @@ -58,7 +59,7 @@ public PoLineAPI() { @Override @Validate public void getOrdersOrderLines(String totalRecords, int offset, int limit, String query, Map okapiHeaders, - Handler> asyncResultHandler, Context vertxContext) { + Handler> asyncResultHandler, Context vertxContext) { helper.getOrderLines(query, offset, limit, new RequestContext(vertxContext, okapiHeaders)) .onSuccess(lines -> asyncResultHandler.handle(succeededFuture(buildOkResponse(lines)))) .onFailure(t -> handleErrorResponse(asyncResultHandler, t)); @@ -67,7 +68,7 @@ public void getOrdersOrderLines(String totalRecords, int offset, int limit, Stri @Override @Validate public void postOrdersOrderLines(PoLine poLine, Map okapiHeaders, - Handler> asyncResultHandler, Context vertxContext) { + Handler> asyncResultHandler, Context vertxContext) { RequestContext requestContext = new RequestContext(vertxContext, okapiHeaders); commonSettingsCache.loadSettings(requestContext) .compose(tenantConfig -> helper.createPoLine(poLine, tenantConfig, requestContext)) @@ -82,8 +83,8 @@ public void postOrdersOrderLines(PoLine poLine, Map okapiHeaders @Override @Validate public void getOrdersOrderLinesById(String lineId, Map okapiHeaders, - Handler> asyncResultHandler, Context vertxContext) { - logger.debug("Started Invocation of POLine Request with id = {}", lineId); + Handler> asyncResultHandler, Context vertxContext) { + logger.debug("getOrdersOrderLinesById:: Handling GET Order Line operation with id={}", lineId); helper.getPoLine(lineId, new RequestContext(vertxContext, okapiHeaders)) .onSuccess(poLine -> asyncResultHandler.handle(succeededFuture(buildOkResponse(poLine)))) .onFailure(t -> handleErrorResponse(asyncResultHandler, t)); @@ -92,7 +93,7 @@ public void getOrdersOrderLinesById(String lineId, Map okapiHead @Override @Validate public void deleteOrdersOrderLinesById(String lineId, Map okapiHeaders, - Handler> asyncResultHandler, Context vertxContext) { + Handler> asyncResultHandler, Context vertxContext) { helper.deleteLine(lineId, new RequestContext(vertxContext, okapiHeaders)) .onSuccess(v -> asyncResultHandler.handle(succeededFuture(buildNoContentResponse()))) .onFailure(t -> handleErrorResponse(asyncResultHandler, t)); @@ -101,8 +102,8 @@ public void deleteOrdersOrderLinesById(String lineId, Map okapiH @Override @Validate public void putOrdersOrderLinesById(String lineId, PoLine poLine, Map okapiHeaders, - Handler> asyncResultHandler, Context vertxContext) { - logger.debug("Handling PUT Order Line operation..."); + Handler> asyncResultHandler, Context vertxContext) { + logger.debug("putOrdersOrderLinesById:: Handling PUT Order Line operation..."); // Set id if this is available only in path RequestContext requestContext = new RequestContext(vertxContext, okapiHeaders); if (StringUtils.isEmpty(poLine.getId())) { @@ -135,14 +136,12 @@ public void putOrdersOrderLinesById(String lineId, PoLine poLine, Map okapiHeaders, Handler> asyncResultHandler, - Context vertxContext) { + public void patchOrdersOrderLinesById(String lineId, PatchOrderLineRequest request, Map okapiHeaders, + Handler> asyncResultHandler, Context vertxContext) { RequestContext requestContext = new RequestContext(vertxContext, okapiHeaders); - orderLinePatchOperationService.patch(lineId, request, requestContext) - .onSuccess(v -> asyncResultHandler.handle(succeededFuture(buildNoContentResponse()))) - .onFailure(t -> handleErrorResponse(asyncResultHandler, t)); + .onSuccess(v -> asyncResultHandler.handle(succeededFuture(buildNoContentResponse()))) + .onFailure(t -> handleErrorResponse(asyncResultHandler, t)); } @Override diff --git a/src/main/java/org/folio/service/inventory/InventoryItemManager.java b/src/main/java/org/folio/service/inventory/InventoryItemManager.java index 26651d1c8..718b2e17e 100644 --- a/src/main/java/org/folio/service/inventory/InventoryItemManager.java +++ b/src/main/java/org/folio/service/inventory/InventoryItemManager.java @@ -71,7 +71,7 @@ public class InventoryItemManager { public static final String BARCODE_ALREADY_EXIST_ERROR = "lower(jsonb ->> 'barcode'::text) value already exists in table item"; private static final String LOOKUP_ITEM_QUERY = "purchaseOrderLineIdentifier==%s and holdingsRecordId==%s"; - private static final String ITEM_STOR_ENDPOINT = "/item-storage/items"; + private static final String ITEM_STORAGE_ENDPOINT = "/item-storage/items"; private static final String BUILDING_PIECE_MESSAGE = "Building {} {} piece(s) for PO Line with id={}"; private final RestClient restClient; @@ -109,7 +109,7 @@ public Future> getItemsByHoldingIdAndOrderLineId(String holding } public Future> getItemsByPoLineIdsAndStatus(List poLineIds, String itemStatus, RequestContext requestContext) { - logger.debug("getItemsByStatus start"); + logger.debug("getItemsByPoLineIdsAndStatus:: Started"); List>> futures = StreamEx .ofSubLists(poLineIds, MAX_IDS_FOR_GET_RQ_15) .map(ids -> { @@ -235,7 +235,7 @@ public Future> handleItemRecords(CompositePurchaseOrder comPO, PoLin private Future> searchStorageExistingItems(String poLineId, String holdingId, int expectedQuantity, RequestContext requestContext) { String query = String.format(LOOKUP_ITEM_QUERY, poLineId, holdingId); - RequestEntry requestEntry = new RequestEntry(ITEM_STOR_ENDPOINT).withQuery(query).withOffset(0).withLimit(expectedQuantity); + RequestEntry requestEntry = new RequestEntry(ITEM_STORAGE_ENDPOINT).withQuery(query).withOffset(0).withLimit(expectedQuantity); return restClient.getAsJsonObject(requestEntry, requestContext) .map(itemsCollection -> { List items = extractEntities(itemsCollection); @@ -318,11 +318,11 @@ public Future openOrderCreateItemRecord(CompositePurchaseOrder compPO, P Piece pieceWithHoldingId = new Piece().withHoldingId(holdingId); if (poLine.getOrderFormat() == ELECTRONIC_RESOURCE) { createMissingElectronicItems(compPO, poLine, pieceWithHoldingId, ITEM_QUANTITY, requestContext) - .onSuccess(idS -> itemFuture.complete(idS.get(0))) + .onSuccess(idS -> itemFuture.complete(idS.getFirst())) .onFailure(itemFuture::fail); } else { createMissingPhysicalItems(compPO, poLine, pieceWithHoldingId, ITEM_QUANTITY, requestContext) - .onSuccess(idS -> itemFuture.complete(idS.get(0))) + .onSuccess(idS -> itemFuture.complete(idS.getFirst())) .onFailure(itemFuture::fail); } } catch (Exception e) { @@ -436,9 +436,9 @@ private Future> createItemRecords(JsonObject itemRecord, int expect private Future createItemInInventory(JsonObject itemData, RequestContext requestContext) { Promise promise = Promise.promise(); - RequestEntry requestEntry = new RequestEntry(ITEM_STOR_ENDPOINT); + RequestEntry requestEntry = new RequestEntry(ITEM_STORAGE_ENDPOINT); String tenantId = TenantTool.tenantId(requestContext.getHeaders()); - logger.info("Trying to create Item in inventory in tenant: {}", tenantId); + logger.info("createItemInInventory:: Trying to create Item in inventory in tenant: {}", tenantId); restClient.postJsonObjectAndGetId(requestEntry, itemData, requestContext) .onSuccess(promise::complete) // In case item creation failed, return null instead of id @@ -463,4 +463,13 @@ private List extractEntities(JsonObject entries) { .orElseGet(List::of); } + public Future batchUpdatePartialItems(List items, RequestContext requestContext) { + logger.info("batchUpdatePartialItems:: Batch updating partial items={}", items.size()); + var requestEntry = new RequestEntry(ITEM_STORAGE_ENDPOINT); + var tenantId = TenantTool.tenantId(requestContext.getHeaders()); + logger.info("batchUpdatePartialItems:: Trying to batch update partial items in inventory for tenant: {}", tenantId); + var payload = new JsonObject() + .put(InventoryUtils.ITEMS, items); + return restClient.patch(requestEntry, payload, requestContext); + } } diff --git a/src/main/java/org/folio/service/orders/lines/update/OrderLinePatchOperationService.java b/src/main/java/org/folio/service/orders/lines/update/OrderLinePatchOperationService.java index 3d8c43ef1..bcc493c6f 100644 --- a/src/main/java/org/folio/service/orders/lines/update/OrderLinePatchOperationService.java +++ b/src/main/java/org/folio/service/orders/lines/update/OrderLinePatchOperationService.java @@ -42,12 +42,12 @@ public OrderLinePatchOperationService(RestClient restClient, } public Future patch(String lineId, PatchOrderLineRequest request, RequestContext requestContext) { - log.info("patch:: start patching operation: {} for poLineId: {}", request.getOperation(), lineId); + log.info("patch:: Start patching operation: {} for poLineId: {}", request.getOperation(), lineId); var newInstanceId = request.getReplaceInstanceRef().getNewInstanceId(); return inventoryInstanceManager.createShadowInstanceIfNeeded(newInstanceId, requestContext) .compose(v -> purchaseOrderLineService.getOrderLineById(lineId, requestContext)) .compose(poLine -> patchOrderLine(request, poLine, requestContext)) - .onSuccess(v -> log.info("patch:: successfully patched operation: {} for poLineId: {}", request.getOperation(), lineId)) + .onSuccess(v -> log.info("patch:: Successfully patched operation: {} for poLineId: {}", request.getOperation(), lineId)) .onFailure(e -> log.error("Failed to patch operation: {} for poLineId: {}", request.getOperation(), lineId, e)) .mapEmpty(); } diff --git a/src/main/java/org/folio/service/orders/lines/update/OrderLineUpdateInstanceStrategyResolver.java b/src/main/java/org/folio/service/orders/lines/update/OrderLineUpdateInstanceStrategyResolver.java index c126e7f05..fdb628a20 100644 --- a/src/main/java/org/folio/service/orders/lines/update/OrderLineUpdateInstanceStrategyResolver.java +++ b/src/main/java/org/folio/service/orders/lines/update/OrderLineUpdateInstanceStrategyResolver.java @@ -5,6 +5,7 @@ import org.folio.rest.jaxrs.model.CreateInventoryType; public class OrderLineUpdateInstanceStrategyResolver { + private final Map strategies; public OrderLineUpdateInstanceStrategyResolver(Map strategies) { diff --git a/src/main/java/org/folio/service/orders/lines/update/instance/WithHoldingOrderLineUpdateInstanceStrategy.java b/src/main/java/org/folio/service/orders/lines/update/instance/WithHoldingOrderLineUpdateInstanceStrategy.java index 0f554a80a..4d48a34ac 100644 --- a/src/main/java/org/folio/service/orders/lines/update/instance/WithHoldingOrderLineUpdateInstanceStrategy.java +++ b/src/main/java/org/folio/service/orders/lines/update/instance/WithHoldingOrderLineUpdateInstanceStrategy.java @@ -49,6 +49,11 @@ public class WithHoldingOrderLineUpdateInstanceStrategy extends BaseOrderLineUpd private static final String HOLDINGS_ITEMS = "holdingsItems"; private static final String BARE_HOLDINGS_ITEMS = "bareHoldingsItems"; + private static final String VERSION = "_version"; + private static final String ITEM_ID = "itemId"; + private static final String TENANT_ID = "tenantId"; + private static final String ORIGINAL_ERROR = "originalError"; + private final PieceStorageService pieceStorageService; private final PurchaseOrderLineService purchaseOrderLineService; @@ -234,24 +239,27 @@ private Future updateItemsHolding(String holdingId, String newHoldingId, S } private Future updateItemsInInventory(List items, String newHoldingId, RequestContext requestContext) { - items.forEach(item -> item.put(ITEM_HOLDINGS_RECORD_ID, newHoldingId)); - List parameters = new ArrayList<>(); - return Future.join( - items - .stream() - .map(item -> inventoryItemManager.updateItem(item, requestContext) - .otherwise(ex -> { - var itemIdParam = new Parameter().withKey("itemId").withValue(item.getString(ID)); - if (ex.getCause() instanceof HttpException httpException) { - itemIdParam.withAdditionalProperty("originalError", httpException.getError().getMessage()); - } - var tenantIdParam = new Parameter().withKey("tenantId").withValue(TenantTool.tenantId(requestContext.getHeaders())); - parameters.add(itemIdParam); - parameters.add(tenantIdParam); - return null; - })) - .toList()) - .mapEmpty() + log.info("updateItemsInInventory:: Updating items in batch, items={}, new holding id={}", items.size(), newHoldingId); + var partialItems = items.stream() + .map(item -> new JsonObject() + .put(ID, item.getString(ID)) + .put(ITEM_HOLDINGS_RECORD_ID, newHoldingId) + .put(VERSION, item.getString(VERSION))) + .toList(); + var parameters = new ArrayList(); + return inventoryItemManager.batchUpdatePartialItems(partialItems, requestContext) + .otherwise(ex -> { + items.forEach(item -> { + var itemIdParam = new Parameter().withKey(ITEM_ID).withValue(item.getString(ID)); + if (ex.getCause() instanceof HttpException httpException) { + itemIdParam.withAdditionalProperty(ORIGINAL_ERROR, httpException.getError().getMessage()); + } + var tenantIdParam = new Parameter().withKey(TENANT_ID).withValue(TenantTool.tenantId(requestContext.getHeaders())); + parameters.add(itemIdParam); + parameters.add(tenantIdParam); + }); + return null; + }) .compose(v -> CollectionUtils.isNotEmpty(parameters) ? Future.failedFuture(new HttpException(500, ErrorCodes.ITEM_UPDATE_FAILED.toError().withParameters(parameters))) : Future.succeededFuture()); @@ -286,5 +294,4 @@ private Future> getDeletableHoldings(OrderLineUpdateInstanceHolde .filter(location -> !usedHoldingIds.contains(location.getHoldingId())) .toList()); } - } diff --git a/src/main/java/org/folio/service/orders/lines/update/instance/WithoutHoldingOrderLineUpdateInstanceStrategy.java b/src/main/java/org/folio/service/orders/lines/update/instance/WithoutHoldingOrderLineUpdateInstanceStrategy.java index 3524dfdaa..1c4a5ed7b 100644 --- a/src/main/java/org/folio/service/orders/lines/update/instance/WithoutHoldingOrderLineUpdateInstanceStrategy.java +++ b/src/main/java/org/folio/service/orders/lines/update/instance/WithoutHoldingOrderLineUpdateInstanceStrategy.java @@ -31,5 +31,4 @@ Future processHoldings(OrderLineUpdateInstanceHolder holder, RequestContex ? Future.succeededFuture() : deleteAbandonedHoldings(holder.getStoragePoLine().getLocations(), requestContext).mapEmpty(); } - } diff --git a/src/test/java/org/folio/service/inventory/InventoryManagerTest.java b/src/test/java/org/folio/service/inventory/InventoryManagerTest.java index 09f8fe431..3a3c33533 100644 --- a/src/test/java/org/folio/service/inventory/InventoryManagerTest.java +++ b/src/test/java/org/folio/service/inventory/InventoryManagerTest.java @@ -54,6 +54,7 @@ import static org.mockito.Mockito.when; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -869,6 +870,230 @@ void shouldCreateShadowInstance() { verify(sharingInstanceService).createShadowInstance(instanceId, configuration.get(), requestContext); } + @Test + void testBatchUpsertItemsShouldUpdateMultipleItems() { + // given + var itemId1 = UUID.randomUUID().toString(); + var itemId2 = UUID.randomUUID().toString(); + var item1 = new JsonObject() + .put(ID, itemId1) + .put(InventoryItemManager.ITEM_BARCODE, "barcode1") + .put(InventoryItemManager.ITEM_STATUS, new JsonObject().put(InventoryItemManager.ITEM_STATUS_NAME, "Available")); + var item2 = new JsonObject() + .put(ID, itemId2) + .put(InventoryItemManager.ITEM_BARCODE, "barcode2") + .put(InventoryItemManager.ITEM_STATUS, new JsonObject().put(InventoryItemManager.ITEM_STATUS_NAME, "Available")); + var items = Arrays.asList(item1, item2); + + doReturn(succeededFuture(null)).when(restClient).patch(any(RequestEntry.class), any(JsonObject.class), eq(requestContext)); + + // When + var result = inventoryItemManager.batchUpdatePartialItems(items, requestContext); + + // Then + assertTrue(result.succeeded()); + verify(restClient, times(1)).patch(any(RequestEntry.class), any(JsonObject.class), eq(requestContext)); + } + + @Test + void testBatchUpsertItemsShouldHandleEmptyList() { + // given + var items = Collections.emptyList(); + doReturn(succeededFuture(null)).when(restClient).patch(any(RequestEntry.class), any(JsonObject.class), eq(requestContext)); + + // When + var result = inventoryItemManager.batchUpdatePartialItems(items, requestContext); + + // Then + assertTrue(result.succeeded()); + verify(restClient, times(1)).patch(any(RequestEntry.class), any(JsonObject.class), eq(requestContext)); + } + + @Test + void testBatchUpsertItemsShouldHandleSingleItem() { + // given + var itemId = UUID.randomUUID().toString(); + var item = new JsonObject() + .put(ID, itemId) + .put(InventoryItemManager.ITEM_BARCODE, "barcode") + .put(InventoryItemManager.ITEM_STATUS, new JsonObject().put(InventoryItemManager.ITEM_STATUS_NAME, "On order")); + var items = Collections.singletonList(item); + + doReturn(succeededFuture(null)).when(restClient).patch(any(RequestEntry.class), any(JsonObject.class), eq(requestContext)); + + // When + var result = inventoryItemManager.batchUpdatePartialItems(items, requestContext); + + // Then + assertTrue(result.succeeded()); + verify(restClient, times(1)).patch(any(RequestEntry.class), any(JsonObject.class), eq(requestContext)); + } + + @Test + void testBatchUpsertItemsShouldHandleFailure() { + // given + var itemId = UUID.randomUUID().toString(); + var item = new JsonObject() + .put(ID, itemId) + .put(InventoryItemManager.ITEM_BARCODE, "barcode"); + var items = Collections.singletonList(item); + + var errorMessage = "Internal server error"; + doReturn(Future.failedFuture(new HttpException(500, errorMessage))) + .when(restClient).patch(any(RequestEntry.class), any(JsonObject.class), eq(requestContext)); + + // When + var result = inventoryItemManager.batchUpdatePartialItems(items, requestContext); + + // Then + assertTrue(result.failed()); + assertThat(result.cause(), IsInstanceOf.instanceOf(HttpException.class)); + var exception = (HttpException) result.cause(); + assertEquals(500, exception.getCode()); + verify(restClient, times(1)).patch(any(RequestEntry.class), any(JsonObject.class), eq(requestContext)); + } + + @Test + void testBatchUpsertItemsShouldHandleLargeNumberOfItems() { + // given + var items = new ArrayList(); + for (int i = 0; i < 100; i++) { + var item = new JsonObject() + .put(ID, UUID.randomUUID().toString()) + .put(InventoryItemManager.ITEM_BARCODE, "barcode" + i) + .put(InventoryItemManager.ITEM_STATUS, new JsonObject().put(InventoryItemManager.ITEM_STATUS_NAME, "Available")); + items.add(item); + } + + doReturn(succeededFuture(null)).when(restClient).patch(any(RequestEntry.class), any(JsonObject.class), eq(requestContext)); + + // When + var result = inventoryItemManager.batchUpdatePartialItems(items, requestContext); + + // Then + assertTrue(result.succeeded()); + verify(restClient, times(1)).patch(any(RequestEntry.class), any(JsonObject.class), eq(requestContext)); + } + + @Test + void testBatchUpsertItemsShouldIncludeUpsertParameter() { + // given + var itemId = UUID.randomUUID().toString(); + var item = new JsonObject() + .put(ID, itemId) + .put(InventoryItemManager.ITEM_BARCODE, "barcode"); + var items = Collections.singletonList(item); + + doReturn(succeededFuture(null)).when(restClient).patch(any(RequestEntry.class), any(JsonObject.class), eq(requestContext)); + + // When + inventoryItemManager.batchUpdatePartialItems(items, requestContext).result(); + + // Then + verify(restClient).patch(any(RequestEntry.class), any(JsonObject.class), eq(requestContext)); + } + + @Test + void testBatchUpsertItemsShouldHandleItemsWithAllFields() { + // given + var itemId = UUID.randomUUID().toString(); + var holdingId = UUID.randomUUID().toString(); + var materialTypeId = UUID.randomUUID().toString(); + var loanTypeId = UUID.randomUUID().toString(); + var item = new JsonObject() + .put(ID, itemId) + .put(InventoryItemManager.ITEM_BARCODE, "barcode123") + .put(InventoryItemManager.ITEM_HOLDINGS_RECORD_ID, holdingId) + .put(InventoryItemManager.ITEM_MATERIAL_TYPE_ID, materialTypeId) + .put(InventoryItemManager.ITEM_PERMANENT_LOAN_TYPE_ID, loanTypeId) + .put(InventoryItemManager.ITEM_STATUS, new JsonObject().put(InventoryItemManager.ITEM_STATUS_NAME, "Available")) + .put(InventoryItemManager.ITEM_ENUMERATION, "v.1") + .put(InventoryItemManager.ITEM_CHRONOLOGY, "2023") + .put(InventoryItemManager.ITEM_DISCOVERY_SUPPRESS, false); + var items = Collections.singletonList(item); + + doReturn(succeededFuture(null)).when(restClient).patch(any(RequestEntry.class), any(JsonObject.class), eq(requestContext)); + + // When + var result = inventoryItemManager.batchUpdatePartialItems(items, requestContext); + + // Then + assertTrue(result.succeeded()); + verify(restClient, times(1)).patch(any(RequestEntry.class), any(JsonObject.class), eq(requestContext)); + } + + @Test + void testBatchUpsertItemsShouldHandleMixedItemTypes() { + // given + var items = new ArrayList(); + // Physical item + items.add(new JsonObject() + .put(ID, UUID.randomUUID().toString()) + .put(InventoryItemManager.ITEM_BARCODE, "physical-barcode") + .put(InventoryItemManager.ITEM_STATUS, new JsonObject().put(InventoryItemManager.ITEM_STATUS_NAME, "On order"))); + // Electronic item + items.add(new JsonObject() + .put(ID, UUID.randomUUID().toString()) + .put(InventoryItemManager.ITEM_STATUS, new JsonObject().put(InventoryItemManager.ITEM_STATUS_NAME, "Available"))); + + doReturn(succeededFuture(null)).when(restClient).patch(any(RequestEntry.class), any(JsonObject.class), eq(requestContext)); + + // When + var result = inventoryItemManager.batchUpdatePartialItems(items, requestContext); + + // Then + assertTrue(result.succeeded()); + verify(restClient, times(1)).patch(any(RequestEntry.class), any(JsonObject.class), eq(requestContext)); + } + + @Test + void testBatchUpsertItemsShouldHandleNetworkFailure() { + // given + var itemId = UUID.randomUUID().toString(); + var item = new JsonObject() + .put(ID, itemId) + .put(InventoryItemManager.ITEM_BARCODE, "barcode"); + var items = Collections.singletonList(item); + + var errorMessage = "Network timeout"; + doReturn(Future.failedFuture(new HttpException(504, errorMessage))) + .when(restClient).patch(any(RequestEntry.class), any(JsonObject.class), eq(requestContext)); + + // When + var result = inventoryItemManager.batchUpdatePartialItems(items, requestContext); + + // Then + assertTrue(result.failed()); + assertThat(result.cause(), IsInstanceOf.instanceOf(HttpException.class)); + var exception = (HttpException) result.cause(); + assertEquals(504, exception.getCode()); + verify(restClient, times(1)).patch(any(RequestEntry.class), any(JsonObject.class), eq(requestContext)); + } + + @Test + void testBatchUpsertItemsShouldHandleBadRequestError() { + // given + var itemId = UUID.randomUUID().toString(); + var item = new JsonObject() + .put(ID, itemId) + .put(InventoryItemManager.ITEM_BARCODE, "invalid-barcode"); + var items = Collections.singletonList(item); + + var errorMessage = "Invalid item data"; + doReturn(Future.failedFuture(new HttpException(400, errorMessage))) + .when(restClient).patch(any(RequestEntry.class), any(JsonObject.class), eq(requestContext)); + + // When + var result = inventoryItemManager.batchUpdatePartialItems(items, requestContext); + + // Then + assertTrue(result.failed()); + assertThat(result.cause(), IsInstanceOf.instanceOf(HttpException.class)); + var exception = (HttpException) result.cause(); + assertEquals(400, exception.getCode()); + verify(restClient, times(1)).patch(any(RequestEntry.class), any(JsonObject.class), eq(requestContext)); + } + /** * Define unit test specific beans to override actual ones */ diff --git a/src/test/java/org/folio/service/orders/lines/update/instance/WithHoldingOrderLineUpdateInstanceStrategyTest.java b/src/test/java/org/folio/service/orders/lines/update/instance/WithHoldingOrderLineUpdateInstanceStrategyTest.java index 0581f3978..069532c5c 100644 --- a/src/test/java/org/folio/service/orders/lines/update/instance/WithHoldingOrderLineUpdateInstanceStrategyTest.java +++ b/src/test/java/org/folio/service/orders/lines/update/instance/WithHoldingOrderLineUpdateInstanceStrategyTest.java @@ -1,7 +1,6 @@ package org.folio.service.orders.lines.update.instance; import static io.vertx.core.Future.succeededFuture; -import static java.util.stream.Collectors.toList; import static org.folio.TestConfig.clearServiceInteractions; import static org.folio.TestUtils.getMockData; import static org.folio.rest.core.exceptions.ErrorCodes.ITEM_UPDATE_FAILED; @@ -23,6 +22,7 @@ import java.util.ArrayList; import java.util.List; import java.util.UUID; +import java.util.stream.Collectors; import org.folio.TestConstants; import org.folio.models.ItemStatus; @@ -59,9 +59,9 @@ import io.vertx.junit5.VertxExtension; import io.vertx.junit5.VertxTestContext; - @ExtendWith(VertxExtension.class) public class WithHoldingOrderLineUpdateInstanceStrategyTest { + @InjectMocks private WithHoldingOrderLineUpdateInstanceStrategy withHoldingOrderLineUpdateInstanceStrategy; @Mock @@ -74,10 +74,10 @@ public class WithHoldingOrderLineUpdateInstanceStrategyTest { private PieceStorageService pieceStorageService; @Mock private PurchaseOrderLineService purchaseOrderLineService; - @Mock - RequestContext requestContext; - AutoCloseable mockitoMocks; + private RequestContext requestContext; + private AutoCloseable mockitoMocks; + @BeforeEach void initMocks(){ mockitoMocks = MockitoAnnotations.openMocks(this); @@ -99,9 +99,9 @@ public void updateInstanceForMoveHoldingOperation() throws IOException { List holdings = holdingsCollection.getJsonArray("holdingsRecords").stream() .map(o -> ((JsonObject) o)) - .collect(toList()); + .toList(); - List holdingIds = holdings.stream().map(holding -> holding.getString(ID)).collect(toList()); + List holdingIds = holdings.stream().map(holding -> holding.getString(ID)).toList(); ArrayList locations = new ArrayList<>(); locations.add(new Location() @@ -150,9 +150,9 @@ public void updateInstanceWithNullReplaceInstanceRef() throws IOException { List holdings = holdingsCollection.getJsonArray("holdingsRecords").stream() .map(o -> ((JsonObject) o)) - .collect(toList()); + .toList(); - List holdingIds = holdings.stream().map(holding -> holding.getString(ID)).collect(toList()); + List holdingIds = holdings.stream().map(holding -> holding.getString(ID)).toList(); ArrayList locations = new ArrayList<>(); locations.add(new Location() @@ -192,9 +192,9 @@ public void updateInstanceForFindOrCreateHoldingOperation() throws IOException { List holdings = holdingsCollection.getJsonArray("holdingsRecords").stream() .map(o -> ((JsonObject) o)) - .collect(toList()); + .toList(); - List holdingIds = holdings.stream().map(holding -> holding.getString(ID)).collect(toList()); + List holdingIds = holdings.stream().map(holding -> holding.getString(ID)).toList(); ArrayList locations = new ArrayList<>(); locations.add(new Location() @@ -229,7 +229,7 @@ public void updateInstanceForFindOrCreateHoldingOperation() throws IOException { item.put(ITEM_STATUS, new JsonObject().put(ITEM_STATUS_NAME, ItemStatus.ON_ORDER.value())); item.put(ITEM_HOLDINGS_RECORD_ID, holdingId); return item; - }).collect(toList()); + }).toList(); doReturn(succeededFuture()).when(inventoryInstanceManager).createShadowInstanceIfNeeded(eq(instanceId), any(RequestContext.class)); doReturn(succeededFuture(UUID.randomUUID().toString())).when(inventoryHoldingManager) @@ -242,11 +242,11 @@ public void updateInstanceForFindOrCreateHoldingOperation() throws IOException { withHoldingOrderLineUpdateInstanceStrategy.updateInstance(orderLineUpdateInstanceHolder, requestContext).result(); - verify(inventoryInstanceManager, times(2)).createShadowInstanceIfNeeded(eq(instanceId), any(RequestContext.class)); + verify(inventoryInstanceManager, times(1)).createShadowInstanceIfNeeded(eq(instanceId), any(RequestContext.class)); verify(inventoryHoldingManager, times(1)).getOrCreateHoldingRecordByInstanceAndLocation(instanceId, locations.get(0), requestContext); - verify(inventoryHoldingManager, times(1)).getOrCreateHoldingRecordByInstanceAndLocation(instanceId, locations.get(1), requestContext); - verify(inventoryItemManager, times(2)).getItemsByHoldingIdAndOrderLineId(anyString(), anyString(), any(RequestContext.class)); - verify(inventoryItemManager, times(2)).updateItem(any(JsonObject.class), any(RequestContext.class)); + verify(inventoryHoldingManager, never()).getOrCreateHoldingRecordByInstanceAndLocation(instanceId, locations.get(1), requestContext); + verify(inventoryItemManager, times(1)).getItemsByHoldingIdAndOrderLineId(anyString(), anyString(), any(RequestContext.class)); + verify(inventoryItemManager, times(1)).batchUpdatePartialItems(any(), any(RequestContext.class)); } @Test @@ -290,16 +290,16 @@ public void updateInstanceAndItemsForFindOrCreateHoldingOperation() throws IOExc doReturn(succeededFuture()).when(inventoryInstanceManager).createShadowInstanceIfNeeded(eq(instanceId), any(RequestContext.class)); doReturn(succeededFuture(UUID.randomUUID().toString())).when(inventoryHoldingManager) - .getOrCreateHoldingRecordByInstanceAndLocation(eq(instanceId), eq(locations.get(0)), eq(requestContext)); + .getOrCreateHoldingRecordByInstanceAndLocation(eq(instanceId), eq(locations.getFirst()), eq(requestContext)); doReturn(succeededFuture(List.of(item))).when(inventoryItemManager).getItemsByHoldingIdAndOrderLineId(eq(holdingId), eq(orderLineId), eq(requestContext)); - doReturn(succeededFuture(null)).when(inventoryItemManager).updateItem(eq(item), eq(requestContext)); + doReturn(succeededFuture(null)).when(inventoryItemManager).batchUpdatePartialItems(any(), eq(requestContext)); withHoldingOrderLineUpdateInstanceStrategy.updateInstance(orderLineUpdateInstanceHolder, requestContext).result(); verify(inventoryInstanceManager, times(1)).createShadowInstanceIfNeeded(eq(instanceId), any(RequestContext.class)); - verify(inventoryHoldingManager, times(1)).getOrCreateHoldingRecordByInstanceAndLocation(instanceId, locations.get(0), requestContext); + verify(inventoryHoldingManager, times(1)).getOrCreateHoldingRecordByInstanceAndLocation(instanceId, locations.getFirst(), requestContext); verify(inventoryItemManager, times(1)).getItemsByHoldingIdAndOrderLineId(holdingId, orderLineId, requestContext); - verify(inventoryItemManager, times(1)).updateItem(item, requestContext); + verify(inventoryItemManager, times(1)).batchUpdatePartialItems(any(), eq(requestContext)); } @Test @@ -338,12 +338,12 @@ public void updateInstanceAndNotUpdateItemsForFindOrCreateHoldingOperationWhenHo doReturn(succeededFuture()).when(inventoryInstanceManager).createShadowInstanceIfNeeded(eq(instanceId), any(RequestContext.class)); doReturn(succeededFuture(holdingId)).when(inventoryHoldingManager) - .getOrCreateHoldingRecordByInstanceAndLocation(eq(instanceId), eq(locations.get(0)), eq(requestContext)); + .getOrCreateHoldingRecordByInstanceAndLocation(eq(instanceId), eq(locations.getFirst()), eq(requestContext)); withHoldingOrderLineUpdateInstanceStrategy.updateInstance(orderLineUpdateInstanceHolder, requestContext).result(); verify(inventoryInstanceManager, times(1)).createShadowInstanceIfNeeded(eq(instanceId), any(RequestContext.class)); - verify(inventoryHoldingManager, times(1)).getOrCreateHoldingRecordByInstanceAndLocation(instanceId, locations.get(0), requestContext); + verify(inventoryHoldingManager, times(1)).getOrCreateHoldingRecordByInstanceAndLocation(instanceId, locations.getFirst(), requestContext); verify(inventoryItemManager, never()).getItemsByHoldingIdAndOrderLineId(anyString(), anyString(), any(RequestContext.class)); verify(inventoryItemManager, never()).updateItem(any(JsonObject.class), any(RequestContext.class)); } @@ -355,13 +355,13 @@ public void updateInstanceForFindOrCreateHoldingOperationAndDeleteAbandonedHoldi List holdings = new JsonObject(getMockData(HOLDINGS_OLD_NEW_PATH)) .getJsonArray("holdingsRecords").stream() .map(o -> ((JsonObject) o)) - .collect(toList()); + .collect(Collectors.toCollection(ArrayList::new)); holdings.addLast(JsonObject.of( ID, UUID.randomUUID().toString(), "purchaseOrderLineIdentifier", UUID.randomUUID().toString() )); - List holdingIds = holdings.stream().map(holding -> holding.getString(ID)).collect(toList()); + List holdingIds = holdings.stream().map(holding -> holding.getString(ID)).toList(); String usedHoldingId = holdingIds.getLast(); String orderLineId = holdings.getFirst().getString("purchaseOrderLineIdentifier"); @@ -408,15 +408,15 @@ ITEM_STATUS, new JsonObject().put(ITEM_STATUS_NAME, ItemStatus.ON_ORDER.value()) when(inventoryHoldingManager.getOrCreateHoldingRecordByInstanceAndLocation(eq(instanceId), eq(locations.get(0)), eq(requestContext))).thenReturn(succeededFuture(UUID.randomUUID().toString())); when(inventoryHoldingManager.getOrCreateHoldingRecordByInstanceAndLocation(eq(instanceId), eq(locations.get(1)), eq(requestContext))).thenReturn(succeededFuture(UUID.randomUUID().toString())); when(inventoryHoldingManager.getOrCreateHoldingRecordByInstanceAndLocation(eq(instanceId), eq(locations.get(2)), eq(requestContext))).thenReturn(succeededFuture(UUID.randomUUID().toString())); - when(inventoryItemManager.getItemsByHoldingId(eq(holdingIds.get(0)), eq(requestContext))).thenReturn(succeededFuture(new ArrayList())); - when(inventoryItemManager.getItemsByHoldingId(eq(holdingIds.get(1)), eq(requestContext))).thenReturn(succeededFuture(new ArrayList())); - when(inventoryItemManager.getItemsByHoldingId(eq(usedHoldingId), eq(requestContext))).thenReturn(succeededFuture(new ArrayList())); + when(inventoryItemManager.getItemsByHoldingId(eq(holdingIds.get(0)), eq(requestContext))).thenReturn(succeededFuture(new ArrayList<>())); + when(inventoryItemManager.getItemsByHoldingId(eq(holdingIds.get(1)), eq(requestContext))).thenReturn(succeededFuture(new ArrayList<>())); + when(inventoryItemManager.getItemsByHoldingId(eq(usedHoldingId), eq(requestContext))).thenReturn(succeededFuture(new ArrayList<>())); when(inventoryItemManager.getItemsByHoldingIdAndOrderLineId(eq(holdingIds.get(0)), eq(orderLineId), any(RequestContext.class))).thenReturn(succeededFuture(List.of(items.get(0)))); when(inventoryItemManager.getItemsByHoldingIdAndOrderLineId(eq(holdingIds.get(1)), eq(orderLineId), any(RequestContext.class))).thenReturn(succeededFuture(List.of(items.get(1)))); when(inventoryItemManager.getItemsByHoldingIdAndOrderLineId(eq(usedHoldingId), eq(orderLineId), any(RequestContext.class))).thenReturn(succeededFuture(List.of())); when(inventoryHoldingManager.deleteHoldingById(eq(holdingIds.get(0)), eq(true), any(RequestContext.class))).thenReturn(succeededFuture(null)); when(inventoryHoldingManager.deleteHoldingById(eq(holdingIds.get(1)), eq(true), any(RequestContext.class))).thenReturn(succeededFuture(null)); - when(inventoryItemManager.updateItem(any(JsonObject.class), eq(requestContext))).thenReturn(succeededFuture(null)); + when(inventoryItemManager.batchUpdatePartialItems(any(), eq(requestContext))).thenReturn(succeededFuture(null)); when(pieceStorageService.getPiecesByHoldingIds(holdingIds, requestContext)).thenReturn(succeededFuture(List.of(new Piece().withHoldingId(usedHoldingId)))); when(purchaseOrderLineService.getPoLinesByHoldingIds(holdingIds, requestContext)).thenReturn(succeededFuture(List.of(new PoLine().withLocations(List.of(new Location().withHoldingId(usedHoldingId)))))); @@ -431,10 +431,9 @@ ITEM_STATUS, new JsonObject().put(ITEM_STATUS_NAME, ItemStatus.ON_ORDER.value()) verify(inventoryHoldingManager, times(1)).deleteHoldingById(eq(holdingIds.get(1)), eq(true), any(RequestContext.class)); verify(inventoryHoldingManager, times(0)).deleteHoldingById(eq(usedHoldingId), eq(true), any(RequestContext.class)); verify(inventoryItemManager, times(3)).getItemsByHoldingIdAndOrderLineId(anyString(), anyString(), any(RequestContext.class)); - verify(inventoryItemManager, times(2)).updateItem(any(JsonObject.class), any(RequestContext.class)); + verify(inventoryItemManager, times(3)).batchUpdatePartialItems(any(), any(RequestContext.class)); vertxTestContext.completeNow(); }); - } @Test @@ -446,9 +445,9 @@ public void updateInstanceForCreateHoldingOperation() throws IOException { List holdings = holdingsCollection.getJsonArray("holdingsRecords").stream() .map(o -> ((JsonObject) o)) - .collect(toList()); + .toList(); - List holdingIds = holdings.stream().map(holding -> holding.getString(ID)).collect(toList()); + List holdingIds = holdings.stream().map(holding -> holding.getString(ID)).toList(); ArrayList locations = new ArrayList<>(); locations.add(new Location() @@ -472,7 +471,7 @@ public void updateInstanceForCreateHoldingOperation() throws IOException { item.put(ITEM_STATUS, new JsonObject().put(ITEM_STATUS_NAME, ItemStatus.ON_ORDER.value())); item.put(ITEM_HOLDINGS_RECORD_ID, holdingId); return item; - }).collect(toList()); + }).toList(); PatchOrderLineRequest patchOrderLineRequest = new PatchOrderLineRequest(); patchOrderLineRequest.withOperation(PatchOrderLineRequest.Operation.REPLACE_INSTANCE_REF) @@ -491,7 +490,7 @@ public void updateInstanceForCreateHoldingOperation() throws IOException { .createHolding(eq(instanceId), eq(locations.get(1)), eq(requestContext)); doReturn(succeededFuture(List.of(items.get(0)))).when(inventoryItemManager).getItemsByHoldingIdAndOrderLineId(eq(holdingIds.get(0)), eq(orderLineId), eq(requestContext)); doReturn(succeededFuture(List.of(items.get(1)))).when(inventoryItemManager).getItemsByHoldingIdAndOrderLineId(eq(holdingIds.get(1)), eq(orderLineId), eq(requestContext)); - doReturn(succeededFuture(null)).when(inventoryItemManager).updateItem(any(JsonObject.class), eq(requestContext)); + doReturn(succeededFuture(null)).when(inventoryItemManager).batchUpdatePartialItems(any(), eq(requestContext)); withHoldingOrderLineUpdateInstanceStrategy.updateInstance(orderLineUpdateInstanceHolder, requestContext).result(); @@ -499,11 +498,11 @@ public void updateInstanceForCreateHoldingOperation() throws IOException { verify(inventoryHoldingManager, times(1)).createHolding(instanceId, locations.get(0), requestContext); verify(inventoryHoldingManager, times(1)).createHolding(instanceId, locations.get(1), requestContext); verify(inventoryItemManager, times(2)).getItemsByHoldingIdAndOrderLineId(anyString(), anyString(), any(RequestContext.class)); - verify(inventoryItemManager, times(2)).updateItem(any(JsonObject.class), any(RequestContext.class)); + verify(inventoryItemManager, times(2)).batchUpdatePartialItems(any(), any(RequestContext.class)); } @Test - public void updateInstanceAndItemsForCreateHoldingOperation() throws IOException { + void updateInstanceAndItemsForCreateHoldingOperation() throws Exception { String instanceId = UUID.randomUUID().toString(); String itemId = UUID.randomUUID().toString(); @@ -543,16 +542,16 @@ public void updateInstanceAndItemsForCreateHoldingOperation() throws IOException doReturn(succeededFuture()).when(inventoryInstanceManager).createShadowInstanceIfNeeded(eq(instanceId), any(RequestContext.class)); doReturn(succeededFuture(UUID.randomUUID().toString())).when(inventoryHoldingManager) - .createHolding(eq(instanceId), eq(locations.get(0)), eq(requestContext)); + .createHolding(eq(instanceId), eq(locations.getFirst()), eq(requestContext)); doReturn(succeededFuture(List.of(item))).when(inventoryItemManager).getItemsByHoldingIdAndOrderLineId(eq(holdingId), eq(orderLineId), eq(requestContext)); - doReturn(succeededFuture(null)).when(inventoryItemManager).updateItem(eq(item), eq(requestContext)); + doReturn(succeededFuture(null)).when(inventoryItemManager).batchUpdatePartialItems(any(), eq(requestContext)); withHoldingOrderLineUpdateInstanceStrategy.updateInstance(orderLineUpdateInstanceHolder, requestContext).result(); verify(inventoryInstanceManager, times(1)).createShadowInstanceIfNeeded(eq(instanceId), any(RequestContext.class)); - verify(inventoryHoldingManager, times(1)).createHolding(instanceId, locations.get(0), requestContext); + verify(inventoryHoldingManager, times(1)).createHolding(instanceId, locations.getFirst(), requestContext); verify(inventoryItemManager, times(1)).getItemsByHoldingIdAndOrderLineId(holdingId, orderLineId, requestContext); - verify(inventoryItemManager, times(1)).updateItem(item, requestContext); + verify(inventoryItemManager, times(1)).batchUpdatePartialItems(any(), eq(requestContext)); } @Test @@ -596,16 +595,16 @@ public void updateInstanceAndItemsWithItemsNotUpdatesCorrect(VertxTestContext ve doReturn(succeededFuture()).when(inventoryInstanceManager).createShadowInstanceIfNeeded(eq(instanceId), any(RequestContext.class)); doReturn(succeededFuture(UUID.randomUUID().toString())).when(inventoryHoldingManager) - .createHolding(eq(instanceId), eq(locations.get(0)), eq(requestContext)); + .createHolding(eq(instanceId), eq(locations.getFirst()), eq(requestContext)); doReturn(succeededFuture(List.of(item))).when(inventoryItemManager).getItemsByHoldingIdAndOrderLineId(eq(holdingId), eq(orderLineId), eq(requestContext)); - doReturn(Future.failedFuture(new HttpException(500, ErrorCodes.GENERIC_ERROR_CODE))).when(inventoryItemManager).updateItem(eq(item), eq(requestContext)); + doReturn(Future.failedFuture(new HttpException(500, ErrorCodes.GENERIC_ERROR_CODE))).when(inventoryItemManager).batchUpdatePartialItems(any(), eq(requestContext)); vertxTestContext.assertFailure(withHoldingOrderLineUpdateInstanceStrategy.updateInstance(orderLineUpdateInstanceHolder, requestContext)) .onComplete(res-> { HttpException actHttpException = (HttpException) res.cause(); - Error actError = actHttpException.getErrors().getErrors().get(0); - Parameter actParameter = actError.getParameters().get(0); + Error actError = actHttpException.getErrors().getErrors().getFirst(); + Parameter actParameter = actError.getParameters().getFirst(); Assertions.assertEquals(ITEM_UPDATE_FAILED.getCode(), actError.getCode()); Assertions.assertEquals(ITEM_UPDATE_FAILED.getDescription(), actError.getMessage()); @@ -613,11 +612,334 @@ public void updateInstanceAndItemsWithItemsNotUpdatesCorrect(VertxTestContext ve Assertions.assertEquals(itemId, actParameter.getValue()); verify(inventoryInstanceManager, times(1)).createShadowInstanceIfNeeded(eq(instanceId), any(RequestContext.class)); - verify(inventoryHoldingManager, times(1)).createHolding(instanceId, locations.get(0), requestContext); + verify(inventoryHoldingManager, times(1)).createHolding(instanceId, locations.getFirst(), requestContext); verify(inventoryItemManager, times(1)).getItemsByHoldingIdAndOrderLineId(holdingId, orderLineId, requestContext); - verify(inventoryItemManager, times(1)).updateItem(item, requestContext); + verify(inventoryItemManager, times(1)).batchUpdatePartialItems(any(), eq(requestContext)); vertxTestContext.completeNow(); }); } + @Test + void testUpdateItemsWithMaterialTypeAndPermanentLoanType() throws IOException { + // given + var instanceId = UUID.randomUUID().toString(); + var itemId = UUID.randomUUID().toString(); + var materialTypeId = UUID.randomUUID().toString(); + var permanentLoanTypeId = UUID.randomUUID().toString(); + + var holdingsCollection = new JsonObject(getMockData(HOLDINGS_OLD_NEW_PATH)); + var holding = holdingsCollection.getJsonArray("holdingsRecords").getJsonObject(0); + var holdingId = holding.getString(ID); + var orderLineId = holding.getString("purchaseOrderLineIdentifier"); + + var locations = new ArrayList(); + locations.add(new Location() + .withHoldingId(holdingId) + .withQuantity(1) + .withQuantityPhysical(1)); + + var item = new JsonObject() + .put(TestConstants.ID, itemId) + .put(ITEM_STATUS, new JsonObject().put(ITEM_STATUS_NAME, ItemStatus.ON_ORDER.value())) + .put(ITEM_HOLDINGS_RECORD_ID, holdingId) + .put("materialType", new JsonObject().put(ID, materialTypeId)) + .put("permanentLoanType", new JsonObject().put(ID, permanentLoanTypeId)) + .put("_version", "1"); + + var poLine = new PoLine() + .withId(orderLineId) + .withOrderFormat(PoLine.OrderFormat.PHYSICAL_RESOURCE) + .withPhysical(new Physical().withCreateInventory(Physical.CreateInventory.INSTANCE_HOLDING_ITEM)) + .withLocations(locations); + + var patchOrderLineRequest = new PatchOrderLineRequest() + .withOperation(PatchOrderLineRequest.Operation.REPLACE_INSTANCE_REF) + .withReplaceInstanceRef(new ReplaceInstanceRef() + .withNewInstanceId(instanceId) + .withHoldingsOperation(ReplaceInstanceRef.HoldingsOperation.CREATE) + .withDeleteAbandonedHoldings(false)); + + var orderLineUpdateInstanceHolder = new OrderLineUpdateInstanceHolder() + .withStoragePoLine(poLine).withPathOrderLineRequest(patchOrderLineRequest); + + doReturn(succeededFuture()).when(inventoryInstanceManager).createShadowInstanceIfNeeded(eq(instanceId), any(RequestContext.class)); + doReturn(succeededFuture(UUID.randomUUID().toString())).when(inventoryHoldingManager) + .createHolding(eq(instanceId), eq(locations.getFirst()), eq(requestContext)); + doReturn(succeededFuture(List.of(item))).when(inventoryItemManager).getItemsByHoldingIdAndOrderLineId(eq(holdingId), eq(orderLineId), eq(requestContext)); + doReturn(succeededFuture(null)).when(inventoryItemManager).batchUpdatePartialItems(any(), eq(requestContext)); + + // when + withHoldingOrderLineUpdateInstanceStrategy.updateInstance(orderLineUpdateInstanceHolder, requestContext).result(); + + // then + verify(inventoryItemManager, times(1)).batchUpdatePartialItems(any(), eq(requestContext)); + } + + @Test + void testUpdateItemsWithMaterialTypeOnly() throws IOException { + // given + var instanceId = UUID.randomUUID().toString(); + var itemId = UUID.randomUUID().toString(); + var materialTypeId = UUID.randomUUID().toString(); + + var holdingsCollection = new JsonObject(getMockData(HOLDINGS_OLD_NEW_PATH)); + var holding = holdingsCollection.getJsonArray("holdingsRecords").getJsonObject(0); + var holdingId = holding.getString(ID); + var orderLineId = holding.getString("purchaseOrderLineIdentifier"); + + var locations = new ArrayList(); + locations.add(new Location() + .withHoldingId(holdingId) + .withQuantity(1) + .withQuantityPhysical(1)); + + var item = new JsonObject() + .put(TestConstants.ID, itemId) + .put(ITEM_STATUS, new JsonObject().put(ITEM_STATUS_NAME, ItemStatus.ON_ORDER.value())) + .put(ITEM_HOLDINGS_RECORD_ID, holdingId) + .put("materialType", new JsonObject().put(ID, materialTypeId)) + .put("_version", "1"); + + var poLine = new PoLine() + .withId(orderLineId) + .withOrderFormat(PoLine.OrderFormat.PHYSICAL_RESOURCE) + .withPhysical(new Physical().withCreateInventory(Physical.CreateInventory.INSTANCE_HOLDING_ITEM)) + .withLocations(locations); + + var patchOrderLineRequest = new PatchOrderLineRequest() + .withOperation(PatchOrderLineRequest.Operation.REPLACE_INSTANCE_REF) + .withReplaceInstanceRef(new ReplaceInstanceRef() + .withNewInstanceId(instanceId) + .withHoldingsOperation(ReplaceInstanceRef.HoldingsOperation.CREATE) + .withDeleteAbandonedHoldings(false)); + + var orderLineUpdateInstanceHolder = new OrderLineUpdateInstanceHolder() + .withStoragePoLine(poLine).withPathOrderLineRequest(patchOrderLineRequest); + + doReturn(succeededFuture()).when(inventoryInstanceManager).createShadowInstanceIfNeeded(eq(instanceId), any(RequestContext.class)); + doReturn(succeededFuture(UUID.randomUUID().toString())).when(inventoryHoldingManager) + .createHolding(eq(instanceId), eq(locations.getFirst()), eq(requestContext)); + doReturn(succeededFuture(List.of(item))).when(inventoryItemManager).getItemsByHoldingIdAndOrderLineId(eq(holdingId), eq(orderLineId), eq(requestContext)); + doReturn(succeededFuture(null)).when(inventoryItemManager).batchUpdatePartialItems(any(), eq(requestContext)); + + // when + withHoldingOrderLineUpdateInstanceStrategy.updateInstance(orderLineUpdateInstanceHolder, requestContext).result(); + + // then + verify(inventoryItemManager, times(1)).batchUpdatePartialItems(any(), eq(requestContext)); + } + + @Test + void testUpdateItemsWithPermanentLoanTypeOnly() throws IOException { + // given + var instanceId = UUID.randomUUID().toString(); + var itemId = UUID.randomUUID().toString(); + var permanentLoanTypeId = UUID.randomUUID().toString(); + + var holdingsCollection = new JsonObject(getMockData(HOLDINGS_OLD_NEW_PATH)); + var holding = holdingsCollection.getJsonArray("holdingsRecords").getJsonObject(0); + var holdingId = holding.getString(ID); + var orderLineId = holding.getString("purchaseOrderLineIdentifier"); + + var locations = new ArrayList(); + locations.add(new Location() + .withHoldingId(holdingId) + .withQuantity(1) + .withQuantityPhysical(1)); + + var item = new JsonObject() + .put(TestConstants.ID, itemId) + .put(ITEM_STATUS, new JsonObject().put(ITEM_STATUS_NAME, ItemStatus.ON_ORDER.value())) + .put(ITEM_HOLDINGS_RECORD_ID, holdingId) + .put("permanentLoanType", new JsonObject().put(ID, permanentLoanTypeId)) + .put("_version", "1"); + + var poLine = new PoLine() + .withId(orderLineId) + .withOrderFormat(PoLine.OrderFormat.PHYSICAL_RESOURCE) + .withPhysical(new Physical().withCreateInventory(Physical.CreateInventory.INSTANCE_HOLDING_ITEM)) + .withLocations(locations); + + var patchOrderLineRequest = new PatchOrderLineRequest() + .withOperation(PatchOrderLineRequest.Operation.REPLACE_INSTANCE_REF) + .withReplaceInstanceRef(new ReplaceInstanceRef() + .withNewInstanceId(instanceId) + .withHoldingsOperation(ReplaceInstanceRef.HoldingsOperation.CREATE) + .withDeleteAbandonedHoldings(false)); + + var orderLineUpdateInstanceHolder = new OrderLineUpdateInstanceHolder() + .withStoragePoLine(poLine).withPathOrderLineRequest(patchOrderLineRequest); + + doReturn(succeededFuture()).when(inventoryInstanceManager).createShadowInstanceIfNeeded(eq(instanceId), any(RequestContext.class)); + doReturn(succeededFuture(UUID.randomUUID().toString())).when(inventoryHoldingManager) + .createHolding(eq(instanceId), eq(locations.getFirst()), eq(requestContext)); + doReturn(succeededFuture(List.of(item))).when(inventoryItemManager).getItemsByHoldingIdAndOrderLineId(eq(holdingId), eq(orderLineId), eq(requestContext)); + doReturn(succeededFuture(null)).when(inventoryItemManager).batchUpdatePartialItems(any(), eq(requestContext)); + + // when + withHoldingOrderLineUpdateInstanceStrategy.updateInstance(orderLineUpdateInstanceHolder, requestContext).result(); + + // then + verify(inventoryItemManager, times(1)).batchUpdatePartialItems(any(), eq(requestContext)); + } + + @Test + void testUpdateItemsWithoutMaterialTypeAndPermanentLoanType() throws IOException { + // given + var instanceId = UUID.randomUUID().toString(); + var itemId = UUID.randomUUID().toString(); + + var holdingsCollection = new JsonObject(getMockData(HOLDINGS_OLD_NEW_PATH)); + var holding = holdingsCollection.getJsonArray("holdingsRecords").getJsonObject(0); + var holdingId = holding.getString(ID); + var orderLineId = holding.getString("purchaseOrderLineIdentifier"); + + var locations = new ArrayList(); + locations.add(new Location() + .withHoldingId(holdingId) + .withQuantity(1) + .withQuantityPhysical(1)); + + var item = new JsonObject() + .put(TestConstants.ID, itemId) + .put(ITEM_STATUS, new JsonObject().put(ITEM_STATUS_NAME, ItemStatus.ON_ORDER.value())) + .put(ITEM_HOLDINGS_RECORD_ID, holdingId) + .put("_version", "1"); + + var poLine = new PoLine() + .withId(orderLineId) + .withOrderFormat(PoLine.OrderFormat.PHYSICAL_RESOURCE) + .withPhysical(new Physical().withCreateInventory(Physical.CreateInventory.INSTANCE_HOLDING_ITEM)) + .withLocations(locations); + + var patchOrderLineRequest = new PatchOrderLineRequest() + .withOperation(PatchOrderLineRequest.Operation.REPLACE_INSTANCE_REF) + .withReplaceInstanceRef(new ReplaceInstanceRef() + .withNewInstanceId(instanceId) + .withHoldingsOperation(ReplaceInstanceRef.HoldingsOperation.CREATE) + .withDeleteAbandonedHoldings(false)); + + var orderLineUpdateInstanceHolder = new OrderLineUpdateInstanceHolder() + .withStoragePoLine(poLine).withPathOrderLineRequest(patchOrderLineRequest); + + doReturn(succeededFuture()).when(inventoryInstanceManager).createShadowInstanceIfNeeded(eq(instanceId), any(RequestContext.class)); + doReturn(succeededFuture(UUID.randomUUID().toString())).when(inventoryHoldingManager) + .createHolding(eq(instanceId), eq(locations.getFirst()), eq(requestContext)); + doReturn(succeededFuture(List.of(item))).when(inventoryItemManager).getItemsByHoldingIdAndOrderLineId(eq(holdingId), eq(orderLineId), eq(requestContext)); + doReturn(succeededFuture(null)).when(inventoryItemManager).batchUpdatePartialItems(any(), eq(requestContext)); + + // when + withHoldingOrderLineUpdateInstanceStrategy.updateInstance(orderLineUpdateInstanceHolder, requestContext).result(); + + // then + verify(inventoryItemManager, times(1)).batchUpdatePartialItems(any(), eq(requestContext)); + } + + @Test + void testUpdateItemsWithBlankMaterialTypeAndPermanentLoanType() throws IOException { + // given + var instanceId = UUID.randomUUID().toString(); + var itemId = UUID.randomUUID().toString(); + + var holdingsCollection = new JsonObject(getMockData(HOLDINGS_OLD_NEW_PATH)); + var holding = holdingsCollection.getJsonArray("holdingsRecords").getJsonObject(0); + var holdingId = holding.getString(ID); + var orderLineId = holding.getString("purchaseOrderLineIdentifier"); + + var locations = new ArrayList(); + locations.add(new Location() + .withHoldingId(holdingId) + .withQuantity(1) + .withQuantityPhysical(1)); + + var item = new JsonObject() + .put(TestConstants.ID, itemId) + .put(ITEM_STATUS, new JsonObject().put(ITEM_STATUS_NAME, ItemStatus.ON_ORDER.value())) + .put(ITEM_HOLDINGS_RECORD_ID, holdingId) + .put("materialType", "") + .put("permanentLoanType", "") + .put("_version", "1"); + + var poLine = new PoLine() + .withId(orderLineId) + .withOrderFormat(PoLine.OrderFormat.PHYSICAL_RESOURCE) + .withPhysical(new Physical().withCreateInventory(Physical.CreateInventory.INSTANCE_HOLDING_ITEM)) + .withLocations(locations); + + var patchOrderLineRequest = new PatchOrderLineRequest() + .withOperation(PatchOrderLineRequest.Operation.REPLACE_INSTANCE_REF) + .withReplaceInstanceRef(new ReplaceInstanceRef() + .withNewInstanceId(instanceId) + .withHoldingsOperation(ReplaceInstanceRef.HoldingsOperation.CREATE) + .withDeleteAbandonedHoldings(false)); + + var orderLineUpdateInstanceHolder = new OrderLineUpdateInstanceHolder() + .withStoragePoLine(poLine).withPathOrderLineRequest(patchOrderLineRequest); + + doReturn(succeededFuture()).when(inventoryInstanceManager).createShadowInstanceIfNeeded(eq(instanceId), any(RequestContext.class)); + doReturn(succeededFuture(UUID.randomUUID().toString())).when(inventoryHoldingManager) + .createHolding(eq(instanceId), eq(locations.getFirst()), eq(requestContext)); + doReturn(succeededFuture(List.of(item))).when(inventoryItemManager).getItemsByHoldingIdAndOrderLineId(eq(holdingId), eq(orderLineId), eq(requestContext)); + doReturn(succeededFuture(null)).when(inventoryItemManager).batchUpdatePartialItems(any(), eq(requestContext)); + + // when + withHoldingOrderLineUpdateInstanceStrategy.updateInstance(orderLineUpdateInstanceHolder, requestContext).result(); + + // then + verify(inventoryItemManager, times(1)).batchUpdatePartialItems(any(), eq(requestContext)); + } + + @Test + void testUpdateItemsWithNonJsonObjectMaterialType() throws IOException { + // given + var instanceId = UUID.randomUUID().toString(); + var itemId = UUID.randomUUID().toString(); + + var holdingsCollection = new JsonObject(getMockData(HOLDINGS_OLD_NEW_PATH)); + var holding = holdingsCollection.getJsonArray("holdingsRecords").getJsonObject(0); + var holdingId = holding.getString(ID); + var orderLineId = holding.getString("purchaseOrderLineIdentifier"); + + var locations = new ArrayList(); + locations.add(new Location() + .withHoldingId(holdingId) + .withQuantity(1) + .withQuantityPhysical(1)); + + var item = new JsonObject() + .put(TestConstants.ID, itemId) + .put(ITEM_STATUS, new JsonObject().put(ITEM_STATUS_NAME, ItemStatus.ON_ORDER.value())) + .put(ITEM_HOLDINGS_RECORD_ID, holdingId) + .put("materialType", "not-a-json-object") + .put("permanentLoanType", "not-a-json-object") + .put("_version", "1"); + + var poLine = new PoLine() + .withId(orderLineId) + .withOrderFormat(PoLine.OrderFormat.PHYSICAL_RESOURCE) + .withPhysical(new Physical().withCreateInventory(Physical.CreateInventory.INSTANCE_HOLDING_ITEM)) + .withLocations(locations); + + var patchOrderLineRequest = new PatchOrderLineRequest() + .withOperation(PatchOrderLineRequest.Operation.REPLACE_INSTANCE_REF) + .withReplaceInstanceRef(new ReplaceInstanceRef() + .withNewInstanceId(instanceId) + .withHoldingsOperation(ReplaceInstanceRef.HoldingsOperation.CREATE) + .withDeleteAbandonedHoldings(false)); + + var orderLineUpdateInstanceHolder = new OrderLineUpdateInstanceHolder() + .withStoragePoLine(poLine).withPathOrderLineRequest(patchOrderLineRequest); + + doReturn(succeededFuture()).when(inventoryInstanceManager).createShadowInstanceIfNeeded(eq(instanceId), any(RequestContext.class)); + doReturn(succeededFuture(UUID.randomUUID().toString())).when(inventoryHoldingManager) + .createHolding(eq(instanceId), eq(locations.getFirst()), eq(requestContext)); + doReturn(succeededFuture(List.of(item))).when(inventoryItemManager).getItemsByHoldingIdAndOrderLineId(eq(holdingId), eq(orderLineId), eq(requestContext)); + doReturn(succeededFuture(null)).when(inventoryItemManager).batchUpdatePartialItems(any(), eq(requestContext)); + + // when + withHoldingOrderLineUpdateInstanceStrategy.updateInstance(orderLineUpdateInstanceHolder, requestContext).result(); + + // then + verify(inventoryItemManager, times(1)).batchUpdatePartialItems(any(), eq(requestContext)); + } } diff --git a/src/test/java/org/folio/service/orders/lines/update/instance/WithoutHoldingOrderLineUpdateInstanceStrategyTest.java b/src/test/java/org/folio/service/orders/lines/update/instance/WithoutHoldingOrderLineUpdateInstanceStrategyTest.java index fa65ea661..299871f54 100644 --- a/src/test/java/org/folio/service/orders/lines/update/instance/WithoutHoldingOrderLineUpdateInstanceStrategyTest.java +++ b/src/test/java/org/folio/service/orders/lines/update/instance/WithoutHoldingOrderLineUpdateInstanceStrategyTest.java @@ -1,7 +1,6 @@ package org.folio.service.orders.lines.update.instance; import static io.vertx.core.Future.succeededFuture; -import static java.util.stream.Collectors.toList; import static org.folio.TestConfig.clearServiceInteractions; import static org.folio.TestUtils.getMockData; import static org.folio.rest.impl.MockServer.HOLDINGS_OLD_NEW_PATH; @@ -40,7 +39,6 @@ import io.vertx.core.json.JsonObject; import io.vertx.junit5.VertxExtension; - @ExtendWith(VertxExtension.class) public class WithoutHoldingOrderLineUpdateInstanceStrategyTest { @@ -52,19 +50,20 @@ public class WithoutHoldingOrderLineUpdateInstanceStrategyTest { private InventoryHoldingManager inventoryHoldingManager; @Mock private RequestContext requestContext; + private AutoCloseable mockitoMocks; @BeforeEach void initMocks(){ - MockitoAnnotations.openMocks(this); + mockitoMocks = MockitoAnnotations.openMocks(this); } @AfterEach - void resetMocks() { + void resetMocks() throws Exception { clearServiceInteractions(); + mockitoMocks.close(); Mockito.reset(inventoryItemManager, inventoryHoldingManager); } - @Test void updateInstanceOnlyTest() throws IOException { String orderLineId = UUID.randomUUID().toString(); @@ -74,9 +73,9 @@ void updateInstanceOnlyTest() throws IOException { List holdings = holdingsCollection.getJsonArray("holdingsRecords").stream() .map(o -> ((JsonObject) o)) - .collect(toList()); + .toList(); - List holdingIds = holdings.stream().map(holding -> holding.getString(ID)).collect(toList()); + List holdingIds = holdings.stream().map(holding -> holding.getString(ID)).toList(); ArrayList locations = new ArrayList<>(); locations.add(new Location() @@ -105,7 +104,6 @@ void updateInstanceOnlyTest() throws IOException { .withStoragePoLine(poLine).withPathOrderLineRequest(patchOrderLineRequest); assertNull(withoutHoldingOrderLineUpdateInstanceStrategy.updateInstance(orderLineUpdateInstanceHolder, requestContext).result()); - } @Test @@ -117,13 +115,13 @@ void updateInstanceAndDeleteAbandonedHoldingsTest() throws IOException { List holdings = holdingsCollection.getJsonArray("holdingsRecords").stream() .map(o -> ((JsonObject) o)) - .collect(toList()); + .toList(); - List holdingIds = holdings.stream().map(holding -> holding.getString(ID)).collect(toList()); + List holdingIds = holdings.stream().map(holding -> holding.getString(ID)).toList(); ArrayList locations = new ArrayList<>(); locations.add(new Location() - .withHoldingId(holdingIds.get(0)) + .withHoldingId(holdingIds.getFirst()) .withQuantity(1) .withQuantityPhysical(1)); @@ -152,7 +150,6 @@ void updateInstanceAndDeleteAbandonedHoldingsTest() throws IOException { assertNull(withoutHoldingOrderLineUpdateInstanceStrategy.updateInstance(orderLineUpdateInstanceHolder, requestContext).result()); verify(inventoryItemManager, times(1)).getItemsByHoldingId(anyString(), eq(requestContext)); verify(inventoryHoldingManager, times(1)).deleteHoldingById(anyString(), anyBoolean(), eq(requestContext)); - } @Test @@ -164,13 +161,13 @@ void updateInstanceAndCantDeleteAbandonedHoldingsTest() throws IOException { List holdings = holdingsCollection.getJsonArray("holdingsRecords").stream() .map(o -> ((JsonObject) o)) - .collect(toList()); + .toList(); - List holdingIds = holdings.stream().map(holding -> holding.getString(ID)).collect(toList()); + List holdingIds = holdings.stream().map(holding -> holding.getString(ID)).toList(); ArrayList locations = new ArrayList<>(); locations.add(new Location() - .withHoldingId(holdingIds.get(0)) + .withHoldingId(holdingIds.getFirst()) .withQuantity(1) .withQuantityPhysical(1)); @@ -203,6 +200,5 @@ void updateInstanceAndCantDeleteAbandonedHoldingsTest() throws IOException { assertNull(withoutHoldingOrderLineUpdateInstanceStrategy.updateInstance(orderLineUpdateInstanceHolder, requestContext).result()); verify(inventoryItemManager, times(1)).getItemsByHoldingId(anyString(), eq(requestContext)); verify(inventoryHoldingManager, times(0)).deleteHoldingById(anyString(), anyBoolean(), eq(requestContext)); - } }