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-1114] Add transferRequests flag to validate binding #936

Merged
merged 14 commits into from
May 16, 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
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
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This group of 4 methods are misplaced in "Errors" section. Please check comments after dashes in this class:
image
Actually this class is already big and hard to manage. We can easily move new methods to corresponding classes but maybe this type of refactoring increases the scope a bit.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will move above the errors, thanks. I guess yes, the class is pretty big and can be refactored, unsure if in scope of this.

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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is much like to what I did in getItemRecrods: lines 544-551. Maybe that place can reuse this?

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
Loading