From d728de1cbda45dd0936a9f2c0585ee45aadaaa82 Mon Sep 17 00:00:00 2001 From: BKadirkhodjaev Date: Fri, 13 Feb 2026 15:18:53 +0500 Subject: [PATCH 01/10] [MODORDERS-1403]. Enhance the holding-detail endpoint to always return all linked PO lines --- .../folio/models/HoldingDetailAggregator.java | 52 ++++ .../org/folio/models/HoldingDetailHolder.java | 10 - .../inventory/InventoryItemManager.java | 17 +- .../service/orders/HoldingDetailService.java | 259 +++++++----------- 4 files changed, 164 insertions(+), 174 deletions(-) create mode 100644 src/main/java/org/folio/models/HoldingDetailAggregator.java delete mode 100644 src/main/java/org/folio/models/HoldingDetailHolder.java diff --git a/src/main/java/org/folio/models/HoldingDetailAggregator.java b/src/main/java/org/folio/models/HoldingDetailAggregator.java new file mode 100644 index 000000000..a5430fb20 --- /dev/null +++ b/src/main/java/org/folio/models/HoldingDetailAggregator.java @@ -0,0 +1,52 @@ +package org.folio.models; + +import io.vertx.core.json.JsonObject; +import org.apache.commons.lang3.StringUtils; +import org.folio.rest.jaxrs.model.Piece; +import org.folio.rest.jaxrs.model.PoLine; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class HoldingDetailAggregator { + private Map> poLinesByHoldingId = new HashMap<>(); + private Map> piecesByHoldingId = new HashMap<>(); + private Map> itemsByHoldingId = new HashMap<>(); + + public Map> getPoLinesByHoldingId() { + return poLinesByHoldingId; + } + + public Map> getPiecesByHoldingId() { + return piecesByHoldingId; + } + + public Map> getItemsByHoldingId() { + return itemsByHoldingId; + } + + public String getPieceTenantIdByItemId(String itemId) { + return piecesByHoldingId.values().stream() + .flatMap(List::stream) + .filter(Objects::nonNull) + .filter(piece -> StringUtils.equals(itemId, piece.getItemId())) + .map(Piece::getReceivingTenantId) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } + + public void setPoLinesByHoldingId(Map> poLines) { + this.poLinesByHoldingId = poLines; + } + + public void setPiecesByHoldingId(Map> pieces) { + this.piecesByHoldingId = pieces; + } + + public void setItemsByHoldingId(Map> items) { + this.itemsByHoldingId = items; + } +} diff --git a/src/main/java/org/folio/models/HoldingDetailHolder.java b/src/main/java/org/folio/models/HoldingDetailHolder.java deleted file mode 100644 index c9bae4cad..000000000 --- a/src/main/java/org/folio/models/HoldingDetailHolder.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.folio.models; - -import org.folio.rest.jaxrs.model.ItemsDetail; -import org.folio.rest.jaxrs.model.PiecesDetail; -import org.folio.rest.jaxrs.model.PoLinesDetail; - -import java.util.List; - -public record HoldingDetailHolder(String holdingId, List poLines, List pieces, List items) { -} diff --git a/src/main/java/org/folio/service/inventory/InventoryItemManager.java b/src/main/java/org/folio/service/inventory/InventoryItemManager.java index 718b2e17e..0f833d015 100644 --- a/src/main/java/org/folio/service/inventory/InventoryItemManager.java +++ b/src/main/java/org/folio/service/inventory/InventoryItemManager.java @@ -30,12 +30,14 @@ import org.folio.service.pieces.PieceUtil; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; import static java.util.stream.Collectors.toList; +import static one.util.streamex.StreamEx.ofSubLists; import static org.folio.orders.utils.HelperUtils.collectResultsOnSuccess; import static org.folio.orders.utils.QueryUtils.convertIdsToCqlQuery; import static org.folio.rest.RestConstants.MAX_IDS_FOR_GET_RQ_15; @@ -266,8 +268,8 @@ private List getItemsByMaterialType(List existingItems, Stri .collect(toList()); } - private List buildPieces(Location location, PoLine poLine, Piece.Format pieceFormat, List createdItemIds, - List existingItemIds) { + private List buildPieces(Location location, PoLine poLine, Piece.Format pieceFormat, + List createdItemIds, List existingItemIds) { List itemIds = ListUtils.union(createdItemIds, existingItemIds); logger.info(BUILDING_PIECE_MESSAGE, itemIds.size(), pieceFormat, poLine.getId()); return StreamEx.of(itemIds).map(itemId -> openOrderBuildPiece(poLine, itemId, pieceFormat, location)).toList(); @@ -309,6 +311,17 @@ public Future> getItemsByHoldingId(String holdingId, RequestCon }); } + public Future> getItemsByHoldingIds(List holdingIds, RequestContext requestContext) { + var futures = ofSubLists(new ArrayList<>(holdingIds), MAX_IDS_FOR_GET_RQ_15) + .map(holdingIdsChunk -> convertIdsToCqlQuery(holdingIdsChunk, ITEM_HOLDINGS_RECORD_ID)) + .map(query -> getItemRecordsByQuery(query, requestContext)) + .toList(); + return collectResultsOnSuccess(futures) + .map(lists -> StreamEx.of(lists) + .flatMap(Collection::stream) + .toList()); + } + public Future openOrderCreateItemRecord(CompositePurchaseOrder compPO, PoLine poLine, String holdingId, RequestContext requestContext) { final int ITEM_QUANTITY = 1; diff --git a/src/main/java/org/folio/service/orders/HoldingDetailService.java b/src/main/java/org/folio/service/orders/HoldingDetailService.java index 2c444fb8b..997c0cbf8 100644 --- a/src/main/java/org/folio/service/orders/HoldingDetailService.java +++ b/src/main/java/org/folio/service/orders/HoldingDetailService.java @@ -4,9 +4,7 @@ import io.vertx.core.json.JsonObject; import lombok.extern.log4j.Log4j2; import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.collections4.MapUtils; -import org.apache.commons.lang.StringUtils; -import org.folio.models.HoldingDetailHolder; +import org.folio.models.HoldingDetailAggregator; import org.folio.rest.core.models.RequestContext; import org.folio.rest.jaxrs.model.HoldingDetailResults; import org.folio.rest.jaxrs.model.HoldingDetailResultsProperty; @@ -21,16 +19,15 @@ import org.folio.service.inventory.InventoryItemManager; import org.folio.service.pieces.PieceStorageService; -import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; -import static org.folio.orders.utils.HelperUtils.collectResultsOnSuccess; -import static org.folio.orders.utils.RequestContextUtil.createContextWithNewTenantId; import static org.folio.service.inventory.InventoryItemManager.ID; +import static org.folio.service.inventory.InventoryItemManager.ITEM_HOLDINGS_RECORD_ID; @Log4j2 public class HoldingDetailService { @@ -52,187 +49,125 @@ public Future postOrdersHoldingDetail(List holding log.info("postOrdersHoldingDetail:: No holding ids were passed"); return Future.succeededFuture(new HoldingDetailResults()); } - return purchaseOrderLineService.getPoLinesByHoldingIds(holdingIds, requestContext) - .compose(poLines -> { - if (CollectionUtils.isEmpty(poLines)) { - log.info("postOrdersHoldingDetail:: No poLines were found by holding ids={}", holdingIds); - return Future.succeededFuture(new HoldingDetailResults()); - } - var poLineIds = poLines.stream().map(PoLine::getId).distinct().toList(); - return pieceStorageService.getPiecesByLineIdsByChunks(poLineIds, requestContext) - .compose(pieces -> { - if (CollectionUtils.isEmpty(pieces)) { - log.info("postOrdersHoldingDetail:: No pieces were found by holding ids={}, poLine ids={}", holdingIds, poLineIds); - var groupedPoLinesDetailsByHoldingId = createGroupedPoLineDetailsByHoldingId(holdingIds, poLines); - return Future.succeededFuture(createHoldingDetailsResultsWithPoLines(groupedPoLinesDetailsByHoldingId)); - } - var holdersFutures = new ArrayList>(); - groupPiecesByTenantIdAndHoldingId(pieces) - .forEach((tenantId, groupedPiecesByHoldingId) -> { - if (MapUtils.isNotEmpty(groupedPiecesByHoldingId)) { - groupedPiecesByHoldingId.forEach((holdingId, groupedPieces) -> { - log.info("postOrdersHoldingDetail:: Processing holding detail by tenant={}, holding id={}, pieces={}", getTenantId(tenantId), holdingId, groupedPieces.size()); - var poLinesDetails = createPoLineDetailFromPieces(groupedPieces); - var piecesDetail = createPieceDetail(groupedPieces); - var holdersFuture = getHolderFuture(tenantId, holdingId, poLinesDetails, piecesDetail, requestContext); - holdersFutures.add(holdersFuture); - }); - } - }); - return collectResultsOnSuccess(holdersFutures) - .map(this::createHoldingDetailResults); - }); + var aggregator = new HoldingDetailAggregator(); + + // Retrieve poLines, pieces, and items independently in parallel + var poLinesFuture = purchaseOrderLineService.getPoLinesByHoldingIds(holdingIds, requestContext) + .map(poLines -> { + log.info("postOrdersHoldingDetail:: Found {} poLines by holding ids={}", poLines.size(), holdingIds); + var poLinesByHoldingIds = groupedPoLinesByHoldingId(holdingIds, poLines); + aggregator.setPoLinesByHoldingId(poLinesByHoldingIds); + return null; }) .recover(throwable -> { - log.error("postOrdersHoldingDetail:: Error processing holding details for holding ids={}", holdingIds, throwable); - return Future.failedFuture(throwable); + log.warn("postOrdersHoldingDetail:: Failed to retrieve poLines for holding ids={}", holdingIds, throwable); + aggregator.setPoLinesByHoldingId(Collections.emptyMap()); + return Future.succeededFuture(null); }); - } - - protected Map>> groupPiecesByTenantIdAndHoldingId(List pieces) { - if (CollectionUtils.isEmpty(pieces)) { - return Collections.emptyMap(); - } - return pieces.stream() - .filter(Objects::nonNull) - .filter(piece -> Objects.nonNull(piece.getHoldingId())) - .map(this::getPieceWithNonNullReceivingTenant) - .collect(Collectors.groupingBy(Piece::getReceivingTenantId, Collectors.groupingBy(Piece::getHoldingId))); - } - private Piece getPieceWithNonNullReceivingTenant(Piece piece) { - return Objects.nonNull(piece.getReceivingTenantId()) ? piece : piece.withReceivingTenantId(""); - } + var piecesFuture = pieceStorageService.getPiecesByHoldingIds(holdingIds, requestContext) + .map(pieces -> { + log.info("postOrdersHoldingDetail:: Found {} pieces by holding ids={}", pieces.size(), holdingIds); + var piecesByHoldingId = groupPiecesByHoldingId(pieces); + aggregator.setPiecesByHoldingId(piecesByHoldingId); + return null; + }) + .recover(throwable -> { + log.warn("postOrdersHoldingDetail:: Failed to retrieve pieces for holding ids={}", holdingIds, throwable); + aggregator.setPiecesByHoldingId(Collections.emptyMap()); + return Future.succeededFuture(null); + }); - protected Future getHolderFuture(String tenantId, String holdingId, List poLinesDetails, - List piecesDetail, RequestContext requestContext) { - var localRequestContext = StringUtils.isNotBlank(tenantId) ? createContextWithNewTenantId(requestContext, tenantId) : requestContext; - return inventoryItemManager.getItemsByHoldingId(holdingId, localRequestContext) + var itemsFuture = inventoryItemManager.getItemsByHoldingIds(holdingIds, requestContext) .map(items -> { - if (CollectionUtils.isEmpty(items)) { - return Collections.emptyList(); - } - return items.stream() - .filter(Objects::nonNull) - .map(item -> createItemDetail(tenantId, item)) - .toList(); + log.info("postOrdersHoldingDetail:: Found {} items by holding ids={}", items.size(), holdingIds); + var itemsByHoldingId = groupItemsByHoldingId(items); + aggregator.setItemsByHoldingId(itemsByHoldingId); + return null; + }) + .recover(throwable -> { + log.warn("postOrdersHoldingDetail:: Failed to retrieve items for holding ids={}", holdingIds, throwable); + aggregator.setItemsByHoldingId(Collections.emptyMap()); + return Future.succeededFuture(null); + }); + + return Future.all(poLinesFuture, piecesFuture, itemsFuture) + .compose(v -> { + var holdingDetailResults = new HoldingDetailResults(); + holdingIds.forEach(holdingId -> { + var poLinesDetailCollection = new PoLinesDetailCollection(); + var piecesDetailCollection = new PiecesDetailCollection(); + var itemsDetailCollection = new ItemsDetailCollection(); + + if (aggregator.getPoLinesByHoldingId().containsKey(holdingId)) { + var poLinesDetails = aggregator.getPoLinesByHoldingId().get(holdingId).stream() + .filter(Objects::nonNull) + .map(poLine -> new PoLinesDetail().withId(poLine.getId())) + .toList(); + poLinesDetailCollection.withPoLinesDetail(poLinesDetails) + .withTotalRecords(poLinesDetails.size()); + } + if (aggregator.getPiecesByHoldingId().containsKey(holdingId)) { + var piecesDetails = aggregator.getPiecesByHoldingId().get(holdingId).stream() + .filter(Objects::nonNull) + .map(piece -> new PiecesDetail().withId(piece.getId()) + .withPoLineId(piece.getPoLineId()) + .withItemId(piece.getItemId()) + .withTenantId(piece.getReceivingTenantId())) + .toList(); + piecesDetailCollection.withPiecesDetail(piecesDetails) + .withTotalRecords(piecesDetails.size()); + } + if (aggregator.getItemsByHoldingId().containsKey(holdingId)) { + var itemsDetails = aggregator.getItemsByHoldingId().get(holdingId).stream() + .filter(Objects::nonNull) + .map(item -> new ItemsDetail().withId(item.getString(ID)) + .withTenantId(aggregator.getPieceTenantIdByItemId(item.getString(ID)))) + .toList(); + itemsDetailCollection.withItemsDetail(itemsDetails) + .withTotalRecords(itemsDetails.size()); + } + var holdingDetailProperty = new HoldingDetailResultsProperty() + .withPoLinesDetailCollection(poLinesDetailCollection) + .withPiecesDetailCollection(piecesDetailCollection) + .withItemsDetailCollection(itemsDetailCollection); + holdingDetailResults.withAdditionalProperty(holdingId, holdingDetailProperty); + }); + return Future.succeededFuture(holdingDetailResults); }) - .map(itemsDetail -> { - var finalPoLinesDetail = CollectionUtils.isNotEmpty(poLinesDetails) ? poLinesDetails : Collections.emptyList(); - var finalPiecesDetail = CollectionUtils.isNotEmpty(piecesDetail) ? piecesDetail : Collections.emptyList(); - return new HoldingDetailHolder(holdingId, finalPoLinesDetail, finalPiecesDetail, itemsDetail); + .recover(throwable -> { + log.error("postOrdersHoldingDetail:: Error building holding detail results for holding ids={}", holdingIds, throwable); + return Future.failedFuture(throwable); }); } - private Map> createGroupedPoLineDetailsByHoldingId(List holdingIds, List poLines) { + private Map> groupedPoLinesByHoldingId(List holdingIds, List poLines) { if (CollectionUtils.isEmpty(poLines)) { return Collections.emptyMap(); } - var holdingIdSet = CollectionUtils.isEmpty(holdingIds) ? Collections.emptySet() : new java.util.HashSet<>(holdingIds); + var holdingIdSet = new HashSet<>(holdingIds); return poLines.stream() .filter(Objects::nonNull) .filter(poLine -> CollectionUtils.isNotEmpty(poLine.getLocations())) .flatMap(poLine -> poLine.getLocations().stream() .filter(location -> Objects.nonNull(location.getHoldingId())) .filter(location -> holdingIdSet.contains(location.getHoldingId())) - .map(location -> Map.entry(location.getHoldingId(), poLine.getId()))) + .map(location -> Map.entry(location.getHoldingId(), poLine))) .distinct() - .collect(Collectors.groupingBy( - Map.Entry::getKey, - Collectors.mapping( - entry -> new PoLinesDetail().withId(entry.getValue()), - Collectors.toList() - ) - )); + .collect(Collectors.groupingBy(Map.Entry::getKey, Collectors.mapping(Map.Entry::getValue, Collectors.toList()))); } - private List createPoLineDetailFromPieces(List groupedPieces) { - if (CollectionUtils.isEmpty(groupedPieces)) { - return Collections.emptyList(); - } - return groupedPieces.stream() + private Map> groupPiecesByHoldingId(List pieces) { + return pieces.stream() .filter(Objects::nonNull) - .map(Piece::getPoLineId) - .distinct() - .map(poLineId -> new PoLinesDetail().withId(poLineId)) - .toList(); + .filter(piece -> Objects.nonNull(piece.getHoldingId())) + .collect(Collectors.groupingBy(Piece::getHoldingId)); } - private List createPieceDetail(List groupedPieces) { - if (CollectionUtils.isEmpty(groupedPieces)) { - return Collections.emptyList(); - } - return groupedPieces.stream() + private Map> groupItemsByHoldingId(List items) { + return items.stream() .filter(Objects::nonNull) - .map(piece -> new PiecesDetail() - .withId(piece.getId()) - .withPoLineId(piece.getPoLineId()) - .withItemId(piece.getItemId()) - .withTenantId(getTenantId(piece.getReceivingTenantId()))) - .toList(); - } - - private ItemsDetail createItemDetail(String tenantId, JsonObject item) { - return new ItemsDetail() - .withId(item.getString(ID)) - .withTenantId(getTenantId(tenantId)); - } - - private String getTenantId(String tenantId) { - return StringUtils.isNotBlank(tenantId) ? tenantId : null; - } - - private HoldingDetailResults createHoldingDetailsResultsWithPoLines(Map> groupedPoLinesDetailsByHoldingId) { - if (MapUtils.isEmpty(groupedPoLinesDetailsByHoldingId)) { - log.info("createHoldingDetailsResultsWithPoLines:: No detail holders were generated"); - return new HoldingDetailResults(); - } - var holdingDetailResults = new HoldingDetailResults(); - groupedPoLinesDetailsByHoldingId.forEach((holdingId, poLinesDetails) -> { - var poLinesDetailCollection = new PoLinesDetailCollection(); - var piecesDetailCollection = new PiecesDetailCollection(); - var itemsDetailCollection = new ItemsDetailCollection(); - if (CollectionUtils.isNotEmpty(poLinesDetails)) { - poLinesDetailCollection.withPoLinesDetail(poLinesDetails) - .withTotalRecords(poLinesDetails.size()); - } - var holdingDetailProperty = new HoldingDetailResultsProperty() - .withPoLinesDetailCollection(poLinesDetailCollection) - .withPiecesDetailCollection(piecesDetailCollection) - .withItemsDetailCollection(itemsDetailCollection); - holdingDetailResults.withAdditionalProperty(holdingId, holdingDetailProperty); - }); - return holdingDetailResults; - } - - private HoldingDetailResults createHoldingDetailResults(List holders) { - if (CollectionUtils.isEmpty(holders)) { - log.info("createHoldingDetailResults:: No detail holders were generated"); - return new HoldingDetailResults(); - } - var holdingDetailResults = new HoldingDetailResults(); - holders.forEach(holder -> { - var poLinesDetailCollection = new PoLinesDetailCollection(); - var piecesDetailCollection = new PiecesDetailCollection(); - var itemsDetailCollection = new ItemsDetailCollection(); - if (CollectionUtils.isNotEmpty(holder.poLines())) { - poLinesDetailCollection.withPoLinesDetail(holder.poLines()) - .withTotalRecords(holder.poLines().size()); - } - if (CollectionUtils.isNotEmpty(holder.pieces())) { - piecesDetailCollection.withPiecesDetail(holder.pieces()) - .withTotalRecords(holder.pieces().size()); - } - if (CollectionUtils.isNotEmpty(holder.items())) { - itemsDetailCollection.withItemsDetail(holder.items()) - .withTotalRecords(holder.items().size()); - } - var holdingDetailProperty = new HoldingDetailResultsProperty() - .withPoLinesDetailCollection(poLinesDetailCollection) - .withPiecesDetailCollection(piecesDetailCollection) - .withItemsDetailCollection(itemsDetailCollection); - holdingDetailResults.withAdditionalProperty(holder.holdingId(), holdingDetailProperty); - }); - return holdingDetailResults; + .filter(item -> Objects.nonNull(item.getString(ITEM_HOLDINGS_RECORD_ID))) + .collect(Collectors.groupingBy(item -> item.getString(ITEM_HOLDINGS_RECORD_ID))); } } From 05e0d6dfdab2935db5a3e3e6673d9ee21a552aca Mon Sep 17 00:00:00 2001 From: BKadirkhodjaev Date: Fri, 13 Feb 2026 15:28:47 +0500 Subject: [PATCH 02/10] [MODORDERS-1403]. Comment out old tests --- .../orders/HoldingDetailServiceTest.java | 577 +++++++++--------- 1 file changed, 286 insertions(+), 291 deletions(-) diff --git a/src/test/java/org/folio/service/orders/HoldingDetailServiceTest.java b/src/test/java/org/folio/service/orders/HoldingDetailServiceTest.java index 02fa660f0..e24c67f2b 100644 --- a/src/test/java/org/folio/service/orders/HoldingDetailServiceTest.java +++ b/src/test/java/org/folio/service/orders/HoldingDetailServiceTest.java @@ -11,7 +11,6 @@ import org.folio.rest.core.models.RequestContext; import org.folio.rest.jaxrs.model.Location; import org.folio.rest.jaxrs.model.Piece; -import org.folio.rest.jaxrs.model.PiecesDetail; import org.folio.rest.jaxrs.model.PoLine; import org.folio.service.inventory.InventoryItemManager; import org.folio.service.pieces.PieceStorageService; @@ -19,9 +18,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; + import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -29,9 +26,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.UUID; -import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -322,284 +317,284 @@ void testPostOrdersHoldingDetailWithEmptyItems(VertxTestContext vertxTestContext }); } - @ParameterizedTest(name = "{0}") - @MethodSource("provideGroupingTestCases") - void testGroupPiecesByTenantIdAndHoldingId(String testName, List pieces, int expectedTenantGroups, - int expectedHoldingGroups) { - var result = holdingDetailService.groupPiecesByTenantIdAndHoldingId(pieces); - - assertNotNull(result); - assertEquals(expectedTenantGroups, result.size()); - - var totalHoldingGroups = result.values().stream() - .mapToInt(Map::size) - .sum(); - assertEquals(expectedHoldingGroups, totalHoldingGroups); - } - - static Stream provideGroupingTestCases() { - var holdingId1 = UUID.randomUUID().toString(); - var holdingId2 = UUID.randomUUID().toString(); - var tenant1 = "tenant-1"; - var tenant2 = "tenant-2"; - - return Stream.of( - Arguments.of( - "Empty list", - Collections.emptyList(), - 0, 0 - ), - Arguments.of( - "Single piece with tenant and holding", - List.of(createPiece(holdingId1, tenant1)), - 1, 1 - ), - Arguments.of( - "Multiple pieces same tenant and holding", - List.of( - createPiece(holdingId1, tenant1), - createPiece(holdingId1, tenant1) - ), - 1, 1 - ), - Arguments.of( - "Multiple pieces same tenant different holdings", - List.of( - createPiece(holdingId1, tenant1), - createPiece(holdingId2, tenant1) - ), - 1, 2 - ), - Arguments.of( - "Multiple pieces different tenants same holding", - List.of( - createPiece(holdingId1, tenant1), - createPiece(holdingId1, tenant2) - ), - 2, 2 - ), - Arguments.of( - "Multiple pieces different tenants different holdings", - List.of( - createPiece(holdingId1, tenant1), - createPiece(holdingId2, tenant1), - createPiece(holdingId1, tenant2), - createPiece(holdingId2, tenant2) - ), - 2, 4 - ), - Arguments.of( - "Pieces with null tenant - converted to empty string", - List.of( - createPiece(holdingId1, null), - createPiece(holdingId1, null) - ), - 1, 1 - ), - Arguments.of( - "Mixed null and non-null tenants", - List.of( - createPiece(holdingId1, tenant1), - createPiece(holdingId1, null) - ), - 2, 2 - ) - ); - } - - @Test - void testGroupPiecesByTenantIdAndHoldingIdWithNullList() { - var result = holdingDetailService.groupPiecesByTenantIdAndHoldingId(null); - - assertNotNull(result); - assertTrue(result.isEmpty()); - } - - @Test - void testGroupPiecesByTenantIdAndHoldingIdFilterNullPieces() { - var pieces = new ArrayList(); - pieces.add(null); - pieces.add(createPiece(UUID.randomUUID().toString(), "tenant-1")); - pieces.add(null); - - var result = holdingDetailService.groupPiecesByTenantIdAndHoldingId(pieces); - - assertNotNull(result); - assertEquals(1, result.size()); - } - - @Test - void testGroupPiecesByTenantIdAndHoldingIdFilterNullHoldingId() { - var pieces = List.of( - createPiece(null, "tenant-1"), - createPiece(UUID.randomUUID().toString(), "tenant-1") - ); - - var result = holdingDetailService.groupPiecesByTenantIdAndHoldingId(pieces); - - assertNotNull(result); - assertEquals(1, result.size()); - assertEquals(1, result.get("tenant-1").size()); - } - - @Test - void testGroupPiecesByTenantIdAndHoldingIdNullTenantIdConversion() { - var holdingId = UUID.randomUUID().toString(); - var pieces = List.of(createPiece(holdingId, null)); - - var result = holdingDetailService.groupPiecesByTenantIdAndHoldingId(pieces); - - assertNotNull(result); - assertEquals(1, result.size()); - assertTrue(result.containsKey("")); - assertEquals(1, result.get("").size()); - assertTrue(result.get("").containsKey(holdingId)); - } - - @Test - void testGetHolderFuture(VertxTestContext vertxTestContext) { - var tenantId = "tenant-1"; - var holdingId = UUID.randomUUID().toString(); - var itemId = UUID.randomUUID().toString(); - var poLineId = UUID.randomUUID().toString(); - - var poLinesDetails = List.of( - new org.folio.rest.jaxrs.model.PoLinesDetail() - .withId(poLineId) - ); - - var piecesDetail = List.of( - new org.folio.rest.jaxrs.model.PiecesDetail() - .withId(UUID.randomUUID().toString()) - .withItemId(itemId) - .withTenantId(tenantId) - ); - - var item = new JsonObject() - .put("id", itemId) - .put("holdingsRecordId", holdingId); - - when(inventoryItemManager.getItemsByHoldingId(eq(holdingId), any(RequestContext.class))) - .thenReturn(Future.succeededFuture(List.of(item))); - - var future = holdingDetailService.getHolderFuture(tenantId, holdingId, poLinesDetails, piecesDetail, requestContext); - - vertxTestContext.assertComplete(future) - .onComplete(result -> { - assertTrue(result.succeeded()); - var holder = result.result(); - assertNotNull(holder); - assertEquals(holdingId, holder.holdingId()); - assertEquals(1, holder.poLines().size()); - assertEquals(poLineId, holder.poLines().getFirst().getId()); - assertEquals(1, holder.pieces().size()); - assertEquals(1, holder.items().size()); - assertEquals(itemId, holder.items().getFirst().getId()); - assertEquals(tenantId, holder.items().getFirst().getTenantId()); - vertxTestContext.completeNow(); - }); - } - - @Test - void testGetHolderFutureWithEmptyTenantId(VertxTestContext vertxTestContext) { - var tenantId = ""; - var holdingId = UUID.randomUUID().toString(); - var poLinesDetails = Collections.emptyList(); - var piecesDetail = Collections.emptyList(); - - when(inventoryItemManager.getItemsByHoldingId(eq(holdingId), any(RequestContext.class))) - .thenReturn(Future.succeededFuture(Collections.emptyList())); - - var future = holdingDetailService.getHolderFuture(tenantId, holdingId, poLinesDetails, piecesDetail, requestContext); - - vertxTestContext.assertComplete(future) - .onComplete(result -> { - assertTrue(result.succeeded()); - verify(inventoryItemManager).getItemsByHoldingId(eq(holdingId), any(RequestContext.class)); - vertxTestContext.completeNow(); - }); - } - - @Test - void testGetHolderFutureWithNullItemsList(VertxTestContext vertxTestContext) { - var tenantId = "tenant-1"; - var holdingId = UUID.randomUUID().toString(); - var poLinesDetails = List.of( - new org.folio.rest.jaxrs.model.PoLinesDetail() - .withId(UUID.randomUUID().toString()) - ); - var piecesDetail = List.of( - new org.folio.rest.jaxrs.model.PiecesDetail() - .withId(UUID.randomUUID().toString()) - .withTenantId(tenantId) - ); - - when(inventoryItemManager.getItemsByHoldingId(eq(holdingId), any(RequestContext.class))) - .thenReturn(Future.succeededFuture(null)); - - var future = holdingDetailService.getHolderFuture(tenantId, holdingId, poLinesDetails, piecesDetail, requestContext); - - vertxTestContext.assertComplete(future) - .onComplete(result -> { - assertTrue(result.succeeded()); - var holder = result.result(); - assertNotNull(holder); - assertEquals(holdingId, holder.holdingId()); - assertEquals(1, holder.poLines().size()); - assertEquals(1, holder.pieces().size()); - assertEquals(0, holder.items().size()); // null items should return empty list - vertxTestContext.completeNow(); - }); - } - - @Test - void testGetHolderFutureWithNullPieces(VertxTestContext vertxTestContext) { - var tenantId = "tenant-1"; - var holdingId = UUID.randomUUID().toString(); - - when(inventoryItemManager.getItemsByHoldingId(eq(holdingId), any(RequestContext.class))) - .thenReturn(Future.succeededFuture(Collections.emptyList())); - - var future = holdingDetailService.getHolderFuture(tenantId, holdingId, null, null, requestContext); - - vertxTestContext.assertComplete(future) - .onComplete(result -> { - assertTrue(result.succeeded()); - var holder = result.result(); - assertNotNull(holder); - assertEquals(holdingId, holder.holdingId()); - assertEquals(0, holder.poLines().size()); // null poLinesDetails should be empty list - assertEquals(0, holder.pieces().size()); // null piecesDetail should be empty list - assertEquals(0, holder.items().size()); - vertxTestContext.completeNow(); - }); - } - - @Test - void testGetHolderFutureFilterNullItems(VertxTestContext vertxTestContext) { - var tenantId = "tenant-1"; - var holdingId = UUID.randomUUID().toString(); - var poLinesDetails = Collections.emptyList(); - var piecesDetail = Collections.emptyList(); - - var items = new ArrayList(); - items.add(null); - items.add(new JsonObject().put("id", UUID.randomUUID().toString())); - items.add(null); - - when(inventoryItemManager.getItemsByHoldingId(eq(holdingId), any(RequestContext.class))) - .thenReturn(Future.succeededFuture(items)); - - var future = holdingDetailService.getHolderFuture(tenantId, holdingId, poLinesDetails, piecesDetail, requestContext); - - vertxTestContext.assertComplete(future) - .onComplete(result -> { - assertTrue(result.succeeded()); - var holder = result.result(); - assertEquals(1, holder.items().size()); - vertxTestContext.completeNow(); - }); - } + // @ParameterizedTest(name = "{0}") + // @MethodSource("provideGroupingTestCases") + // void testGroupPiecesByTenantIdAndHoldingId(String testName, List pieces, int expectedTenantGroups, + // int expectedHoldingGroups) { + // var result = holdingDetailService.groupPiecesByTenantIdAndHoldingId(pieces); + // + // assertNotNull(result); + // assertEquals(expectedTenantGroups, result.size()); + // + // var totalHoldingGroups = result.values().stream() + // .mapToInt(Map::size) + // .sum(); + // assertEquals(expectedHoldingGroups, totalHoldingGroups); + // } + // + // static Stream provideGroupingTestCases() { + // var holdingId1 = UUID.randomUUID().toString(); + // var holdingId2 = UUID.randomUUID().toString(); + // var tenant1 = "tenant-1"; + // var tenant2 = "tenant-2"; + // + // return Stream.of( + // Arguments.of( + // "Empty list", + // Collections.emptyList(), + // 0, 0 + // ), + // Arguments.of( + // "Single piece with tenant and holding", + // List.of(createPiece(holdingId1, tenant1)), + // 1, 1 + // ), + // Arguments.of( + // "Multiple pieces same tenant and holding", + // List.of( + // createPiece(holdingId1, tenant1), + // createPiece(holdingId1, tenant1) + // ), + // 1, 1 + // ), + // Arguments.of( + // "Multiple pieces same tenant different holdings", + // List.of( + // createPiece(holdingId1, tenant1), + // createPiece(holdingId2, tenant1) + // ), + // 1, 2 + // ), + // Arguments.of( + // "Multiple pieces different tenants same holding", + // List.of( + // createPiece(holdingId1, tenant1), + // createPiece(holdingId1, tenant2) + // ), + // 2, 2 + // ), + // Arguments.of( + // "Multiple pieces different tenants different holdings", + // List.of( + // createPiece(holdingId1, tenant1), + // createPiece(holdingId2, tenant1), + // createPiece(holdingId1, tenant2), + // createPiece(holdingId2, tenant2) + // ), + // 2, 4 + // ), + // Arguments.of( + // "Pieces with null tenant - converted to empty string", + // List.of( + // createPiece(holdingId1, null), + // createPiece(holdingId1, null) + // ), + // 1, 1 + // ), + // Arguments.of( + // "Mixed null and non-null tenants", + // List.of( + // createPiece(holdingId1, tenant1), + // createPiece(holdingId1, null) + // ), + // 2, 2 + // ) + // ); + // } + + // @Test + // void testGroupPiecesByTenantIdAndHoldingIdWithNullList() { + // var result = holdingDetailService.groupPiecesByTenantIdAndHoldingId(null); + // + // assertNotNull(result); + // assertTrue(result.isEmpty()); + // } + // + // @Test + // void testGroupPiecesByTenantIdAndHoldingIdFilterNullPieces() { + // var pieces = new ArrayList(); + // pieces.add(null); + // pieces.add(createPiece(UUID.randomUUID().toString(), "tenant-1")); + // pieces.add(null); + // + // var result = holdingDetailService.groupPiecesByTenantIdAndHoldingId(pieces); + // + // assertNotNull(result); + // assertEquals(1, result.size()); + // } + // + // @Test + // void testGroupPiecesByTenantIdAndHoldingIdFilterNullHoldingId() { + // var pieces = List.of( + // createPiece(null, "tenant-1"), + // createPiece(UUID.randomUUID().toString(), "tenant-1") + // ); + // + // var result = holdingDetailService.groupPiecesByTenantIdAndHoldingId(pieces); + // + // assertNotNull(result); + // assertEquals(1, result.size()); + // assertEquals(1, result.get("tenant-1").size()); + // } + // + // @Test + // void testGroupPiecesByTenantIdAndHoldingIdNullTenantIdConversion() { + // var holdingId = UUID.randomUUID().toString(); + // var pieces = List.of(createPiece(holdingId, null)); + // + // var result = holdingDetailService.groupPiecesByTenantIdAndHoldingId(pieces); + // + // assertNotNull(result); + // assertEquals(1, result.size()); + // assertTrue(result.containsKey("")); + // assertEquals(1, result.get("").size()); + // assertTrue(result.get("").containsKey(holdingId)); + // } + + // @Test + // void testGetHolderFuture(VertxTestContext vertxTestContext) { + // var tenantId = "tenant-1"; + // var holdingId = UUID.randomUUID().toString(); + // var itemId = UUID.randomUUID().toString(); + // var poLineId = UUID.randomUUID().toString(); + // + // var poLinesDetails = List.of( + // new org.folio.rest.jaxrs.model.PoLinesDetail() + // .withId(poLineId) + // ); + // + // var piecesDetail = List.of( + // new org.folio.rest.jaxrs.model.PiecesDetail() + // .withId(UUID.randomUUID().toString()) + // .withItemId(itemId) + // .withTenantId(tenantId) + // ); + // + // var item = new JsonObject() + // .put("id", itemId) + // .put("holdingsRecordId", holdingId); + // + // when(inventoryItemManager.getItemsByHoldingId(eq(holdingId), any(RequestContext.class))) + // .thenReturn(Future.succeededFuture(List.of(item))); + // + // var future = holdingDetailService.getHolderFuture(tenantId, holdingId, poLinesDetails, piecesDetail, requestContext); + // + // vertxTestContext.assertComplete(future) + // .onComplete(result -> { + // assertTrue(result.succeeded()); + // var holder = result.result(); + // assertNotNull(holder); + // assertEquals(holdingId, holder.holdingId()); + // assertEquals(1, holder.poLines().size()); + // assertEquals(poLineId, holder.poLines().getFirst().getId()); + // assertEquals(1, holder.pieces().size()); + // assertEquals(1, holder.items().size()); + // assertEquals(itemId, holder.items().getFirst().getId()); + // assertEquals(tenantId, holder.items().getFirst().getTenantId()); + // vertxTestContext.completeNow(); + // }); + // } + // + // @Test + // void testGetHolderFutureWithEmptyTenantId(VertxTestContext vertxTestContext) { + // var tenantId = ""; + // var holdingId = UUID.randomUUID().toString(); + // var poLinesDetails = Collections.emptyList(); + // var piecesDetail = Collections.emptyList(); + // + // when(inventoryItemManager.getItemsByHoldingId(eq(holdingId), any(RequestContext.class))) + // .thenReturn(Future.succeededFuture(Collections.emptyList())); + // + // var future = holdingDetailService.getHolderFuture(tenantId, holdingId, poLinesDetails, piecesDetail, requestContext); + // + // vertxTestContext.assertComplete(future) + // .onComplete(result -> { + // assertTrue(result.succeeded()); + // verify(inventoryItemManager).getItemsByHoldingId(eq(holdingId), any(RequestContext.class)); + // vertxTestContext.completeNow(); + // }); + // } + // + // @Test + // void testGetHolderFutureWithNullItemsList(VertxTestContext vertxTestContext) { + // var tenantId = "tenant-1"; + // var holdingId = UUID.randomUUID().toString(); + // var poLinesDetails = List.of( + // new org.folio.rest.jaxrs.model.PoLinesDetail() + // .withId(UUID.randomUUID().toString()) + // ); + // var piecesDetail = List.of( + // new org.folio.rest.jaxrs.model.PiecesDetail() + // .withId(UUID.randomUUID().toString()) + // .withTenantId(tenantId) + // ); + // + // when(inventoryItemManager.getItemsByHoldingId(eq(holdingId), any(RequestContext.class))) + // .thenReturn(Future.succeededFuture(null)); + // + // var future = holdingDetailService.getHolderFuture(tenantId, holdingId, poLinesDetails, piecesDetail, requestContext); + // + // vertxTestContext.assertComplete(future) + // .onComplete(result -> { + // assertTrue(result.succeeded()); + // var holder = result.result(); + // assertNotNull(holder); + // assertEquals(holdingId, holder.holdingId()); + // assertEquals(1, holder.poLines().size()); + // assertEquals(1, holder.pieces().size()); + // assertEquals(0, holder.items().size()); // null items should return empty list + // vertxTestContext.completeNow(); + // }); + // } + // + // @Test + // void testGetHolderFutureWithNullPieces(VertxTestContext vertxTestContext) { + // var tenantId = "tenant-1"; + // var holdingId = UUID.randomUUID().toString(); + // + // when(inventoryItemManager.getItemsByHoldingId(eq(holdingId), any(RequestContext.class))) + // .thenReturn(Future.succeededFuture(Collections.emptyList())); + // + // var future = holdingDetailService.getHolderFuture(tenantId, holdingId, null, null, requestContext); + // + // vertxTestContext.assertComplete(future) + // .onComplete(result -> { + // assertTrue(result.succeeded()); + // var holder = result.result(); + // assertNotNull(holder); + // assertEquals(holdingId, holder.holdingId()); + // assertEquals(0, holder.poLines().size()); // null poLinesDetails should be empty list + // assertEquals(0, holder.pieces().size()); // null piecesDetail should be empty list + // assertEquals(0, holder.items().size()); + // vertxTestContext.completeNow(); + // }); + // } + // + // @Test + // void testGetHolderFutureFilterNullItems(VertxTestContext vertxTestContext) { + // var tenantId = "tenant-1"; + // var holdingId = UUID.randomUUID().toString(); + // var poLinesDetails = Collections.emptyList(); + // var piecesDetail = Collections.emptyList(); + // + // var items = new ArrayList(); + // items.add(null); + // items.add(new JsonObject().put("id", UUID.randomUUID().toString())); + // items.add(null); + // + // when(inventoryItemManager.getItemsByHoldingId(eq(holdingId), any(RequestContext.class))) + // .thenReturn(Future.succeededFuture(items)); + // + // var future = holdingDetailService.getHolderFuture(tenantId, holdingId, poLinesDetails, piecesDetail, requestContext); + // + // vertxTestContext.assertComplete(future) + // .onComplete(result -> { + // assertTrue(result.succeeded()); + // var holder = result.result(); + // assertEquals(1, holder.items().size()); + // vertxTestContext.completeNow(); + // }); + // } @Test void testPostOrdersHoldingDetailPieceStorageFailure(VertxTestContext vertxTestContext) { @@ -1326,13 +1321,13 @@ void testPoLinesDetailCollectionTotalRecordsAccuracy(VertxTestContext vertxTestC }); } - private static Piece createPiece(String holdingId, String tenantId) { - return new Piece() - .withId(UUID.randomUUID().toString()) - .withHoldingId(holdingId) - .withItemId(UUID.randomUUID().toString()) - .withReceivingTenantId(tenantId); - } + // private static Piece createPiece(String holdingId, String tenantId) { + // return new Piece() + // .withId(UUID.randomUUID().toString()) + // .withHoldingId(holdingId) + // .withItemId(UUID.randomUUID().toString()) + // .withReceivingTenantId(tenantId); + // } private static PoLine createPoLine(String poLineId, String holdingId) { return new PoLine() From 738084e30d123c3ea8ec63353b4d33a5267785bc Mon Sep 17 00:00:00 2001 From: BKadirkhodjaev Date: Fri, 13 Feb 2026 16:09:30 +0500 Subject: [PATCH 03/10] [MODORDERS-1403]. Update tests --- .../service/orders/HoldingDetailService.java | 6 + .../orders/HoldingDetailServiceTest.java | 800 +++++++----------- 2 files changed, 309 insertions(+), 497 deletions(-) diff --git a/src/main/java/org/folio/service/orders/HoldingDetailService.java b/src/main/java/org/folio/service/orders/HoldingDetailService.java index 997c0cbf8..83fd34ad8 100644 --- a/src/main/java/org/folio/service/orders/HoldingDetailService.java +++ b/src/main/java/org/folio/service/orders/HoldingDetailService.java @@ -158,6 +158,9 @@ private Map> groupedPoLinesByHoldingId(List holding } private Map> groupPiecesByHoldingId(List pieces) { + if (CollectionUtils.isEmpty(pieces)) { + return Collections.emptyMap(); + } return pieces.stream() .filter(Objects::nonNull) .filter(piece -> Objects.nonNull(piece.getHoldingId())) @@ -165,6 +168,9 @@ private Map> groupPiecesByHoldingId(List pieces) { } private Map> groupItemsByHoldingId(List items) { + if (CollectionUtils.isEmpty(items)) { + return Collections.emptyMap(); + } return items.stream() .filter(Objects::nonNull) .filter(item -> Objects.nonNull(item.getString(ITEM_HOLDINGS_RECORD_ID))) diff --git a/src/test/java/org/folio/service/orders/HoldingDetailServiceTest.java b/src/test/java/org/folio/service/orders/HoldingDetailServiceTest.java index e24c67f2b..db97cfa38 100644 --- a/src/test/java/org/folio/service/orders/HoldingDetailServiceTest.java +++ b/src/test/java/org/folio/service/orders/HoldingDetailServiceTest.java @@ -18,6 +18,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.InjectMocks; import org.mockito.Mock; @@ -27,13 +30,12 @@ import java.util.Collections; import java.util.List; import java.util.UUID; +import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -67,9 +69,10 @@ void tearDown() throws Exception { } } - @Test - void testPostOrdersHoldingDetailWithNullHoldingIds(VertxTestContext vertxTestContext) { - var future = holdingDetailService.postOrdersHoldingDetail(null, requestContext); + @ParameterizedTest(name = "{0}") + @MethodSource("provideNullOrEmptyHoldingIds") + void testPostOrdersHoldingDetailWithInvalidHoldingIds(String testName, List holdingIds, VertxTestContext vertxTestContext) { + var future = holdingDetailService.postOrdersHoldingDetail(holdingIds, requestContext); vertxTestContext.assertComplete(future) .onComplete(result -> { @@ -78,25 +81,17 @@ void testPostOrdersHoldingDetailWithNullHoldingIds(VertxTestContext vertxTestCon assertNotNull(holdingDetailResults); assertEquals(0, holdingDetailResults.getAdditionalProperties().size()); verify(purchaseOrderLineService, never()).getPoLinesByHoldingIds(any(), any()); - verify(pieceStorageService, never()).getPiecesByLineIdsByChunks(any(), any()); + verify(pieceStorageService, never()).getPiecesByHoldingIds(any(), any()); + verify(inventoryItemManager, never()).getItemsByHoldingIds(any(), any()); vertxTestContext.completeNow(); }); } - @Test - void testPostOrdersHoldingDetailWithEmptyHoldingIds(VertxTestContext vertxTestContext) { - var future = holdingDetailService.postOrdersHoldingDetail(Collections.emptyList(), requestContext); - - vertxTestContext.assertComplete(future) - .onComplete(result -> { - assertTrue(result.succeeded()); - var holdingDetailResults = result.result(); - assertNotNull(holdingDetailResults); - assertEquals(0, holdingDetailResults.getAdditionalProperties().size()); - verify(purchaseOrderLineService, never()).getPoLinesByHoldingIds(any(), any()); - verify(pieceStorageService, never()).getPiecesByLineIdsByChunks(any(), any()); - vertxTestContext.completeNow(); - }); + static Stream provideNullOrEmptyHoldingIds() { + return Stream.of( + Arguments.of("Null holding IDs", null), + Arguments.of("Empty holding IDs list", Collections.emptyList()) + ); } @Test @@ -108,7 +103,9 @@ void testPostOrdersHoldingDetailWithNoPieces(VertxTestContext vertxTestContext) when(purchaseOrderLineService.getPoLinesByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(List.of(poLine))); - when(pieceStorageService.getPiecesByLineIdsByChunks(List.of(poLineId), requestContext)) + when(pieceStorageService.getPiecesByHoldingIds(holdingIds, requestContext)) + .thenReturn(Future.succeededFuture(Collections.emptyList())); + when(inventoryItemManager.getItemsByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(Collections.emptyList())); var future = holdingDetailService.postOrdersHoldingDetail(holdingIds, requestContext); @@ -123,9 +120,10 @@ void testPostOrdersHoldingDetailWithNoPieces(VertxTestContext vertxTestContext) var property = holdingDetailResults.getAdditionalProperties().get(holdingId); assertEquals(1, property.getPoLinesDetailCollection().getPoLinesDetail().size()); assertEquals(0, property.getPiecesDetailCollection().getPiecesDetail().size()); + assertEquals(0, property.getItemsDetailCollection().getItemsDetail().size()); verify(purchaseOrderLineService).getPoLinesByHoldingIds(holdingIds, requestContext); - verify(pieceStorageService).getPiecesByLineIdsByChunks(List.of(poLineId), requestContext); - verify(inventoryItemManager, never()).getItemsByHoldingId(anyString(), any()); + verify(pieceStorageService).getPiecesByHoldingIds(holdingIds, requestContext); + verify(inventoryItemManager).getItemsByHoldingIds(holdingIds, requestContext); vertxTestContext.completeNow(); }); } @@ -153,9 +151,9 @@ void testPostOrdersHoldingDetailSuccess(VertxTestContext vertxTestContext) { when(purchaseOrderLineService.getPoLinesByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(List.of(poLine))); - when(pieceStorageService.getPiecesByLineIdsByChunks(List.of(poLineId), requestContext)) + when(pieceStorageService.getPiecesByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(List.of(piece))); - when(inventoryItemManager.getItemsByHoldingId(eq(holdingId), any(RequestContext.class))) + when(inventoryItemManager.getItemsByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(List.of(item))); var future = holdingDetailService.postOrdersHoldingDetail(holdingIds, requestContext); @@ -179,8 +177,8 @@ void testPostOrdersHoldingDetailSuccess(VertxTestContext vertxTestContext) { assertEquals(1, property.getItemsDetailCollection().getItemsDetail().size()); verify(purchaseOrderLineService).getPoLinesByHoldingIds(holdingIds, requestContext); - verify(pieceStorageService).getPiecesByLineIdsByChunks(List.of(poLineId), requestContext); - verify(inventoryItemManager).getItemsByHoldingId(eq(holdingId), any(RequestContext.class)); + verify(pieceStorageService).getPiecesByHoldingIds(holdingIds, requestContext); + verify(inventoryItemManager).getItemsByHoldingIds(holdingIds, requestContext); vertxTestContext.completeNow(); }); } @@ -212,17 +210,15 @@ void testPostOrdersHoldingDetailMultipleHoldingsAndTenants(VertxTestContext vert .withItemId(UUID.randomUUID().toString()) .withReceivingTenantId(tenant2); - var item1 = new JsonObject().put("id", UUID.randomUUID().toString()); - var item2 = new JsonObject().put("id", UUID.randomUUID().toString()); + var item1 = new JsonObject().put("id", UUID.randomUUID().toString()).put("holdingsRecordId", holdingId1); + var item2 = new JsonObject().put("id", UUID.randomUUID().toString()).put("holdingsRecordId", holdingId2); when(purchaseOrderLineService.getPoLinesByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(List.of(poLine1, poLine2))); - when(pieceStorageService.getPiecesByLineIdsByChunks(List.of(poLineId1, poLineId2), requestContext)) + when(pieceStorageService.getPiecesByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(List.of(piece1, piece2))); - when(inventoryItemManager.getItemsByHoldingId(eq(holdingId1), any(RequestContext.class))) - .thenReturn(Future.succeededFuture(List.of(item1))); - when(inventoryItemManager.getItemsByHoldingId(eq(holdingId2), any(RequestContext.class))) - .thenReturn(Future.succeededFuture(List.of(item2))); + when(inventoryItemManager.getItemsByHoldingIds(holdingIds, requestContext)) + .thenReturn(Future.succeededFuture(List.of(item1, item2))); var future = holdingDetailService.postOrdersHoldingDetail(holdingIds, requestContext); @@ -236,9 +232,8 @@ void testPostOrdersHoldingDetailMultipleHoldingsAndTenants(VertxTestContext vert assertTrue(holdingDetailResults.getAdditionalProperties().containsKey(holdingId2)); verify(purchaseOrderLineService).getPoLinesByHoldingIds(holdingIds, requestContext); - verify(pieceStorageService).getPiecesByLineIdsByChunks(List.of(poLineId1, poLineId2), requestContext); - verify(inventoryItemManager).getItemsByHoldingId(eq(holdingId1), any(RequestContext.class)); - verify(inventoryItemManager).getItemsByHoldingId(eq(holdingId2), any(RequestContext.class)); + verify(pieceStorageService).getPiecesByHoldingIds(holdingIds, requestContext); + verify(inventoryItemManager).getItemsByHoldingIds(holdingIds, requestContext); vertxTestContext.completeNow(); }); } @@ -257,13 +252,13 @@ void testPostOrdersHoldingDetailWithNullTenantId(VertxTestContext vertxTestConte .withItemId(UUID.randomUUID().toString()) .withReceivingTenantId(null); - var item = new JsonObject().put("id", UUID.randomUUID().toString()); + var item = new JsonObject().put("id", UUID.randomUUID().toString()).put("holdingsRecordId", holdingId); when(purchaseOrderLineService.getPoLinesByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(List.of(poLine))); - when(pieceStorageService.getPiecesByLineIdsByChunks(List.of(poLineId), requestContext)) + when(pieceStorageService.getPiecesByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(List.of(piece))); - when(inventoryItemManager.getItemsByHoldingId(eq(holdingId), any(RequestContext.class))) + when(inventoryItemManager.getItemsByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(List.of(item))); var future = holdingDetailService.postOrdersHoldingDetail(holdingIds, requestContext); @@ -295,9 +290,9 @@ void testPostOrdersHoldingDetailWithEmptyItems(VertxTestContext vertxTestContext when(purchaseOrderLineService.getPoLinesByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(List.of(poLine))); - when(pieceStorageService.getPiecesByLineIdsByChunks(List.of(poLineId), requestContext)) + when(pieceStorageService.getPiecesByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(List.of(piece))); - when(inventoryItemManager.getItemsByHoldingId(eq(holdingId), any(RequestContext.class))) + when(inventoryItemManager.getItemsByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(Collections.emptyList())); var future = holdingDetailService.postOrdersHoldingDetail(holdingIds, requestContext); @@ -317,309 +312,56 @@ void testPostOrdersHoldingDetailWithEmptyItems(VertxTestContext vertxTestContext }); } - // @ParameterizedTest(name = "{0}") - // @MethodSource("provideGroupingTestCases") - // void testGroupPiecesByTenantIdAndHoldingId(String testName, List pieces, int expectedTenantGroups, - // int expectedHoldingGroups) { - // var result = holdingDetailService.groupPiecesByTenantIdAndHoldingId(pieces); - // - // assertNotNull(result); - // assertEquals(expectedTenantGroups, result.size()); - // - // var totalHoldingGroups = result.values().stream() - // .mapToInt(Map::size) - // .sum(); - // assertEquals(expectedHoldingGroups, totalHoldingGroups); - // } - // - // static Stream provideGroupingTestCases() { - // var holdingId1 = UUID.randomUUID().toString(); - // var holdingId2 = UUID.randomUUID().toString(); - // var tenant1 = "tenant-1"; - // var tenant2 = "tenant-2"; - // - // return Stream.of( - // Arguments.of( - // "Empty list", - // Collections.emptyList(), - // 0, 0 - // ), - // Arguments.of( - // "Single piece with tenant and holding", - // List.of(createPiece(holdingId1, tenant1)), - // 1, 1 - // ), - // Arguments.of( - // "Multiple pieces same tenant and holding", - // List.of( - // createPiece(holdingId1, tenant1), - // createPiece(holdingId1, tenant1) - // ), - // 1, 1 - // ), - // Arguments.of( - // "Multiple pieces same tenant different holdings", - // List.of( - // createPiece(holdingId1, tenant1), - // createPiece(holdingId2, tenant1) - // ), - // 1, 2 - // ), - // Arguments.of( - // "Multiple pieces different tenants same holding", - // List.of( - // createPiece(holdingId1, tenant1), - // createPiece(holdingId1, tenant2) - // ), - // 2, 2 - // ), - // Arguments.of( - // "Multiple pieces different tenants different holdings", - // List.of( - // createPiece(holdingId1, tenant1), - // createPiece(holdingId2, tenant1), - // createPiece(holdingId1, tenant2), - // createPiece(holdingId2, tenant2) - // ), - // 2, 4 - // ), - // Arguments.of( - // "Pieces with null tenant - converted to empty string", - // List.of( - // createPiece(holdingId1, null), - // createPiece(holdingId1, null) - // ), - // 1, 1 - // ), - // Arguments.of( - // "Mixed null and non-null tenants", - // List.of( - // createPiece(holdingId1, tenant1), - // createPiece(holdingId1, null) - // ), - // 2, 2 - // ) - // ); - // } - - // @Test - // void testGroupPiecesByTenantIdAndHoldingIdWithNullList() { - // var result = holdingDetailService.groupPiecesByTenantIdAndHoldingId(null); - // - // assertNotNull(result); - // assertTrue(result.isEmpty()); - // } - // - // @Test - // void testGroupPiecesByTenantIdAndHoldingIdFilterNullPieces() { - // var pieces = new ArrayList(); - // pieces.add(null); - // pieces.add(createPiece(UUID.randomUUID().toString(), "tenant-1")); - // pieces.add(null); - // - // var result = holdingDetailService.groupPiecesByTenantIdAndHoldingId(pieces); - // - // assertNotNull(result); - // assertEquals(1, result.size()); - // } - // - // @Test - // void testGroupPiecesByTenantIdAndHoldingIdFilterNullHoldingId() { - // var pieces = List.of( - // createPiece(null, "tenant-1"), - // createPiece(UUID.randomUUID().toString(), "tenant-1") - // ); - // - // var result = holdingDetailService.groupPiecesByTenantIdAndHoldingId(pieces); - // - // assertNotNull(result); - // assertEquals(1, result.size()); - // assertEquals(1, result.get("tenant-1").size()); - // } - // - // @Test - // void testGroupPiecesByTenantIdAndHoldingIdNullTenantIdConversion() { - // var holdingId = UUID.randomUUID().toString(); - // var pieces = List.of(createPiece(holdingId, null)); - // - // var result = holdingDetailService.groupPiecesByTenantIdAndHoldingId(pieces); - // - // assertNotNull(result); - // assertEquals(1, result.size()); - // assertTrue(result.containsKey("")); - // assertEquals(1, result.get("").size()); - // assertTrue(result.get("").containsKey(holdingId)); - // } - - // @Test - // void testGetHolderFuture(VertxTestContext vertxTestContext) { - // var tenantId = "tenant-1"; - // var holdingId = UUID.randomUUID().toString(); - // var itemId = UUID.randomUUID().toString(); - // var poLineId = UUID.randomUUID().toString(); - // - // var poLinesDetails = List.of( - // new org.folio.rest.jaxrs.model.PoLinesDetail() - // .withId(poLineId) - // ); - // - // var piecesDetail = List.of( - // new org.folio.rest.jaxrs.model.PiecesDetail() - // .withId(UUID.randomUUID().toString()) - // .withItemId(itemId) - // .withTenantId(tenantId) - // ); - // - // var item = new JsonObject() - // .put("id", itemId) - // .put("holdingsRecordId", holdingId); - // - // when(inventoryItemManager.getItemsByHoldingId(eq(holdingId), any(RequestContext.class))) - // .thenReturn(Future.succeededFuture(List.of(item))); - // - // var future = holdingDetailService.getHolderFuture(tenantId, holdingId, poLinesDetails, piecesDetail, requestContext); - // - // vertxTestContext.assertComplete(future) - // .onComplete(result -> { - // assertTrue(result.succeeded()); - // var holder = result.result(); - // assertNotNull(holder); - // assertEquals(holdingId, holder.holdingId()); - // assertEquals(1, holder.poLines().size()); - // assertEquals(poLineId, holder.poLines().getFirst().getId()); - // assertEquals(1, holder.pieces().size()); - // assertEquals(1, holder.items().size()); - // assertEquals(itemId, holder.items().getFirst().getId()); - // assertEquals(tenantId, holder.items().getFirst().getTenantId()); - // vertxTestContext.completeNow(); - // }); - // } - // - // @Test - // void testGetHolderFutureWithEmptyTenantId(VertxTestContext vertxTestContext) { - // var tenantId = ""; - // var holdingId = UUID.randomUUID().toString(); - // var poLinesDetails = Collections.emptyList(); - // var piecesDetail = Collections.emptyList(); - // - // when(inventoryItemManager.getItemsByHoldingId(eq(holdingId), any(RequestContext.class))) - // .thenReturn(Future.succeededFuture(Collections.emptyList())); - // - // var future = holdingDetailService.getHolderFuture(tenantId, holdingId, poLinesDetails, piecesDetail, requestContext); - // - // vertxTestContext.assertComplete(future) - // .onComplete(result -> { - // assertTrue(result.succeeded()); - // verify(inventoryItemManager).getItemsByHoldingId(eq(holdingId), any(RequestContext.class)); - // vertxTestContext.completeNow(); - // }); - // } - // - // @Test - // void testGetHolderFutureWithNullItemsList(VertxTestContext vertxTestContext) { - // var tenantId = "tenant-1"; - // var holdingId = UUID.randomUUID().toString(); - // var poLinesDetails = List.of( - // new org.folio.rest.jaxrs.model.PoLinesDetail() - // .withId(UUID.randomUUID().toString()) - // ); - // var piecesDetail = List.of( - // new org.folio.rest.jaxrs.model.PiecesDetail() - // .withId(UUID.randomUUID().toString()) - // .withTenantId(tenantId) - // ); - // - // when(inventoryItemManager.getItemsByHoldingId(eq(holdingId), any(RequestContext.class))) - // .thenReturn(Future.succeededFuture(null)); - // - // var future = holdingDetailService.getHolderFuture(tenantId, holdingId, poLinesDetails, piecesDetail, requestContext); - // - // vertxTestContext.assertComplete(future) - // .onComplete(result -> { - // assertTrue(result.succeeded()); - // var holder = result.result(); - // assertNotNull(holder); - // assertEquals(holdingId, holder.holdingId()); - // assertEquals(1, holder.poLines().size()); - // assertEquals(1, holder.pieces().size()); - // assertEquals(0, holder.items().size()); // null items should return empty list - // vertxTestContext.completeNow(); - // }); - // } - // - // @Test - // void testGetHolderFutureWithNullPieces(VertxTestContext vertxTestContext) { - // var tenantId = "tenant-1"; - // var holdingId = UUID.randomUUID().toString(); - // - // when(inventoryItemManager.getItemsByHoldingId(eq(holdingId), any(RequestContext.class))) - // .thenReturn(Future.succeededFuture(Collections.emptyList())); - // - // var future = holdingDetailService.getHolderFuture(tenantId, holdingId, null, null, requestContext); - // - // vertxTestContext.assertComplete(future) - // .onComplete(result -> { - // assertTrue(result.succeeded()); - // var holder = result.result(); - // assertNotNull(holder); - // assertEquals(holdingId, holder.holdingId()); - // assertEquals(0, holder.poLines().size()); // null poLinesDetails should be empty list - // assertEquals(0, holder.pieces().size()); // null piecesDetail should be empty list - // assertEquals(0, holder.items().size()); - // vertxTestContext.completeNow(); - // }); - // } - // - // @Test - // void testGetHolderFutureFilterNullItems(VertxTestContext vertxTestContext) { - // var tenantId = "tenant-1"; - // var holdingId = UUID.randomUUID().toString(); - // var poLinesDetails = Collections.emptyList(); - // var piecesDetail = Collections.emptyList(); - // - // var items = new ArrayList(); - // items.add(null); - // items.add(new JsonObject().put("id", UUID.randomUUID().toString())); - // items.add(null); - // - // when(inventoryItemManager.getItemsByHoldingId(eq(holdingId), any(RequestContext.class))) - // .thenReturn(Future.succeededFuture(items)); - // - // var future = holdingDetailService.getHolderFuture(tenantId, holdingId, poLinesDetails, piecesDetail, requestContext); - // - // vertxTestContext.assertComplete(future) - // .onComplete(result -> { - // assertTrue(result.succeeded()); - // var holder = result.result(); - // assertEquals(1, holder.items().size()); - // vertxTestContext.completeNow(); - // }); - // } - - @Test - void testPostOrdersHoldingDetailPieceStorageFailure(VertxTestContext vertxTestContext) { + @ParameterizedTest(name = "{0}") + @MethodSource("provideServiceFailureScenarios") + void testServiceFailureGracefulDegradation(String testName, boolean poLinesFail, boolean piecesFail, boolean itemsFail, + int expectedPoLines, int expectedPieces, int expectedItems, + VertxTestContext vertxTestContext) { var holdingId = UUID.randomUUID().toString(); var holdingIds = List.of(holdingId); var poLineId = UUID.randomUUID().toString(); - var errorMessage = "Storage service error"; - var poLine = createPoLine(poLineId, holdingId); + var piece = new Piece() + .withId(UUID.randomUUID().toString()) + .withPoLineId(poLineId) + .withHoldingId(holdingId) + .withReceivingTenantId("tenant-1"); when(purchaseOrderLineService.getPoLinesByHoldingIds(holdingIds, requestContext)) - .thenReturn(Future.succeededFuture(List.of(poLine))); - when(pieceStorageService.getPiecesByLineIdsByChunks(List.of(poLineId), requestContext)) - .thenReturn(Future.failedFuture(new RuntimeException(errorMessage))); + .thenReturn(poLinesFail ? Future.failedFuture(new RuntimeException("PoLine service error")) + : Future.succeededFuture(List.of(poLine))); + when(pieceStorageService.getPiecesByHoldingIds(holdingIds, requestContext)) + .thenReturn(piecesFail ? Future.failedFuture(new RuntimeException("Piece service error")) + : Future.succeededFuture(List.of(piece))); + when(inventoryItemManager.getItemsByHoldingIds(holdingIds, requestContext)) + .thenReturn(itemsFail ? Future.failedFuture(new RuntimeException("Inventory service error")) + : Future.succeededFuture(Collections.emptyList())); var future = holdingDetailService.postOrdersHoldingDetail(holdingIds, requestContext); - vertxTestContext.assertFailure(future) + vertxTestContext.assertComplete(future) .onComplete(result -> { - assertTrue(result.failed()); - assertTrue(result.cause().getMessage().contains(errorMessage)); + assertTrue(result.succeeded(), "Should succeed with graceful degradation"); + var holdingDetailResults = result.result(); + assertNotNull(holdingDetailResults); + var property = holdingDetailResults.getAdditionalProperties().get(holdingId); + assertNotNull(property); + assertEquals(expectedPoLines, property.getPoLinesDetailCollection().getPoLinesDetail().size()); + assertEquals(expectedPieces, property.getPiecesDetailCollection().getPiecesDetail().size()); + assertEquals(expectedItems, property.getItemsDetailCollection().getItemsDetail().size()); vertxTestContext.completeNow(); }); } + static Stream provideServiceFailureScenarios() { + return Stream.of( + Arguments.of("All services fail", true, true, true, 0, 0, 0), + Arguments.of("Only pieces service fails", false, true, false, 1, 0, 0), + Arguments.of("Only inventory service fails", false, false, true, 1, 1, 0), + Arguments.of("PoLines and pieces fail", true, true, false, 0, 0, 0) + ); + } + @Test void testCreateItemDetailWithNullItem(VertxTestContext vertxTestContext) { var holdingId = UUID.randomUUID().toString(); @@ -637,13 +379,13 @@ void testCreateItemDetailWithNullItem(VertxTestContext vertxTestContext) { // Create a list with a null item var items = new ArrayList(); items.add(null); - items.add(new JsonObject().put("id", UUID.randomUUID().toString())); + items.add(new JsonObject().put("id", UUID.randomUUID().toString()).put("holdingsRecordId", holdingId)); when(purchaseOrderLineService.getPoLinesByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(List.of(poLine))); - when(pieceStorageService.getPiecesByLineIdsByChunks(List.of(poLineId), requestContext)) + when(pieceStorageService.getPiecesByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(List.of(piece))); - when(inventoryItemManager.getItemsByHoldingId(eq(holdingId), any(RequestContext.class))) + when(inventoryItemManager.getItemsByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(items)); var future = holdingDetailService.postOrdersHoldingDetail(holdingIds, requestContext); @@ -675,8 +417,10 @@ void testPostOrdersHoldingDetailWithNullGroupedPieces(VertxTestContext vertxTest when(purchaseOrderLineService.getPoLinesByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(List.of(poLine))); - when(pieceStorageService.getPiecesByLineIdsByChunks(List.of(poLineId), requestContext)) + when(pieceStorageService.getPiecesByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(List.of(piece))); + when(inventoryItemManager.getItemsByHoldingIds(holdingIds, requestContext)) + .thenReturn(Future.succeededFuture(Collections.emptyList())); var future = holdingDetailService.postOrdersHoldingDetail(holdingIds, requestContext); @@ -685,9 +429,12 @@ void testPostOrdersHoldingDetailWithNullGroupedPieces(VertxTestContext vertxTest assertTrue(result.succeeded()); var holdingDetailResults = result.result(); // Since piece has null holdingId, it gets filtered during grouping - // No holders are created, so we get empty results (because pieces exist, just all filtered) - assertEquals(0, holdingDetailResults.getAdditionalProperties().size()); - verify(inventoryItemManager, never()).getItemsByHoldingId(anyString(), any()); + // With new parallel execution, we still create results for all requested holdingIds + assertEquals(1, holdingDetailResults.getAdditionalProperties().size()); + var property = holdingDetailResults.getAdditionalProperties().get(holdingId); + assertEquals(1, property.getPoLinesDetailCollection().getPoLinesDetail().size()); + assertEquals(0, property.getPiecesDetailCollection().getPiecesDetail().size()); + assertEquals(0, property.getItemsDetailCollection().getItemsDetail().size()); vertxTestContext.completeNow(); }); } @@ -708,92 +455,98 @@ void testPostOrdersHoldingDetailInventoryFailure(VertxTestContext vertxTestConte when(purchaseOrderLineService.getPoLinesByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(List.of(poLine))); - when(pieceStorageService.getPiecesByLineIdsByChunks(List.of(poLineId), requestContext)) + when(pieceStorageService.getPiecesByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(List.of(piece))); - when(inventoryItemManager.getItemsByHoldingId(eq(holdingId), any(RequestContext.class))) + when(inventoryItemManager.getItemsByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.failedFuture(new RuntimeException(errorMessage))); var future = holdingDetailService.postOrdersHoldingDetail(holdingIds, requestContext); - vertxTestContext.assertFailure(future) - .onComplete(result -> { - assertTrue(result.failed()); - assertNotNull(result.cause()); - vertxTestContext.completeNow(); - }); - } - - @Test - void testCreatePieceDetailWithMissingItemId(VertxTestContext vertxTestContext) { - var holdingId = UUID.randomUUID().toString(); - var holdingIds = List.of(holdingId); - var poLineId = UUID.randomUUID().toString(); - - var poLine = createPoLine(poLineId, holdingId); - var piece1 = new Piece() - .withId(UUID.randomUUID().toString()) - .withPoLineId(poLineId) - .withHoldingId(holdingId) - .withItemId(null) // null itemId - .withReceivingTenantId("tenant-1"); - - var piece2 = new Piece() - .withId(UUID.randomUUID().toString()) - .withPoLineId(poLineId) - .withHoldingId(holdingId) - .withItemId(UUID.randomUUID().toString()) - .withReceivingTenantId("tenant-1"); - - when(purchaseOrderLineService.getPoLinesByHoldingIds(holdingIds, requestContext)) - .thenReturn(Future.succeededFuture(List.of(poLine))); - when(pieceStorageService.getPiecesByLineIdsByChunks(List.of(poLineId), requestContext)) - .thenReturn(Future.succeededFuture(List.of(piece1, piece2))); - when(inventoryItemManager.getItemsByHoldingId(eq(holdingId), any(RequestContext.class))) - .thenReturn(Future.succeededFuture(Collections.emptyList())); - - var future = holdingDetailService.postOrdersHoldingDetail(holdingIds, requestContext); - + // With parallel execution and error recovery, inventory failure should be handled gracefully vertxTestContext.assertComplete(future) .onComplete(result -> { - assertTrue(result.succeeded()); + assertTrue(result.succeeded(), "Should succeed with graceful degradation"); var holdingDetailResults = result.result(); - assertTrue(holdingDetailResults.getAdditionalProperties().containsKey(holdingId)); + assertNotNull(holdingDetailResults); + // Should have poLines and pieces, but no items var property = holdingDetailResults.getAdditionalProperties().get(holdingId); - assertEquals(2, property.getPiecesDetailCollection().getPiecesDetail().size()); - // Verify both pieces are included even with null itemId - var piecesDetail = property.getPiecesDetailCollection().getPiecesDetail(); - assertTrue(piecesDetail.stream().anyMatch(pd -> pd.getItemId() == null)); - assertTrue(piecesDetail.stream().anyMatch(pd -> pd.getItemId() != null)); + assertNotNull(property); + assertEquals(1, property.getPoLinesDetailCollection().getPoLinesDetail().size()); + assertEquals(1, property.getPiecesDetailCollection().getPiecesDetail().size()); + assertEquals(0, property.getItemsDetailCollection().getItemsDetail().size()); vertxTestContext.completeNow(); }); } - @Test - void testCreatePieceDetailWithMissingPieceId(VertxTestContext vertxTestContext) { + @ParameterizedTest(name = "{0}") + @MethodSource("providePieceNullScenarios") + void testPieceHandlingWithNulls(String testName, String scenarioType, int expectedPieceCount, + boolean expectNullId, boolean expectNonNullId, VertxTestContext vertxTestContext) { var holdingId = UUID.randomUUID().toString(); var holdingIds = List.of(holdingId); var poLineId = UUID.randomUUID().toString(); - var poLine = createPoLine(poLineId, holdingId); - var piece1 = new Piece() - .withId(null) // null piece id - .withPoLineId(poLineId) - .withHoldingId(holdingId) - .withItemId(UUID.randomUUID().toString()) - .withReceivingTenantId("tenant-1"); - var piece2 = new Piece() - .withId(UUID.randomUUID().toString()) - .withPoLineId(poLineId) - .withHoldingId(holdingId) - .withItemId(UUID.randomUUID().toString()) - .withReceivingTenantId("tenant-1"); + // Create pieces based on scenario type + var pieces = switch (scenarioType) { + case "NULL_ITEM_ID" -> { + var pieceWithNullItemId = new Piece() + .withId(UUID.randomUUID().toString()) + .withPoLineId(poLineId) + .withHoldingId(holdingId) + .withItemId(null) + .withReceivingTenantId("tenant-1"); + var pieceWithItemId = new Piece() + .withId(UUID.randomUUID().toString()) + .withPoLineId(poLineId) + .withHoldingId(holdingId) + .withItemId(UUID.randomUUID().toString()) + .withReceivingTenantId("tenant-1"); + yield List.of(pieceWithNullItemId, pieceWithItemId); + } + case "NULL_PIECE_ID" -> { + var pieceWithNullId = new Piece() + .withId(null) + .withPoLineId(poLineId) + .withHoldingId(holdingId) + .withItemId(UUID.randomUUID().toString()) + .withReceivingTenantId("tenant-1"); + var pieceWithId = new Piece() + .withId(UUID.randomUUID().toString()) + .withPoLineId(poLineId) + .withHoldingId(holdingId) + .withItemId(UUID.randomUUID().toString()) + .withReceivingTenantId("tenant-1"); + yield List.of(pieceWithNullId, pieceWithId); + } + case "MIXED_NULL" -> { + var validPiece = new Piece() + .withId(UUID.randomUUID().toString()) + .withPoLineId(poLineId) + .withHoldingId(holdingId) + .withItemId(UUID.randomUUID().toString()) + .withReceivingTenantId("tenant-1"); + var mixedPieces = new ArrayList(); + mixedPieces.add(null); + mixedPieces.add(validPiece); + mixedPieces.add(null); + yield mixedPieces; + } + case "ALL_NULL" -> { + var allNullPieces = new ArrayList(); + allNullPieces.add(null); + allNullPieces.add(null); + allNullPieces.add(null); + yield allNullPieces; + } + default -> throw new IllegalArgumentException("Unknown scenario: " + scenarioType); + }; when(purchaseOrderLineService.getPoLinesByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(List.of(poLine))); - when(pieceStorageService.getPiecesByLineIdsByChunks(List.of(poLineId), requestContext)) - .thenReturn(Future.succeededFuture(List.of(piece1, piece2))); - when(inventoryItemManager.getItemsByHoldingId(eq(holdingId), any(RequestContext.class))) + when(pieceStorageService.getPiecesByHoldingIds(holdingIds, requestContext)) + .thenReturn(Future.succeededFuture(pieces)); + when(inventoryItemManager.getItemsByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(Collections.emptyList())); var future = holdingDetailService.postOrdersHoldingDetail(holdingIds, requestContext); @@ -804,39 +557,39 @@ void testCreatePieceDetailWithMissingPieceId(VertxTestContext vertxTestContext) var holdingDetailResults = result.result(); assertTrue(holdingDetailResults.getAdditionalProperties().containsKey(holdingId)); var property = holdingDetailResults.getAdditionalProperties().get(holdingId); - assertEquals(2, property.getPiecesDetailCollection().getPiecesDetail().size()); - // Verify both pieces are included even with null id - var piecesDetail = property.getPiecesDetailCollection().getPiecesDetail(); - assertTrue(piecesDetail.stream().anyMatch(pd -> pd.getId() == null)); - assertTrue(piecesDetail.stream().anyMatch(pd -> pd.getId() != null)); + assertEquals(expectedPieceCount, property.getPiecesDetailCollection().getPiecesDetail().size()); + + if (expectedPieceCount > 0) { + var piecesDetail = property.getPiecesDetailCollection().getPiecesDetail(); + if (expectNullId) { + assertTrue(piecesDetail.stream().anyMatch(pd -> pd.getId() == null || pd.getItemId() == null)); + } + if (expectNonNullId) { + assertTrue(piecesDetail.stream().anyMatch(pd -> pd.getId() != null || pd.getItemId() != null)); + } + } vertxTestContext.completeNow(); }); } - @Test - void testCreatePieceDetailWithMixedNullPieces(VertxTestContext vertxTestContext) { - var holdingId = UUID.randomUUID().toString(); - var holdingIds = List.of(holdingId); - var poLineId = UUID.randomUUID().toString(); - - var poLine = createPoLine(poLineId, holdingId); - var validPiece = new Piece() - .withId(UUID.randomUUID().toString()) - .withPoLineId(poLineId) - .withHoldingId(holdingId) - .withItemId(UUID.randomUUID().toString()) - .withReceivingTenantId("tenant-1"); - - var pieces = new ArrayList(); - pieces.add(null); // null piece - pieces.add(validPiece); - pieces.add(null); // another null piece + static Stream providePieceNullScenarios() { + return Stream.of( + Arguments.of("Pieces with null itemId", "NULL_ITEM_ID", 2, true, true), + Arguments.of("Pieces with null piece ID", "NULL_PIECE_ID", 2, true, true), + Arguments.of("Mixed null and valid pieces", "MIXED_NULL", 1, false, true), + Arguments.of("All null pieces", "ALL_NULL", 0, false, false) + ); + } + @ParameterizedTest(name = "{0}") + @MethodSource("providePoLineGroupingScenarios") + void testGroupedPoLinesByHoldingId(String testName, List holdingIds, List poLines, + int expectedHoldingsWithPoLines, VertxTestContext vertxTestContext) { when(purchaseOrderLineService.getPoLinesByHoldingIds(holdingIds, requestContext)) - .thenReturn(Future.succeededFuture(List.of(poLine))); - when(pieceStorageService.getPiecesByLineIdsByChunks(List.of(poLineId), requestContext)) - .thenReturn(Future.succeededFuture(pieces)); - when(inventoryItemManager.getItemsByHoldingId(eq(holdingId), any(RequestContext.class))) + .thenReturn(Future.succeededFuture(poLines)); + when(pieceStorageService.getPiecesByHoldingIds(holdingIds, requestContext)) + .thenReturn(Future.succeededFuture(Collections.emptyList())); + when(inventoryItemManager.getItemsByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(Collections.emptyList())); var future = holdingDetailService.postOrdersHoldingDetail(holdingIds, requestContext); @@ -845,43 +598,105 @@ void testCreatePieceDetailWithMixedNullPieces(VertxTestContext vertxTestContext) .onComplete(result -> { assertTrue(result.succeeded()); var holdingDetailResults = result.result(); - assertTrue(holdingDetailResults.getAdditionalProperties().containsKey(holdingId)); - var property = holdingDetailResults.getAdditionalProperties().get(holdingId); - // Only the valid piece should be included - assertEquals(1, property.getPiecesDetailCollection().getPiecesDetail().size()); - assertEquals(validPiece.getId(), property.getPiecesDetailCollection().getPiecesDetail().getFirst().getId()); + assertNotNull(holdingDetailResults); + + // Count how many holdingIds actually have poLines + var holdingsWithPoLines = holdingDetailResults.getAdditionalProperties().values().stream() + .filter(prop -> !prop.getPoLinesDetailCollection().getPoLinesDetail().isEmpty()) + .count(); + assertEquals(expectedHoldingsWithPoLines, holdingsWithPoLines); vertxTestContext.completeNow(); }); } - @Test - void testCreatePieceDetailWithAllNullPieces(VertxTestContext vertxTestContext) { - var holdingId = UUID.randomUUID().toString(); - var holdingIds = List.of(holdingId); - - var poLine = createPoLine(UUID.randomUUID().toString(), holdingId); - // Create a list with only null pieces - var pieces = new ArrayList(); - pieces.add(null); - pieces.add(null); - pieces.add(null); - - when(purchaseOrderLineService.getPoLinesByHoldingIds(holdingIds, requestContext)) - .thenReturn(Future.succeededFuture(List.of(poLine))); - when(pieceStorageService.getPiecesByLineIdsByChunks(List.of(poLine.getId()), requestContext)) - .thenReturn(Future.succeededFuture(pieces)); - - var future = holdingDetailService.postOrdersHoldingDetail(holdingIds, requestContext); + static Stream providePoLineGroupingScenarios() { + var holdingId1 = UUID.randomUUID().toString(); + var holdingId2 = UUID.randomUUID().toString(); + var holdingId3 = UUID.randomUUID().toString(); + var poLineId1 = UUID.randomUUID().toString(); + var poLineId2 = UUID.randomUUID().toString(); - vertxTestContext.assertComplete(future) - .onComplete(result -> { - assertTrue(result.succeeded()); - var holdingDetailResults = result.result(); - // All pieces are null, after filtering we get empty grouping, so no holders created - assertEquals(0, holdingDetailResults.getAdditionalProperties().size()); - verify(inventoryItemManager, never()).getItemsByHoldingId(anyString(), any()); - vertxTestContext.completeNow(); - }); + // Create list with null poLine + var poLinesWithNull = new ArrayList(); + poLinesWithNull.add(null); + + // Create mixed list with null and valid poLines + var mixedPoLines = new ArrayList(); + mixedPoLines.add(null); + mixedPoLines.add(createPoLine(poLineId1, holdingId1)); + mixedPoLines.add(new PoLine().withId(poLineId2).withLocations(List.of(new Location().withHoldingId(null)))); + + return Stream.of( + Arguments.of("Empty poLines list", List.of(holdingId1), Collections.emptyList(), 0), + + Arguments.of("Null poLine in list", List.of(holdingId1), poLinesWithNull, 0), + + Arguments.of("PoLine with null locations", + List.of(holdingId1), + List.of(new PoLine().withId(poLineId1).withLocations(null)), + 0), + + Arguments.of("PoLine with empty locations", + List.of(holdingId1), + List.of(new PoLine().withId(poLineId1).withLocations(Collections.emptyList())), + 0), + + Arguments.of("Location with null holdingId", + List.of(holdingId1), + List.of(new PoLine().withId(poLineId1).withLocations(List.of(new Location().withHoldingId(null)))), + 0), + + Arguments.of("Location with holdingId not in request", + List.of(holdingId1), + List.of(new PoLine().withId(poLineId1).withLocations(List.of(new Location().withHoldingId(holdingId2)))), + 0), + + Arguments.of("Valid single poLine", + List.of(holdingId1), + List.of(createPoLine(poLineId1, holdingId1)), + 1), + + Arguments.of("Multiple poLines same holding", + List.of(holdingId1), + List.of(createPoLine(poLineId1, holdingId1), createPoLine(poLineId2, holdingId1)), + 1), + + Arguments.of("Multiple poLines different holdings", + List.of(holdingId1, holdingId2), + List.of(createPoLine(poLineId1, holdingId1), createPoLine(poLineId2, holdingId2)), + 2), + + Arguments.of("PoLine with multiple locations same holding", + List.of(holdingId1), + List.of(new PoLine().withId(poLineId1).withLocations(List.of( + new Location().withHoldingId(holdingId1), + new Location().withHoldingId(holdingId1) + ))), + 1), + + Arguments.of("PoLine with multiple locations different holdings", + List.of(holdingId1, holdingId2), + List.of(new PoLine().withId(poLineId1).withLocations(List.of( + new Location().withHoldingId(holdingId1), + new Location().withHoldingId(holdingId2) + ))), + 2), + + Arguments.of("Mixed: null poLine, valid poLine, poLine with null location", + List.of(holdingId1, holdingId2), + mixedPoLines, + 1), + + Arguments.of("PoLine with locations containing nulls and valid holdingId", + List.of(holdingId1, holdingId2), + List.of(new PoLine().withId(poLineId1).withLocations(List.of( + new Location().withHoldingId(null), + new Location().withHoldingId(holdingId1), + new Location().withHoldingId(holdingId3), // not in requested holdingIds + new Location().withHoldingId(holdingId2) + ))), + 2) + ); } @Test @@ -900,9 +715,9 @@ void testCreatePoLineDetailWithSinglePiece(VertxTestContext vertxTestContext) { when(purchaseOrderLineService.getPoLinesByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(List.of(poLine))); - when(pieceStorageService.getPiecesByLineIdsByChunks(List.of(poLineId), requestContext)) + when(pieceStorageService.getPiecesByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(List.of(piece))); - when(inventoryItemManager.getItemsByHoldingId(eq(holdingId), any(RequestContext.class))) + when(inventoryItemManager.getItemsByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(Collections.emptyList())); var future = holdingDetailService.postOrdersHoldingDetail(holdingIds, requestContext); @@ -946,9 +761,9 @@ void testCreatePoLineDetailWithDuplicatePoLineIdSameTenant(VertxTestContext vert when(purchaseOrderLineService.getPoLinesByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(List.of(poLine))); - when(pieceStorageService.getPiecesByLineIdsByChunks(List.of(poLineId), requestContext)) + when(pieceStorageService.getPiecesByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(pieces)); - when(inventoryItemManager.getItemsByHoldingId(eq(holdingId), any(RequestContext.class))) + when(inventoryItemManager.getItemsByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(Collections.emptyList())); var future = holdingDetailService.postOrdersHoldingDetail(holdingIds, requestContext); @@ -990,9 +805,9 @@ void testCreatePoLineDetailWithSamePoLineIdDifferentTenants(VertxTestContext ver // Need to setup expectations for both tenants when(purchaseOrderLineService.getPoLinesByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(List.of(poLine))); - when(pieceStorageService.getPiecesByLineIdsByChunks(List.of(poLineId), requestContext)) + when(pieceStorageService.getPiecesByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(pieces)); - when(inventoryItemManager.getItemsByHoldingId(eq(holdingId), any(RequestContext.class))) + when(inventoryItemManager.getItemsByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(Collections.emptyList())); var future = holdingDetailService.postOrdersHoldingDetail(holdingIds, requestContext); @@ -1048,9 +863,9 @@ void testCreatePoLineDetailWithMultipleDifferentPoLines(VertxTestContext vertxTe when(purchaseOrderLineService.getPoLinesByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(List.of(poLine1, poLine2, poLine3))); - when(pieceStorageService.getPiecesByLineIdsByChunks(List.of(poLineId1, poLineId2, poLineId3), requestContext)) + when(pieceStorageService.getPiecesByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(pieces)); - when(inventoryItemManager.getItemsByHoldingId(eq(holdingId), any(RequestContext.class))) + when(inventoryItemManager.getItemsByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(Collections.emptyList())); var future = holdingDetailService.postOrdersHoldingDetail(holdingIds, requestContext); @@ -1091,9 +906,9 @@ void testCreatePoLineDetailWithNullPoLineId(VertxTestContext vertxTestContext) { when(purchaseOrderLineService.getPoLinesByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(List.of(poLine))); - when(pieceStorageService.getPiecesByLineIdsByChunks(List.of(poLineId), requestContext)) + when(pieceStorageService.getPiecesByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(pieces)); - when(inventoryItemManager.getItemsByHoldingId(eq(holdingId), any(RequestContext.class))) + when(inventoryItemManager.getItemsByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(Collections.emptyList())); var future = holdingDetailService.postOrdersHoldingDetail(holdingIds, requestContext); @@ -1105,11 +920,10 @@ void testCreatePoLineDetailWithNullPoLineId(VertxTestContext vertxTestContext) { assertTrue(additionalProps.containsKey(holdingId)); var property = additionalProps.get(holdingId); var poLinesDetail = property.getPoLinesDetailCollection().getPoLinesDetail(); - // PoLines are created from pieces using createPoLineDetailFromPieces - // 1 piece with null poLineId + 1 piece with poLineId = 2 PoLinesDetail (one null, one with id) - assertEquals(2, poLinesDetail.size()); - assertTrue(poLinesDetail.stream().anyMatch(pl -> pl.getId() == null)); - assertTrue(poLinesDetail.stream().anyMatch(pl -> poLineId.equals(pl.getId()))); + // PoLinesDetail are created from poLines fetched, not from pieces + // We have 1 poLine, so 1 PoLinesDetail + assertEquals(1, poLinesDetail.size()); + assertEquals(poLineId, poLinesDetail.getFirst().getId()); // Both pieces should be included assertEquals(2, property.getPiecesDetailCollection().getPiecesDetail().size()); vertxTestContext.completeNow(); @@ -1131,9 +945,9 @@ void testCreatePoLineDetailWithNullTenantId(VertxTestContext vertxTestContext) { when(purchaseOrderLineService.getPoLinesByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(List.of(poLine))); - when(pieceStorageService.getPiecesByLineIdsByChunks(List.of(poLineId), requestContext)) + when(pieceStorageService.getPiecesByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(List.of(piece))); - when(inventoryItemManager.getItemsByHoldingId(eq(holdingId), any(RequestContext.class))) + when(inventoryItemManager.getItemsByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(Collections.emptyList())); var future = holdingDetailService.postOrdersHoldingDetail(holdingIds, requestContext); @@ -1201,9 +1015,9 @@ void testCreatePoLineDetailComplexScenario(VertxTestContext vertxTestContext) { when(purchaseOrderLineService.getPoLinesByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(List.of(poLine1, poLine2))); - when(pieceStorageService.getPiecesByLineIdsByChunks(List.of(poLineId1, poLineId2), requestContext)) + when(pieceStorageService.getPiecesByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(pieces)); - when(inventoryItemManager.getItemsByHoldingId(eq(holdingId), any(RequestContext.class))) + when(inventoryItemManager.getItemsByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(Collections.emptyList())); var future = holdingDetailService.postOrdersHoldingDetail(holdingIds, requestContext); @@ -1254,9 +1068,9 @@ void testCreatePoLineDetailWithMixedNullPoLineIds(VertxTestContext vertxTestCont when(purchaseOrderLineService.getPoLinesByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(List.of(poLine))); - when(pieceStorageService.getPiecesByLineIdsByChunks(List.of(poLineId), requestContext)) + when(pieceStorageService.getPiecesByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(pieces)); - when(inventoryItemManager.getItemsByHoldingId(eq(holdingId), any(RequestContext.class))) + when(inventoryItemManager.getItemsByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(Collections.emptyList())); var future = holdingDetailService.postOrdersHoldingDetail(holdingIds, requestContext); @@ -1268,10 +1082,10 @@ void testCreatePoLineDetailWithMixedNullPoLineIds(VertxTestContext vertxTestCont assertTrue(additionalProps.containsKey(holdingId)); var property = additionalProps.get(holdingId); var poLinesDetail = property.getPoLinesDetailCollection().getPoLinesDetail(); - // PoLines created from pieces: 2 null poLineIds (distinct = 1) + 1 poLineId = 2 total - assertEquals(2, poLinesDetail.size()); - assertEquals(1, poLinesDetail.stream().filter(pl -> pl.getId() == null).count()); - assertEquals(1, poLinesDetail.stream().filter(pl -> poLineId.equals(pl.getId())).count()); + // PoLinesDetail are created from poLines fetched, not from pieces + // We have 1 poLine, so 1 PoLinesDetail + assertEquals(1, poLinesDetail.size()); + assertEquals(poLineId, poLinesDetail.getFirst().getId()); // All 3 pieces should be included assertEquals(3, property.getPiecesDetailCollection().getPiecesDetail().size()); vertxTestContext.completeNow(); @@ -1303,9 +1117,9 @@ void testPoLinesDetailCollectionTotalRecordsAccuracy(VertxTestContext vertxTestC when(purchaseOrderLineService.getPoLinesByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(List.of(poLine1, poLine2))); - when(pieceStorageService.getPiecesByLineIdsByChunks(List.of(poLineId1, poLineId2), requestContext)) + when(pieceStorageService.getPiecesByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(pieces)); - when(inventoryItemManager.getItemsByHoldingId(eq(holdingId), any(RequestContext.class))) + when(inventoryItemManager.getItemsByHoldingIds(holdingIds, requestContext)) .thenReturn(Future.succeededFuture(Collections.emptyList())); var future = holdingDetailService.postOrdersHoldingDetail(holdingIds, requestContext); @@ -1321,14 +1135,6 @@ void testPoLinesDetailCollectionTotalRecordsAccuracy(VertxTestContext vertxTestC }); } - // private static Piece createPiece(String holdingId, String tenantId) { - // return new Piece() - // .withId(UUID.randomUUID().toString()) - // .withHoldingId(holdingId) - // .withItemId(UUID.randomUUID().toString()) - // .withReceivingTenantId(tenantId); - // } - private static PoLine createPoLine(String poLineId, String holdingId) { return new PoLine() .withId(poLineId) From c2685ae733fc417eb81140f5502c59973545e40a Mon Sep 17 00:00:00 2001 From: BKadirkhodjaev Date: Fri, 13 Feb 2026 16:13:37 +0500 Subject: [PATCH 04/10] [MODORDERS-1403]. Improve HoldingDetailAggregator --- src/main/java/org/folio/models/HoldingDetailAggregator.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/folio/models/HoldingDetailAggregator.java b/src/main/java/org/folio/models/HoldingDetailAggregator.java index a5430fb20..c9b431598 100644 --- a/src/main/java/org/folio/models/HoldingDetailAggregator.java +++ b/src/main/java/org/folio/models/HoldingDetailAggregator.java @@ -11,6 +11,7 @@ import java.util.Objects; public class HoldingDetailAggregator { + private Map> poLinesByHoldingId = new HashMap<>(); private Map> piecesByHoldingId = new HashMap<>(); private Map> itemsByHoldingId = new HashMap<>(); @@ -19,11 +20,11 @@ public Map> getPoLinesByHoldingId() { return poLinesByHoldingId; } - public Map> getPiecesByHoldingId() { + public Map> getPiecesByHoldingId() { return piecesByHoldingId; } - public Map> getItemsByHoldingId() { + public Map> getItemsByHoldingId() { return itemsByHoldingId; } From 4ef515c75d566c63152e7e40d195c1500c0efd17 Mon Sep 17 00:00:00 2001 From: BKadirkhodjaev Date: Fri, 13 Feb 2026 17:25:23 +0500 Subject: [PATCH 05/10] [MODORDERS-1403]. Add code to support ECS (multiple-tenants) --- .../org/folio/config/ApplicationConfig.java | 8 +- .../folio/models/HoldingDetailAggregator.java | 18 +- .../service/orders/HoldingDetailService.java | 182 ++++++--- .../orders/HoldingDetailServiceTest.java | 369 ++++++++++++++++++ 4 files changed, 520 insertions(+), 57 deletions(-) diff --git a/src/main/java/org/folio/config/ApplicationConfig.java b/src/main/java/org/folio/config/ApplicationConfig.java index 03574a966..19ef541f1 100644 --- a/src/main/java/org/folio/config/ApplicationConfig.java +++ b/src/main/java/org/folio/config/ApplicationConfig.java @@ -425,10 +425,14 @@ HoldingsSummaryService holdingsSummaryService(PurchaseOrderStorageService purcha } @Bean - HoldingDetailService holdingsDetailService(PurchaseOrderLineService purchaseOrderLineService, + HoldingDetailService holdingsDetailService(ConsortiumConfigurationService consortiumConfigurationService, + ConsortiumUserTenantsRetriever consortiumUserTenantsRetriever, + SettingsRetriever settingsRetriever, + PurchaseOrderLineService purchaseOrderLineService, PieceStorageService pieceStorageService, InventoryItemManager inventoryItemManager) { - return new HoldingDetailService(purchaseOrderLineService, pieceStorageService, inventoryItemManager); + return new HoldingDetailService(consortiumConfigurationService, consortiumUserTenantsRetriever, settingsRetriever, + purchaseOrderLineService, pieceStorageService, inventoryItemManager); } @Bean diff --git a/src/main/java/org/folio/models/HoldingDetailAggregator.java b/src/main/java/org/folio/models/HoldingDetailAggregator.java index c9b431598..32f8430e8 100644 --- a/src/main/java/org/folio/models/HoldingDetailAggregator.java +++ b/src/main/java/org/folio/models/HoldingDetailAggregator.java @@ -12,10 +12,15 @@ public class HoldingDetailAggregator { + private String tenant; private Map> poLinesByHoldingId = new HashMap<>(); private Map> piecesByHoldingId = new HashMap<>(); private Map> itemsByHoldingId = new HashMap<>(); + public String getTenant() { + return tenant; + } + public Map> getPoLinesByHoldingId() { return poLinesByHoldingId; } @@ -29,6 +34,9 @@ public Map> getItemsByHoldingId() { } public String getPieceTenantIdByItemId(String itemId) { + if (Objects.isNull(itemId)) { + return null; + } return piecesByHoldingId.values().stream() .flatMap(List::stream) .filter(Objects::nonNull) @@ -39,15 +47,19 @@ public String getPieceTenantIdByItemId(String itemId) { .orElse(null); } + public void setTenant(String tenant) { + this.tenant = tenant; + } + public void setPoLinesByHoldingId(Map> poLines) { - this.poLinesByHoldingId = poLines; + this.poLinesByHoldingId = Objects.requireNonNullElse(poLines, new HashMap<>()); } public void setPiecesByHoldingId(Map> pieces) { - this.piecesByHoldingId = pieces; + this.piecesByHoldingId = Objects.requireNonNullElse(pieces, new HashMap<>()); } public void setItemsByHoldingId(Map> items) { - this.itemsByHoldingId = items; + this.itemsByHoldingId = Objects.requireNonNullElse(items, new HashMap<>()); } } diff --git a/src/main/java/org/folio/service/orders/HoldingDetailService.java b/src/main/java/org/folio/service/orders/HoldingDetailService.java index 83fd34ad8..ef1fa3a58 100644 --- a/src/main/java/org/folio/service/orders/HoldingDetailService.java +++ b/src/main/java/org/folio/service/orders/HoldingDetailService.java @@ -5,6 +5,7 @@ import lombok.extern.log4j.Log4j2; import org.apache.commons.collections4.CollectionUtils; import org.folio.models.HoldingDetailAggregator; +import org.folio.rest.acq.model.Setting; import org.folio.rest.core.models.RequestContext; import org.folio.rest.jaxrs.model.HoldingDetailResults; import org.folio.rest.jaxrs.model.HoldingDetailResultsProperty; @@ -16,9 +17,15 @@ import org.folio.rest.jaxrs.model.PoLine; import org.folio.rest.jaxrs.model.PoLinesDetail; import org.folio.rest.jaxrs.model.PoLinesDetailCollection; +import org.folio.rest.tools.utils.TenantTool; +import org.folio.service.consortium.ConsortiumConfigurationService; +import org.folio.service.consortium.ConsortiumUserTenantsRetriever; import org.folio.service.inventory.InventoryItemManager; import org.folio.service.pieces.PieceStorageService; +import org.folio.service.settings.SettingsRetriever; +import org.folio.service.settings.util.SettingKey; +import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -26,19 +33,31 @@ import java.util.Objects; import java.util.stream.Collectors; +import static org.folio.orders.utils.HelperUtils.collectResultsOnSuccess; +import static org.folio.orders.utils.RequestContextUtil.createContextWithNewTenantId; import static org.folio.service.inventory.InventoryItemManager.ID; import static org.folio.service.inventory.InventoryItemManager.ITEM_HOLDINGS_RECORD_ID; @Log4j2 public class HoldingDetailService { + private static final String TRUE = "true"; + private static final String FALSE = "false"; + private final ConsortiumConfigurationService consortiumConfigurationService; + private final ConsortiumUserTenantsRetriever consortiumUserTenantsRetriever; + private final SettingsRetriever settingsRetriever; private final PurchaseOrderLineService purchaseOrderLineService; private final PieceStorageService pieceStorageService; private final InventoryItemManager inventoryItemManager; - public HoldingDetailService(PurchaseOrderLineService purchaseOrderLineService, + public HoldingDetailService(ConsortiumConfigurationService consortiumConfigurationService, + ConsortiumUserTenantsRetriever consortiumUserTenantsRetriever, SettingsRetriever settingsRetriever, + PurchaseOrderLineService purchaseOrderLineService, PieceStorageService pieceStorageService, InventoryItemManager inventoryItemManager) { + this.consortiumConfigurationService = consortiumConfigurationService; + this.consortiumUserTenantsRetriever = consortiumUserTenantsRetriever; + this.settingsRetriever = settingsRetriever; this.purchaseOrderLineService = purchaseOrderLineService; this.pieceStorageService = pieceStorageService; this.inventoryItemManager = inventoryItemManager; @@ -49,95 +68,154 @@ public Future postOrdersHoldingDetail(List holding log.info("postOrdersHoldingDetail:: No holding ids were passed"); return Future.succeededFuture(new HoldingDetailResults()); } + return getUserTenantsIfNeeded(requestContext) + .compose(userTenants -> { + var aggregatorFutures = new ArrayList>(); + if (CollectionUtils.isNotEmpty(userTenants)) { + userTenants.forEach(tenantId -> { + var localRequestContext = createContextWithNewTenantId(requestContext, tenantId); + aggregatorFutures.add(aggregateHoldingDetailByTenant(holdingIds, localRequestContext)); + }); + } else { + aggregatorFutures.add(aggregateHoldingDetailByTenant(holdingIds, requestContext)); + } + return collectResultsOnSuccess(aggregatorFutures); + }) + .compose(aggregators -> { + var holdingDetailResults = new HoldingDetailResults(); + holdingIds.forEach(holdingId -> { + // Accumulate data from all aggregators for this holdingId + var allPoLinesDetails = new ArrayList(); + var allPiecesDetails = new ArrayList(); + var allItemsDetails = new ArrayList(); + + aggregators.forEach(aggregator -> { + if (aggregator.getPoLinesByHoldingId().containsKey(holdingId)) { + var poLinesDetails = aggregator.getPoLinesByHoldingId().get(holdingId).stream() + .filter(Objects::nonNull) + .map(poLine -> new PoLinesDetail().withId(poLine.getId())) + .toList(); + allPoLinesDetails.addAll(poLinesDetails); + } + if (aggregator.getPiecesByHoldingId().containsKey(holdingId)) { + var piecesDetails = aggregator.getPiecesByHoldingId().get(holdingId).stream() + .filter(Objects::nonNull) + .map(piece -> new PiecesDetail().withId(piece.getId()) + .withPoLineId(piece.getPoLineId()) + .withItemId(piece.getItemId()) + .withTenantId(piece.getReceivingTenantId())) + .toList(); + allPiecesDetails.addAll(piecesDetails); + } + if (aggregator.getItemsByHoldingId().containsKey(holdingId)) { + var itemsDetails = aggregator.getItemsByHoldingId().get(holdingId).stream() + .filter(Objects::nonNull) + .map(item -> new ItemsDetail().withId(item.getString(ID)) + .withTenantId(aggregator.getPieceTenantIdByItemId(item.getString(ID)))) + .toList(); + allItemsDetails.addAll(itemsDetails); + } + }); + + // Build collections with accumulated data + var poLinesDetailCollection = new PoLinesDetailCollection() + .withPoLinesDetail(allPoLinesDetails) + .withTotalRecords(allPoLinesDetails.size()); + var piecesDetailCollection = new PiecesDetailCollection() + .withPiecesDetail(allPiecesDetails) + .withTotalRecords(allPiecesDetails.size()); + var itemsDetailCollection = new ItemsDetailCollection() + .withItemsDetail(allItemsDetails) + .withTotalRecords(allItemsDetails.size()); + + log.info("postOrdersHoldingDetail:: Prepared accumulated data for {} holding with poLines={}, pieces={}, items={} for tenants={}", holdingId, + allPoLinesDetails.size(), allPiecesDetails.size(), allItemsDetails.size(), + aggregators.stream().map(HoldingDetailAggregator::getTenant).filter(Objects::nonNull).toList()); + + var holdingDetailProperty = new HoldingDetailResultsProperty() + .withPoLinesDetailCollection(poLinesDetailCollection) + .withPiecesDetailCollection(piecesDetailCollection) + .withItemsDetailCollection(itemsDetailCollection); + holdingDetailResults.withAdditionalProperty(holdingId, holdingDetailProperty); + }); + return Future.succeededFuture(holdingDetailResults); + }) + .recover(throwable -> { + log.error("postOrdersHoldingDetail:: Error building holding detail results for holding ids={}", holdingIds, throwable); + return Future.failedFuture(throwable); + }); + } + + private Future aggregateHoldingDetailByTenant(List holdingIds, RequestContext requestContext) { var aggregator = new HoldingDetailAggregator(); + aggregator.setTenant(TenantTool.tenantId(requestContext.getHeaders())); - // Retrieve poLines, pieces, and items independently in parallel + log.info("aggregateHoldingDetailByTenant:: Aggregating poLines, pieces and items for tenant={}", aggregator.getTenant()); + + // Aggregate poLines, pieces, and items independently in parallel var poLinesFuture = purchaseOrderLineService.getPoLinesByHoldingIds(holdingIds, requestContext) .map(poLines -> { - log.info("postOrdersHoldingDetail:: Found {} poLines by holding ids={}", poLines.size(), holdingIds); + log.info("aggregateHoldingDetailByTenant:: Found {} poLines by holding ids={}", poLines.size(), holdingIds); var poLinesByHoldingIds = groupedPoLinesByHoldingId(holdingIds, poLines); aggregator.setPoLinesByHoldingId(poLinesByHoldingIds); return null; }) .recover(throwable -> { - log.warn("postOrdersHoldingDetail:: Failed to retrieve poLines for holding ids={}", holdingIds, throwable); + log.warn("aggregateHoldingDetailByTenant:: Failed to retrieve poLines for holding ids={}", holdingIds, throwable); aggregator.setPoLinesByHoldingId(Collections.emptyMap()); return Future.succeededFuture(null); }); var piecesFuture = pieceStorageService.getPiecesByHoldingIds(holdingIds, requestContext) .map(pieces -> { - log.info("postOrdersHoldingDetail:: Found {} pieces by holding ids={}", pieces.size(), holdingIds); + log.info("aggregateHoldingDetailByTenant:: Found {} pieces by holding ids={}", pieces.size(), holdingIds); var piecesByHoldingId = groupPiecesByHoldingId(pieces); aggregator.setPiecesByHoldingId(piecesByHoldingId); return null; }) .recover(throwable -> { - log.warn("postOrdersHoldingDetail:: Failed to retrieve pieces for holding ids={}", holdingIds, throwable); + log.warn("aggregateHoldingDetailByTenant:: Failed to retrieve pieces for holding ids={}", holdingIds, throwable); aggregator.setPiecesByHoldingId(Collections.emptyMap()); return Future.succeededFuture(null); }); var itemsFuture = inventoryItemManager.getItemsByHoldingIds(holdingIds, requestContext) .map(items -> { - log.info("postOrdersHoldingDetail:: Found {} items by holding ids={}", items.size(), holdingIds); + log.info("aggregateHoldingDetailByTenant:: Found {} items by holding ids={}", items.size(), holdingIds); var itemsByHoldingId = groupItemsByHoldingId(items); aggregator.setItemsByHoldingId(itemsByHoldingId); return null; }) .recover(throwable -> { - log.warn("postOrdersHoldingDetail:: Failed to retrieve items for holding ids={}", holdingIds, throwable); + log.warn("aggregateHoldingDetailByTenant:: Failed to retrieve items for holding ids={}", holdingIds, throwable); aggregator.setItemsByHoldingId(Collections.emptyMap()); return Future.succeededFuture(null); }); return Future.all(poLinesFuture, piecesFuture, itemsFuture) - .compose(v -> { - var holdingDetailResults = new HoldingDetailResults(); - holdingIds.forEach(holdingId -> { - var poLinesDetailCollection = new PoLinesDetailCollection(); - var piecesDetailCollection = new PiecesDetailCollection(); - var itemsDetailCollection = new ItemsDetailCollection(); - - if (aggregator.getPoLinesByHoldingId().containsKey(holdingId)) { - var poLinesDetails = aggregator.getPoLinesByHoldingId().get(holdingId).stream() - .filter(Objects::nonNull) - .map(poLine -> new PoLinesDetail().withId(poLine.getId())) - .toList(); - poLinesDetailCollection.withPoLinesDetail(poLinesDetails) - .withTotalRecords(poLinesDetails.size()); - } - if (aggregator.getPiecesByHoldingId().containsKey(holdingId)) { - var piecesDetails = aggregator.getPiecesByHoldingId().get(holdingId).stream() - .filter(Objects::nonNull) - .map(piece -> new PiecesDetail().withId(piece.getId()) - .withPoLineId(piece.getPoLineId()) - .withItemId(piece.getItemId()) - .withTenantId(piece.getReceivingTenantId())) - .toList(); - piecesDetailCollection.withPiecesDetail(piecesDetails) - .withTotalRecords(piecesDetails.size()); - } - if (aggregator.getItemsByHoldingId().containsKey(holdingId)) { - var itemsDetails = aggregator.getItemsByHoldingId().get(holdingId).stream() - .filter(Objects::nonNull) - .map(item -> new ItemsDetail().withId(item.getString(ID)) - .withTenantId(aggregator.getPieceTenantIdByItemId(item.getString(ID)))) - .toList(); - itemsDetailCollection.withItemsDetail(itemsDetails) - .withTotalRecords(itemsDetails.size()); - } - var holdingDetailProperty = new HoldingDetailResultsProperty() - .withPoLinesDetailCollection(poLinesDetailCollection) - .withPiecesDetailCollection(piecesDetailCollection) - .withItemsDetailCollection(itemsDetailCollection); - holdingDetailResults.withAdditionalProperty(holdingId, holdingDetailProperty); - }); - return Future.succeededFuture(holdingDetailResults); + .map(v -> aggregator); + } + + protected Future> getUserTenantsIfNeeded(RequestContext requestContext) { + return consortiumConfigurationService.getConsortiumConfiguration(requestContext) + .compose(consortiumConfiguration -> { + if (consortiumConfiguration.isEmpty()) { + return Future.succeededFuture(Collections.emptyList()); + } + var configuration = consortiumConfiguration.get(); + return settingsRetriever.getSettingByKey(SettingKey.CENTRAL_ORDERING_ENABLED, requestContext) + .compose(centralOrdering -> { + var isEnabled = centralOrdering.map(Setting::getValue).orElse(FALSE); + if (!TRUE.equalsIgnoreCase(isEnabled)) { + log.info("getUserTenantsIfNeeded:: Central ordering is disabled or not configured"); + return Future.succeededFuture(Collections.emptyList()); + } + return consortiumUserTenantsRetriever.getUserTenants(configuration.consortiumId(), configuration.centralTenantId(), requestContext); + }); }) .recover(throwable -> { - log.error("postOrdersHoldingDetail:: Error building holding detail results for holding ids={}", holdingIds, throwable); - return Future.failedFuture(throwable); + log.warn("getUserTenantsIfNeeded:: Failed to retrieve user tenants, falling back to current tenant only", throwable); + return Future.succeededFuture(Collections.emptyList()); }); } diff --git a/src/test/java/org/folio/service/orders/HoldingDetailServiceTest.java b/src/test/java/org/folio/service/orders/HoldingDetailServiceTest.java index db97cfa38..406f9bcb8 100644 --- a/src/test/java/org/folio/service/orders/HoldingDetailServiceTest.java +++ b/src/test/java/org/folio/service/orders/HoldingDetailServiceTest.java @@ -9,11 +9,19 @@ import org.folio.CopilotGenerated; import org.folio.okapi.common.XOkapiHeaders; import org.folio.rest.core.models.RequestContext; +import org.folio.rest.jaxrs.model.ItemsDetail; import org.folio.rest.jaxrs.model.Location; import org.folio.rest.jaxrs.model.Piece; +import org.folio.rest.jaxrs.model.PiecesDetail; import org.folio.rest.jaxrs.model.PoLine; +import org.folio.rest.tools.utils.TenantTool; +import org.folio.models.consortium.ConsortiumConfiguration; +import org.folio.rest.acq.model.Setting; +import org.folio.service.consortium.ConsortiumConfigurationService; +import org.folio.service.consortium.ConsortiumUserTenantsRetriever; import org.folio.service.inventory.InventoryItemManager; import org.folio.service.pieces.PieceStorageService; +import org.folio.service.settings.SettingsRetriever; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -36,6 +44,8 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -45,6 +55,9 @@ public class HoldingDetailServiceTest { @InjectMocks private HoldingDetailService holdingDetailService; + @Mock private ConsortiumConfigurationService consortiumConfigurationService; + @Mock private ConsortiumUserTenantsRetriever consortiumUserTenantsRetriever; + @Mock private SettingsRetriever settingsRetriever; @Mock private PurchaseOrderLineService purchaseOrderLineService; @Mock private PieceStorageService pieceStorageService; @Mock private InventoryItemManager inventoryItemManager; @@ -60,6 +73,11 @@ void setUp() { okapiHeaders.put(XOkapiHeaders.USER_ID, "test-user"); when(requestContext.getHeaders()).thenReturn(okapiHeaders); when(requestContext.getContext()).thenReturn(vertxContext); + + // Setup default behavior for consortium and settings mocks to return empty results + // This ensures existing tests continue to work without modification (non-consortium mode) + when(consortiumConfigurationService.getConsortiumConfiguration(any())) + .thenReturn(Future.succeededFuture(java.util.Optional.empty())); } @AfterEach @@ -1135,6 +1153,357 @@ void testPoLinesDetailCollectionTotalRecordsAccuracy(VertxTestContext vertxTestC }); } + @Test + void testCentralOrderingEnabledWithMultipleTenants(VertxTestContext vertxTestContext) { + var holdingId1 = UUID.randomUUID().toString(); + var holdingId2 = UUID.randomUUID().toString(); + var holdingIds = List.of(holdingId1, holdingId2); + var consortiumId = UUID.randomUUID().toString(); + var centralTenantId = "central-tenant"; + var memberTenant1 = "member-tenant-1"; + var memberTenant2 = "member-tenant-2"; + var userTenants = List.of(memberTenant1, memberTenant2); + + // Setup consortium configuration + var consortiumConfig = new ConsortiumConfiguration(centralTenantId, consortiumId); + when(consortiumConfigurationService.getConsortiumConfiguration(requestContext)) + .thenReturn(Future.succeededFuture(java.util.Optional.of(consortiumConfig))); + + // Setup central ordering setting + doReturn(Future.succeededFuture(java.util.Optional.of(new Setting().withValue("true")))) + .when(settingsRetriever).getSettingByKey(any(), any()); + + // Setup user tenants retrieval + when(consortiumUserTenantsRetriever.getUserTenants(consortiumId, centralTenantId, requestContext)) + .thenReturn(Future.succeededFuture(userTenants)); + + // Create test data for member-tenant-1 + var poLine1Tenant1 = createPoLine(UUID.randomUUID().toString(), holdingId1); + var piece1Tenant1 = new Piece() + .withId(UUID.randomUUID().toString()) + .withPoLineId(poLine1Tenant1.getId()) + .withHoldingId(holdingId1) + .withReceivingTenantId(memberTenant1); + var item1Tenant1 = new JsonObject() + .put("id", UUID.randomUUID().toString()) + .put("holdingsRecordId", holdingId1); + + // Create test data for member-tenant-2 + var poLine1Tenant2 = createPoLine(UUID.randomUUID().toString(), holdingId2); + var piece1Tenant2 = new Piece() + .withId(UUID.randomUUID().toString()) + .withPoLineId(poLine1Tenant2.getId()) + .withHoldingId(holdingId2) + .withReceivingTenantId(memberTenant2); + var item1Tenant2 = new JsonObject() + .put("id", UUID.randomUUID().toString()) + .put("holdingsRecordId", holdingId2); + + // Mock responses for member-tenant-1 context + when(purchaseOrderLineService.getPoLinesByHoldingIds(eq(holdingIds), any())) + .thenAnswer(invocation -> { + var ctx = (RequestContext) invocation.getArgument(1); + var tenant = TenantTool.tenantId(ctx.getHeaders()); + if (memberTenant1.equals(tenant)) { + return Future.succeededFuture(List.of(poLine1Tenant1)); + } else if (memberTenant2.equals(tenant)) { + return Future.succeededFuture(List.of(poLine1Tenant2)); + } + return Future.succeededFuture(Collections.emptyList()); + }); + + when(pieceStorageService.getPiecesByHoldingIds(eq(holdingIds), any())) + .thenAnswer(invocation -> { + var ctx = (RequestContext) invocation.getArgument(1); + var tenant = TenantTool.tenantId(ctx.getHeaders()); + if (memberTenant1.equals(tenant)) { + return Future.succeededFuture(List.of(piece1Tenant1)); + } else if (memberTenant2.equals(tenant)) { + return Future.succeededFuture(List.of(piece1Tenant2)); + } + return Future.succeededFuture(Collections.emptyList()); + }); + + when(inventoryItemManager.getItemsByHoldingIds(eq(holdingIds), any())) + .thenAnswer(invocation -> { + var ctx = (RequestContext) invocation.getArgument(1); + var tenant = TenantTool.tenantId(ctx.getHeaders()); + if (memberTenant1.equals(tenant)) { + return Future.succeededFuture(List.of(item1Tenant1)); + } else if (memberTenant2.equals(tenant)) { + return Future.succeededFuture(List.of(item1Tenant2)); + } + return Future.succeededFuture(Collections.emptyList()); + }); + + var future = holdingDetailService.postOrdersHoldingDetail(holdingIds, requestContext); + + vertxTestContext.assertComplete(future) + .onComplete(result -> { + assertTrue(result.succeeded()); + var holdingDetailResults = result.result(); + assertNotNull(holdingDetailResults); + + // Both holdings should be present + assertEquals(2, holdingDetailResults.getAdditionalProperties().size()); + assertTrue(holdingDetailResults.getAdditionalProperties().containsKey(holdingId1)); + assertTrue(holdingDetailResults.getAdditionalProperties().containsKey(holdingId2)); + + // Verify holdingId1 has data from tenant1 + var property1 = holdingDetailResults.getAdditionalProperties().get(holdingId1); + assertEquals(1, property1.getPoLinesDetailCollection().getPoLinesDetail().size()); + assertEquals(1, property1.getPiecesDetailCollection().getPiecesDetail().size()); + assertEquals(1, property1.getItemsDetailCollection().getItemsDetail().size()); + assertEquals(memberTenant1, property1.getPiecesDetailCollection().getPiecesDetail().getFirst().getTenantId()); + + // Verify holdingId2 has data from tenant2 + var property2 = holdingDetailResults.getAdditionalProperties().get(holdingId2); + assertEquals(1, property2.getPoLinesDetailCollection().getPoLinesDetail().size()); + assertEquals(1, property2.getPiecesDetailCollection().getPiecesDetail().size()); + assertEquals(1, property2.getItemsDetailCollection().getItemsDetail().size()); + assertEquals(memberTenant2, property2.getPiecesDetailCollection().getPiecesDetail().getFirst().getTenantId()); + + vertxTestContext.completeNow(); + }); + } + + @Test + void testCentralOrderingWithMultipleTenantsAndOverlappingData(VertxTestContext vertxTestContext) { + var holdingId = UUID.randomUUID().toString(); + var holdingIds = List.of(holdingId); + var consortiumId = UUID.randomUUID().toString(); + var centralTenantId = "central-tenant"; + var memberTenant1 = "member-tenant-1"; + var memberTenant2 = "member-tenant-2"; + var userTenants = List.of(memberTenant1, memberTenant2); + + // Setup consortium configuration + var consortiumConfig = new ConsortiumConfiguration(centralTenantId, consortiumId); + when(consortiumConfigurationService.getConsortiumConfiguration(requestContext)) + .thenReturn(Future.succeededFuture(java.util.Optional.of(consortiumConfig))); + + doReturn(Future.succeededFuture(java.util.Optional.of(new Setting().withValue("true")))) + .when(settingsRetriever).getSettingByKey(any(), any()); + + when(consortiumUserTenantsRetriever.getUserTenants(consortiumId, centralTenantId, requestContext)) + .thenReturn(Future.succeededFuture(userTenants)); + + // Both tenants have data for the same holdingId + var poLineId1 = UUID.randomUUID().toString(); + var poLineId2 = UUID.randomUUID().toString(); + var poLine1 = createPoLine(poLineId1, holdingId); + var poLine2 = createPoLine(poLineId2, holdingId); + + var piece1 = new Piece() + .withId(UUID.randomUUID().toString()) + .withPoLineId(poLineId1) + .withHoldingId(holdingId) + .withItemId(UUID.randomUUID().toString()) + .withReceivingTenantId(memberTenant1); + + var piece2 = new Piece() + .withId(UUID.randomUUID().toString()) + .withPoLineId(poLineId2) + .withHoldingId(holdingId) + .withItemId(UUID.randomUUID().toString()) + .withReceivingTenantId(memberTenant2); + + var item1 = new JsonObject() + .put("id", piece1.getItemId()) + .put("holdingsRecordId", holdingId); + + var item2 = new JsonObject() + .put("id", piece2.getItemId()) + .put("holdingsRecordId", holdingId); + + when(purchaseOrderLineService.getPoLinesByHoldingIds(eq(holdingIds), any())) + .thenAnswer(invocation -> { + var ctx = (RequestContext) invocation.getArgument(1); + var tenant = TenantTool.tenantId(ctx.getHeaders()); + if (memberTenant1.equals(tenant)) { + return Future.succeededFuture(List.of(poLine1)); + } else if (memberTenant2.equals(tenant)) { + return Future.succeededFuture(List.of(poLine2)); + } + return Future.succeededFuture(Collections.emptyList()); + }); + + when(pieceStorageService.getPiecesByHoldingIds(eq(holdingIds), any())) + .thenAnswer(invocation -> { + var ctx = (RequestContext) invocation.getArgument(1); + var tenant = TenantTool.tenantId(ctx.getHeaders()); + if (memberTenant1.equals(tenant)) { + return Future.succeededFuture(List.of(piece1)); + } else if (memberTenant2.equals(tenant)) { + return Future.succeededFuture(List.of(piece2)); + } + return Future.succeededFuture(Collections.emptyList()); + }); + + when(inventoryItemManager.getItemsByHoldingIds(eq(holdingIds), any())) + .thenAnswer(invocation -> { + var ctx = (RequestContext) invocation.getArgument(1); + var tenant = TenantTool.tenantId(ctx.getHeaders()); + if (memberTenant1.equals(tenant)) { + return Future.succeededFuture(List.of(item1)); + } else if (memberTenant2.equals(tenant)) { + return Future.succeededFuture(List.of(item2)); + } + return Future.succeededFuture(Collections.emptyList()); + }); + + var future = holdingDetailService.postOrdersHoldingDetail(holdingIds, requestContext); + + vertxTestContext.assertComplete(future) + .onComplete(result -> { + assertTrue(result.succeeded()); + var holdingDetailResults = result.result(); + assertNotNull(holdingDetailResults); + assertEquals(1, holdingDetailResults.getAdditionalProperties().size()); + + var property = holdingDetailResults.getAdditionalProperties().get(holdingId); + assertNotNull(property); + + // Should have aggregated data from both tenants + assertEquals(2, property.getPoLinesDetailCollection().getPoLinesDetail().size()); + assertEquals(2, property.getPiecesDetailCollection().getPiecesDetail().size()); + assertEquals(2, property.getItemsDetailCollection().getItemsDetail().size()); + + // Verify both tenant IDs are present in pieces + var tenantIds = property.getPiecesDetailCollection().getPiecesDetail().stream() + .map(PiecesDetail::getTenantId) + .toList(); + assertTrue(tenantIds.contains(memberTenant1)); + assertTrue(tenantIds.contains(memberTenant2)); + + // Verify item tenant IDs are correctly mapped from pieces + var itemTenantIds = property.getItemsDetailCollection().getItemsDetail().stream() + .map(ItemsDetail::getTenantId) + .toList(); + assertTrue(itemTenantIds.contains(memberTenant1)); + assertTrue(itemTenantIds.contains(memberTenant2)); + + vertxTestContext.completeNow(); + }); + } + + @Test + void testCentralOrderingDisabled(VertxTestContext vertxTestContext) { + var holdingId = UUID.randomUUID().toString(); + var holdingIds = List.of(holdingId); + var consortiumId = UUID.randomUUID().toString(); + var centralTenantId = "central-tenant"; + + // Consortium exists but central ordering is disabled + var consortiumConfig = new ConsortiumConfiguration(centralTenantId, consortiumId); + when(consortiumConfigurationService.getConsortiumConfiguration(requestContext)) + .thenReturn(Future.succeededFuture(java.util.Optional.of(consortiumConfig))); + + // Central ordering is disabled or not set + when(settingsRetriever.getSettingByKey(any(), any())) + .thenReturn(Future.succeededFuture(java.util.Optional.empty())); + + // Should not call getUserTenants when central ordering is disabled + when(consortiumUserTenantsRetriever.getUserTenants(any(), any(), any())) + .thenReturn(Future.succeededFuture(Collections.emptyList())); + + var poLine = createPoLine(UUID.randomUUID().toString(), holdingId); + when(purchaseOrderLineService.getPoLinesByHoldingIds(holdingIds, requestContext)) + .thenReturn(Future.succeededFuture(List.of(poLine))); + when(pieceStorageService.getPiecesByHoldingIds(holdingIds, requestContext)) + .thenReturn(Future.succeededFuture(Collections.emptyList())); + when(inventoryItemManager.getItemsByHoldingIds(holdingIds, requestContext)) + .thenReturn(Future.succeededFuture(Collections.emptyList())); + + var future = holdingDetailService.postOrdersHoldingDetail(holdingIds, requestContext); + + vertxTestContext.assertComplete(future) + .onComplete(result -> { + assertTrue(result.succeeded()); + var holdingDetailResults = result.result(); + assertNotNull(holdingDetailResults); + assertEquals(1, holdingDetailResults.getAdditionalProperties().size()); + + var property = holdingDetailResults.getAdditionalProperties().get(holdingId); + assertEquals(1, property.getPoLinesDetailCollection().getPoLinesDetail().size()); + + vertxTestContext.completeNow(); + }); + } + + @Test + void testCentralOrderingWithPartialTenantFailure(VertxTestContext vertxTestContext) { + var holdingId = UUID.randomUUID().toString(); + var holdingIds = List.of(holdingId); + var consortiumId = UUID.randomUUID().toString(); + var centralTenantId = "central-tenant"; + var memberTenant1 = "member-tenant-1"; + var memberTenant2 = "member-tenant-2"; + var userTenants = List.of(memberTenant1, memberTenant2); + + var consortiumConfig = new ConsortiumConfiguration(centralTenantId, consortiumId); + when(consortiumConfigurationService.getConsortiumConfiguration(requestContext)) + .thenReturn(Future.succeededFuture(java.util.Optional.of(consortiumConfig))); + + doReturn(Future.succeededFuture(java.util.Optional.of(new Setting().withValue("true")))) + .when(settingsRetriever).getSettingByKey(any(), any()); + + when(consortiumUserTenantsRetriever.getUserTenants(consortiumId, centralTenantId, requestContext)) + .thenReturn(Future.succeededFuture(userTenants)); + + var poLine1 = createPoLine(UUID.randomUUID().toString(), holdingId); + var piece1 = new Piece() + .withId(UUID.randomUUID().toString()) + .withPoLineId(poLine1.getId()) + .withHoldingId(holdingId) + .withReceivingTenantId(memberTenant1); + + // Tenant 1 succeeds, Tenant 2 fails for poLines + when(purchaseOrderLineService.getPoLinesByHoldingIds(eq(holdingIds), any())) + .thenAnswer(invocation -> { + var ctx = (RequestContext) invocation.getArgument(1); + var tenant = TenantTool.tenantId(ctx.getHeaders()); + if (memberTenant1.equals(tenant)) { + return Future.succeededFuture(List.of(poLine1)); + } else if (memberTenant2.equals(tenant)) { + return Future.failedFuture(new RuntimeException("Tenant 2 poLines service error")); + } + return Future.succeededFuture(Collections.emptyList()); + }); + + when(pieceStorageService.getPiecesByHoldingIds(eq(holdingIds), any())) + .thenAnswer(invocation -> { + var ctx = (RequestContext) invocation.getArgument(1); + var tenant = TenantTool.tenantId(ctx.getHeaders()); + if (memberTenant1.equals(tenant)) { + return Future.succeededFuture(List.of(piece1)); + } + return Future.succeededFuture(Collections.emptyList()); + }); + + when(inventoryItemManager.getItemsByHoldingIds(eq(holdingIds), any())) + .thenReturn(Future.succeededFuture(Collections.emptyList())); + + var future = holdingDetailService.postOrdersHoldingDetail(holdingIds, requestContext); + + vertxTestContext.assertComplete(future) + .onComplete(result -> { + assertTrue(result.succeeded(), "Should succeed even with partial tenant failure due to graceful degradation"); + var holdingDetailResults = result.result(); + assertNotNull(holdingDetailResults); + + var property = holdingDetailResults.getAdditionalProperties().get(holdingId); + assertNotNull(property); + + // Should have data from tenant1 only, tenant2 failed gracefully + assertEquals(1, property.getPoLinesDetailCollection().getPoLinesDetail().size()); + assertEquals(1, property.getPiecesDetailCollection().getPiecesDetail().size()); + assertEquals(memberTenant1, property.getPiecesDetailCollection().getPiecesDetail().getFirst().getTenantId()); + + vertxTestContext.completeNow(); + }); + } + private static PoLine createPoLine(String poLineId, String holdingId) { return new PoLine() .withId(poLineId) From 8cc3b35a361283b975d90708d8d6d244a0c45ad8 Mon Sep 17 00:00:00 2001 From: BKadirkhodjaev Date: Fri, 13 Feb 2026 17:27:36 +0500 Subject: [PATCH 06/10] [MODORDERS-1403]. Fix indentation --- .../java/org/folio/service/inventory/InventoryItemManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/folio/service/inventory/InventoryItemManager.java b/src/main/java/org/folio/service/inventory/InventoryItemManager.java index 0f833d015..b7aec341f 100644 --- a/src/main/java/org/folio/service/inventory/InventoryItemManager.java +++ b/src/main/java/org/folio/service/inventory/InventoryItemManager.java @@ -269,7 +269,7 @@ private List getItemsByMaterialType(List existingItems, Stri } private List buildPieces(Location location, PoLine poLine, Piece.Format pieceFormat, - List createdItemIds, List existingItemIds) { + List createdItemIds, List existingItemIds) { List itemIds = ListUtils.union(createdItemIds, existingItemIds); logger.info(BUILDING_PIECE_MESSAGE, itemIds.size(), pieceFormat, poLine.getId()); return StreamEx.of(itemIds).map(itemId -> openOrderBuildPiece(poLine, itemId, pieceFormat, location)).toList(); From 1e82e754da90f4f23016ef0e80a05cdbc98ad90a Mon Sep 17 00:00:00 2001 From: BKadirkhodjaev Date: Fri, 13 Feb 2026 18:28:33 +0500 Subject: [PATCH 07/10] [MODORDERS-1403]. Set checkinItems in PoLinesDetail --- ramls/acq-models | 2 +- .../service/orders/HoldingDetailService.java | 3 +- .../orders/HoldingDetailServiceTest.java | 41 +++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/ramls/acq-models b/ramls/acq-models index 82e7e5606..96d86e360 160000 --- a/ramls/acq-models +++ b/ramls/acq-models @@ -1 +1 @@ -Subproject commit 82e7e5606af023935220552ef00391b13f192229 +Subproject commit 96d86e360bf18a6f384ffed46b79ec949a6f1caa diff --git a/src/main/java/org/folio/service/orders/HoldingDetailService.java b/src/main/java/org/folio/service/orders/HoldingDetailService.java index ef1fa3a58..3f48a0955 100644 --- a/src/main/java/org/folio/service/orders/HoldingDetailService.java +++ b/src/main/java/org/folio/service/orders/HoldingDetailService.java @@ -93,7 +93,8 @@ public Future postOrdersHoldingDetail(List holding if (aggregator.getPoLinesByHoldingId().containsKey(holdingId)) { var poLinesDetails = aggregator.getPoLinesByHoldingId().get(holdingId).stream() .filter(Objects::nonNull) - .map(poLine -> new PoLinesDetail().withId(poLine.getId())) + .map(poLine -> new PoLinesDetail().withId(poLine.getId()) + .withCheckinItems(poLine.getCheckinItems())) .toList(); allPoLinesDetails.addAll(poLinesDetails); } diff --git a/src/test/java/org/folio/service/orders/HoldingDetailServiceTest.java b/src/test/java/org/folio/service/orders/HoldingDetailServiceTest.java index 406f9bcb8..538ec58be 100644 --- a/src/test/java/org/folio/service/orders/HoldingDetailServiceTest.java +++ b/src/test/java/org/folio/service/orders/HoldingDetailServiceTest.java @@ -1153,6 +1153,47 @@ void testPoLinesDetailCollectionTotalRecordsAccuracy(VertxTestContext vertxTestC }); } + @ParameterizedTest(name = "checkinItems = {0}") + @MethodSource("provideCheckinItemsValues") + void testPoLinesDetailWithCheckinItems(Boolean checkinItems, VertxTestContext vertxTestContext) { + var holdingId = UUID.randomUUID().toString(); + var holdingIds = List.of(holdingId); + var poLineId = UUID.randomUUID().toString(); + + var poLine = new PoLine() + .withId(poLineId) + .withCheckinItems(checkinItems) + .withLocations(List.of(new Location().withHoldingId(holdingId))); + + when(purchaseOrderLineService.getPoLinesByHoldingIds(holdingIds, requestContext)) + .thenReturn(Future.succeededFuture(List.of(poLine))); + when(pieceStorageService.getPiecesByHoldingIds(holdingIds, requestContext)) + .thenReturn(Future.succeededFuture(Collections.emptyList())); + when(inventoryItemManager.getItemsByHoldingIds(holdingIds, requestContext)) + .thenReturn(Future.succeededFuture(Collections.emptyList())); + + var future = holdingDetailService.postOrdersHoldingDetail(holdingIds, requestContext); + + vertxTestContext.assertComplete(future) + .onComplete(result -> { + assertTrue(result.succeeded()); + var property = result.result().getAdditionalProperties().get(holdingId); + var poLinesDetail = property.getPoLinesDetailCollection().getPoLinesDetail(); + assertEquals(1, poLinesDetail.size()); + assertEquals(poLineId, poLinesDetail.getFirst().getId()); + assertEquals(checkinItems, poLinesDetail.getFirst().getCheckinItems()); + vertxTestContext.completeNow(); + }); + } + + static Stream provideCheckinItemsValues() { + return Stream.of( + Arguments.of(true), + Arguments.of(false), + Arguments.of((Boolean) null) + ); + } + @Test void testCentralOrderingEnabledWithMultipleTenants(VertxTestContext vertxTestContext) { var holdingId1 = UUID.randomUUID().toString(); From f9da115f96cca5c3483b36ef7ba7711a1960d555 Mon Sep 17 00:00:00 2001 From: BKadirkhodjaev Date: Mon, 16 Feb 2026 14:26:05 +0500 Subject: [PATCH 08/10] [MODORDERS-1403]. Update unit tests --- .../service/orders/HoldingDetailService.java | 3 + .../orders/HoldingDetailServiceTest.java | 120 ++++++++++++------ 2 files changed, 84 insertions(+), 39 deletions(-) diff --git a/src/main/java/org/folio/service/orders/HoldingDetailService.java b/src/main/java/org/folio/service/orders/HoldingDetailService.java index 3f48a0955..b9505c7ad 100644 --- a/src/main/java/org/folio/service/orders/HoldingDetailService.java +++ b/src/main/java/org/folio/service/orders/HoldingDetailService.java @@ -83,6 +83,8 @@ public Future postOrdersHoldingDetail(List holding }) .compose(aggregators -> { var holdingDetailResults = new HoldingDetailResults(); + + // Traverse aggregates and find if any contain necessary values by a holding id holdingIds.forEach(holdingId -> { // Accumulate data from all aggregators for this holdingId var allPoLinesDetails = new ArrayList(); @@ -139,6 +141,7 @@ public Future postOrdersHoldingDetail(List holding .withItemsDetailCollection(itemsDetailCollection); holdingDetailResults.withAdditionalProperty(holdingId, holdingDetailProperty); }); + return Future.succeededFuture(holdingDetailResults); }) .recover(throwable -> { diff --git a/src/test/java/org/folio/service/orders/HoldingDetailServiceTest.java b/src/test/java/org/folio/service/orders/HoldingDetailServiceTest.java index 538ec58be..5da3334bb 100644 --- a/src/test/java/org/folio/service/orders/HoldingDetailServiceTest.java +++ b/src/test/java/org/folio/service/orders/HoldingDetailServiceTest.java @@ -5,14 +5,13 @@ import io.vertx.core.json.JsonObject; import io.vertx.junit5.VertxExtension; import io.vertx.junit5.VertxTestContext; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.map.CaseInsensitiveMap; import org.folio.CopilotGenerated; import org.folio.okapi.common.XOkapiHeaders; import org.folio.rest.core.models.RequestContext; -import org.folio.rest.jaxrs.model.ItemsDetail; import org.folio.rest.jaxrs.model.Location; import org.folio.rest.jaxrs.model.Piece; -import org.folio.rest.jaxrs.model.PiecesDetail; import org.folio.rest.jaxrs.model.PoLine; import org.folio.rest.tools.utils.TenantTool; import org.folio.models.consortium.ConsortiumConfiguration; @@ -50,6 +49,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +@Slf4j @ExtendWith(VertxExtension.class) @CopilotGenerated(model = "Claude Sonnet 4.5") public class HoldingDetailServiceTest { @@ -1194,7 +1194,16 @@ static Stream provideCheckinItemsValues() { ); } + // ===== Central Ordering / Consortium Tests ===== + // IMPORTANT: Holdings are tenant-specific resources in FOLIO. + // In a consortium with multiple member tenants, each tenant has its own separate holdings. + // A holding ID in tenant A is a completely different resource than a holding ID in tenant B, + // even if they happen to have the same UUID value. + // Therefore, tests should use DIFFERENT holding IDs for DIFFERENT tenants to reflect reality. + @Test + // Test that when central ordering is enabled and multiple tenants have data, + // each tenant's holdings are retrieved and returned separately in the results void testCentralOrderingEnabledWithMultipleTenants(VertxTestContext vertxTestContext) { var holdingId1 = UUID.randomUUID().toString(); var holdingId2 = UUID.randomUUID().toString(); @@ -1309,9 +1318,13 @@ void testCentralOrderingEnabledWithMultipleTenants(VertxTestContext vertxTestCon } @Test + // Test that when multiple tenants have data for their respective holdings, + // the results are properly aggregated with each holding maintaining its tenant-specific data void testCentralOrderingWithMultipleTenantsAndOverlappingData(VertxTestContext vertxTestContext) { - var holdingId = UUID.randomUUID().toString(); - var holdingIds = List.of(holdingId); + // Holdings are tenant-specific, so each tenant has its own holding IDs + var holdingId1 = UUID.randomUUID().toString(); // holding in tenant1 + var holdingId2 = UUID.randomUUID().toString(); // holding in tenant2 + var holdingIds = List.of(holdingId1, holdingId2); var consortiumId = UUID.randomUUID().toString(); var centralTenantId = "central-tenant"; var memberTenant1 = "member-tenant-1"; @@ -1329,38 +1342,39 @@ void testCentralOrderingWithMultipleTenantsAndOverlappingData(VertxTestContext v when(consortiumUserTenantsRetriever.getUserTenants(consortiumId, centralTenantId, requestContext)) .thenReturn(Future.succeededFuture(userTenants)); - // Both tenants have data for the same holdingId + // Each tenant has data for their own holding var poLineId1 = UUID.randomUUID().toString(); var poLineId2 = UUID.randomUUID().toString(); - var poLine1 = createPoLine(poLineId1, holdingId); - var poLine2 = createPoLine(poLineId2, holdingId); + var poLine1 = createPoLine(poLineId1, holdingId1); + var poLine2 = createPoLine(poLineId2, holdingId2); var piece1 = new Piece() .withId(UUID.randomUUID().toString()) .withPoLineId(poLineId1) - .withHoldingId(holdingId) + .withHoldingId(holdingId1) .withItemId(UUID.randomUUID().toString()) .withReceivingTenantId(memberTenant1); var piece2 = new Piece() .withId(UUID.randomUUID().toString()) .withPoLineId(poLineId2) - .withHoldingId(holdingId) + .withHoldingId(holdingId2) .withItemId(UUID.randomUUID().toString()) .withReceivingTenantId(memberTenant2); var item1 = new JsonObject() .put("id", piece1.getItemId()) - .put("holdingsRecordId", holdingId); + .put("holdingsRecordId", holdingId1); var item2 = new JsonObject() .put("id", piece2.getItemId()) - .put("holdingsRecordId", holdingId); + .put("holdingsRecordId", holdingId2); when(purchaseOrderLineService.getPoLinesByHoldingIds(eq(holdingIds), any())) .thenAnswer(invocation -> { var ctx = (RequestContext) invocation.getArgument(1); var tenant = TenantTool.tenantId(ctx.getHeaders()); + // Each tenant only returns data for their own holdings if (memberTenant1.equals(tenant)) { return Future.succeededFuture(List.of(poLine1)); } else if (memberTenant2.equals(tenant)) { @@ -1400,29 +1414,33 @@ void testCentralOrderingWithMultipleTenantsAndOverlappingData(VertxTestContext v assertTrue(result.succeeded()); var holdingDetailResults = result.result(); assertNotNull(holdingDetailResults); - assertEquals(1, holdingDetailResults.getAdditionalProperties().size()); + System.out.println(JsonObject.mapFrom(holdingDetailResults).encodePrettily()); - var property = holdingDetailResults.getAdditionalProperties().get(holdingId); - assertNotNull(property); + // Should have 2 holdings, one from each tenant + assertEquals(2, holdingDetailResults.getAdditionalProperties().size()); + assertTrue(holdingDetailResults.getAdditionalProperties().containsKey(holdingId1)); + assertTrue(holdingDetailResults.getAdditionalProperties().containsKey(holdingId2)); + System.out.println(JsonObject.mapFrom(holdingDetailResults).encodePrettily()); - // Should have aggregated data from both tenants - assertEquals(2, property.getPoLinesDetailCollection().getPoLinesDetail().size()); - assertEquals(2, property.getPiecesDetailCollection().getPiecesDetail().size()); - assertEquals(2, property.getItemsDetailCollection().getItemsDetail().size()); - - // Verify both tenant IDs are present in pieces - var tenantIds = property.getPiecesDetailCollection().getPiecesDetail().stream() - .map(PiecesDetail::getTenantId) - .toList(); - assertTrue(tenantIds.contains(memberTenant1)); - assertTrue(tenantIds.contains(memberTenant2)); - - // Verify item tenant IDs are correctly mapped from pieces - var itemTenantIds = property.getItemsDetailCollection().getItemsDetail().stream() - .map(ItemsDetail::getTenantId) - .toList(); - assertTrue(itemTenantIds.contains(memberTenant1)); - assertTrue(itemTenantIds.contains(memberTenant2)); + // Verify holding1 has data from tenant1 + var property1 = holdingDetailResults.getAdditionalProperties().get(holdingId1); + assertNotNull(property1); + assertEquals(1, property1.getPoLinesDetailCollection().getPoLinesDetail().size()); + assertEquals(1, property1.getPiecesDetailCollection().getPiecesDetail().size()); + assertEquals(1, property1.getItemsDetailCollection().getItemsDetail().size()); + assertEquals(memberTenant1, property1.getPiecesDetailCollection().getPiecesDetail().getFirst().getTenantId()); + assertEquals(memberTenant1, property1.getItemsDetailCollection().getItemsDetail().getFirst().getTenantId()); + assertEquals(poLineId1, property1.getPoLinesDetailCollection().getPoLinesDetail().getFirst().getId()); + + // Verify holding2 has data from tenant2 + var property2 = holdingDetailResults.getAdditionalProperties().get(holdingId2); + assertNotNull(property2); + assertEquals(1, property2.getPoLinesDetailCollection().getPoLinesDetail().size()); + assertEquals(1, property2.getPiecesDetailCollection().getPiecesDetail().size()); + assertEquals(1, property2.getItemsDetailCollection().getItemsDetail().size()); + assertEquals(memberTenant2, property2.getPiecesDetailCollection().getPiecesDetail().getFirst().getTenantId()); + assertEquals(memberTenant2, property2.getItemsDetailCollection().getItemsDetail().getFirst().getTenantId()); + assertEquals(poLineId2, property2.getPoLinesDetailCollection().getPoLinesDetail().getFirst().getId()); vertxTestContext.completeNow(); }); @@ -1464,6 +1482,7 @@ void testCentralOrderingDisabled(VertxTestContext vertxTestContext) { var holdingDetailResults = result.result(); assertNotNull(holdingDetailResults); assertEquals(1, holdingDetailResults.getAdditionalProperties().size()); + System.out.println(JsonObject.mapFrom(holdingDetailResults).encodePrettily()); var property = holdingDetailResults.getAdditionalProperties().get(holdingId); assertEquals(1, property.getPoLinesDetailCollection().getPoLinesDetail().size()); @@ -1473,9 +1492,13 @@ void testCentralOrderingDisabled(VertxTestContext vertxTestContext) { } @Test + // Test that when one tenant experiences service failures, we still get results from successful tenants + // This verifies graceful degradation in a consortium environment void testCentralOrderingWithPartialTenantFailure(VertxTestContext vertxTestContext) { - var holdingId = UUID.randomUUID().toString(); - var holdingIds = List.of(holdingId); + // Each tenant has its own holdings since holdings are tenant-specific + var holdingId1 = UUID.randomUUID().toString(); // holding in tenant1 + var holdingId2 = UUID.randomUUID().toString(); // holding in tenant2 (but will fail to retrieve) + var holdingIds = List.of(holdingId1, holdingId2); var consortiumId = UUID.randomUUID().toString(); var centralTenantId = "central-tenant"; var memberTenant1 = "member-tenant-1"; @@ -1492,14 +1515,14 @@ void testCentralOrderingWithPartialTenantFailure(VertxTestContext vertxTestConte when(consortiumUserTenantsRetriever.getUserTenants(consortiumId, centralTenantId, requestContext)) .thenReturn(Future.succeededFuture(userTenants)); - var poLine1 = createPoLine(UUID.randomUUID().toString(), holdingId); + var poLine1 = createPoLine(UUID.randomUUID().toString(), holdingId1); var piece1 = new Piece() .withId(UUID.randomUUID().toString()) .withPoLineId(poLine1.getId()) - .withHoldingId(holdingId) + .withHoldingId(holdingId1) .withReceivingTenantId(memberTenant1); - // Tenant 1 succeeds, Tenant 2 fails for poLines + // Tenant 1 succeeds with its holding, Tenant 2 fails for all services (simulating service outage) when(purchaseOrderLineService.getPoLinesByHoldingIds(eq(holdingIds), any())) .thenAnswer(invocation -> { var ctx = (RequestContext) invocation.getArgument(1); @@ -1518,12 +1541,23 @@ void testCentralOrderingWithPartialTenantFailure(VertxTestContext vertxTestConte var tenant = TenantTool.tenantId(ctx.getHeaders()); if (memberTenant1.equals(tenant)) { return Future.succeededFuture(List.of(piece1)); + } else if (memberTenant2.equals(tenant)) { + return Future.failedFuture(new RuntimeException("Tenant 2 pieces service error")); } return Future.succeededFuture(Collections.emptyList()); }); when(inventoryItemManager.getItemsByHoldingIds(eq(holdingIds), any())) - .thenReturn(Future.succeededFuture(Collections.emptyList())); + .thenAnswer(invocation -> { + var ctx = (RequestContext) invocation.getArgument(1); + var tenant = TenantTool.tenantId(ctx.getHeaders()); + if (memberTenant1.equals(tenant)) { + return Future.succeededFuture(Collections.emptyList()); + } else if (memberTenant2.equals(tenant)) { + return Future.failedFuture(new RuntimeException("Tenant 2 items service error")); + } + return Future.succeededFuture(Collections.emptyList()); + }); var future = holdingDetailService.postOrdersHoldingDetail(holdingIds, requestContext); @@ -1532,13 +1566,21 @@ void testCentralOrderingWithPartialTenantFailure(VertxTestContext vertxTestConte assertTrue(result.succeeded(), "Should succeed even with partial tenant failure due to graceful degradation"); var holdingDetailResults = result.result(); assertNotNull(holdingDetailResults); + System.out.println(JsonObject.mapFrom(holdingDetailResults).encodePrettily()); - var property = holdingDetailResults.getAdditionalProperties().get(holdingId); + // Should have holding from tenant1 and tenant2, tenant2 data will consist of empty arrays + assertEquals(2, holdingDetailResults.getAdditionalProperties().size()); + assertTrue(holdingDetailResults.getAdditionalProperties().containsKey(holdingId1)); + assertTrue(holdingDetailResults.getAdditionalProperties().containsKey(holdingId2), + "Holding from failed tenant should not be present"); + + var property = holdingDetailResults.getAdditionalProperties().get(holdingId1); assertNotNull(property); // Should have data from tenant1 only, tenant2 failed gracefully assertEquals(1, property.getPoLinesDetailCollection().getPoLinesDetail().size()); assertEquals(1, property.getPiecesDetailCollection().getPiecesDetail().size()); + assertEquals(0, property.getItemsDetailCollection().getItemsDetail().size()); assertEquals(memberTenant1, property.getPiecesDetailCollection().getPiecesDetail().getFirst().getTenantId()); vertxTestContext.completeNow(); From 5863870f915f804f24f2f054ca4e282a3a0e02ce Mon Sep 17 00:00:00 2001 From: BKadirkhodjaev Date: Mon, 16 Feb 2026 14:31:45 +0500 Subject: [PATCH 09/10] [MODORDERS-1403]. Remove logger --- .../java/org/folio/service/orders/HoldingDetailServiceTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/test/java/org/folio/service/orders/HoldingDetailServiceTest.java b/src/test/java/org/folio/service/orders/HoldingDetailServiceTest.java index 5da3334bb..7f99a7f69 100644 --- a/src/test/java/org/folio/service/orders/HoldingDetailServiceTest.java +++ b/src/test/java/org/folio/service/orders/HoldingDetailServiceTest.java @@ -5,7 +5,6 @@ import io.vertx.core.json.JsonObject; import io.vertx.junit5.VertxExtension; import io.vertx.junit5.VertxTestContext; -import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.map.CaseInsensitiveMap; import org.folio.CopilotGenerated; import org.folio.okapi.common.XOkapiHeaders; @@ -49,7 +48,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -@Slf4j @ExtendWith(VertxExtension.class) @CopilotGenerated(model = "Claude Sonnet 4.5") public class HoldingDetailServiceTest { From c3b01ff5b79713f8ca2053eba0b9d61668174258 Mon Sep 17 00:00:00 2001 From: BKadirkhodjaev Date: Tue, 17 Feb 2026 11:16:31 +0500 Subject: [PATCH 10/10] [MODORDERS-1403]. CI retrigger --- src/main/java/org/folio/service/orders/HoldingDetailService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/folio/service/orders/HoldingDetailService.java b/src/main/java/org/folio/service/orders/HoldingDetailService.java index b9505c7ad..44a3409fb 100644 --- a/src/main/java/org/folio/service/orders/HoldingDetailService.java +++ b/src/main/java/org/folio/service/orders/HoldingDetailService.java @@ -38,6 +38,7 @@ import static org.folio.service.inventory.InventoryItemManager.ID; import static org.folio.service.inventory.InventoryItemManager.ITEM_HOLDINGS_RECORD_ID; +// TODO CI retrigger @Log4j2 public class HoldingDetailService {