Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MODORDERS-989] - Implement batch endpoint to move multiple pieces to Expected status #823

Merged
merged 6 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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> {
SerhiiNosko marked this conversation as resolved.
Show resolved Hide resolved

SerhiiNosko marked this conversation as resolved.
Show resolved Hide resolved
/**
* 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