diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 4ef676758..7996457f3 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -436,6 +436,34 @@ "orders-storage.reporting-codes.item.get" ] }, + { + "methods": [ + "POST" + ], + "pathPattern": "/orders/restore", + "permissionsRequired": [ + "orders.restore.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", + "inventory.items.collection.get", + "inventory.items.item.put", + "inventory-storage.holdings.collection.get", + "inventory-storage.holdings.item.post", + "acquisitions-units-storage.units.collection.get", + "acquisitions-units-storage.memberships.collection.get", + "orders-storage.purchase-orders.collection.get", + "finance.encumbrances.item.put", + "finance.order-transaction-summaries.item.put", + "finance.transactions.collection.get" + ] + }, { "methods": [ "GET" @@ -1277,6 +1305,11 @@ "displayName": "Orders - Check-in items", "description": "Check-in items spanning one or more po-lines in this order" }, + { + "permissionName": "orders.restore.collection.post", + "displayName": "Orders - Restore items", + "description": "Restore items spanning one or more po-lines in this order" + }, { "permissionName": "orders.receiving-history.collection.get", "displayName": "Orders - Receiving history", @@ -1697,6 +1730,7 @@ "orders.po-number.item.post", "orders.receiving.collection.post", "orders.check-in.collection.post", + "orders.restore.collection.post", "orders.receiving-history.collection.get", "orders.pieces.all", "orders.acquisitions-units-assignments.all", diff --git a/ramls/restoration.raml b/ramls/restoration.raml new file mode 100644 index 000000000..b4fd4f4d1 --- /dev/null +++ b/ramls/restoration.raml @@ -0,0 +1,38 @@ +#%RAML 1.0 +title: Restoration +baseUri: https://github.com/folio-org/mod-orders +version: v1 +protocols: [ HTTP, HTTPS ] + +documentation: + - title: Orders Business Logic API + content: API for transition pieces status from unreceivable to expected + +types: + receiving-collection: !include acq-models/mod-orders/schemas/receivingCollection.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/restore: + displayName: Restore items + description: | + Restore items spanning one or more PO lines. The endpoint is used to: + - move a received piece back to "Expected" in case "receivedItems" element's "itemStatus" is "On order" + type: + post-with-200: + requestSchema: receiving-collection + responseSchema: receiving-results + requestExample: !include acq-models/mod-orders/examples/receivingCollection.sample + responseExample: !include acq-models/mod-orders/examples/receivingResults.sample + is: [validate] + post: + description: Restore items spanning one or more PO lines diff --git a/src/main/java/org/folio/helper/ReceivingHelper.java b/src/main/java/org/folio/helper/ReceivingHelper.java index 8a07b206f..9052344ba 100644 --- a/src/main/java/org/folio/helper/ReceivingHelper.java +++ b/src/main/java/org/folio/helper/ReceivingHelper.java @@ -251,7 +251,7 @@ protected Map> updatePieceRecordsWithoutItems(Map okapiHeaders, Context ctx) { + super(receivingCollection, okapiHeaders, ctx); + } + + public Future restorePiece(ReceivingCollection restorationCollection, RequestContext requestContext) { + return receiveItems(restorationCollection, requestContext); + } + + @Override + protected boolean isRevertToOnOrder(Piece piece) { + return piece.getReceivingStatus() == Piece.ReceivingStatus.UNRECEIVABLE + && inventoryManager + .isOnOrderItemStatus(piecesByLineId.get(piece.getPoLineId()).get(piece.getId())); + } + + @Override + protected void updatePieceWithReceivingInfo(Piece piece) { + super.updatePieceWithReceivingInfo(piece); + + piece.setReceivedDate(null); + piece.setReceivingStatus(Piece.ReceivingStatus.EXPECTED); + } +} diff --git a/src/main/java/org/folio/rest/impl/ReceivingAPI.java b/src/main/java/org/folio/rest/impl/ReceivingAPI.java index 24bada2f9..a8d3a1176 100644 --- a/src/main/java/org/folio/rest/impl/ReceivingAPI.java +++ b/src/main/java/org/folio/rest/impl/ReceivingAPI.java @@ -11,12 +11,14 @@ import org.apache.logging.log4j.Logger; import org.folio.helper.CheckinHelper; import org.folio.helper.ReceivingHelper; +import org.folio.helper.RestoreHelper; 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.ReceivingCollection; import org.folio.rest.jaxrs.resource.OrdersCheckIn; import org.folio.rest.jaxrs.resource.OrdersReceive; +import org.folio.rest.jaxrs.resource.OrdersRestore; import org.folio.rest.jaxrs.resource.OrdersReceivingHistory; import io.vertx.core.AsyncResult; @@ -24,7 +26,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, OrdersReceivingHistory, OrdersRestore { private static final Logger logger = LogManager.getLogger(); @@ -66,4 +68,13 @@ public void getOrdersReceivingHistory(String totalRecords, int offset, int limit }) .onFailure(t -> handleErrorResponse(asyncResultHandler, helper, t)); } + + @Override + public void postOrdersRestore(ReceivingCollection entity, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { + logger.info("Restoring {} items", entity.getTotalRecords()); + RestoreHelper helper = new RestoreHelper(entity, okapiHeaders, vertxContext); + helper.restorePiece(entity, new RequestContext(vertxContext, okapiHeaders)) + .onSuccess(result -> asyncResultHandler.handle(succeededFuture(helper.buildOkResponse(result)))) + .onFailure(t -> handleErrorResponse(asyncResultHandler, helper, t)); + } } diff --git a/src/test/java/org/folio/TestConstants.java b/src/test/java/org/folio/TestConstants.java index 40d4fe68e..c330aad6e 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_RESTORE_ENDPOINT = "/orders/restore"; 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 ece371129..2a05ffec9 100644 --- a/src/test/java/org/folio/rest/impl/CheckinReceivingApiTest.java +++ b/src/test/java/org/folio/rest/impl/CheckinReceivingApiTest.java @@ -9,6 +9,7 @@ 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_RECEIVING_ENDPOINT; +import static org.folio.TestConstants.ORDERS_RESTORE_ENDPOINT; import static org.folio.TestUtils.getInstanceId; import static org.folio.TestUtils.getMinimalContentCompositePoLine; import static org.folio.TestUtils.getMinimalContentCompositePurchaseOrder; @@ -111,6 +112,7 @@ public class CheckinReceivingApiTest { private static final String RECEIVING_RQ_MOCK_DATA_PATH = BASE_MOCK_DATA_PATH + "receiving/"; private static final String CHECKIN_RQ_MOCK_DATA_PATH = BASE_MOCK_DATA_PATH + "checkIn/"; + private static final String RESTORE_RQ_MOCK_DATA_PATH = BASE_MOCK_DATA_PATH + "restore/"; private static final String ITEM_BARCODE = "barcode"; private static final String ITEM_LEVEL_CALL_NUMBER = "itemLevelCallNumber"; private static final String HOLDING_PERMANENT_LOCATION_ID = "permanentLocationId"; @@ -1025,6 +1027,50 @@ void testPostReceivingRevertElectronicResource() { verifyOrderStatusUpdateEvent(1); } + @Test + void testRestoreUnreceivablePiece() { + logger.info("=== Test POST Restore"); + + CompositePoLine poLines = getMockAsJson(POLINES_COLLECTION).getJsonArray("poLines").getJsonObject(9).mapTo(CompositePoLine.class); + MockServer.addMockTitles(Collections.singletonList(poLines)); + + ReceivingCollection receivingRq = getMockAsJson(RESTORE_RQ_MOCK_DATA_PATH + "receive-physical-ongoing.json").mapTo(ReceivingCollection.class); + receivingRq.getToBeReceived().get(0).setPoLineId(COMPOSITE_POLINE_ONGOING_ID); + + ReceivingResults results = verifyPostResponse(ORDERS_RESTORE_ENDPOINT, JsonObject.mapFrom(receivingRq).encode(), + prepareHeaders(EXIST_CONFIG_X_OKAPI_TENANT_LIMIT_10), APPLICATION_JSON, 200).as(ReceivingResults.class); + + assertThat(results.getTotalRecords(), equalTo(receivingRq.getTotalRecords())); + + Map> pieceIdsByPol = verifyReceivingSuccessRs(results); + + List pieceSearches = getPieceSearches(); + List pieceUpdates = getPieceUpdates(); + List polSearches = getPoLineSearches(); + List polUpdates = getPoLineUpdates(); + + assertThat(pieceSearches, not(nullValue())); + assertThat(pieceUpdates, not(nullValue())); + + assertThat(polSearches, not(nullValue())); + + int expectedSearchRqQty = Math.floorDiv(receivingRq.getTotalRecords(), MAX_IDS_FOR_GET_RQ_15) + 1; + + // The piece searches should be made 1 time: 1st time to get all required piece records + assertThat(pieceSearches, hasSize(expectedSearchRqQty)); + assertThat(pieceUpdates, hasSize(receivingRq.getTotalRecords())); + assertThat(pieceUpdates, hasSize(1)); + + pieceUpdates.forEach(pieceUpdated -> { + Piece pieceMapped = pieceUpdated.mapTo(Piece.class); + assertThat(pieceMapped.getReceivedDate(), nullValue()); + assertThat(pieceMapped.getReceivingStatus(), is(Piece.ReceivingStatus.EXPECTED)); + }); + + // Verify no status updated for ongoing order + verifyOrderStatusUpdateEvent(0); + } + private void checkResultWithErrors(CheckinCollection request, int expectedNumOfErrors) { ReceivingResult response = verifyPostResponse(ORDERS_CHECKIN_ENDPOINT, JsonObject.mapFrom(request).encode(), diff --git a/src/test/resources/mockdata/restore/receive-physical-ongoing.json b/src/test/resources/mockdata/restore/receive-physical-ongoing.json new file mode 100644 index 000000000..1141bc6b9 --- /dev/null +++ b/src/test/resources/mockdata/restore/receive-physical-ongoing.json @@ -0,0 +1,20 @@ +{ + "toBeReceived": [ + { + "poLineId": "0dd8f1d2-ac2e-4155-a407-72071f6d5f4a", + "received": 1, + "receivedItems": [ + { + "barcode": 21111111122, + "callNumber": "PR 8923 W6 L36 1990 c.3", + "comment": "Very important note", + "caption": "Vol. 1", + "itemStatus": "On order", + "locationId": "fcd64ce1-6995-48f0-840e-89ffa2288371", + "pieceId": "05a95f03-eb00-4248-9f2e-2bd05957ff04" + } + ] + } + ], + "totalRecords": 1 +}