From 5c879c4722bc8f0cd05f2820af92818b6f444fbe Mon Sep 17 00:00:00 2001 From: Khamidulla Abdulkhakimov Date: Mon, 22 Jan 2024 16:48:20 +0500 Subject: [PATCH 1/6] [MODORDERS-989] - update acq-models --- ramls/acq-models | 2 +- ramls/expect.raml | 0 src/main/java/org/folio/helper/ExpectHelper.java | 2 ++ 3 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 ramls/expect.raml create mode 100644 src/main/java/org/folio/helper/ExpectHelper.java diff --git a/ramls/acq-models b/ramls/acq-models index 4aab2ae48..49e5e16ca 160000 --- a/ramls/acq-models +++ b/ramls/acq-models @@ -1 +1 @@ -Subproject commit 4aab2ae4808333aac379cae40828dacdf0ef986c +Subproject commit 49e5e16cafc41db6fc65cf644fd742843d3c5287 diff --git a/ramls/expect.raml b/ramls/expect.raml new file mode 100644 index 000000000..e69de29bb diff --git a/src/main/java/org/folio/helper/ExpectHelper.java b/src/main/java/org/folio/helper/ExpectHelper.java new file mode 100644 index 000000000..2c86afdd0 --- /dev/null +++ b/src/main/java/org/folio/helper/ExpectHelper.java @@ -0,0 +1,2 @@ +package org.folio.helper;public class ExpectHelper { +} From ff4e6919fe7b26aac9393793e637b51e5ecfaaa7 Mon Sep 17 00:00:00 2001 From: Khamidulla Abdulkhakimov Date: Mon, 22 Jan 2024 16:51:08 +0500 Subject: [PATCH 2/6] [MODORDERS-989] - Implement logic to expect --- descriptors/ModuleDescriptor-template.json | 26 ++++ ramls/expect.raml | 38 +++++ .../java/org/folio/helper/ExpectHelper.java | 147 +++++++++++++++++- .../org/folio/rest/impl/ReceivingAPI.java | 14 +- 4 files changed, 223 insertions(+), 2 deletions(-) diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 4ef676758..d88b53873 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -436,6 +436,26 @@ "orders-storage.reporting-codes.item.get" ] }, + { + "methods": [ + "POST" + ], + "pathPattern": "/orders/expect", + "permissionsRequired": [ + "orders.expect.collection.post" + ], + "modulePermissions": [ + "orders-storage.pieces.collection.get", + "orders-storage.pieces.item.put", + "orders-storage.po-lines.collection.get", + "orders-storage.po-lines.item.put", + "orders-storage.purchase-orders.item.get", + "orders-storage.purchase-orders.item.put", + "orders-storage.titles.collection.get", + "acquisitions-units-storage.units.collection.get", + "acquisitions-units-storage.memberships.collection.get" + ] + }, { "methods": [ "GET" @@ -1277,6 +1297,11 @@ "displayName": "Orders - Check-in items", "description": "Check-in items spanning one or more po-lines in this order" }, + { + "permissionName": "orders.expect.collection.post", + "displayName": "Orders - Expect pieces", + "description": "Expect pieces spanning one or more po-lines in this order" + }, { "permissionName": "orders.receiving-history.collection.get", "displayName": "Orders - Receiving history", @@ -1697,6 +1722,7 @@ "orders.po-number.item.post", "orders.receiving.collection.post", "orders.check-in.collection.post", + "orders.expect.collection.post", "orders.receiving-history.collection.get", "orders.pieces.all", "orders.acquisitions-units-assignments.all", diff --git a/ramls/expect.raml b/ramls/expect.raml index e69de29bb..46e99379c 100644 --- a/ramls/expect.raml +++ b/ramls/expect.raml @@ -0,0 +1,38 @@ +#%RAML 1.0 +title: Receive +baseUri: https://github.com/folio-org/mod-orders +version: v1 +protocols: [ HTTP, HTTPS ] + +documentation: + - title: Orders Business Logic API + content: API for transitioning pieces status from Unreceivable to Expected + +types: + expect-collection: !include acq-models/mod-orders/schemas/expectCollection.json + receiving-results: !include acq-models/mod-orders/schemas/receivingResults.json + errors: !include raml-util/schemas/errors.schema + UUID: + type: string + pattern: ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$ + +traits: + validate: !include raml-util/traits/validation.raml + +resourceTypes: + post-with-200: !include rtypes/post-json-200.raml + +/orders/expect: + displayName: Expect pieces + description: | + Expect pieces spanning one or more PO lines. The endpoint is used to: + - move a unreceivable piece back to "Expected" + type: + post-with-200: + requestSchema: expect-collection + responseSchema: receiving-results + requestExample: !include acq-models/mod-orders/examples/expectCollection.sample + responseExample: !include acq-models/mod-orders/examples/receivingResults.sample + is: [validate] + post: + description: Expect pieces spanning one or more PO lines diff --git a/src/main/java/org/folio/helper/ExpectHelper.java b/src/main/java/org/folio/helper/ExpectHelper.java index 2c86afdd0..628457ef0 100644 --- a/src/main/java/org/folio/helper/ExpectHelper.java +++ b/src/main/java/org/folio/helper/ExpectHelper.java @@ -1,2 +1,147 @@ -package org.folio.helper;public class ExpectHelper { +package org.folio.helper; + +import io.vertx.core.Context; +import io.vertx.core.Future; +import io.vertx.core.json.JsonObject; +import one.util.streamex.StreamEx; +import org.apache.commons.lang3.StringUtils; +import org.folio.rest.core.models.RequestContext; +import org.folio.rest.jaxrs.model.ExpectCollection; +import org.folio.rest.jaxrs.model.ExpectPiece; +import org.folio.rest.jaxrs.model.Piece; +import org.folio.rest.jaxrs.model.ProcessingStatus; +import org.folio.rest.jaxrs.model.ReceivingResult; +import org.folio.rest.jaxrs.model.ReceivingResults; +import org.folio.rest.jaxrs.model.ToBeExpected; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static java.util.stream.Collectors.collectingAndThen; +import static java.util.stream.Collectors.mapping; +import static java.util.stream.Collectors.toList; + +public class ExpectHelper extends CheckinReceivePiecesHelper { + + private final Map> expectPieces; + + public ExpectHelper(ExpectCollection expectCollection, Map okapiHeaders, Context ctx) { + super(okapiHeaders, ctx); + // Convert request to map representation + expectPieces = groupExpectPieceByPoLineId(expectCollection); + + // Logging quantity of the piece records to be expected + if (logger.isDebugEnabled()) { + int poLinesQty = expectPieces.size(); + int piecesQty = StreamEx.ofValues(expectPieces) + .mapToInt(Map::size) + .sum(); + logger.debug("{} piece record(s) are going to be expected for {} PO line(s)", piecesQty, poLinesQty); + } + } + + public Future expectPieces(ExpectCollection expectCollection, RequestContext requestContext) { + return getPoLines(new ArrayList<>(expectPieces.keySet()), requestContext) + .compose(poLines -> removeForbiddenEntities(poLines, expectPieces, requestContext)) + .compose(vVoid -> processExpectPieces(expectCollection, requestContext)); + } + + private Future processExpectPieces(ExpectCollection expectCollection, RequestContext requestContext) { + // 1. Get piece records from storage + return retrievePieceRecords(expectPieces, requestContext) + // 2. Update piece status to Expected + .map(this::updatePieceRecords) + // 3. Update received piece records in the storage + .compose(piecesGroupedByPoLine -> storeUpdatedPieceRecords(piecesGroupedByPoLine, requestContext)) + // 4. Return results to the client + .map(piecesGroupedByPoLine -> prepareResponseBody(expectCollection, piecesGroupedByPoLine)); + } + + private Map> groupExpectPieceByPoLineId(ExpectCollection expectCollection) { + return StreamEx + .of(expectCollection.getToBeExpected()) + .distinct() + .groupingBy(ToBeExpected::getPoLineId, + mapping(ToBeExpected::getExpectPieces, + collectingAndThen(toList(), + lists -> StreamEx.of(lists) + .flatMap(List::stream) + .toMap(ExpectPiece::getId, expectPiece -> expectPiece)))); + } + + private ReceivingResults prepareResponseBody(ExpectCollection expectCollection, + Map> piecesGroupedByPoLine) { + ReceivingResults results = new ReceivingResults(); + results.setTotalRecords(expectCollection.getTotalRecords()); + for (ToBeExpected toBeExpected : expectCollection.getToBeExpected()) { + String poLineId = toBeExpected.getPoLineId(); + ReceivingResult result = new ReceivingResult(); + results.getReceivingResults().add(result); + + // Get all processed piece records for PO Line + Map processedPiecesForPoLine = StreamEx + .of(piecesGroupedByPoLine.getOrDefault(poLineId, Collections.emptyList())) + .toMap(Piece::getId, piece -> piece); + + Map resultCounts = new HashMap<>(); + resultCounts.put(ProcessingStatus.Type.SUCCESS.toString(), 0); + resultCounts.put(ProcessingStatus.Type.FAILURE.toString(), 0); + for (ExpectPiece expectPiece : toBeExpected.getExpectPieces()) { + String pieceId = expectPiece.getId(); + + calculateProcessingErrors(poLineId, result, processedPiecesForPoLine, resultCounts, pieceId); + } + + result.withPoLineId(poLineId) + .withProcessedSuccessfully(resultCounts.get(ProcessingStatus.Type.SUCCESS.toString())) + .withProcessedWithError(resultCounts.get(ProcessingStatus.Type.FAILURE.toString())); + } + + return results; + } + + private Map> updatePieceRecords(Map> piecesGroupedByPoLine) { + StreamEx.ofValues(piecesGroupedByPoLine) + .flatMap(List::stream) + .forEach(this::updatePieceWithExpectInfo); + + return piecesGroupedByPoLine; + } + + private void updatePieceWithExpectInfo(Piece piece) { + ExpectPiece expectPiece = piecesByLineId.get(piece.getPoLineId()) + .get(piece.getId()); + + piece.setComment(expectPiece.getComment()); + piece.setReceivedDate(null); + piece.setReceivingStatus(Piece.ReceivingStatus.EXPECTED); + } + + @Override + protected boolean isRevertToOnOrder(Piece piece) { + return false; + } + + @Override + protected Future receiveInventoryItemAndUpdatePiece(JsonObject item, Piece piece, RequestContext requestContext) { + return Future.succeededFuture(false); + } + + @Override + protected Map> updatePieceRecordsWithoutItems(Map> piecesGroupedByPoLine) { + return Collections.emptyMap(); + } + + @Override + protected String getHoldingId(Piece piece) { + return StringUtils.EMPTY; + } + + @Override + protected String getLocationId(Piece piece) { + return StringUtils.EMPTY; + } } diff --git a/src/main/java/org/folio/rest/impl/ReceivingAPI.java b/src/main/java/org/folio/rest/impl/ReceivingAPI.java index 24bada2f9..79a3bdd2e 100644 --- a/src/main/java/org/folio/rest/impl/ReceivingAPI.java +++ b/src/main/java/org/folio/rest/impl/ReceivingAPI.java @@ -10,12 +10,15 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.folio.helper.CheckinHelper; +import org.folio.helper.ExpectHelper; import org.folio.helper.ReceivingHelper; import org.folio.rest.annotations.Validate; import org.folio.rest.core.models.RequestContext; import org.folio.rest.jaxrs.model.CheckinCollection; +import org.folio.rest.jaxrs.model.ExpectCollection; import org.folio.rest.jaxrs.model.ReceivingCollection; import org.folio.rest.jaxrs.resource.OrdersCheckIn; +import org.folio.rest.jaxrs.resource.OrdersExpect; import org.folio.rest.jaxrs.resource.OrdersReceive; import org.folio.rest.jaxrs.resource.OrdersReceivingHistory; @@ -24,7 +27,7 @@ import io.vertx.core.Handler; import io.vertx.core.json.JsonObject; -public class ReceivingAPI implements OrdersReceive, OrdersCheckIn, OrdersReceivingHistory { +public class ReceivingAPI implements OrdersReceive, OrdersCheckIn, OrdersExpect, OrdersReceivingHistory { private static final Logger logger = LogManager.getLogger(); @@ -50,6 +53,15 @@ public void postOrdersCheckIn(CheckinCollection entity, Map okap .onFailure(t -> handleErrorResponse(asyncResultHandler, helper, t)); } + @Override + public void postOrdersExpect(ExpectCollection entity, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { + logger.info("Expect {} pieces", entity.getTotalRecords()); + ExpectHelper helper = new ExpectHelper(entity, okapiHeaders, vertxContext); + helper.expectPieces(entity, new RequestContext(vertxContext, okapiHeaders)) + .onSuccess(result -> asyncResultHandler.handle(succeededFuture(helper.buildOkResponse(result)))) + .onFailure(t -> handleErrorResponse(asyncResultHandler, helper, t)); + } + @Override @Validate public void getOrdersReceivingHistory(String totalRecords, int offset, int limit, String query, Map okapiHeaders, From f2a7f210d101d9384fc6593adba5b77d7012a8e1 Mon Sep 17 00:00:00 2001 From: Khamidulla Abdulkhakimov Date: Mon, 22 Jan 2024 19:23:14 +0500 Subject: [PATCH 3/6] [MODORDERS-989] - Added logger --- src/main/java/org/folio/helper/ExpectHelper.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/folio/helper/ExpectHelper.java b/src/main/java/org/folio/helper/ExpectHelper.java index 628457ef0..68a4a35d6 100644 --- a/src/main/java/org/folio/helper/ExpectHelper.java +++ b/src/main/java/org/folio/helper/ExpectHelper.java @@ -5,6 +5,8 @@ import io.vertx.core.json.JsonObject; import one.util.streamex.StreamEx; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.folio.rest.core.models.RequestContext; import org.folio.rest.jaxrs.model.ExpectCollection; import org.folio.rest.jaxrs.model.ExpectPiece; @@ -25,7 +27,11 @@ import static java.util.stream.Collectors.toList; public class ExpectHelper extends CheckinReceivePiecesHelper { - + private static final Logger logger = LogManager.getLogger(ExpectHelper.class); + /** + * Map with PO line id as a key and value is map with piece id as a key and + * {@link ExpectPiece} as a value + */ private final Map> expectPieces; public ExpectHelper(ExpectCollection expectCollection, Map okapiHeaders, Context ctx) { From 89a2d068868fade476dfcc7bc20bf61612a6fb55 Mon Sep 17 00:00:00 2001 From: Khamidulla Abdulkhakimov Date: Tue, 23 Jan 2024 15:50:23 +0500 Subject: [PATCH 4/6] [MODORDERS-989] - Increased coverage --- src/test/java/org/folio/TestConstants.java | 1 + .../rest/impl/CheckinReceivingApiTest.java | 51 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/test/java/org/folio/TestConstants.java b/src/test/java/org/folio/TestConstants.java index 7877a34d3..25b64d563 100644 --- a/src/test/java/org/folio/TestConstants.java +++ b/src/test/java/org/folio/TestConstants.java @@ -16,6 +16,7 @@ private TestConstants() {} public static final String ORDERS_RECEIVING_ENDPOINT = "/orders/receive"; public static final String ORDERS_CHECKIN_ENDPOINT = "/orders/check-in"; + public static final String ORDERS_EXPECT_ENDPOINT = "/orders/expect"; public static final String PO_LINE_NUMBER_VALUE = "1"; diff --git a/src/test/java/org/folio/rest/impl/CheckinReceivingApiTest.java b/src/test/java/org/folio/rest/impl/CheckinReceivingApiTest.java index 76c5e5f61..4c28fc338 100644 --- a/src/test/java/org/folio/rest/impl/CheckinReceivingApiTest.java +++ b/src/test/java/org/folio/rest/impl/CheckinReceivingApiTest.java @@ -8,6 +8,7 @@ import static org.folio.TestConfig.isVerticleNotDeployed; import static org.folio.TestConstants.EXIST_CONFIG_X_OKAPI_TENANT_LIMIT_10; import static org.folio.TestConstants.ORDERS_CHECKIN_ENDPOINT; +import static org.folio.TestConstants.ORDERS_EXPECT_ENDPOINT; import static org.folio.TestConstants.ORDERS_RECEIVING_ENDPOINT; import static org.folio.TestUtils.getInstanceId; import static org.folio.TestUtils.getMinimalContentCompositePoLine; @@ -87,6 +88,8 @@ import org.folio.rest.jaxrs.model.Eresource; import org.folio.rest.jaxrs.model.Error; import org.folio.rest.jaxrs.model.Errors; +import org.folio.rest.jaxrs.model.ExpectCollection; +import org.folio.rest.jaxrs.model.ExpectPiece; import org.folio.rest.jaxrs.model.Physical; import org.folio.rest.jaxrs.model.Piece; import org.folio.rest.jaxrs.model.PoLine; @@ -98,6 +101,7 @@ import org.folio.rest.jaxrs.model.ReceivingResult; import org.folio.rest.jaxrs.model.ReceivingResults; import org.folio.rest.jaxrs.model.ToBeCheckedIn; +import org.folio.rest.jaxrs.model.ToBeExpected; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; @@ -836,6 +840,53 @@ void testPostReceivingPhysicalWithErrors() throws IOException { verifyOrderStatusUpdateEvent(1); } + @Test + void testMovePieceStatusFromUnreceivableToExpected() { + logger.info("=== Test POST Expect"); + + CompositePurchaseOrder order = getMinimalContentCompositePurchaseOrder(); + CompositePoLine poLine = getMinimalContentCompositePoLine(order.getId()); + poLine.setIsPackage(true); + poLine.setOrderFormat(CompositePoLine.OrderFormat.ELECTRONIC_RESOURCE); + poLine.setEresource(new Eresource().withCreateInventory(Eresource.CreateInventory.INSTANCE_HOLDING_ITEM)); + + Piece electronicPiece = getMinimalContentPiece(poLine.getId()).withReceivingStatus(Piece.ReceivingStatus.UNRECEIVABLE) + .withFormat(org.folio.rest.jaxrs.model.Piece.Format.ELECTRONIC) + .withId(UUID.randomUUID().toString()) + .withTitleId(UUID.randomUUID().toString()) + .withItemId(UUID.randomUUID().toString()); + + addMockEntry(PURCHASE_ORDER_STORAGE, order.withWorkflowStatus(CompositePurchaseOrder.WorkflowStatus.OPEN)); + addMockEntry(PO_LINES_STORAGE, poLine); + addMockEntry(PIECES_STORAGE, electronicPiece); + + List toBeCheckedInList = new ArrayList<>(); + toBeCheckedInList.add(new ToBeExpected() + .withExpected(1) + .withPoLineId(poLine.getId()) + .withExpectPieces(Collections.singletonList(new ExpectPiece().withId(electronicPiece.getId()).withComment("test")))); + + ExpectCollection request = new ExpectCollection() + .withToBeExpected(toBeCheckedInList) + .withTotalRecords(1); + + Response response = verifyPostResponse(ORDERS_EXPECT_ENDPOINT, JsonObject.mapFrom(request).encode(), + prepareHeaders(EXIST_CONFIG_X_OKAPI_TENANT_LIMIT_10), APPLICATION_JSON, HttpStatus.HTTP_OK.toInt()); + assertThat(response.as(ReceivingResults.class).getReceivingResults().get(0).getProcessedSuccessfully(), is(1)); + + List pieceUpdates = getPieceUpdates(); + + assertThat(pieceUpdates, not(nullValue())); + assertThat(pieceUpdates, hasSize(request.getTotalRecords())); + + pieceUpdates.forEach(pol -> { + Piece piece = pol.mapTo(Piece.class); + assertThat(piece.getId(), is(electronicPiece.getId())); + assertThat(piece.getReceivingStatus(), is(Piece.ReceivingStatus.EXPECTED)); + assertThat(piece.getComment(), is("test")); + }); + } + private void verifyProperQuantityOfHoldingsCreated(ReceivingCollection receivingRq) throws IOException { Set expectedHoldings = new HashSet<>(); From 88a1e777351af6a068660dfbb48e1c76b6852f90 Mon Sep 17 00:00:00 2001 From: Khamidulla Abdulkhakimov Date: Tue, 23 Jan 2024 16:34:07 +0500 Subject: [PATCH 5/6] [MODORDERS-989] - Resolved sonar issue --- src/main/java/org/folio/helper/ExpectHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/folio/helper/ExpectHelper.java b/src/main/java/org/folio/helper/ExpectHelper.java index 68a4a35d6..639d47886 100644 --- a/src/main/java/org/folio/helper/ExpectHelper.java +++ b/src/main/java/org/folio/helper/ExpectHelper.java @@ -27,7 +27,7 @@ import static java.util.stream.Collectors.toList; public class ExpectHelper extends CheckinReceivePiecesHelper { - private static final Logger logger = LogManager.getLogger(ExpectHelper.class); + /** * Map with PO line id as a key and value is map with piece id as a key and * {@link ExpectPiece} as a value From d3702b35a9bd5fb43951e826565a10bc42209f47 Mon Sep 17 00:00:00 2001 From: Khamidulla Abdulkhakimov Date: Tue, 23 Jan 2024 16:50:39 +0500 Subject: [PATCH 6/6] [MODORDERS-989] - Removed unused imports --- src/main/java/org/folio/helper/ExpectHelper.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/org/folio/helper/ExpectHelper.java b/src/main/java/org/folio/helper/ExpectHelper.java index 639d47886..f4f97aeba 100644 --- a/src/main/java/org/folio/helper/ExpectHelper.java +++ b/src/main/java/org/folio/helper/ExpectHelper.java @@ -5,8 +5,6 @@ import io.vertx.core.json.JsonObject; import one.util.streamex.StreamEx; import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.folio.rest.core.models.RequestContext; import org.folio.rest.jaxrs.model.ExpectCollection; import org.folio.rest.jaxrs.model.ExpectPiece;