Skip to content

Commit

Permalink
[MODORDERS-989] - Implement batch endpoint to move multiple pieces to…
Browse files Browse the repository at this point in the history
… Expected status (#823)
  • Loading branch information
Abdulkhakimov authored Jan 23, 2024
1 parent 2073932 commit d250836
Show file tree
Hide file tree
Showing 7 changed files with 281 additions and 2 deletions.
26 changes: 26 additions & 0 deletions descriptors/ModuleDescriptor-template.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
38 changes: 38 additions & 0 deletions ramls/expect.raml
Original file line number Diff line number Diff line change
@@ -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: <b>API for transitioning pieces status from Unreceivable to Expected</b>

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
151 changes: 151 additions & 0 deletions src/main/java/org/folio/helper/ExpectHelper.java
Original file line number Diff line number Diff line change
@@ -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<ExpectPiece> {

/**
* 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<String, Map<String, ExpectPiece>> expectPieces;

public ExpectHelper(ExpectCollection expectCollection, Map<String, String> 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<ReceivingResults> 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<ReceivingResults> 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<String, Map<String, ExpectPiece>> 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<String, List<Piece>> 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<String, Piece> processedPiecesForPoLine = StreamEx
.of(piecesGroupedByPoLine.getOrDefault(poLineId, Collections.emptyList()))
.toMap(Piece::getId, piece -> piece);

Map<String, Integer> 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<String, List<Piece>> updatePieceRecords(Map<String, List<Piece>> 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<Boolean> receiveInventoryItemAndUpdatePiece(JsonObject item, Piece piece, RequestContext requestContext) {
return Future.succeededFuture(false);
}

@Override
protected Map<String, List<Piece>> updatePieceRecordsWithoutItems(Map<String, List<Piece>> piecesGroupedByPoLine) {
return Collections.emptyMap();
}

@Override
protected String getHoldingId(Piece piece) {
return StringUtils.EMPTY;
}

@Override
protected String getLocationId(Piece piece) {
return StringUtils.EMPTY;
}
}
14 changes: 13 additions & 1 deletion src/main/java/org/folio/rest/impl/ReceivingAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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();

Expand All @@ -50,6 +53,15 @@ public void postOrdersCheckIn(CheckinCollection entity, Map<String, String> okap
.onFailure(t -> handleErrorResponse(asyncResultHandler, helper, t));
}

@Override
public void postOrdersExpect(ExpectCollection entity, Map<String, String> okapiHeaders, Handler<AsyncResult<Response>> 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<String, String> okapiHeaders,
Expand Down
1 change: 1 addition & 0 deletions src/test/java/org/folio/TestConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
51 changes: 51 additions & 0 deletions src/test/java/org/folio/rest/impl/CheckinReceivingApiTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<ToBeExpected> 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<JsonObject> 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<String> expectedHoldings = new HashSet<>();

Expand Down

0 comments on commit d250836

Please sign in to comment.