From e9a95a5d8e3268746d687fe188e0131109874736 Mon Sep 17 00:00:00 2001 From: imerabishvili <144257054+imerabishvili@users.noreply.github.com> Date: Wed, 5 Jun 2024 15:38:34 +0400 Subject: [PATCH] [MODORDERS-1120] - Support populating searchLocationId for correct tenant during order opening (#957) [MODORDERS-1120] - Support populating searchLocationId for correct tenant during order opening --- ramls/acq-models | 2 +- .../folio/helper/PurchaseOrderLineHelper.java | 11 +- .../org/folio/orders/utils/HelperUtils.java | 128 ++++++++++-------- .../org/folio/orders/utils/StreamUtils.java | 64 +++++++++ .../inventory/InventoryHoldingManager.java | 83 ++++++------ .../orders/PurchaseOrderLineService.java | 36 +++-- src/test/java/org/folio/ApiTestSuite.java | 5 + .../helper/PurchaseOrderLineHelperTest.java | 8 +- .../folio/orders/utils/HelperUtilsTest.java | 47 ++++--- .../folio/orders/utils/StreamUtilsTest.java | 78 +++++++++++ .../inventory/InventoryManagerTest.java | 6 +- 11 files changed, 316 insertions(+), 152 deletions(-) create mode 100644 src/main/java/org/folio/orders/utils/StreamUtils.java create mode 100644 src/test/java/org/folio/orders/utils/StreamUtilsTest.java diff --git a/ramls/acq-models b/ramls/acq-models index 25b76842f..c25ef4c2a 160000 --- a/ramls/acq-models +++ b/ramls/acq-models @@ -1 +1 @@ -Subproject commit 25b76842f353ba1d943e52619b1484b3d6f35d18 +Subproject commit c25ef4c2a626f52b85c185f02c80b12f0e8ab8ba diff --git a/src/main/java/org/folio/helper/PurchaseOrderLineHelper.java b/src/main/java/org/folio/helper/PurchaseOrderLineHelper.java index 6aaa88884..36db30c9d 100644 --- a/src/main/java/org/folio/helper/PurchaseOrderLineHelper.java +++ b/src/main/java/org/folio/helper/PurchaseOrderLineHelper.java @@ -7,7 +7,6 @@ import static org.folio.helper.BaseHelper.EVENT_PAYLOAD; import static org.folio.helper.BaseHelper.ORDER_ID; import static org.folio.orders.utils.HelperUtils.calculateEstimatedPrice; -import static org.folio.orders.utils.HelperUtils.convertToPoLine; import static org.folio.orders.utils.HelperUtils.getPoLineLimit; import static org.folio.orders.utils.PoLineCommonUtil.verifyProtectedFieldsChanged; import static org.folio.orders.utils.ProtectedOperationType.DELETE; @@ -224,7 +223,7 @@ public Future createPoLineWithOrder(CompositePoLine compPoLine, return GenericCompositeFuture.join(new ArrayList<>(subObjFuts)) .compose(v -> generateLineNumber(compOrder, requestContext)) .map(lineNumber -> line.put(PO_LINE_NUMBER, lineNumber)) - .compose(v -> updateSearchLocations(compPoLine, requestContext)) + .compose(v -> purchaseOrderLineService.updateSearchLocations(compPoLine, requestContext)) .map(v -> line.put(SEARCH_LOCATION_IDS, compPoLine.getSearchLocationIds())) .compose(v -> createPoLineSummary(compPoLine, line, requestContext)); } @@ -355,7 +354,7 @@ private boolean isUnreleasedEncumbrances(CompositePoLine compOrderLine, PoLine p public Future updateOrderLine(CompositePoLine compOrderLine, JsonObject lineFromStorage, RequestContext requestContext) { Promise promise = Promise.promise(); - updateSearchLocations(compOrderLine, requestContext) + purchaseOrderLineService.updateSearchLocations(compOrderLine, requestContext) .compose(v -> purchaseOrderLineService.updatePoLineSubObjects(compOrderLine, lineFromStorage, requestContext)) .compose(poLine -> purchaseOrderLineService.updateOrderLineSummary(compOrderLine.getId(), poLine, requestContext)) .onSuccess(json -> promise.complete()) @@ -549,12 +548,6 @@ private List> processPoLinesUpdate(CompositePurchaseOrder compOrder, L return futures; } - private Future updateSearchLocations(CompositePoLine compositePoLine, RequestContext requestContext) { - return purchaseOrderLineService.retrieveSearchLocationIds(convertToPoLine(compositePoLine), requestContext) - .map(compositePoLine::withSearchLocationIds) - .mapEmpty(); - } - private List> processPoLinesCreation(CompositePurchaseOrder compOrder, List poLinesFromStorage, RequestContext requestContext) { return getNewPoLines(compOrder, poLinesFromStorage) diff --git a/src/main/java/org/folio/orders/utils/HelperUtils.java b/src/main/java/org/folio/orders/utils/HelperUtils.java index ed783e779..fe294461d 100644 --- a/src/main/java/org/folio/orders/utils/HelperUtils.java +++ b/src/main/java/org/folio/orders/utils/HelperUtils.java @@ -1,41 +1,14 @@ package org.folio.orders.utils; -import static io.vertx.core.Future.succeededFuture; -import static java.util.stream.Collectors.toList; -import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; -import static org.apache.commons.lang3.StringUtils.EMPTY; -import static org.apache.commons.lang3.StringUtils.isEmpty; -import static org.folio.orders.utils.ResourcePathResolver.ALERTS; -import static org.folio.orders.utils.ResourcePathResolver.REPORTING_CODES; -import static org.folio.rest.RestConstants.EN; -import static org.folio.rest.core.exceptions.ErrorCodes.MULTIPLE_NONPACKAGE_TITLES; -import static org.folio.rest.core.exceptions.ErrorCodes.TITLE_NOT_FOUND; -import static org.folio.rest.jaxrs.model.PoLine.PaymentStatus.FULLY_PAID; -import static org.folio.rest.jaxrs.model.PoLine.PaymentStatus.PAYMENT_NOT_REQUIRED; -import static org.folio.rest.jaxrs.model.PoLine.ReceiptStatus.FULLY_RECEIVED; -import static org.folio.rest.jaxrs.model.PoLine.ReceiptStatus.RECEIPT_NOT_REQUIRED; -import static org.folio.service.exchange.ExchangeRateProviderResolver.RATE_KEY; - -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.EnumMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.CompletionException; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import javax.money.CurrencyUnit; -import javax.money.Monetary; -import javax.money.MonetaryAmount; -import javax.money.convert.ConversionQuery; -import javax.money.convert.ConversionQueryBuilder; - +import io.vertx.core.AsyncResult; +import io.vertx.core.CompositeFuture; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.core.eventbus.DeliveryOptions; +import io.vertx.core.eventbus.Message; +import io.vertx.core.json.JsonObject; +import one.util.streamex.IntStreamEx; +import one.util.streamex.StreamEx; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.map.CaseInsensitiveMap; import org.apache.commons.lang3.ArrayUtils; @@ -65,15 +38,40 @@ import org.javamoney.moneta.function.MonetaryFunctions; import org.javamoney.moneta.function.MonetaryOperators; -import io.vertx.core.AsyncResult; -import io.vertx.core.CompositeFuture; -import io.vertx.core.Future; -import io.vertx.core.Handler; -import io.vertx.core.eventbus.DeliveryOptions; -import io.vertx.core.eventbus.Message; -import io.vertx.core.json.JsonObject; -import one.util.streamex.IntStreamEx; -import one.util.streamex.StreamEx; +import javax.money.CurrencyUnit; +import javax.money.Monetary; +import javax.money.MonetaryAmount; +import javax.money.convert.ConversionQuery; +import javax.money.convert.ConversionQueryBuilder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CompletionException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static io.vertx.core.Future.succeededFuture; +import static java.util.stream.Collectors.toList; +import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; +import static org.apache.commons.lang3.StringUtils.EMPTY; +import static org.apache.commons.lang3.StringUtils.isEmpty; +import static org.folio.orders.utils.ResourcePathResolver.ALERTS; +import static org.folio.orders.utils.ResourcePathResolver.REPORTING_CODES; +import static org.folio.rest.RestConstants.EN; +import static org.folio.rest.core.exceptions.ErrorCodes.MULTIPLE_NONPACKAGE_TITLES; +import static org.folio.rest.core.exceptions.ErrorCodes.TITLE_NOT_FOUND; +import static org.folio.rest.jaxrs.model.PoLine.PaymentStatus.FULLY_PAID; +import static org.folio.rest.jaxrs.model.PoLine.PaymentStatus.PAYMENT_NOT_REQUIRED; +import static org.folio.rest.jaxrs.model.PoLine.ReceiptStatus.FULLY_RECEIVED; +import static org.folio.rest.jaxrs.model.PoLine.ReceiptStatus.RECEIPT_NOT_REQUIRED; +import static org.folio.service.exchange.ExchangeRateProviderResolver.RATE_KEY; public class HelperUtils { @@ -189,7 +187,7 @@ public static int calculateInventoryItemsQuantity(CompositePoLine compPOL, List< /** * Calculates pieces quantity for list of locations and return map where piece format is a key and corresponding quantity of pieces as value. * - * @param compPOL composite PO Line + * @param compPOL composite PO Line * @param locations list of locations to calculate quantity for * @return quantity of pieces per piece format either required Inventory item for PO Line */ @@ -231,7 +229,7 @@ public static Map calculatePiecesWithItemIdQuantity(Compo /** * Calculates pieces quantity for specified locations based on piece format. * - * @param format piece format + * @param format piece format * @param locations list of locations to calculate quantity for * @return quantity of items expected in the inventory for PO Line */ @@ -244,6 +242,7 @@ public static int calculatePiecesQuantity(Piece.Format format, List lo /** * Calculates total estimated price. See MODORDERS-180 for more details. + * * @param cost PO Line's cost */ public static MonetaryAmount calculateEstimatedPrice(Cost cost) { @@ -318,19 +317,34 @@ public static int getElectronicLocationsQuantity(List locations) { .sum(); } + /** + * Almost same as collectResultsOnSuccess with addition that each future itself returns a list and + * this implementation combines all elements of all lists into the single list. + * @param futures The list of futures to be combined + * @return Single list containing all elements returned from all futures + * @param element type + * @param list type + */ + public static > Future> combineResultListsOnSuccess(Collection> futures) { + return collectResultsOnSuccess(futures) + .map(lists -> lists.stream().flatMap(List::stream).toList()); + } + /** * Wait for all requests completion and collect all resulting objects. In case any failed, complete resulting future with the exception + * * @param futures list of futures and each produces resulting object on completion - * @param resulting type + * @param resulting type * @return resulting objects */ - public static Future> collectResultsOnSuccess(List> futures) { + public static Future> collectResultsOnSuccess(Collection> futures) { return GenericCompositeFuture.join(new ArrayList<>(futures)) .map(CompositeFuture::list); } /** * Transform list of id's to CQL query using 'or' operation + * * @param ids list of id's * @return String representing CQL query to get records by id's */ @@ -344,8 +358,9 @@ public static String convertIdsToCqlQuery(Collection ids) { /** * Transform list of values for some property to CQL query using 'or' operation - * @param values list of field values - * @param fieldName the property name to search by + * + * @param values list of field values + * @param fieldName the property name to search by * @param strictMatch indicates whether strict match mode (i.e. ==) should be used or not (i.e. =) * @return String representing CQL query to get records by some property values */ @@ -365,6 +380,7 @@ public static int getPoLineLimit(JsonObject config) { /** * Convert {@link JsonObject} which actually represents org.folio.rest.acq.model.PurchaseOrder to {@link CompositePurchaseOrder} * These objects are the same except PurchaseOrder doesn't contain poLines field. + * * @param poJson {@link JsonObject} representing org.folio.rest.acq.model.PurchaseOrder * @return {@link CompositePurchaseOrder} */ @@ -450,15 +466,16 @@ public static boolean isProductIdsExist(CompositePoLine compPOL) { } public static Void handleErrorResponse(Handler> asyncResultHandler, BaseHelper helper, - Throwable t) { + Throwable t) { asyncResultHandler.handle(succeededFuture(helper.buildErrorResponse(t))); return null; } /** * Check the number of titles per po line. + * * @param lineIdTitles Map po line id -> list of titles - * @param poLineById Map po line id -> composite po line + * @param poLineById Map po line id -> composite po line */ public static void verifyTitles(Map> lineIdTitles, Map poLineById) { verifyAllTitlesExist(lineIdTitles, poLineById); @@ -466,7 +483,7 @@ public static void verifyTitles(Map> lineIdTitles, Map> titles, - Map poLineById) { + Map poLineById) { if (titles.keySet().stream().anyMatch(lineId -> titles.get(lineId).size() > 1 && !poLineById.get(lineId).getIsPackage())) { throw new HttpException(400, MULTIPLE_NONPACKAGE_TITLES); } @@ -508,6 +525,7 @@ public static ConversionQuery buildConversionQuery(PoLine poLine, String systemC /** * Accepts response with collection of the elements and tries to extract the first one. * In case the response is incorrect or empty, the {@link CompletionException} will be thrown + * * @param response {@link JsonObject} representing service response which should contain array of objects * @param propertyName name of the property which holds array of objects * @return the first element of the array @@ -517,7 +535,7 @@ public static JsonObject getFirstObjectFromResponse(JsonObject response, String .flatMap(items -> items.stream().findFirst()) .map(JsonObject.class::cast) .orElseThrow(() -> new CompletionException(new NoInventoryRecordException( - String.format("No records of '%s' can be found", propertyName)))); + String.format("No records of '%s' can be found", propertyName)))); } public static String extractId(JsonObject json) { diff --git a/src/main/java/org/folio/orders/utils/StreamUtils.java b/src/main/java/org/folio/orders/utils/StreamUtils.java new file mode 100644 index 000000000..0dbb36d23 --- /dev/null +++ b/src/main/java/org/folio/orders/utils/StreamUtils.java @@ -0,0 +1,64 @@ +package org.folio.orders.utils; + +import lombok.experimental.UtilityClass; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * Utility class allowing to perform stream operations in a more convenient way + */ +@UtilityClass +public class StreamUtils { + + public static T find(Collection collection, Predicate predicate) { + for (T t : collection) { + if (predicate.test(t)) { + return t; + } + } + return null; + } + + public static List filter(Collection collection, Predicate predicate) { + return collection.stream().filter(predicate).toList(); + } + + public static List map(Collection collection, Function mapper) { + return collection.stream().map(mapper).toList(); + } + + public static Set mapToSet(Collection collection, Function mapper) { + return collection.stream().map(mapper).collect(Collectors.toSet()); + } + + public static Map listToMap(Collection collection, Function toKey) { + return listToMap(collection, toKey, Function.identity()); + } + + public static Map listToMap(Collection collection, Function toKey, Function toValue) { + return collection.stream().collect(Collectors.toMap(toKey, toValue, (a, b) -> a)); + } + + public static Map> groupBy(Collection collection, Function toKey) { + return groupBy(collection, toKey, Function.identity()); + } + + public static Map> groupBy(Collection collection, Function toKey, Function toValue) { + Map> map = new HashMap<>(); + for (T t : collection) { + K key = toKey.apply(t); + List sublist = map.computeIfAbsent(key, k -> new ArrayList<>()); + sublist.add(toValue.apply(t)); + } + return map; + } + +} diff --git a/src/main/java/org/folio/service/inventory/InventoryHoldingManager.java b/src/main/java/org/folio/service/inventory/InventoryHoldingManager.java index 515a6a4d6..42f1f647a 100644 --- a/src/main/java/org/folio/service/inventory/InventoryHoldingManager.java +++ b/src/main/java/org/folio/service/inventory/InventoryHoldingManager.java @@ -10,6 +10,7 @@ import org.folio.okapi.common.GenericCompositeFuture; import org.folio.orders.utils.HelperUtils; import org.folio.orders.utils.RequestContextUtil; +import org.folio.orders.utils.StreamUtils; import org.folio.rest.core.RestClient; import org.folio.rest.core.exceptions.HttpException; import org.folio.rest.core.models.RequestContext; @@ -22,7 +23,6 @@ import org.folio.rest.tools.utils.TenantTool; import org.folio.service.caches.ConfigurationEntriesCache; import org.folio.service.caches.InventoryCache; -import org.folio.service.orders.utils.HelperUtils.BiFunctionReturningFuture; import java.util.ArrayList; import java.util.Collection; @@ -30,7 +30,6 @@ import java.util.Map; import java.util.Objects; import java.util.concurrent.CompletionException; -import java.util.function.UnaryOperator; import java.util.stream.Collectors; import static java.util.stream.Collectors.toList; @@ -138,42 +137,31 @@ private static void handleHoldingsError(String holdingId, Throwable throwable) { } public Future> getHoldingsByIds(List holdingIds, RequestContext requestContext) { - return getHoldingsByIds(holdingIds, requestContext, this::fetchHoldingsByHoldingIds); - } - - private Future> fetchHoldingsByHoldingIds(List holdingIds, RequestContext requestContext) { - return fetchHoldingsByHoldingIds(holdingIds, requestContext, holdings -> { - if (holdings.size() == holdingIds.size()) { - return holdings; - } - List parameters = holdingIds.stream() - .filter(id -> holdings.stream() - .noneMatch(holding -> holding.getString(ID).equals(id))) - .map(id -> new Parameter().withValue(id).withKey("holdings")) - .collect(Collectors.toList()); - throw new HttpException(404, PARTIALLY_RETURNED_COLLECTION.toError().withParameters(parameters)); - }); - } - - public Future> getHoldingsByIdsWithoutVerification(List holdingIds, RequestContext requestContext) { - return getHoldingsByIds(holdingIds, requestContext, this::fetchHoldingsByHoldingIdsWithoutVerification); - } - - private Future> fetchHoldingsByHoldingIdsWithoutVerification(List holdingIds, RequestContext requestContext) { - return fetchHoldingsByHoldingIds(holdingIds, requestContext, UnaryOperator.identity()); + return getHoldingsByIdsWithoutVerification(holdingIds, requestContext) + .map(holdings -> { + if (holdings.size() == holdingIds.size()) { + return holdings; + } + List parameters = holdingIds.stream() + .filter(id -> holdings.stream() + .noneMatch(holding -> holding.getString(ID).equals(id))) + .map(id -> new Parameter().withValue(id).withKey("holdings")) + .toList(); + throw new HttpException(404, PARTIALLY_RETURNED_COLLECTION, parameters); + }); } - private Future> getHoldingsByIds(List holdingIds, RequestContext requestContext, - BiFunctionReturningFuture, RequestContext, List> biFunction) { + private Future> getHoldingsByIdsWithoutVerification(List holdingIds, RequestContext requestContext) { return collectResultsOnSuccess( - ofSubLists(new ArrayList<>(holdingIds), MAX_IDS_FOR_GET_RQ_15).map(ids -> biFunction.apply(ids, requestContext)).toList()) + ofSubLists(new ArrayList<>(holdingIds), MAX_IDS_FOR_GET_RQ_15) + .map(ids -> fetchHoldingsByHoldingIds(ids, requestContext)).toList() + ) .map(lists -> lists.stream() .flatMap(Collection::stream) - .collect(Collectors.toList())); + .toList()); } - private Future> fetchHoldingsByHoldingIds(List holdingIds, RequestContext requestContext, - UnaryOperator> unaryOperator) { + private Future> fetchHoldingsByHoldingIds(List holdingIds, RequestContext requestContext) { String query = convertIdsToCqlQuery(holdingIds); RequestEntry requestEntry = new RequestEntry(INVENTORY_LOOKUP_ENDPOINTS.get(HOLDINGS_RECORDS)) .withQuery(query).withOffset(0).withLimit(MAX_IDS_FOR_GET_RQ_15); @@ -181,8 +169,7 @@ private Future> fetchHoldingsByHoldingIds(List holdingI .map(jsonHoldings -> jsonHoldings.getJsonArray(HOLDINGS_RECORDS) .stream() .map(JsonObject.class::cast) - .collect(toList())) - .map(unaryOperator); + .toList()); } public Future getHoldingById(String holdingId, boolean skipNotFoundException, RequestContext requestContext) { @@ -307,15 +294,6 @@ public Future deleteHoldingById(String holdingId, boolean skipNotFoundExce return Future.succeededFuture(); } - public Future> getHoldingsForAllLocationTenants(CompositePoLine poLine, RequestContext requestContext) { - var holdingsByTenants = getHoldingsByLocationTenants(poLine, requestContext); - return GenericCompositeFuture.all(new ArrayList<>(holdingsByTenants.values())) - .map(ar -> holdingsByTenants.values().stream() - .flatMap(future -> future.result().stream()) - .toList() - ); - } - public Map>> getHoldingsByLocationTenants(CompositePoLine poLine, RequestContext requestContext) { String currentTenantId = TenantTool.tenantId(requestContext.getHeaders()); Map> holdingsByTenant = poLine.getLocations() @@ -346,4 +324,25 @@ private Future> getHoldings(String tenantId, List holdi }); } + public Future> getLocationIdsFromHoldings(List locations, RequestContext requestContext) { + var holdingLocations = StreamUtils.filter(locations, location -> location.getHoldingId() != null); + if (holdingLocations.isEmpty()) { + return Future.succeededFuture(List.of()); + } + Map> holdingIdsByTenant = StreamUtils.groupBy( + holdingLocations, + Location::getTenantId, + Location::getHoldingId + ); + return HelperUtils.combineResultListsOnSuccess( + holdingIdsByTenant.entrySet() + .stream() + .map(entry -> { + var locationContext = RequestContextUtil.createContextWithNewTenantId(requestContext, entry.getKey()); + return getHoldingsByIdsWithoutVerification(entry.getValue(), locationContext); + }) + .toList() + ).map(holdings -> StreamUtils.map(holdings, holding -> holding.getString(HOLDING_PERMANENT_LOCATION_ID))); + } + } diff --git a/src/main/java/org/folio/service/orders/PurchaseOrderLineService.java b/src/main/java/org/folio/service/orders/PurchaseOrderLineService.java index 5e061a3d6..c0f752683 100644 --- a/src/main/java/org/folio/service/orders/PurchaseOrderLineService.java +++ b/src/main/java/org/folio/service/orders/PurchaseOrderLineService.java @@ -14,7 +14,6 @@ import static org.folio.rest.RestConstants.MAX_IDS_FOR_GET_RQ_15; import static org.folio.rest.RestConstants.SEMAPHORE_MAX_ACTIVE_THREADS; import static org.folio.rest.jaxrs.model.PoLine.ReceiptStatus.FULLY_RECEIVED; -import static org.folio.service.inventory.InventoryHoldingManager.HOLDING_PERMANENT_LOCATION_ID; import static org.folio.service.orders.utils.ProductIdUtils.buildSetOfProductIdsFromCompositePoLines; import static org.folio.service.orders.utils.ProductIdUtils.isISBN; import static org.folio.service.orders.utils.ProductIdUtils.extractQualifier; @@ -22,7 +21,6 @@ import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -501,33 +499,31 @@ public Future validateAndNormalizeISBNCommon(List composi }); } - public Future> retrieveSearchLocationIds(PoLine poLine, RequestContext requestContext) { - if (CollectionUtils.isEmpty(poLine.getLocations())) { - return Future.succeededFuture(Collections.emptyList()); - } + public Future updateSearchLocations(CompositePoLine poLine, RequestContext requestContext) { + return updateSearchLocations(HelperUtils.convertToPoLine(poLine), requestContext); + } + + private Future updateSearchLocations(PoLine poLine, RequestContext requestContext) { + return retrieveSearchLocationIds(poLine, requestContext) + .map(poLine::withSearchLocationIds) + .mapEmpty(); + } - var holdingIds = StreamEx.of(poLine.getLocations()).map(Location::getHoldingId).nonNull().toList(); - var locationIds = StreamEx.of(poLine.getLocations()).map(Location::getLocationId).nonNull().toList(); - if (CollectionUtils.isEmpty(holdingIds)) { - return Future.succeededFuture(new ArrayList<>(locationIds)); + private Future> retrieveSearchLocationIds(PoLine poLine, RequestContext requestContext) { + List locations = poLine.getLocations(); + if (CollectionUtils.isEmpty(locations)) { + return Future.succeededFuture(List.of()); } + var locationIds = StreamEx.of(locations).map(Location::getLocationId).nonNull().toList(); + /* * Possible scenarios where holding can be removed but the operation is not yet complete, and this would * result in halting the entire flow. To avoid this, we do not compare the number of holdingIds with * the final result from the inventory. */ - return inventoryHoldingManager.getHoldingsByIdsWithoutVerification(holdingIds, requestContext) - .map(holdings -> StreamEx.of(holdings).map(holding -> holding.getString(HOLDING_PERMANENT_LOCATION_ID)) - .nonNull().toList()) + return inventoryHoldingManager.getLocationIdsFromHoldings(locations, requestContext) .map(holdingsPermanentLocationIds -> StreamEx.of(locationIds).append(holdingsPermanentLocationIds) .distinct().toList()); } - - private Future updateSearchLocations(PoLine poLine, RequestContext requestContext) { - return retrieveSearchLocationIds(poLine, requestContext) - .map(poLine::withSearchLocationIds) - .mapEmpty(); - } } - diff --git a/src/test/java/org/folio/ApiTestSuite.java b/src/test/java/org/folio/ApiTestSuite.java index ee1af7b8e..efc3bfccb 100644 --- a/src/test/java/org/folio/ApiTestSuite.java +++ b/src/test/java/org/folio/ApiTestSuite.java @@ -19,6 +19,7 @@ import org.folio.orders.utils.FundDistributionUtilsTest; import org.folio.orders.utils.HelperUtilsTest; import org.folio.orders.utils.PoLineCommonUtilTest; +import org.folio.orders.utils.StreamUtilsTest; import org.folio.orders.utils.validators.LocationsAndPiecesConsistencyValidatorTest; import org.folio.rest.core.ResponseUtilTest; import org.folio.rest.core.RestClientTest; @@ -244,6 +245,10 @@ class PurchaseOrderLineHelperTestNested extends PurchaseOrderLineHelperTest { class FundDistributionUtilsTestNested extends FundDistributionUtilsTest { } + @Nested + class StreamUtilsTestNested extends StreamUtilsTest { + } + @Nested class HelperUtilsTestNested extends HelperUtilsTest { } diff --git a/src/test/java/org/folio/helper/PurchaseOrderLineHelperTest.java b/src/test/java/org/folio/helper/PurchaseOrderLineHelperTest.java index 6b10a63cf..d8922ff45 100644 --- a/src/test/java/org/folio/helper/PurchaseOrderLineHelperTest.java +++ b/src/test/java/org/folio/helper/PurchaseOrderLineHelperTest.java @@ -92,8 +92,8 @@ void testCreatePoLineWithOrder() { .withSequenceNumbers(List.of("1")); doReturn(succeededFuture(JsonObject.mapFrom(seqNumbers))) .when(restClient).getAsJsonObject(any(RequestEntry.class), eq(requestContext)); - doReturn(succeededFuture(List.of())) - .when(purchaseOrderLineService).retrieveSearchLocationIds(any(PoLine.class), eq(requestContext)); + doReturn(succeededFuture()) + .when(purchaseOrderLineService).updateSearchLocations(any(CompositePoLine.class), eq(requestContext)); doAnswer((Answer>) invocation -> { PoLine pol = invocation.getArgument(1); return succeededFuture(pol); @@ -137,8 +137,8 @@ void testErrorWhenCreatingAReportingCode() { .withSequenceNumbers(List.of("1")); doReturn(succeededFuture(JsonObject.mapFrom(seqNumbers))) .when(restClient).getAsJsonObject(any(RequestEntry.class), eq(requestContext)); - doReturn(succeededFuture(List.of())) - .when(purchaseOrderLineService).retrieveSearchLocationIds(any(PoLine.class), eq(requestContext)); + doReturn(succeededFuture()) + .when(purchaseOrderLineService).updateSearchLocations(any(CompositePoLine.class), eq(requestContext)); doAnswer((Answer>) invocation -> { PoLine pol = invocation.getArgument(1); return succeededFuture(pol); diff --git a/src/test/java/org/folio/orders/utils/HelperUtilsTest.java b/src/test/java/org/folio/orders/utils/HelperUtilsTest.java index 366ce6ba1..20af0d035 100644 --- a/src/test/java/org/folio/orders/utils/HelperUtilsTest.java +++ b/src/test/java/org/folio/orders/utils/HelperUtilsTest.java @@ -1,5 +1,17 @@ package org.folio.orders.utils; +import io.vertx.core.Future; +import org.folio.rest.core.exceptions.HttpException; +import org.folio.rest.jaxrs.model.CloseReason; +import org.folio.rest.jaxrs.model.Cost; +import org.folio.rest.jaxrs.model.PoLine; +import org.folio.rest.jaxrs.model.PurchaseOrder; +import org.junit.jupiter.api.Test; + +import javax.money.convert.ConversionQuery; +import java.util.List; +import java.util.UUID; + import static org.folio.orders.utils.HelperUtils.REASON_CANCELLED; import static org.folio.orders.utils.HelperUtils.isNotFound; import static org.folio.rest.core.exceptions.ErrorCodes.PREFIX_NOT_FOUND; @@ -12,29 +24,27 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.util.List; -import java.util.UUID; - -import javax.money.convert.ConversionQuery; - -import org.folio.rest.core.exceptions.ErrorCodes; -import org.folio.rest.core.exceptions.HttpException; -import org.folio.rest.jaxrs.model.CloseReason; -import org.folio.rest.jaxrs.model.Cost; -import org.folio.rest.jaxrs.model.PoLine; -import org.folio.rest.jaxrs.model.PurchaseOrder; -import org.junit.jupiter.api.Test; - public class HelperUtilsTest { @Test - public void testShouldReturnEmptyString(){ + void testShouldReturnEmptyString() { String act = HelperUtils.combineCqlExpressions(""); assertThat(act, is(emptyOrNullString())); } @Test - void testShouldReturnBooleanWhenIsNotFound(){ + void testCombineResultListsOnSuccess() { + var f1 = Future.succeededFuture(List.of(1, 2, 3)); + var f2 = Future.succeededFuture(List.of(4, 5, 6)); + var f3 = Future.succeededFuture(List.of(7, 8, 9)); + Future> listFuture = HelperUtils.combineResultListsOnSuccess(List.of(f1, f2, f3)); + listFuture.onSuccess( + combined -> assertEquals(List.of(1, 2, 3, 4, 5, 6, 7, 8, 9), combined) + ); + } + + @Test + void testShouldReturnBooleanWhenIsNotFound() { var actual = isNotFound(new HttpException(404, PREFIX_NOT_FOUND)); assertTrue(actual); var actual2 = isNotFound(new Throwable(new HttpException(401, PREFIX_NOT_FOUND))); @@ -42,7 +52,7 @@ void testShouldReturnBooleanWhenIsNotFound(){ } @Test - public void testShouldReturnConversionQueryWithRateKeyForGetConversionQueryWithExchangeRate(){ + void testShouldReturnConversionQueryWithRateKeyForGetConversionQueryWithExchangeRate() { ConversionQuery conversionQuery = HelperUtils.getConversionQuery(2.0d, "USD", "EUR"); assertThat(conversionQuery.get(RATE_KEY, Double.class), is(2.0d)); assertThat(conversionQuery.getBaseCurrency().getCurrencyCode(), is("USD")); @@ -50,7 +60,7 @@ public void testShouldReturnConversionQueryWithRateKeyForGetConversionQueryWithE } @Test - public void testShouldBuildQueryWithoutExchangeRate(){ + void testShouldBuildQueryWithoutExchangeRate() { String systemCurrency = "USD"; Cost costOneTime = new Cost().withListUnitPrice(595d).withQuantityPhysical(1).withCurrency("EUR").withPoLineEstimatedPrice(595d); PoLine poLineOneTime = new PoLine().withId(UUID.randomUUID().toString()).withPurchaseOrderId(UUID.randomUUID().toString()).withCost(costOneTime); @@ -59,7 +69,8 @@ public void testShouldBuildQueryWithoutExchangeRate(){ assertNull(actQuery.get(RATE_KEY, Double.class)); } - @Test void testOrderStatusToBeCancelled() { + @Test + void testOrderStatusToBeCancelled() { PurchaseOrder purchaseOrder = new PurchaseOrder(); purchaseOrder.setId(UUID.randomUUID().toString()); purchaseOrder.setWorkflowStatus(PurchaseOrder.WorkflowStatus.OPEN); diff --git a/src/test/java/org/folio/orders/utils/StreamUtilsTest.java b/src/test/java/org/folio/orders/utils/StreamUtilsTest.java new file mode 100644 index 000000000..9b0abec9c --- /dev/null +++ b/src/test/java/org/folio/orders/utils/StreamUtilsTest.java @@ -0,0 +1,78 @@ +package org.folio.orders.utils; + +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class StreamUtilsTest { + + @Test + void testFind() { + List list = List.of("1", "2", "3"); + + assertNull(StreamUtils.find(list, s -> s.equals("4"))); + + assertEquals("2", StreamUtils.find(list, s -> s.equals("2"))); + } + + @Test + void testFilter() { + List list = List.of("1", "2", "3"); + + assertTrue(StreamUtils.filter(list, s -> s.equals("4")).isEmpty()); + + assertEquals(List.of("1", "3"), StreamUtils.filter(list, s -> s.matches("[13]"))); + } + + @Test + void testMap() { + List list = List.of("1", "22", "333"); + + List lengthList = StreamUtils.map(list, String::length); + assertEquals(List.of(1, 2, 3), lengthList); + } + + @Test + void testMapToSet() { + List list = List.of("1", "2", "3", "1", "2", "3"); + + assertEquals(Set.of("1", "2", "3"), StreamUtils.mapToSet(list, s -> s)); + } + + @Test + void testListToMapKey() { + List list = List.of("1", "22", "3"); + + Map map = StreamUtils.listToMap(list, String::length); + assertEquals(2, map.size()); + assertEquals("1", map.get(1)); + assertEquals("22", map.get(2)); + } + + @Test + void testListToMapKeyValue() { + List list = List.of("1", "22", "3"); + + Map map = StreamUtils.listToMap(list, String::length, s -> s + "x"); + assertEquals(2, map.size()); + assertEquals("1x", map.get(1)); + assertEquals("22x", map.get(2)); + } + + @Test + void testGroupBy() { + List list = List.of("1", "22", "3"); + + Map> map = StreamUtils.groupBy(list, String::length); + assertEquals(2, map.size()); + assertEquals(List.of("1", "3"), map.get(1)); + assertEquals(List.of("22"), map.get(2)); + } + +} diff --git a/src/test/java/org/folio/service/inventory/InventoryManagerTest.java b/src/test/java/org/folio/service/inventory/InventoryManagerTest.java index 5981a2466..1234be279 100644 --- a/src/test/java/org/folio/service/inventory/InventoryManagerTest.java +++ b/src/test/java/org/folio/service/inventory/InventoryManagerTest.java @@ -78,10 +78,10 @@ import java.util.stream.Stream; import org.apache.commons.lang3.RandomStringUtils; -import org.apache.commons.lang3.StringUtils; import org.folio.ApiTestSuite; import org.folio.Instance; import org.folio.models.consortium.ConsortiumConfiguration; +import org.folio.orders.utils.HelperUtils; import org.folio.rest.core.RestClient; import org.folio.rest.core.exceptions.HttpException; import org.folio.rest.core.models.RequestContext; @@ -748,10 +748,10 @@ void shouldReturnHoldingsByLocationTenants(VertxTestContext vertxTestContext) { doReturn(succeededFuture(holdings2)).when(restClient).getAsJsonObject(any(RequestEntry.class), RequestContextMatcher.matchTenant("T2")); // when - var future = inventoryHoldingManager.getHoldingsForAllLocationTenants(poLine, requestContext); + var holdingsByTenants = inventoryHoldingManager.getHoldingsByLocationTenants(poLine, requestContext); // then - vertxTestContext.assertComplete(future) + vertxTestContext.assertComplete(HelperUtils.combineResultListsOnSuccess(holdingsByTenants.values())) .onComplete(result -> { List holdings = result.result(); assertEquals(3, holdings.size());