Skip to content
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
8 changes: 8 additions & 0 deletions ramls/check-in.raml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,11 @@ resourceTypes:
is: [validate]
post:
description: Check-in items spanning one or more po_lines in this order
queryParameters:
deleteHoldings:
displayName: The associated holdings should be removed
description: The associated holdings should be removed
type: boolean
required: false
example: true
default: false
33 changes: 23 additions & 10 deletions src/main/java/org/folio/helper/CheckinHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.apache.commons.lang3.tuple.Pair;
import org.folio.models.pieces.PiecesHolder;
import org.folio.orders.events.handlers.MessageAddress;
import org.folio.orders.utils.StreamUtils;
import org.folio.rest.core.models.RequestContext;
import org.folio.rest.jaxrs.model.CheckInPiece;
import org.folio.rest.jaxrs.model.CheckinCollection;
Expand All @@ -22,6 +23,8 @@
import org.folio.rest.jaxrs.model.ReceivingResults;
import org.folio.rest.jaxrs.model.ToBeCheckedIn;
import org.folio.service.inventory.InventoryUtils;
import org.folio.service.pieces.PieceUpdateInventoryService;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.ArrayList;
import java.util.Collection;
Expand All @@ -40,8 +43,10 @@ public class CheckinHelper extends CheckinReceivePiecesHelper<CheckInPiece> {

public static final String IS_ITEM_ORDER_CLOSED_PRESENT = "isItemOrderClosedPresent";

public CheckinHelper(CheckinCollection checkinCollection, Map<String, String> okapiHeaders,
Context ctx) {
@Autowired
private PieceUpdateInventoryService pieceUpdateInventoryService;

public CheckinHelper(CheckinCollection checkinCollection, Map<String, String> okapiHeaders, Context ctx) {
super(okapiHeaders, ctx);
// Convert request to map representation
CheckinCollection checkinCollectionClone = JsonObject.mapFrom(checkinCollection).mapTo(CheckinCollection.class);
Expand All @@ -57,12 +62,12 @@ public CheckinHelper(CheckinCollection checkinCollection, Map<String, String> ok
}
}

public Future<ReceivingResults> checkinPieces(CheckinCollection checkinCollection, RequestContext requestContext) {
public Future<ReceivingResults> checkinPieces(CheckinCollection checkinCollection, boolean deleteHoldings, RequestContext requestContext) {
return removeForbiddenEntities(requestContext)
.compose(voidResult -> processCheckInPieces(checkinCollection, requestContext));
.compose(voidResult -> processCheckInPieces(checkinCollection, deleteHoldings, requestContext));
}

private Future<ReceivingResults> processCheckInPieces(CheckinCollection checkinCollection, RequestContext requestContext) {
private Future<ReceivingResults> processCheckInPieces(CheckinCollection checkinCollection, boolean deleteHoldings, RequestContext requestContext) {
PiecesHolder holder = new PiecesHolder();
// 1. Get purchase order and poLine from storage
return findAndSetPurchaseOrderPoLinePair(extractPoLineId(checkinCollection), holder, requestContext)
Expand All @@ -72,14 +77,15 @@ private Future<ReceivingResults> processCheckInPieces(CheckinCollection checkinC
.compose(piecesByPoLineIds -> filterMissingLocations(piecesByPoLineIds, requestContext))
// 4. Update items in the Inventory if required
.compose(pieces -> updateInventoryItemsAndHoldings(pieces, holder, requestContext))
// 5. Update piece records with checkIn details which do not have
// associated item
// 5. Delete abandoned holdings if deleteHoldings flag is set to true
.compose(pieces -> deleteHoldings(pieces, deleteHoldings, requestContext))
// 6. Update piece records with checkIn details which do not have an associated item
.map(this::updatePieceRecordsWithoutItems)
// 6. Update received piece records in the storage
// 7. Update received piece records in the storage
.compose(piecesGroupedByPoLine -> storeUpdatedPieceRecords(piecesGroupedByPoLine, requestContext))
// 7. Update PO Line status
// 8. Update PO Line status
.compose(piecesGroupedByPoLine -> updateOrderAndPoLinesStatus(holder.getPiecesFromStorage(), piecesGroupedByPoLine, checkinCollection, requestContext))
// 8. Return results to the client
// 9. Return results to the client
.map(piecesGroupedByPoLine -> prepareResponseBody(checkinCollection, piecesGroupedByPoLine));
}

Expand Down Expand Up @@ -205,6 +211,13 @@ protected Future<Boolean> receiveInventoryItemAndUpdatePiece(PiecesHolder holder
return promise.future();
}

private Future<Map<String, List<Piece>>> deleteHoldings(Map<String, List<Piece>> piecesGroupedByPoLine, boolean deleteHoldings, RequestContext requestContext) {
return deleteHoldings
? pieceUpdateInventoryService.deleteHoldingsConnectedToPieces(StreamUtils.flatten(piecesGroupedByPoLine.values()), requestContext).map(piecesGroupedByPoLine)
: Future.succeededFuture(piecesGroupedByPoLine);

}

private void updatePieceWithCheckinInfo(Piece piece) {
CheckInPiece checkinPiece = getByPiece(piece);

Expand Down
4 changes: 4 additions & 0 deletions src/main/java/org/folio/orders/utils/StreamUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ public static <K, V> Set<V> mapToSet(Collection<K> collection, Function<K, V> ma
return collection.stream().map(mapper).collect(Collectors.toSet());
}

public static <T, C extends Collection<T>> List<T> flatten(Collection<C> collection) {
return collection.stream().flatMap(Collection::stream).toList();
}

public static <T, K> Map<K, T> listToMap(Collection<T> collection, Function<T, K> toKey) {
return listToMap(collection, toKey, Function.identity());
}
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/org/folio/rest/impl/ReceivingAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,11 @@ public void postOrdersReceive(ReceivingCollection entity, Map<String, String> ok

@Override
@Validate
public void postOrdersCheckIn(CheckinCollection entity, Map<String, String> okapiHeaders,
public void postOrdersCheckIn(boolean deleteHoldings, CheckinCollection entity, Map<String, String> okapiHeaders,
Handler<AsyncResult<Response>> asyncResultHandler, Context vertxContext) {
logger.debug("CheckIn {} items", entity.getTotalRecords());
CheckinHelper helper = new CheckinHelper(entity, okapiHeaders, vertxContext);
helper.checkinPieces(entity, new RequestContext(vertxContext, okapiHeaders))
helper.checkinPieces(entity, deleteHoldings, new RequestContext(vertxContext, okapiHeaders))
.onSuccess(result -> asyncResultHandler.handle(succeededFuture(helper.buildOkResponse(result))))
.onFailure(t -> handleErrorResponse(asyncResultHandler, helper, t));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package org.folio.service.pieces;

import static org.folio.orders.utils.HelperUtils.collectResultsOnSuccess;
import static org.folio.orders.utils.RequestContextUtil.createContextWithNewTenantId;
import static org.folio.service.inventory.InventoryHoldingManager.HOLDING_PERMANENT_LOCATION_ID;

import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import one.util.streamex.StreamEx;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.folio.rest.core.models.RequestContext;
import org.folio.rest.jaxrs.model.PoLine;
import org.folio.rest.jaxrs.model.CompositePurchaseOrder;
Expand All @@ -21,68 +25,75 @@
import io.vertx.core.Future;
import io.vertx.core.json.JsonObject;

@Log4j2
@RequiredArgsConstructor
public class PieceUpdateInventoryService {
private static final Logger logger = LogManager.getLogger(PieceUpdateInventoryService.class);

private static final int ITEM_QUANTITY = 1;

private final InventoryItemManager inventoryItemManager;
private final InventoryHoldingManager inventoryHoldingManager;
private final PieceStorageService pieceStorageService;

public PieceUpdateInventoryService(InventoryItemManager inventoryItemManager,
InventoryHoldingManager inventoryHoldingManager,
PieceStorageService pieceStorageService) {
this.inventoryItemManager = inventoryItemManager;
this.inventoryHoldingManager = inventoryHoldingManager;
this.pieceStorageService = pieceStorageService;
}

/**
* Return id of created Item
*/
public Future<String> manualPieceFlowCreateItemRecord(Piece piece, CompositePurchaseOrder compPO,
PoLine poLine, RequestContext requestContext) {
final int ITEM_QUANTITY = 1;
logger.debug("manualPieceFlowCreateItemRecord:: Handling {} items for PO Line and holdings with id={}, receivingTenantId={}",
log.debug("manualPieceFlowCreateItemRecord:: Handling {} items for PO Line and holdings with id={}, receivingTenantId={}",
ITEM_QUANTITY, piece.getHoldingId(), piece.getReceivingTenantId());
if (piece.getFormat() == Piece.Format.ELECTRONIC && DefaultPieceFlowsValidator.isCreateItemForElectronicPiecePossible(piece, poLine)) {
return inventoryItemManager.createMissingElectronicItems(compPO, poLine, piece, ITEM_QUANTITY, requestContext)
.map(idS -> idS.get(0));
.map(List::getFirst);
} else if (DefaultPieceFlowsValidator.isCreateItemForNonElectronicPiecePossible(piece, poLine)) {
return inventoryItemManager.createMissingPhysicalItems(compPO, poLine, piece, ITEM_QUANTITY, requestContext)
.map(idS -> idS.get(0));
.map(List::getFirst);
} else {
logger.warn("manualPieceFlowCreateItemRecord:: Creating Item is not possible for piece: {}, poLine: {}",
log.warn("manualPieceFlowCreateItemRecord:: Creating Item is not possible for piece: {}, poLine: {}",
piece.getId(), poLine.getId());
return Future.succeededFuture();
}
}

public Future<Pair<String, String>> deleteHoldingConnectedToPiece(Piece piece, RequestContext requestContext) {
if (piece == null || piece.getHoldingId() == null) {
return Future.succeededFuture();
}
var locationContext = createContextWithNewTenantId(requestContext, piece.getReceivingTenantId());
String holdingId = piece.getHoldingId();
return inventoryHoldingManager.getHoldingById(holdingId, true, locationContext)
.compose(holding -> getUpdatePossibleForHolding(holding, holdingId, piece, locationContext, requestContext))
.compose(isUpdatePossibleVsHolding -> deleteHoldingIfPossible(isUpdatePossibleVsHolding, holdingId, locationContext));
return deleteHoldingsConnectedToPieces(Optional.ofNullable(piece).map(List::of).orElseGet(List::of), requestContext)
.compose(result -> Optional.ofNullable(result).map(List::getFirst).map(Future::succeededFuture).orElseGet(Future::succeededFuture));
}

public Future<List<Pair<String, String>>> deleteHoldingsConnectedToPieces(List<Piece> pieces, RequestContext requestContext) {
var viablePieces = StreamEx.of(pieces)
.filter(piece -> piece != null && piece.getHoldingId() != null)
.toList();
return viablePieces.isEmpty()
? Future.succeededFuture()
: collectResultsOnSuccess(doDeleteHolding(viablePieces, requestContext));
}

private List<Future<Pair<String, String>>> doDeleteHolding(List<Piece> pieces, RequestContext requestContext) {
return StreamEx.of(pieces)
.groupingBy(piece -> Pair.of(piece.getHoldingId(), piece.getReceivingTenantId()), Collectors.mapping(Piece::getId, Collectors.toSet()))
.entrySet().stream()
.map(entry -> {
var holdingId = entry.getKey().getKey();
var receivingTenantId = entry.getKey().getValue();
var pieceIds = entry.getValue();
var locationContext = createContextWithNewTenantId(requestContext, receivingTenantId);
return inventoryHoldingManager.getHoldingById(holdingId, true, locationContext)
.compose(holding -> getUpdatePossibleForHolding(holding, holdingId, pieceIds, locationContext, requestContext))
.compose(isUpdatePossibleVsHolding -> deleteHoldingIfPossible(isUpdatePossibleVsHolding, holdingId, locationContext));
}).toList();
}

private Future<Pair<Boolean, JsonObject>> getUpdatePossibleForHolding(JsonObject holding, String holdingId, Piece piece,
private Future<Pair<Boolean, JsonObject>> getUpdatePossibleForHolding(JsonObject holding, String holdingId, Set<String> pieceIds,
RequestContext locationContext, RequestContext requestContext) {
if (holding == null || holding.isEmpty()) {
return Future.succeededFuture(Pair.of(false, new JsonObject()));
}
return pieceStorageService.getPiecesByHoldingId(holdingId, requestContext)
.map(pieces -> skipPieceToProcess(piece, pieces))
.compose(existingPieces -> inventoryItemManager.getItemsByHoldingId(holdingId, locationContext)
.map(existingItems-> {
List<Piece> remainingPieces = skipPieceToProcess(piece, existingPieces);
if (CollectionUtils.isEmpty(remainingPieces) && CollectionUtils.isEmpty(existingItems)) {
return Pair.of(true, holding);
}
return Pair.of(false, new JsonObject());
})
.compose(pieces -> inventoryItemManager.getItemsByHoldingId(holdingId, locationContext)
.map(items -> CollectionUtils.isEmpty(filterPiecesToProcess(pieceIds, pieces)) && CollectionUtils.isEmpty(items)
? Pair.of(true, holding)
: Pair.of(false, new JsonObject()))
);
}

Expand All @@ -91,15 +102,16 @@ private Future<Pair<String, String>> deleteHoldingIfPossible(Pair<Boolean, JsonO
var isUpdatePossible = isUpdatePossibleVsHolding.getKey();
var holding = isUpdatePossibleVsHolding.getValue();
if (isUpdatePossible && !holding.isEmpty()) {
String permanentLocationId = holding.getString(HOLDING_PERMANENT_LOCATION_ID);
return inventoryHoldingManager.deleteHoldingById(holdingId, true, locationContext)
.map(v -> Pair.of(holdingId, permanentLocationId));
.map(v -> Pair.of(holdingId, holding.getString(HOLDING_PERMANENT_LOCATION_ID)));
}
return Future.succeededFuture();
}

private List<Piece> skipPieceToProcess(Piece piece, List<Piece> pieces) {
return pieces.stream().filter(aPiece -> !aPiece.getId().equals(piece.getId())).collect(
Collectors.toList());
private List<Piece> filterPiecesToProcess(Set<String> pieceIdsToSkip, List<Piece> pieces) {
return pieces.stream()
.filter(piece -> !pieceIdsToSkip.contains(piece.getId()))
.toList();
}

}
15 changes: 9 additions & 6 deletions src/test/java/org/folio/helper/CheckinHelperTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
import org.folio.rest.jaxrs.model.Piece;
import org.folio.rest.jaxrs.model.ToBeCheckedIn;
import org.folio.service.ProtectionService;
import org.folio.service.pieces.PieceUpdateInventoryService;
import org.folio.service.settings.CommonSettingsRetriever;
import org.folio.service.inventory.InventoryHoldingManager;
import org.folio.service.inventory.InventoryInstanceManager;
Expand All @@ -66,11 +67,11 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;

@ExtendWith(VertxExtension.class)
@ExtendWith({VertxExtension.class, MockitoExtension.class})
public class CheckinHelperTest {

@Autowired
Expand All @@ -80,6 +81,8 @@ public class CheckinHelperTest {
@Autowired
PieceUpdateFlowPoLineService pieceUpdateFlowPoLineService;
@Autowired
PieceUpdateInventoryService pieceUpdateInventoryService;
@Autowired
ItemRecreateInventoryService itemRecreateInventoryService;
@Autowired
PurchaseOrderStorageService purchaseOrderStorageService;
Expand Down Expand Up @@ -108,7 +111,6 @@ public static void after() {

@BeforeEach
void beforeEach() {
MockitoAnnotations.openMocks(this);
autowireDependencies(this);
var ctxMock = getFirstContextFromVertx(getVertx());
okapiHeadersMock = new HashMap<>();
Expand Down Expand Up @@ -327,17 +329,18 @@ private static class ContextConfiguration {
PieceCreateFlowInventoryManager pieceCreateFlowInventoryManager() {
return mock(PieceCreateFlowInventoryManager.class);
}

@Bean
PieceUpdateFlowPoLineService pieceUpdateFlowPoLineService() {
return mock(PieceUpdateFlowPoLineService.class);
}

@Bean
PieceUpdateInventoryService pieceUpdateInventoryService() {
return mock(PieceUpdateInventoryService.class);
}
@Bean
CommonSettingsRetriever configurationEntriesService() {
return mock(CommonSettingsRetriever.class);
}

@Bean
ProtectionService protectionService() {
return mock(ProtectionService.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,18 @@
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;

import io.vertx.core.Context;
import io.vertx.core.json.JsonObject;

@ExtendWith(MockitoExtension.class)
public class PieceUpdateInventoryServiceTest {

@Autowired
private PieceUpdateInventoryService pieceUpdateInventoryService;
@Autowired
Expand All @@ -54,14 +57,13 @@ public class PieceUpdateInventoryServiceTest {
private PieceStorageService pieceStorageService;
@Mock
private Map<String, String> okapiHeadersMock;
private final Context ctx = getFirstContextFromVertx(getVertx());

private final Context ctx = getFirstContextFromVertx(getVertx());
private RequestContext requestContext;
private static boolean runningOnOwn;

@BeforeEach
void initMocks(){
MockitoAnnotations.openMocks(this);
autowireDependencies(this);
requestContext = new RequestContext(ctx, okapiHeadersMock);
}
Expand Down