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/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..46e99379c
--- /dev/null
+++ 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
new file mode 100644
index 000000000..f4f97aeba
--- /dev/null
+++ b/src/main/java/org/folio/helper/ExpectHelper.java
@@ -0,0 +1,151 @@
+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 {
+
+ /**
+ * 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) {
+ 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,
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<>();