Skip to content

Commit

Permalink
[MODORDERS-1138] Implement endpoint to fetch Circulation requests for…
Browse files Browse the repository at this point in the history
… pieces (#974)

* [MODORDERS-1138] Implement endpoint to fetch Circulation requests for pieces

* [MODORDERS-1138] Implement endpoint to fetch Circulation requests for pieces

* [MODORDERS-1138] Add tenantId to requests

* [MODORDERS-1138] Pass locationContext when fetching requests

* [MODORDERS-1138] Fix sonarcloud

* [MODORDERS-1138] Add /pieces-requests in a separate raml file
  • Loading branch information
Saba-Zedginidze-EPAM authored Jul 1, 2024
1 parent 3b28311 commit bc235ad
Show file tree
Hide file tree
Showing 11 changed files with 187 additions and 23 deletions.
9 changes: 9 additions & 0 deletions descriptors/ModuleDescriptor-template.json
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,15 @@
"invoice.invoice-lines.collection.get",
"invoice.invoice-lines.item.put"
]
},
{
"methods": ["GET"],
"pathPattern": "/orders/pieces-requests",
"permissionsRequired": ["orders.pieces.collection.get"],
"modulePermissions": [
"orders-storage.pieces.collection.get",
"circulation.requests.collection.get"
]
}
]
},
Expand Down
33 changes: 33 additions & 0 deletions ramls/pieces-requests.raml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#%RAML 1.0
title: Pieces Requests
baseUri: https://github.com/folio-org/mod-orders
version: v1
protocols: [ HTTP, HTTPS ]

documentation:
- title: Endpoint for fetching Circulation Requests for Pieces
content: <b>Endpoint for fetching Circulation Requests for Pieces</b>

types:
requests-collection: !include acq-models/mod-orders/schemas/circulationRequestsCollection.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}$

/orders/pieces-requests:
get:
description: Return a collection of circulation requests by Piece IDs
queryParameters:
pieceIds:
displayName: IDs of the pieces associated with the requests
type: string[]
description: IDs of the pieces associated with the requests
example: [ 7e74f1f1-f19e-482e-a02a-669fd632c5e0, afd0c802-2d9c-49ed-be89-770b7ef564b5 ]
required: true
status:
displayName: Status by which the requests should be filtered
type: string
description: Status by which the requests should be filtered
example: Open - In transit
required: true
4 changes: 2 additions & 2 deletions src/main/java/org/folio/config/ApplicationConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -850,8 +850,8 @@ TitleInstanceService titleInstanceService(InventoryInstanceManager inventoryInst
}

@Bean
CirculationRequestsRetriever circulationRequestsRetriever(RestClient restClient) {
return new CirculationRequestsRetriever(restClient);
CirculationRequestsRetriever circulationRequestsRetriever(PieceStorageService pieceStorageService, RestClient restClient) {
return new CirculationRequestsRetriever(pieceStorageService, restClient);
}

}
3 changes: 2 additions & 1 deletion src/main/java/org/folio/orders/utils/CommonFields.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ public enum CommonFields {

ID("id"),
METADATA("metadata"),
CREATED_DATE("createdDate");
CREATED_DATE("createdDate"),
TENANT_ID("tenantId");

private final String value;

Expand Down
26 changes: 20 additions & 6 deletions src/main/java/org/folio/rest/impl/PiecesAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static io.vertx.core.Future.succeededFuture;

import java.util.List;
import java.util.Map;

import javax.ws.rs.core.Response;
Expand All @@ -13,6 +14,8 @@
import org.folio.rest.core.models.RequestContext;
import org.folio.rest.jaxrs.model.Piece;
import org.folio.rest.jaxrs.resource.OrdersPieces;
import org.folio.rest.jaxrs.resource.OrdersPiecesRequests;
import org.folio.service.CirculationRequestsRetriever;
import org.folio.service.pieces.PieceStorageService;
import org.folio.service.pieces.flows.create.PieceCreateFlowManager;
import org.folio.service.pieces.flows.delete.PieceDeleteFlowManager;
Expand All @@ -26,12 +29,14 @@
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject;

public class PiecesAPI extends BaseApi implements OrdersPieces {
public class PiecesAPI extends BaseApi implements OrdersPieces, OrdersPiecesRequests {

private static final Logger logger = LogManager.getLogger();
@Autowired
private PieceStorageService pieceStorageService;
@Autowired
private CirculationRequestsRetriever circulationRequestsRetriever;
@Autowired
private PieceCreateFlowManager pieceCreateFlowManager;
@Autowired
private PieceDeleteFlowManager pieceDeleteFlowManager;
Expand All @@ -45,7 +50,7 @@ public PiecesAPI() {
@Override
@Validate
public void getOrdersPieces(String totalRecords, int offset, int limit, String query, Map<String, String> okapiHeaders,
Handler<AsyncResult<Response>> asyncResultHandler, Context vertxContext) {
Handler<AsyncResult<Response>> asyncResultHandler, Context vertxContext) {
pieceStorageService.getPieces(limit, offset, query, new RequestContext(vertxContext, okapiHeaders))
.onSuccess(pieces -> asyncResultHandler.handle(succeededFuture(buildOkResponse(pieces))))
.onFailure(fail -> handleErrorResponse(asyncResultHandler, fail));
Expand All @@ -54,7 +59,7 @@ public void getOrdersPieces(String totalRecords, int offset, int limit, String q
@Override
@Validate
public void postOrdersPieces(boolean createItem, Piece entity, Map<String, String> okapiHeaders,
Handler<AsyncResult<Response>> asyncResultHandler, Context vertxContext) {
Handler<AsyncResult<Response>> asyncResultHandler, Context vertxContext) {
pieceCreateFlowManager.createPiece(entity, createItem, new RequestContext(vertxContext, okapiHeaders))
.onSuccess(piece -> {
if (logger.isInfoEnabled()) {
Expand All @@ -69,7 +74,7 @@ public void postOrdersPieces(boolean createItem, Piece entity, Map<String, Strin
@Override
@Validate
public void getOrdersPiecesById(String id, Map<String, String> okapiHeaders,
Handler<AsyncResult<Response>> asyncResultHandler, Context vertxContext) {
Handler<AsyncResult<Response>> asyncResultHandler, Context vertxContext) {
pieceStorageService.getPieceById(id, new RequestContext(vertxContext, okapiHeaders))
.onSuccess(piece -> asyncResultHandler.handle(succeededFuture(buildOkResponse(piece))))
.onFailure(fail -> handleErrorResponse(asyncResultHandler, fail));
Expand All @@ -78,7 +83,7 @@ public void getOrdersPiecesById(String id, Map<String, String> okapiHeaders,
@Override
@Validate
public void putOrdersPiecesById(String pieceId, boolean createItem, boolean deleteHolding, Piece piece,
Map<String, String> okapiHeaders, Handler<AsyncResult<Response>> asyncResultHandler, Context vertxContext) {
Map<String, String> okapiHeaders, Handler<AsyncResult<Response>> asyncResultHandler, Context vertxContext) {
if (StringUtils.isEmpty(piece.getId())) {
piece.setId(pieceId);
}
Expand All @@ -91,9 +96,18 @@ public void putOrdersPiecesById(String pieceId, boolean createItem, boolean dele
@Override
@Validate
public void deleteOrdersPiecesById(String pieceId, boolean deleteHolding, Map<String, String> okapiHeaders,
Handler<AsyncResult<Response>> asyncResultHandler, Context vertxContext) {
Handler<AsyncResult<Response>> asyncResultHandler, Context vertxContext) {
pieceDeleteFlowManager.deletePiece(pieceId, deleteHolding, new RequestContext(vertxContext, okapiHeaders))
.onSuccess(ok -> asyncResultHandler.handle(succeededFuture(buildNoContentResponse())))
.onFailure(fail -> handleErrorResponse(asyncResultHandler, fail));
}

@Override
public void getOrdersPiecesRequests(List<String> pieceIds, String status, Map<String, String> okapiHeaders,
Handler<AsyncResult<Response>> asyncResultHandler, Context vertxContext) {
circulationRequestsRetriever.getRequesterIdsToRequestsByPieceIds(pieceIds, status, new RequestContext(vertxContext, okapiHeaders))
.onSuccess(requests -> asyncResultHandler.handle(succeededFuture(buildOkResponse(requests))))
.onFailure(fail -> handleErrorResponse(asyncResultHandler, fail));
}

}
65 changes: 57 additions & 8 deletions src/main/java/org/folio/service/CirculationRequestsRetriever.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,28 @@
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.okapi.common.GenericCompositeFuture;
import org.folio.orders.utils.CommonFields;
import org.folio.rest.core.RestClient;
import org.folio.rest.core.models.RequestContext;
import org.folio.rest.core.models.RequestEntry;
import org.folio.rest.jaxrs.model.CirculationRequest;
import org.folio.rest.jaxrs.model.Piece;
import org.folio.rest.jaxrs.model.RequestsCollection;
import org.folio.rest.tools.utils.TenantTool;
import org.folio.service.pieces.PieceStorageService;

import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.toList;
import static org.folio.orders.utils.HelperUtils.convertIdsToCqlQuery;
import static org.folio.orders.utils.RequestContextUtil.createContextWithNewTenantId;
import static org.folio.rest.RestConstants.MAX_IDS_FOR_GET_RQ_15;
import static org.folio.service.inventory.InventoryUtils.INVENTORY_LOOKUP_ENDPOINTS;
import static org.folio.service.inventory.InventoryUtils.REQUESTS;
Expand All @@ -24,38 +35,41 @@

public class CirculationRequestsRetriever {

private final static String OUTSTANDING_REQUEST_STATUS_PREFIX = "Open - ";
private static final String OPEN_REQUEST_STATUS = "Open - *";

private final PieceStorageService pieceStorageService;

private final RestClient restClient;

public CirculationRequestsRetriever(RestClient restClient) {
public CirculationRequestsRetriever(PieceStorageService pieceStorageService, RestClient restClient) {
this.pieceStorageService = pieceStorageService;
this.restClient = restClient;
}

public Future<Integer> getNumberOfRequestsByItemId(String itemId, RequestContext requestContext) {
String query = String.format("(itemId==%s and status=\"%s*\")", itemId, OUTSTANDING_REQUEST_STATUS_PREFIX);
String query = String.format("(itemId==%s and status=\"%s*\")", itemId, OPEN_REQUEST_STATUS);
RequestEntry requestEntry = new RequestEntry(INVENTORY_LOOKUP_ENDPOINTS.get(REQUESTS))
.withQuery(query).withOffset(0).withLimit(0); // limit = 0 means payload will include only totalRecords value
return restClient.getAsJsonObject(requestEntry, requestContext)
.map(json -> json.getInteger(COLLECTION_TOTAL.getValue()));
}

public Future<Map<String, Long>> getNumbersOfRequestsByItemIds(List<String> itemIds, RequestContext requestContext) {
return getRequestsByItemIds(itemIds, requestContext)
return getRequestsByItemIds(itemIds, OPEN_REQUEST_STATUS, requestContext)
.map(jsonList -> jsonList.stream()
.collect(Collectors.groupingBy(json -> json.getString(ITEM_ID.getValue()), Collectors.counting()))
);
}

public Future<Map<String, List<JsonObject>>> getRequesterIdsToRequestsByItemIds(List<String> itemIds, RequestContext requestContext) {
return getRequestsByItemIds(itemIds, requestContext)
return getRequestsByItemIds(itemIds, OPEN_REQUEST_STATUS, requestContext)
.map(jsonList -> jsonList.stream()
.collect(Collectors.groupingBy(json -> json.getString(REQUESTER_ID.getValue()))));
}

private Future<List<JsonObject>> getRequestsByItemIds(List<String> itemIds, RequestContext requestContext) {
private Future<List<JsonObject>> getRequestsByItemIds(List<String> itemIds, String status, RequestContext requestContext) {
var futures = StreamEx.ofSubLists(itemIds, MAX_IDS_FOR_GET_RQ_15)
.map(ids -> String.format("(%s and status=\"%s*\")", convertIdsToCqlQuery(ids, ITEM_ID.getValue()), OUTSTANDING_REQUEST_STATUS_PREFIX))
.map(ids -> String.format("(%s and status=\"%s\")", convertIdsToCqlQuery(ids, ITEM_ID.getValue()), status))
.map(query -> new RequestEntry(INVENTORY_LOOKUP_ENDPOINTS.get(REQUESTS))
.withQuery(query).withOffset(0).withLimit(Integer.MAX_VALUE))
.map(entry -> restClient.getAsJsonObject(entry, requestContext))
Expand All @@ -67,9 +81,44 @@ private Future<List<JsonObject>> getRequestsByItemIds(List<String> itemIds, Requ
var totalRecords = json.getInteger(COLLECTION_TOTAL.getValue());
var requests = json.getJsonArray(COLLECTION_RECORDS.getValue());
return IntStream.range(0, totalRecords)
.mapToObj(requests::getJsonObject);
.mapToObj(requests::getJsonObject)
.map(request -> request.put(CommonFields.TENANT_ID.getValue(), TenantTool.tenantId(requestContext.getHeaders())));
})
.toList());
}

public Future<RequestsCollection> getRequesterIdsToRequestsByPieceIds(List<String> pieceIds, String status, RequestContext requestContext) {
// 1. Fetch all pieces
return pieceStorageService.getPiecesByIds(pieceIds, requestContext)
// 2. Convert list of pieces to a map where we map tenants to items that are in this tenant
.map(pieces -> getLocationContextToItems(pieces, requestContext))
// 3. For each tenant fetch its items -> resulting in a list of Future<List<JsonObject>>
.map(ctxToItemsMap -> StreamEx.of(ctxToItemsMap.entrySet())
.map(ctxToItems -> getRequestsByItemIds(ctxToItems.getValue(), status, ctxToItems.getKey())).toList())
// 4. Put the list of futures in a composite one and flatMap all futures into a single list
.compose(requests -> GenericCompositeFuture.all(requests)
.map(cf -> StreamEx.of(cf.<List<JsonObject>>list()).toFlatList(Function.identity())))
// 5. Prepare response body
.map(this::prepareRequestsCollection);
}

private Map<RequestContext, List<String>> getLocationContextToItems(List<Piece> pieces, RequestContext requestContext) {
return StreamEx.of(pieces)
.filter(piece -> StringUtils.isNotEmpty(piece.getItemId()))
.groupingBy(piece -> createContextWithNewTenantId(requestContext, piece.getReceivingTenantId()),
mapping(Piece::getItemId, toList()));
}

private RequestsCollection prepareRequestsCollection(List<JsonObject> requests) {
return new RequestsCollection()
.withTotalRecords(requests.size())
.withCirculationRequests(requests.stream()
.map(requestJson -> {
var request = new CirculationRequest();
requestJson.getMap().forEach(request::setAdditionalProperty);
return request;
}).toList()
);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ private Future<SharingInstance> shareInstance(String consortiumId, SharingInstan
return Future.succeededFuture(response);
} else {
String message = String.format(SHARING_INSTANCE_ERROR, sharingInstance.sourceTenantId(), sharingInstance.targetTenantId(),
sharingInstance.instanceIdentifier(), sharingInstance.error());
sharingInstance.instanceIdentifier(), response.error());
return Future.failedFuture(new ConsortiumException(message));
}
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.folio.service.pieces.flows.delete;

import static org.folio.orders.utils.ProtectedOperationType.DELETE;
import static org.folio.orders.utils.RequestContextUtil.createContextWithNewTenantId;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
Expand Down Expand Up @@ -60,7 +61,8 @@ private Future<Void> isDeletePieceRequestValid(PieceDeletionHolder holder, Reque
return Future.succeededFuture();
}

return circulationRequestsRetriever.getNumberOfRequestsByItemId(piece.getItemId(), requestContext)
var locationContext = createContextWithNewTenantId(requestContext, holder.getPieceToDelete().getReceivingTenantId());
return circulationRequestsRetriever.getNumberOfRequestsByItemId(piece.getItemId(), locationContext)
.compose(totalRequests -> {
if (totalRequests != null && totalRequests > 0) {
logger.error("isDeletePieceRequestValid:: {} Request(s) were found for the given item {} when deleting piece {}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import org.folio.rest.core.RestClient;
import org.folio.rest.core.models.RequestContext;
import org.folio.rest.core.models.RequestEntry;
import org.folio.rest.jaxrs.model.PieceCollection;
import org.folio.service.pieces.PieceStorageService;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
Expand Down Expand Up @@ -54,7 +56,6 @@
@ExtendWith(VertxExtension.class)
public class CirculationRequestsRetrieverTest {


private static final String REQUESTS_PATH = BASE_MOCK_DATA_PATH + "requests/";
private static final String REQUESTS_MOCK = "requests";
private static final List<String> MOCK_ITEM_IDS = List.of(
Expand All @@ -63,9 +64,20 @@ public class CirculationRequestsRetrieverTest {
"24645dbd-2dd9-429c-ac24-44cae19adae4"
);

private static final String PIECES_PATH = BASE_MOCK_DATA_PATH + "pieces/";
private static final String PIECES_MOCK = "pieces-for-requests";
private static final List<String> MOCK_PIECE_IDS = List.of(
"05a95f03-eb00-4248-9f2e-2bd05957ff04",
"05a95f03-eb00-4248-9f2e-2bd05957ff05",
"05a95f03-eb00-4248-9f2e-2bd05957ff06"
);

@Autowired
RestClient restClient;

@Autowired
PieceStorageService pieceStorageService;

@Autowired
CirculationRequestsRetriever circulationRequestsRetriever;

Expand Down Expand Up @@ -166,6 +178,28 @@ void getRequesterIdsToRequestsByItemIdsTest(VertxTestContext vertxTestContext) {
});
}

@Test
void getRequesterIdsToRequestsByPieceIdsTest(VertxTestContext vertxTestContext) {
var requestsMockData = getMockAsJson(REQUESTS_PATH, REQUESTS_MOCK);
var piecesMockData = getMockAsJson(PIECES_PATH, PIECES_MOCK).mapTo(PieceCollection.class);
var status = "Open - In transit";

doReturn(Future.succeededFuture(requestsMockData)).when(restClient).getAsJsonObject(any(RequestEntry.class), eq(requestContext));
doReturn(Future.succeededFuture(piecesMockData.getPieces())).when(pieceStorageService).getPiecesByIds(eq(MOCK_PIECE_IDS), eq(requestContext));

var future = circulationRequestsRetriever.getRequesterIdsToRequestsByPieceIds(MOCK_PIECE_IDS, status, requestContext);

vertxTestContext.assertComplete(future).onComplete(f -> {
assertTrue(f.succeeded());
var requestsCollection = f.result();
assertEquals(7, requestsCollection.getTotalRecords());
assertEquals(7, requestsCollection.getCirculationRequests().size());
verify(restClient, times(1)).getAsJsonObject(any(RequestEntry.class), eq(requestContext));
verify(pieceStorageService, times(1)).getPiecesByIds(eq(MOCK_PIECE_IDS), eq(requestContext));
vertxTestContext.completeNow();
});
}


/**
* Define unit test specific beans to override actual ones
Expand All @@ -178,8 +212,13 @@ public RestClient restClient() {
}

@Bean
public CirculationRequestsRetriever circulationRequestsRetriever(RestClient restClient) {
return new CirculationRequestsRetriever(restClient);
public PieceStorageService pieceStorageService(RestClient restClient) {
return spy(new PieceStorageService(restClient));
}

@Bean
public CirculationRequestsRetriever circulationRequestsRetriever(PieceStorageService pieceStorageService, RestClient restClient) {
return new CirculationRequestsRetriever(pieceStorageService, restClient);
}

}
Expand Down
Loading

0 comments on commit bc235ad

Please sign in to comment.