Skip to content

Commit

Permalink
[MODORDERS-1114] Add transferRequests flag to validate binding (#936)
Browse files Browse the repository at this point in the history
* [MODORDERS-1114] Add transferRequests flag to validate binding

* [MODORDERS-1114] Add transferRequests flag to validate binding

* [MODORDERS-1114] Add new unit test for throwing error code

* [MODORDERS-1114] Update transfer logic

* [MODORDERS-1114] Replace transferRequests flag with an enum

* [MODORDERS-1114] Update acq-models

* [MODORDERS-1114] Update permissions

* [MODORDERS-1114] Fix comment numbering

* [MODORDERS-1114] Address comments

* [MODORDERS-1114] Modify leftover comment

* [MODORDERS-1114] Validate receivingTenantId if requests exist

* [MODORDERS-1114] Add support for pieces in different tenants

* [MODORDERS-1114] Minor refactoring and simplifications

* [MODORDERS-1114] Filter out pieces with invalid itemIds when mapping by tenants
  • Loading branch information
Saba-Zedginidze-EPAM authored May 16, 2024
1 parent 4eab561 commit f285901
Show file tree
Hide file tree
Showing 13 changed files with 210 additions and 111 deletions.
4 changes: 3 additions & 1 deletion descriptors/ModuleDescriptor-template.json
Original file line number Diff line number Diff line change
Expand Up @@ -1100,7 +1100,9 @@
"orders-storage.titles.item.put",
"inventory-storage.items.item.post",
"acquisitions-units-storage.units.collection.get",
"acquisitions-units-storage.memberships.collection.get"
"acquisitions-units-storage.memberships.collection.get",
"circulation.requests.collection.get",
"circulation.requests.item.move.post"
]
}
]
Expand Down
2 changes: 1 addition & 1 deletion ramls/acq-models
130 changes: 88 additions & 42 deletions src/main/java/org/folio/helper/BindHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
import io.vertx.core.Context;
import io.vertx.core.Future;
import io.vertx.core.json.JsonObject;
import one.util.streamex.StreamEx;
import org.folio.models.ItemFields;
import org.folio.okapi.common.GenericCompositeFuture;
import org.folio.orders.utils.PoLineCommonUtil;
import org.folio.orders.utils.RequestContextUtil;
import org.folio.rest.RestConstants;
import org.folio.rest.core.exceptions.ErrorCodes;
import org.folio.rest.core.exceptions.HttpException;
import org.folio.rest.core.models.RequestContext;
import org.folio.rest.jaxrs.model.BindPiecesCollection;
Expand All @@ -17,19 +20,26 @@
import org.folio.rest.jaxrs.model.ReceivingResult;
import org.folio.rest.jaxrs.model.ReceivingResults;
import org.folio.rest.jaxrs.model.Title;
import org.folio.service.inventory.InventoryItemRequestService;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

import static org.folio.rest.jaxrs.model.BindPiecesCollection.RequestsAction.TRANSFER;


public class BindHelper extends CheckinReceivePiecesHelper<BindPiecesCollection> {

private static final String TITLE_BY_POLINE_QUERY = "poLineId==%s";

@Autowired
private InventoryItemRequestService inventoryItemRequestService;

public BindHelper(BindPiecesCollection bindPiecesCollection,
Map<String, String> okapiHeaders, Context ctx) {
super(okapiHeaders, ctx);
Expand All @@ -55,48 +65,83 @@ public Future<ReceivingResults> bindPieces(BindPiecesCollection bindPiecesCollec
}

private Future<ReceivingResults> processBindPieces(BindPiecesCollection bindPiecesCollection, RequestContext requestContext) {
// 1. Get piece records from storage
// 1. Get piece records from storage
return retrievePieceRecords(requestContext)
// 2. Update piece isBound flag
// 2. Check if there are any outstanding requests for items
.compose(piecesGroupedByPoLine -> checkRequestsForPieceItems(piecesGroupedByPoLine, bindPiecesCollection, requestContext))
// 3. Update piece isBound flag
.map(this::updatePieceRecords)
// 3. Update currently associated items
.map(piecesGroupedByPoLine -> updateItemStatus(piecesGroupedByPoLine, requestContext))
// 4. Crate item for pieces with specific fields
// 4. Update currently associated items
.compose(piecesGroupedByPoLine -> updateItemStatus(piecesGroupedByPoLine, requestContext))
// 5. Crate item for pieces with specific fields
.compose(piecesGroupedByPoLine -> createItemForPiece(piecesGroupedByPoLine, bindPiecesCollection, requestContext))
// 5. Update received piece records in the storage
// 6. Update received piece records in the storage
.compose(piecesGroupedByPoLine -> storeUpdatedPieceRecords(piecesGroupedByPoLine, requestContext))
// 6. Update Title with new bind items
// 7. Update Title with new bind items
.map(piecesGroupedByPoLine -> updateTitleWithBindItems(piecesGroupedByPoLine, requestContext))
// 7. Return results to the client
// 8. Return results to the client
.map(piecesGroupedByPoLine -> prepareResponseBody(piecesGroupedByPoLine, bindPiecesCollection));
}

private Future<Map<String, List<Piece>>> checkRequestsForPieceItems(Map<String, List<Piece>> piecesGroupedByPoLine,
BindPiecesCollection bindPiecesCollection,
RequestContext requestContext) {
var tenantToItem = mapTenantIdsToItemIds(piecesGroupedByPoLine, requestContext);
return GenericCompositeFuture.all(
tenantToItem.entrySet().stream()
.map(entry -> {
var locationContext = RequestContextUtil.createContextWithNewTenantId(requestContext, entry.getKey());
return inventoryItemRequestService.getItemsWithActiveRequests(entry.getValue(), locationContext)
.compose(items -> {
if (items.isEmpty()) {
return Future.succeededFuture();
}

// requestsAction is required to handle outstanding requests
if (Objects.isNull(bindPiecesCollection.getRequestsAction())) {
logger.warn("checkRequestsForPieceItems:: Found outstanding requests on items with ids: {}", items);
throw new HttpException(RestConstants.VALIDATION_ERROR, ErrorCodes.REQUESTS_ACTION_REQUIRED);
}

// When requests are to be transferred check if pieces contain different receivingTenantIds
// Transferring requests between tenants is not a requirement for R
if (bindPiecesCollection.getRequestsAction().equals(TRANSFER) && !tenantToItem.keySet().isEmpty()) {
logger.warn("checkRequestsForPieceItems:: All pieces do not have the same receivingTenantId: {}", tenantToItem.keySet());
throw new HttpException(RestConstants.VALIDATION_ERROR, ErrorCodes.PIECES_HAVE_DIFFERENT_RECEIVING_TENANT_IDS);
}
return Future.succeededFuture();
});
})
.toList())
.map(f -> piecesGroupedByPoLine);
}

private Map<String, List<Piece>> updatePieceRecords(Map<String, List<Piece>> piecesGroupedByPoLine) {
logger.debug("updatePieceRecords:: Updating the piece records to set isBound flag as TRUE");
piecesGroupedByPoLine.values().stream()
.flatMap(List::stream)
extractAllPieces(piecesGroupedByPoLine)
.forEach(piece -> piece.setIsBound(true));
return piecesGroupedByPoLine;
}

private Map<String, List<Piece>> updateItemStatus(Map<String, List<Piece>> piecesGroupedByPoLine,
RequestContext requestContext) {
private Future<Map<String, List<Piece>>> updateItemStatus(Map<String, List<Piece>> piecesGroupedByPoLine, RequestContext requestContext) {
logger.debug("updateItemStatus:: Updating previous item status to 'Unavailable'");
var itemIds = piecesGroupedByPoLine.values()
.stream().flatMap(List::stream)
.map(Piece::getItemId).toList();
inventoryItemManager.getItemRecordsByIds(itemIds, requestContext)
.compose(items -> {
items.forEach(item -> {
logger.info("updateItemStatus:: '{}' item status set to 'Unavailable'", item.getString(ItemFields.ID.value()));
item.put(ItemFields.STATUS.value(), new JsonObject()
.put(ItemFields.STATUS_DATE.value(), new Date())
.put(ItemFields.STATUS_NAME.value(), ReceivedItem.ItemStatus.UNAVAILABLE));
}
);
return inventoryItemManager.updateItemRecords(items, requestContext);
});
return piecesGroupedByPoLine;
return GenericCompositeFuture.all(
mapTenantIdsToItemIds(piecesGroupedByPoLine, requestContext).entrySet().stream()
.map(entry -> {
var locationContext = RequestContextUtil.createContextWithNewTenantId(requestContext, entry.getKey());
return inventoryItemManager.getItemRecordsByIds(entry.getValue(), locationContext)
.compose(items -> {
items.forEach(item -> {
logger.info("updateItemStatus:: '{}' item status set to 'Unavailable'", item.getString(ItemFields.ID.value()));
item.put(ItemFields.STATUS.value(), new JsonObject()
.put(ItemFields.STATUS_DATE.value(), new Date())
.put(ItemFields.STATUS_NAME.value(), ReceivedItem.ItemStatus.UNAVAILABLE));
});
return inventoryItemManager.updateItemRecords(items, locationContext);
});
})
.toList()
).map(f -> piecesGroupedByPoLine);
}

private Future<Map<String, List<Piece>>> createItemForPiece(Map<String, List<Piece>> piecesGroupedByPoLine,
Expand All @@ -112,11 +157,16 @@ private Future<Map<String, List<Piece>>> createItemForPiece(Map<String, List<Pie
.map(PoLineCommonUtil::convertToCompositePoLine)
.compose(compPOL ->
inventoryItemManager.createBindItem(compPOL, holdingIds.get(0), bindPiecesCollection.getBindItem(), requestContext))
.map(itemId -> {
piecesGroupedByPoLine.get(poLineId).forEach(piece -> piece.setItemId(itemId));
return piecesGroupedByPoLine;
.map(newItemId -> {
// Move requests if requestsAction is TRANSFER, otherwise do nothing
if (TRANSFER.equals(bindPiecesCollection.getRequestsAction())) {
var itemIds = extractAllPieces(piecesGroupedByPoLine).map(Piece::getItemId).toList();
inventoryItemRequestService.transferItemsRequests(itemIds, newItemId, requestContext);
}
);
// Set new item ids for pieces
piecesGroupedByPoLine.get(poLineId).forEach(piece -> piece.setItemId(newItemId));
return piecesGroupedByPoLine;
});
}

private void validateHoldingIds(List<String> holdingIds, BindPiecesCollection bindPiecesCollection) {
Expand Down Expand Up @@ -155,21 +205,17 @@ private ReceivingResults prepareResponseBody(Map<String, List<Piece>> piecesGrou
String poLineId = bindPiecesCollection.getPoLineId();

// 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, Piece> processedPiecesForPoLine = getProcessedPiecesForPoLine(poLineId, piecesGroupedByPoLine);

Map<String, Integer> resultCounts = new HashMap<>();
resultCounts.put(ProcessingStatus.Type.SUCCESS.toString(), 0);
resultCounts.put(ProcessingStatus.Type.FAILURE.toString(), 0);
var resultCounts = getEmptyResultCounts();
ReceivingResult result = new ReceivingResult();
for (String pieceId : bindPiecesCollection.getBindPieceIds()) {
calculateProcessingErrors(poLineId, result, processedPiecesForPoLine, resultCounts, pieceId);
}

result.withPoLineId(poLineId)
.withProcessedSuccessfully(resultCounts.get(ProcessingStatus.Type.SUCCESS.toString()))
.withProcessedWithError(resultCounts.get(ProcessingStatus.Type.FAILURE.toString()));
.withProcessedSuccessfully(resultCounts.get(ProcessingStatus.Type.SUCCESS))
.withProcessedWithError(resultCounts.get(ProcessingStatus.Type.FAILURE));
return new ReceivingResults()
.withTotalRecords(1)
.withReceivingResults(List.of(result));
Expand Down
16 changes: 5 additions & 11 deletions src/main/java/org/folio/helper/CheckinHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
Expand Down Expand Up @@ -124,22 +123,18 @@ private ReceivingResults prepareResponseBody(CheckinCollection checkinCollection
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, Piece> processedPiecesForPoLine = getProcessedPiecesForPoLine(poLineId, piecesGroupedByPoLine);

Map<String, Integer> resultCounts = new HashMap<>();
resultCounts.put(ProcessingStatus.Type.SUCCESS.toString(), 0);
resultCounts.put(ProcessingStatus.Type.FAILURE.toString(), 0);
Map<ProcessingStatus.Type, Integer> resultCounts = getEmptyResultCounts();
for (CheckInPiece checkinPiece : toBeCheckedIn.getCheckInPieces()) {
String pieceId = checkinPiece.getId();

calculateProcessingErrors(poLineId, result, processedPiecesForPoLine, resultCounts, pieceId);
}

result.withPoLineId(poLineId)
.withProcessedSuccessfully(resultCounts.get(ProcessingStatus.Type.SUCCESS.toString()))
.withProcessedWithError(resultCounts.get(ProcessingStatus.Type.FAILURE.toString()));
.withProcessedSuccessfully(resultCounts.get(ProcessingStatus.Type.SUCCESS))
.withProcessedWithError(resultCounts.get(ProcessingStatus.Type.FAILURE));
}

return results;
Expand Down Expand Up @@ -231,8 +226,7 @@ private void updatePieceWithCheckinInfo(Piece piece) {

@Override
protected Map<String, List<Piece>> updatePieceRecordsWithoutItems(Map<String, List<Piece>> piecesGroupedByPoLine) {
StreamEx.ofValues(piecesGroupedByPoLine)
.flatMap(List::stream)
extractAllPieces(piecesGroupedByPoLine)
.filter(piece -> StringUtils.isEmpty(piece.getItemId()))
.forEach(this::updatePieceWithCheckinInfo);

Expand Down
72 changes: 43 additions & 29 deletions src/main/java/org/folio/helper/CheckinReceivePiecesHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@
import java.util.function.Function;
import java.util.stream.Collectors;

import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.toList;
import static org.apache.commons.lang3.StringUtils.EMPTY;
import static org.folio.orders.utils.HelperUtils.collectResultsOnSuccess;
import static org.folio.orders.utils.ResourcePathResolver.PIECES_STORAGE;
Expand Down Expand Up @@ -465,8 +467,7 @@ private Future<Void> processHoldingsUpdate(Map<String, List<Piece>> piecesGroupe
PoLineAndTitleById poLinesAndTitlesById,
RequestContext requestContext) {
List<Future<Boolean>> futuresForHoldingsUpdates = new ArrayList<>();
StreamEx.ofValues(piecesGroupedByPoLine)
.flatMap(List::stream)
extractAllPieces(piecesGroupedByPoLine)
.forEach(piece -> {
CompositePoLine poLine = poLinesAndTitlesById.poLineById.get(piece.getPoLineId());
if (poLine == null) {
Expand Down Expand Up @@ -539,30 +540,18 @@ private boolean ifHoldingNotProcessed(String key) {
private Future<List<JsonObject>> getItemRecords(Map<String, List<Piece>> piecesGroupedByPoLine,
Map<String, Piece> piecesByItemId,
RequestContext requestContext) {

Map<String, List<String>> itemIdsByTenantId = new HashMap<>();
for (List<Piece> pieceList : piecesGroupedByPoLine.values()) {
for (Piece piece : pieceList) {
if (StringUtils.isNotEmpty(piece.getItemId())) {
itemIdsByTenantId.computeIfAbsent(piece.getReceivingTenantId(), k -> new ArrayList<>()).add(piece.getItemId());
}
}
}

// split all id lists by maximum number of id's for get query
List<Future<List<JsonObject>>> futures = itemIdsByTenantId.entrySet()
.stream()
.flatMap(
entry -> StreamEx.ofSubLists(entry.getValue(), MAX_IDS_FOR_GET_RQ_15)
.map(ids -> {
var locationContext = RequestContextUtil.createContextWithNewTenantId(requestContext, entry.getKey());
return getItemRecordsByIds(ids, piecesByItemId, locationContext);
})
)
.toList();

return collectResultsOnSuccess(futures)
.map(lists -> StreamEx.of(lists).toFlatList(jsonObjects -> jsonObjects));
// Split all id lists by maximum number of id's for get query
return collectResultsOnSuccess(
mapTenantIdsToItemIds(piecesGroupedByPoLine, requestContext).entrySet().stream()
.flatMap(entry ->
StreamEx.ofSubLists(entry.getValue(), MAX_IDS_FOR_GET_RQ_15)
.map(ids -> {
var locationContext = RequestContextUtil.createContextWithNewTenantId(requestContext, entry.getKey());
return getItemRecordsByIds(ids, piecesByItemId, locationContext);
})
)
.toList())
.map(lists -> StreamEx.of(lists).toFlatList(items -> items));
}

/**
Expand Down Expand Up @@ -767,6 +756,31 @@ private List<String> getPieceIds() {
.toFlatList(ids -> ids);
}

protected Map<String, Piece> getProcessedPiecesForPoLine(String poLineId, Map<String, List<Piece>> piecesGroupedByPoLine) {
return StreamEx
.of(piecesGroupedByPoLine.getOrDefault(poLineId, Collections.emptyList()))
.toMap(Piece::getId, piece -> piece);
}

protected Map<ProcessingStatus.Type, Integer> getEmptyResultCounts() {
Map<ProcessingStatus.Type, Integer> resultCounts = new HashMap<>();
resultCounts.put(ProcessingStatus.Type.SUCCESS, 0);
resultCounts.put(ProcessingStatus.Type.FAILURE, 0);
return resultCounts;
}

protected StreamEx<Piece> extractAllPieces(Map<String, List<Piece>> piecesGroupedByPoLine) {
return StreamEx.ofValues(piecesGroupedByPoLine).flatMap(List::stream);
}

protected Map<String, List<String>> mapTenantIdsToItemIds(Map<String, List<Piece>> piecesGroupedByPoLine, RequestContext requestContext) {
return extractAllPieces(piecesGroupedByPoLine)
.filter(piece -> StringUtils.isNotEmpty(piece.getItemId()))
.groupingBy(piece -> Optional.ofNullable(piece.getReceivingTenantId())
.orElse(RequestContextUtil.getContextTenantId(requestContext)),
mapping(Piece::getItemId, toList()));
}

//-------------------------------------------------------------------------------------
/*
Errors
Expand Down Expand Up @@ -813,17 +827,17 @@ private void addError(String polId, String pieceId, Error error) {

public void calculateProcessingErrors(String poLineId, ReceivingResult result,
Map<String, Piece> processedPiecesForPoLine,
Map<String, Integer> resultCounts, String pieceId) {
Map<ProcessingStatus.Type, Integer> resultCounts, String pieceId) {
// Calculate processing status
ProcessingStatus status = new ProcessingStatus();
Error error = getError(poLineId, pieceId);
if (processedPiecesForPoLine.get(pieceId) != null && error == null) {
status.setType(ProcessingStatus.Type.SUCCESS);
resultCounts.merge(ProcessingStatus.Type.SUCCESS.toString(), 1, Integer::sum);
resultCounts.merge(ProcessingStatus.Type.SUCCESS, 1, Integer::sum);
} else {
status.setType(ProcessingStatus.Type.FAILURE);
status.setError(error);
resultCounts.merge(ProcessingStatus.Type.FAILURE.toString(), 1, Integer::sum);
resultCounts.merge(ProcessingStatus.Type.FAILURE, 1, Integer::sum);
}

ReceivingItemResult itemResult = new ReceivingItemResult();
Expand Down
Loading

0 comments on commit f285901

Please sign in to comment.