Skip to content

Commit

Permalink
[MODORDERS-1131] Fix piece, item, title relationship in bind piece fe…
Browse files Browse the repository at this point in the history
…atures (#961)

* [MODORDERS-1131] Bind endpoint payload fixes
  • Loading branch information
Saba-Zedginidze-EPAM authored Jun 17, 2024
1 parent 92616ca commit 2c7ce43
Show file tree
Hide file tree
Showing 14 changed files with 254 additions and 82 deletions.
34 changes: 25 additions & 9 deletions src/main/java/org/folio/helper/BindHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ public Future<BindPiecesResult> bindPieces(BindPiecesCollection bindPiecesCollec
}

private Future<BindPiecesResult> processBindPieces(BindPiecesCollection bindPiecesCollection, RequestContext requestContext) {
// 1. Get piece records from storage
return retrievePieceRecords(requestContext)
// 1. Get valid piece records from storage
return getValidPieces(requestContext)
// 2. Generate holder object to include necessary data
.map(piecesGroupedByPoLine -> generateHolder(piecesGroupedByPoLine, bindPiecesCollection))
// 3. Check if there are any open requests for items
Expand All @@ -98,13 +98,25 @@ private BindPiecesHolder generateHolder(Map<String, List<Piece>> piecesGroupedBy
.withPiecesGroupedByPoLine(piecesGroupedByPoLine);
}

private Future<Map<String, List<Piece>>> getValidPieces(RequestContext requestContext) {
return retrievePieceRecords(requestContext)
.map(piecesGroupedByPoLine -> {
var areAllPiecesReceived = extractAllPieces(piecesGroupedByPoLine)
.allMatch(piece -> RECEIVED_STATUSES.contains(piece.getReceivingStatus()));
if (areAllPiecesReceived) {
return piecesGroupedByPoLine;
}
throw new HttpException(RestConstants.VALIDATION_ERROR, ErrorCodes.PIECES_MUST_HAVE_RECEIVED_STATUS);
});
}

private Future<BindPiecesHolder> checkRequestsForPieceItems(BindPiecesHolder holder, RequestContext requestContext) {
var tenantToItem = mapTenantIdsToItemIds(holder.getPiecesGroupedByPoLine(), requestContext);
return GenericCompositeFuture.all(
tenantToItem.entrySet().stream()
.map(entry -> {
var locationContext = createContextWithNewTenantId(requestContext, entry.getKey());
return inventoryItemRequestService.getItemsWithActiveRequests(entry.getValue(), locationContext)
return inventoryItemRequestService.getItemIdsWithActiveRequests(entry.getValue(), locationContext)
.compose(items -> validateItemsForRequestTransfer(tenantToItem.keySet(), items, holder.getBindPiecesCollection()));
})
.toList())
Expand Down Expand Up @@ -168,23 +180,26 @@ private Future<BindPiecesHolder> createItemForPieces(BindPiecesHolder holder, Re
logger.debug("createItemForPiece:: Trying to get poLine by id '{}'", poLineId);
return purchaseOrderLineService.getOrderLineById(poLineId, requestContext)
.map(PoLineCommonUtil::convertToCompositePoLine)
.compose(compPOL -> createInventoryObjects(compPOL, bindPiecesCollection.getBindItem(), requestContext))
.compose(compPOL -> createInventoryObjects(compPOL, bindPiecesCollection.getInstanceId(), bindPiecesCollection.getBindItem(), requestContext))
.map(newItemId -> {
// Move requests if requestsAction is TRANSFER, otherwise do nothing
if (TRANSFER.equals(bindPiecesCollection.getRequestsAction())) {
var itemIds = holder.getPieces().map(Piece::getItemId).toList();
inventoryItemRequestService.transferItemsRequests(itemIds, newItemId, requestContext);
inventoryItemRequestService.transferItemRequests(itemIds, newItemId, requestContext);
}
// Set new item ids for pieces and holder
holder.getPieces().forEach(piece -> piece.setItemId(newItemId));
return holder.withBindItemId(newItemId);
});
}

private Future<String> createInventoryObjects(CompositePoLine compPOL, BindItem bindItem, RequestContext requestContext) {
private Future<String> createInventoryObjects(CompositePoLine compPOL, String instanceId, BindItem bindItem, RequestContext requestContext) {
if (!Boolean.TRUE.equals(compPOL.getIsPackage())) {
instanceId = compPOL.getInstanceId();
}
var locationContext = createContextWithNewTenantId(requestContext, bindItem.getTenantId());
return handleInstance(compPOL.getInstanceId(), bindItem.getTenantId(), locationContext, requestContext)
.compose(instanceId -> handleHolding(bindItem, instanceId, locationContext))
return handleInstance(instanceId, bindItem.getTenantId(), locationContext, requestContext)
.compose(instId -> handleHolding(bindItem, instId, locationContext))
.compose(holdingId -> inventoryItemManager.createBindItem(compPOL, holdingId, bindItem, locationContext));
}

Expand Down Expand Up @@ -236,7 +251,8 @@ private BindPiecesResult prepareResponseBody(BindPiecesHolder holder) {

@Override
protected boolean isRevertToOnOrder(Piece piece) {
return false;
// Set to true for piece validation while fetching
return true;
}

@Override
Expand Down
19 changes: 19 additions & 0 deletions src/main/java/org/folio/orders/utils/CommonFields.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.folio.orders.utils;

public enum CommonFields {

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

private final String value;

CommonFields(String value) {
this.value = value;
}

public String getValue() {
return value;
}

}
5 changes: 5 additions & 0 deletions src/main/java/org/folio/orders/utils/HelperUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,11 @@ public static String extractId(JsonObject json) {
return json.getString(ID);
}

public static String extractCreatedDate(JsonObject json) {
return json.getJsonObject(CommonFields.METADATA.getValue())
.getString(CommonFields.CREATED_DATE.getValue());
}

public static CompositePurchaseOrder convertToCompositePurchaseOrder(PurchaseOrder purchaseOrder, List<PoLine> poLineList) {
List<CompositePoLine> compositePoLines = new ArrayList<>(poLineList.size());
if (CollectionUtils.isNotEmpty(poLineList)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ public enum ErrorCodes {
REQUEST_FOUND("thereAreRequestsOnItem", "There are requests on item"),
REQUESTS_ACTION_REQUIRED("requestsActionRequired", "There are circulation requests on items and requestsAction should be populated"),
PIECES_HAVE_DIFFERENT_RECEIVING_TENANT_IDS("piecesHaveDifferentReceivingTenantIds", "All pieces do not have the same receivingTenantId"),
PIECES_MUST_HAVE_RECEIVED_STATUS("piecesMustHaveReceivedStatus", "All pieces must have received status in order to be bound"),
BUDGET_EXPENSE_CLASS_NOT_FOUND("budgetExpenseClassNotFound", "Budget expense class not found"),
INACTIVE_EXPENSE_CLASS("inactiveExpenseClass", "Expense class is Inactive"),
HOLDINGS_BY_INSTANCE_AND_LOCATION_NOT_FOUND("holdingsByInstanceAndLocationNotFoundError", "Holdings by instance id %s and location id %s not found"),
Expand Down
20 changes: 9 additions & 11 deletions src/main/java/org/folio/service/CirculationRequestsRetriever.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
import static org.folio.service.inventory.InventoryUtils.REQUESTS;
import static org.folio.service.inventory.util.RequestFields.COLLECTION_RECORDS;
import static org.folio.service.inventory.util.RequestFields.COLLECTION_TOTAL;
import static org.folio.service.inventory.util.RequestFields.ID_KEY;
import static org.folio.service.inventory.util.RequestFields.ITEM_ID_KEY;
import static org.folio.service.inventory.util.RequestFields.REQUESTER_ID;
import static org.folio.service.inventory.util.RequestFields.ITEM_ID;

public class CirculationRequestsRetriever {

Expand All @@ -41,23 +41,21 @@ public Future<Integer> getNumberOfRequestsByItemId(String itemId, RequestContext
}

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

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

private Future<List<JsonObject>> getRequestsByIds(List<String> itemIds, RequestContext requestContext) {
private Future<List<JsonObject>> getRequestsByItemIds(List<String> itemIds, 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_KEY.getValue()), OUTSTANDING_REQUEST_STATUS_PREFIX))
.map(ids -> String.format("(%s and status=\"%s*\")", convertIdsToCqlQuery(ids, ITEM_ID.getValue()), OUTSTANDING_REQUEST_STATUS_PREFIX))
.map(query -> new RequestEntry(INVENTORY_LOOKUP_ENDPOINTS.get(REQUESTS))
.withQuery(query).withOffset(0).withLimit(Integer.MAX_VALUE))
.map(entry -> restClient.getAsJsonObject(entry, requestContext))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ public Future<List<String>> updateItemRecords(List<JsonObject> itemRecords, Requ
}

public Future<Void> updateItemWithPieceFields(Piece piece, RequestContext requestContext) {
if (piece.getItemId() == null || piece.getPoLineId() == null) {
if (piece.getItemId() == null || piece.getPoLineId() == null || piece.getIsBound()) {
return Future.succeededFuture();
}
String itemId = piece.getItemId();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,27 @@
import org.folio.rest.core.models.RequestEntry;
import org.folio.service.CirculationRequestsRetriever;

import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

import static org.folio.orders.utils.HelperUtils.extractCreatedDate;
import static org.folio.orders.utils.HelperUtils.extractId;
import static org.folio.service.inventory.InventoryUtils.INVENTORY_LOOKUP_ENDPOINTS;
import static org.folio.service.inventory.InventoryUtils.REQUESTS;
import static org.folio.service.inventory.util.RequestFields.DESTINATION_KEY;
import static org.folio.service.inventory.util.RequestFields.DESTINATION_ITEM_ID;
import static org.folio.service.inventory.util.RequestFields.STATUS;

public class InventoryItemRequestService {

private static final Logger logger = LogManager.getLogger(InventoryItemRequestService.class);

private static final String REQUEST_ENDPOINT = INVENTORY_LOOKUP_ENDPOINTS.get(REQUESTS) + "/%s";
private static final String REQUEST_MOVE_ENDPOINT = REQUEST_ENDPOINT + "/move";
private static final String REQUEST_CANCEL_STATUS = "Closed - Cancelled";

private final CirculationRequestsRetriever circulationRequestsRetriever;
private final RestClient restClient;
Expand All @@ -33,7 +40,7 @@ public InventoryItemRequestService(RestClient restClient, CirculationRequestsRet
this.circulationRequestsRetriever = circulationRequestsRetriever;
}

public Future<List<String>> getItemsWithActiveRequests(List<String> itemIds, RequestContext requestContext) {
public Future<List<String>> getItemIdsWithActiveRequests(List<String> itemIds, RequestContext requestContext) {
if (logger.isDebugEnabled()) {
logger.debug("getItemsWithActiveRequests:: Filtering itemIds with active requests: {}", itemIds);
}
Expand All @@ -44,35 +51,63 @@ public Future<List<String>> getItemsWithActiveRequests(List<String> itemIds, Req
.toList());
}

public Future<Void> cancelItemsRequests(List<String> itemIds, RequestContext requestContext) {
return handleItemsRequests(itemIds, requestContext, reqId -> cancelRequest(reqId, requestContext));
public Future<Void> cancelItemRequests(List<String> itemIds, RequestContext requestContext) {
return circulationRequestsRetriever.getRequesterIdsToRequestsByItemIds(itemIds, requestContext)
.compose(requesterToRequestsMap -> {
var requestsToCancel = requesterToRequestsMap.values().stream()
.flatMap(Collection::stream)
.toList();
return handleItemRequests(requestsToCancel, request -> cancelRequest(request, requestContext));
});
}

public Future<Void> transferItemsRequests(List<String> originItemIds, String destinationItemId, RequestContext requestContext) {
return handleItemsRequests(originItemIds, requestContext, reqId -> transferRequest(reqId, destinationItemId, requestContext));
public Future<Void> transferItemRequests(List<String> originItemIds, String destinationItemId, RequestContext requestContext) {
return circulationRequestsRetriever.getRequesterIdsToRequestsByItemIds(originItemIds, requestContext)
.compose(requesterToRequestsMap -> {
var requestsToCancel = new ArrayList<JsonObject>();
var requestsToTransfer = new ArrayList<JsonObject>();
for (var requests : requesterToRequestsMap.values()) {
requestsToCancel.addAll(requests);
requests.stream().min(this::compareRequests).ifPresent(request -> {
requestsToTransfer.add(request);
requestsToCancel.remove(request);
});
}

return handleItemRequests(requestsToTransfer, request -> transferRequest(request, destinationItemId, requestContext))
.compose(v -> handleItemRequests(requestsToCancel, request -> cancelRequest(request, requestContext)));
});
}

private Future<Void> handleItemsRequests(List<String> itemId, RequestContext requestContext, Function<String, Future<Void>> handler) {
return circulationRequestsRetriever.getRequestIdsByItemIds(itemId, requestContext)
.compose(reqIds -> {
var futures = reqIds.stream().map(handler).toList();
return GenericCompositeFuture.all(futures);
})
private Future<Void> handleItemRequests(List<JsonObject> requests, Function<JsonObject, Future<Void>> handler) {
return GenericCompositeFuture.all(
requests.stream()
.map(handler)
.toList())
.mapEmpty();
}

private Future<Void> cancelRequest(String reqId, RequestContext requestContext) {
logger.info("cancelRequest:: Cancelling Request with id='{}'", reqId);
private Future<Void> cancelRequest(JsonObject request, RequestContext requestContext) {
String reqId = extractId(request);
request.put(STATUS.getValue(), REQUEST_CANCEL_STATUS);
RequestEntry requestEntry = new RequestEntry(String.format(REQUEST_ENDPOINT, reqId));
return restClient.delete(requestEntry, requestContext);
logger.info("cancelRequest:: Cancelling Request with id='{}'", reqId);
return restClient.put(requestEntry, request, requestContext);
}

private Future<Void> transferRequest(JsonObject request, String itemId, RequestContext requestContext) {
String reqId = extractId(request);
JsonObject jsonObject = JsonObject.of(DESTINATION_ITEM_ID.getValue(), itemId);
RequestEntry requestEntry = new RequestEntry(String.format(REQUEST_MOVE_ENDPOINT, reqId));
logger.info("transferRequest:: Moving Request with id='{}' to item with id='{}'", reqId, itemId);
return restClient.postJsonObject(requestEntry, jsonObject, requestContext)
.mapEmpty();
}

private Future<Void> transferRequest(String reqId, String itemId, RequestContext requestContext) {
logger.info("transferRequest:: Moving Request with id='{}' to item with id='{}'", reqId, itemId);
JsonObject jsonObject = JsonObject.of(DESTINATION_KEY.getValue(), itemId);
RequestEntry requestEntry = new RequestEntry(String.format(REQUEST_MOVE_ENDPOINT, reqId));
return restClient.postJsonObject(requestEntry, jsonObject, requestContext)
.mapEmpty();
private int compareRequests(JsonObject r1, JsonObject r2) {
Instant createdDate1 = Instant.parse(extractCreatedDate(r1));
Instant createdDate2 = Instant.parse(extractCreatedDate(r2));
return createdDate1.compareTo(createdDate2);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

public enum RequestFields {

ID_KEY("id"),
ITEM_ID_KEY("itemId"),
DESTINATION_KEY("destinationItemId"),
ITEM_ID("itemId"),
REQUESTER_ID("itemId"),
STATUS("status"),
DESTINATION_ITEM_ID("destinationItemId"),
COLLECTION_RECORDS("requests"),
COLLECTION_TOTAL("totalRecords");

private String value;
private final String value;

RequestFields(String value) {
this.value = value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ private Future<Location> handleHolding(PieceUpdateHolder holder, RequestContext
private Future<String> handleItem(PieceUpdateHolder holder, RequestContext requestContext) {
CompositePoLine poLineToSave = holder.getPoLineToSave();
Piece pieceToUpdate = holder.getPieceToUpdate();
if (!DefaultPieceFlowsValidator.isCreateItemForPiecePossible(pieceToUpdate, poLineToSave)) {
if (!DefaultPieceFlowsValidator.isCreateItemForPiecePossible(pieceToUpdate, poLineToSave) || pieceToUpdate.getIsBound()) {
return Future.succeededFuture();
}
return inventoryItemManager.getItemRecordById(pieceToUpdate.getItemId(), true, requestContext)
Expand Down
35 changes: 35 additions & 0 deletions src/test/java/org/folio/rest/impl/CheckinReceivingApiTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
import static org.folio.rest.core.exceptions.ErrorCodes.LOC_NOT_PROVIDED;
import static org.folio.rest.core.exceptions.ErrorCodes.MULTIPLE_NONPACKAGE_TITLES;
import static org.folio.rest.core.exceptions.ErrorCodes.PIECES_HAVE_DIFFERENT_RECEIVING_TENANT_IDS;
import static org.folio.rest.core.exceptions.ErrorCodes.PIECES_MUST_HAVE_RECEIVED_STATUS;
import static org.folio.rest.core.exceptions.ErrorCodes.PIECE_ALREADY_RECEIVED;
import static org.folio.rest.core.exceptions.ErrorCodes.PIECE_NOT_FOUND;
import static org.folio.rest.core.exceptions.ErrorCodes.PIECE_NOT_RETRIEVED;
Expand Down Expand Up @@ -1245,6 +1246,40 @@ void testBindPiecesToTitleWithTransferRequestsAction() {
assertThat(pieceList.size(), is(1));
}

@Test
void testBindExpectedPieces() {
logger.info("=== Test POST Bind to Title with Expected Piece ");

var order = getMinimalContentCompositePurchaseOrder()
.withWorkflowStatus(CompositePurchaseOrder.WorkflowStatus.OPEN);
var poLine = getMinimalContentCompositePoLine(order.getId())
.withLocations(List.of(new Location().withHoldingId(UUID.randomUUID().toString())
.withQuantityPhysical(1).withQuantity(1)));
var bindingPiece = getMinimalContentPiece(poLine.getId())
.withItemId("522a501a-56b5-48d9-b28a-3a8f02482d98") // Present in mockdata/itemRequests/itemRequests.json
.withReceivingStatus(Piece.ReceivingStatus.EXPECTED)
.withFormat(org.folio.rest.jaxrs.model.Piece.Format.ELECTRONIC);
var bindPiecesCollection = new BindPiecesCollection()
.withPoLineId(poLine.getId())
.withBindItem(getMinimalContentBindItem()
.withLocationId(null)
.withHoldingId(UUID.randomUUID().toString()))
.withBindPieceIds(List.of(bindingPiece.getId()))
.withRequestsAction(BindPiecesCollection.RequestsAction.TRANSFER);

addMockEntry(PURCHASE_ORDER_STORAGE, order);
addMockEntry(PO_LINES_STORAGE, poLine);
addMockEntry(PIECES_STORAGE, bindingPiece);
addMockEntry(TITLES, getTitle(poLine));

var errors = verifyPostResponse(ORDERS_BIND_ENDPOINT, JsonObject.mapFrom(bindPiecesCollection).encode(),
prepareHeaders(EXIST_CONFIG_X_OKAPI_TENANT_LIMIT_10), APPLICATION_JSON, VALIDATION_ERROR)
.as(Errors.class)
.getErrors();

assertThat(errors.get(0).getMessage(), equalTo(PIECES_MUST_HAVE_RECEIVED_STATUS.getDescription()));
}

@Test
void testPostReceivingWithErrorSearchingForPiece() {
logger.info("=== Test POST Receiving - Receive resources with error searching for piece");
Expand Down
Loading

0 comments on commit 2c7ce43

Please sign in to comment.