diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 8a882315b..175a06ef3 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -627,6 +627,31 @@ "invoice.invoice-lines.item.put" ] }, + { + "methods": ["PUT"], + "pathPattern": "/orders/pieces-batch/status", + "permissionsRequired": ["orders.pieces.collection.put"], + "modulePermissions": [ + "orders-storage.pieces.item.put", + "orders-storage.pieces.collection.get", + "orders-storage.po-lines.item.get", + "orders-storage.po-lines.item.put", + "orders-storage.po-lines.collection.get", + "orders-storage.purchase-orders.item.get", + "orders-storage.purchase-orders.item.put", + "orders-storage.titles.collection.get", + "finance.funds.item.get", + "finance.ledgers.current-fiscal-year.item.get", + "finance.transactions.collection.get", + "finance.transactions.batch.execute", + "inventory.items.item.get", + "inventory.items.item.put", + "inventory.items.collection.get", + "inventory-storage.holdings.collection.get", + "acquisitions-units-storage.units.collection.get", + "acquisitions-units-storage.memberships.collection.get" + ] + }, { "methods": ["GET"], "pathPattern": "/orders/pieces-requests", @@ -1519,6 +1544,11 @@ "displayName": "orders - delete an existing piece record", "description": "Delete an existing piece" }, + { + "permissionName": "orders.pieces.collection.put", + "displayName": "orders - batch update piece statuses", + "description": "Batch update piece statuses" + }, { "permissionName": "orders.piece-requests.collection.get", "displayName": "Orders - Get piece requests", @@ -1530,6 +1560,7 @@ "description" : "All permissions for the pieces", "subPermissions" : [ "orders.pieces.collection.get", + "orders.pieces.collection.put", "orders.pieces.item.post", "orders.pieces.item.get", "orders.pieces.item.put", diff --git a/ramls/acq-models b/ramls/acq-models index c1f931704..c0c72775d 160000 --- a/ramls/acq-models +++ b/ramls/acq-models @@ -1 +1 @@ -Subproject commit c1f931704fc6d2dedfc671e308b27f56499750a7 +Subproject commit c0c72775d08c28a77d1475df435497e8b0020667 diff --git a/ramls/pieces-batch.raml b/ramls/pieces-batch.raml new file mode 100644 index 000000000..65bcdc992 --- /dev/null +++ b/ramls/pieces-batch.raml @@ -0,0 +1,49 @@ +#%RAML 1.0 +title: Pieces Batch Operations +baseUri: https://github.com/folio-org/mod-orders +version: v1 +protocols: [ HTTP, HTTPS ] + +documentation: + - title: Endpoint for batch operations on Pieces + content: Endpoint for batch operations on Pieces + +types: + piece-batch-status-collection: !include acq-models/mod-orders/schemas/pieceStatusBatchCollection.json + +/orders/pieces-batch: + /status: + put: + description: Batch status update for pieces + body: + application/json: + type: piece-batch-status-collection + example: + strict: false + value: !include acq-models/mod-orders/examples/pieceStatusBatchCollection.sample + responses: + 204: + description: "Piece records successfully updated" + 400: + description: "Bad request" + body: + application/json: + example: + strict: false + value: !include examples/errors_400.sample + text/plain: + example: "Unable to update pieces - Bad request" + 404: + description: "Pieces do not exist" + body: + text/plain: + example: "Following pieces are not found: [id1, id2]" + 500: + description: "Internal server error, e.g. due to misconfiguration" + body: + application/json: + example: + strict: false + value: !include examples/errors_500.sample + text/plain: + example: "Unable to update pieces - Internal server error, e.g. due to misconfiguration" diff --git a/src/main/java/org/folio/config/ApplicationConfig.java b/src/main/java/org/folio/config/ApplicationConfig.java index 53adb0228..a5fd0af89 100644 --- a/src/main/java/org/folio/config/ApplicationConfig.java +++ b/src/main/java/org/folio/config/ApplicationConfig.java @@ -641,13 +641,12 @@ PieceUpdateFlowPoLineService pieceUpdateFlowPoLineService(PurchaseOrderStorageSe } @Bean - PieceUpdateFlowManager pieceUpdateFlowManager(PieceStorageService pieceStorageService, PieceService pieceService, ProtectionService protectionService, - PieceUpdateFlowPoLineService pieceUpdateFlowPoLineService, PieceUpdateFlowInventoryManager pieceUpdateFlowInventoryManager, - BasePieceFlowHolderBuilder basePieceFlowHolderBuilder, DefaultPieceFlowsValidator defaultPieceFlowsValidator, - PurchaseOrderLineService purchaseOrderLineService) { - return new PieceUpdateFlowManager(pieceStorageService, pieceService, protectionService, pieceUpdateFlowPoLineService, - pieceUpdateFlowInventoryManager, basePieceFlowHolderBuilder, defaultPieceFlowsValidator, - purchaseOrderLineService); + PieceUpdateFlowManager pieceUpdateFlowManager(PieceStorageService pieceStorageService, PieceService pieceService, TitlesService titlesService, + ProtectionService protectionService, PieceUpdateFlowPoLineService pieceUpdateFlowPoLineService, + PieceUpdateFlowInventoryManager pieceUpdateFlowInventoryManager, BasePieceFlowHolderBuilder basePieceFlowHolderBuilder, + DefaultPieceFlowsValidator defaultPieceFlowsValidator, PurchaseOrderLineService purchaseOrderLineService) { + return new PieceUpdateFlowManager(pieceStorageService, pieceService, titlesService, protectionService, pieceUpdateFlowPoLineService, + pieceUpdateFlowInventoryManager, basePieceFlowHolderBuilder, defaultPieceFlowsValidator, purchaseOrderLineService); } @Bean diff --git a/src/main/java/org/folio/models/pieces/PieceBatchStatusUpdateHolder.java b/src/main/java/org/folio/models/pieces/PieceBatchStatusUpdateHolder.java new file mode 100644 index 000000000..b65c1663f --- /dev/null +++ b/src/main/java/org/folio/models/pieces/PieceBatchStatusUpdateHolder.java @@ -0,0 +1,28 @@ +package org.folio.models.pieces; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.folio.rest.jaxrs.model.Piece; + +import java.util.List; + +@AllArgsConstructor +public class PieceBatchStatusUpdateHolder extends BasePieceFlowHolder { + + @Getter + private Piece.ReceivingStatus receivingStatus; + @Getter + private List pieces; + private String poLineId; + + @Override + public String getOrderLineId() { + return poLineId; + } + + @Override + public String getTitleId() { + return null; + } + +} diff --git a/src/main/java/org/folio/orders/events/handlers/ReceiptStatusConsistency.java b/src/main/java/org/folio/orders/events/handlers/ReceiptStatusConsistency.java index 7b96003da..e234e935e 100644 --- a/src/main/java/org/folio/orders/events/handlers/ReceiptStatusConsistency.java +++ b/src/main/java/org/folio/orders/events/handlers/ReceiptStatusConsistency.java @@ -1,162 +1,88 @@ package org.folio.orders.events.handlers; -import static org.folio.helper.CheckinReceivePiecesHelper.EXPECTED_STATUSES; -import static org.folio.helper.CheckinReceivePiecesHelper.RECEIVED_STATUSES; -import static org.folio.orders.utils.ResourcePathResolver.PIECES_STORAGE; -import static org.folio.orders.utils.ResourcePathResolver.resourcesPath; -import static org.folio.rest.jaxrs.model.PoLine.ReceiptStatus.AWAITING_RECEIPT; -import static org.folio.rest.jaxrs.model.PoLine.ReceiptStatus.FULLY_RECEIVED; -import static org.folio.rest.jaxrs.model.PoLine.ReceiptStatus.PARTIALLY_RECEIVED; +import static org.folio.orders.events.utils.EventUtils.getPoLineId; +import static org.folio.orders.utils.HelperUtils.getOkapiHeaders; +import static org.folio.service.orders.utils.StatusUtils.calculatePoLineReceiptStatus; -import java.util.ArrayList; import java.util.List; import java.util.Map; -import org.apache.commons.collections4.CollectionUtils; import org.folio.helper.BaseHelper; import org.folio.orders.utils.HelperUtils; import org.folio.orders.utils.PoLineCommonUtil; -import org.folio.rest.core.RestClient; import org.folio.rest.core.models.RequestContext; import org.folio.rest.jaxrs.model.Piece; -import org.folio.rest.jaxrs.model.Piece.ReceivingStatus; -import org.folio.rest.jaxrs.model.PieceCollection; import org.folio.rest.jaxrs.model.PoLine; -import org.folio.rest.jaxrs.model.PoLine.ReceiptStatus; import org.folio.service.orders.PurchaseOrderLineService; +import org.folio.service.pieces.PieceStorageService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import io.vertx.core.Future; import io.vertx.core.Handler; -import io.vertx.core.Promise; import io.vertx.core.Vertx; import io.vertx.core.eventbus.Message; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; -import one.util.streamex.StreamEx; @Component("receiptStatusHandler") public class ReceiptStatusConsistency extends BaseHelper implements Handler> { - private static final int LIMIT = Integer.MAX_VALUE; - private static final String PIECES_ENDPOINT = resourcesPath(PIECES_STORAGE) + "?query=poLineId==%s&limit=%s"; - + private final PieceStorageService pieceStorageService; private final PurchaseOrderLineService purchaseOrderLineService; @Autowired - public ReceiptStatusConsistency(Vertx vertx, PurchaseOrderLineService purchaseOrderLineService) { + public ReceiptStatusConsistency(Vertx vertx, PieceStorageService pieceStorageService, PurchaseOrderLineService purchaseOrderLineService) { super(vertx.getOrCreateContext()); + this.pieceStorageService = pieceStorageService; this.purchaseOrderLineService = purchaseOrderLineService; } @Override public void handle(Message message) { - JsonObject messageFromEventBus = message.body(); - + var messageFromEventBus = message.body(); logger.info("Received message body: {}", messageFromEventBus); - Map okapiHeaders = org.folio.orders.utils.HelperUtils.getOkapiHeaders(message); + var okapiHeaders = getOkapiHeaders(message); var requestContext = new RequestContext(ctx, okapiHeaders); - List> futures = new ArrayList<>(); - Promise promise = Promise.promise(); - futures.add(promise.future()); - - String poLineIdUpdate = messageFromEventBus.getString("poLineIdUpdate"); - String query = String.format(PIECES_ENDPOINT, poLineIdUpdate, LIMIT); - - // 1. Get all pieces for poLineId - getPieces(query, requestContext) - .compose(listOfPieces -> - // 2. Get PoLine for the poLineId which will be used to calculate PoLineReceiptStatus - purchaseOrderLineService.getOrderLineById(poLineIdUpdate, requestContext) - .map(poLine -> { - if (PoLineCommonUtil.isCancelledOrOngoingStatus(poLine)) { - promise.complete(); - return null; - } - ReceiptStatus receivingStatus = calculatePoLineReceiptStatus(poLine, listOfPieces); - boolean statusUpdated = purchaseOrderLineService.updatePoLineReceiptStatusWithoutSave(poLine, receivingStatus); - if (statusUpdated) { - purchaseOrderLineService.saveOrderLine(poLine, requestContext) - .map(aVoid -> { - // send event to update order status - updateOrderStatus(poLine, okapiHeaders, requestContext); - promise.complete(); - return null; - }) - .onFailure(e -> { - logger.error("The error updating poLine by id {}", poLineIdUpdate, e); - promise.fail(e); - }); - } else { - promise.complete(); - } - return null; - }) - .onFailure(e -> { - logger.error("The error getting poLine by id {}", poLineIdUpdate, e); - promise.fail(e); - })) - .onFailure(e -> { - logger.error("The error happened getting all pieces by poLine {}", poLineIdUpdate, e); - promise.fail(e); - }); - // Now wait for all operations to be completed and send reply - completeAllFutures(futures, message); - } + var poLineId = getPoLineId(messageFromEventBus); + var future = pieceStorageService.getPiecesByLineId(poLineId, requestContext) + .compose(pieces -> purchaseOrderLineService.getOrderLineById(poLineId, requestContext) + .compose(poLine -> updatePoLineAndOrderStatuses(pieces, poLine, requestContext)) + .onFailure(e -> logger.error("Exception occurred while fetching PoLine by id: '{}'", poLineId, e))) + .onFailure(e -> logger.error("Exception occurred while fetching pieces by PoLine id: '{}'", poLineId, e)); - private void updateOrderStatus(PoLine poLine, Map okapiHeaders, RequestContext requestContext) { - List poIds = StreamEx - .of(poLine) - .map(PoLine::getPurchaseOrderId) - .distinct() - .map(orderId -> new JsonObject().put(ORDER_ID, orderId)) - .toList(); - JsonObject messageContent = new JsonObject(); - messageContent.put(OKAPI_HEADERS, okapiHeaders); - // Collect order ids which should be processed - messageContent.put(EVENT_PAYLOAD, new JsonArray(poIds)); - HelperUtils.sendEvent(MessageAddress.RECEIVE_ORDER_STATUS_UPDATE, messageContent, requestContext); + completeAllFutures(List.of(future), message); } - private ReceiptStatus calculatePoLineReceiptStatus(PoLine poLine, List pieces) { - - if (CollectionUtils.isEmpty(pieces)) { - logger.info("No pieces processed - receipt status unchanged for PO Line '{}'", poLine.getId()); - return poLine.getReceiptStatus(); + private Future updatePoLineAndOrderStatuses(List pieces, PoLine poLine, RequestContext requestContext) { + if (PoLineCommonUtil.isCancelledOrOngoingStatus(poLine)) { + logger.info("updatePoLineAndOrderStatuses:: PoLine with id: '{}' has status: '{}', skipping...", poLine.getId(), poLine.getReceiptStatus()); + return Future.succeededFuture(); } - - long expectedQty = getPiecesQuantityByPoLineAndStatus(EXPECTED_STATUSES, pieces); - return calculatePoLineReceiptStatus(expectedQty, pieces); - } - - private ReceiptStatus calculatePoLineReceiptStatus(long expectedPiecesQuantity, List pieces) { - if (expectedPiecesQuantity == 0) { - logger.info("calculatePoLineReceiptStatus:: Fully received"); - return FULLY_RECEIVED; - } - - if (StreamEx.of(pieces).anyMatch(piece -> RECEIVED_STATUSES.contains(piece.getReceivingStatus()))) { - logger.info("calculatePoLineReceiptStatus:: Partially Received - In case there is at least one successfully received piece"); - return PARTIALLY_RECEIVED; + var newStatus = pieces.isEmpty() + ? poLine.getReceiptStatus() + : calculatePoLineReceiptStatus(poLine.getId(), pieces); + boolean statusUpdated = purchaseOrderLineService.updatePoLineReceiptStatusWithoutSave(poLine, newStatus); + if (!statusUpdated) { + logger.info("updatePoLineAndOrderStatuses:: PoLine receipt status is not updated, skipping..."); + return Future.succeededFuture(); } - - logger.info("calculatePoLineReceiptStatus::Pieces were rolled-back to Expected, checking if there is any Received piece in the storage"); - long receivedQty = getPiecesQuantityByPoLineAndStatus(RECEIVED_STATUSES, pieces); - return receivedQty == 0 ? AWAITING_RECEIPT : PARTIALLY_RECEIVED; + return purchaseOrderLineService.saveOrderLine(poLine, requestContext) + .compose(v -> updateOrderStatus(poLine, okapiHeaders, requestContext)) + .onSuccess(v -> logger.info("updatePoLineAndOrderStatuses:: Order '{}' and PoLine '{}' updated successfully", poLine.getId(), poLine.getPurchaseOrderId())) + .onFailure(e -> logger.error("Exception occurred while updating Order '{}' and PoLine '{}'", poLine.getId(), poLine.getPurchaseOrderId(), e)); } - private long getPiecesQuantityByPoLineAndStatus(List receivingStatuses, List pieces) { - return pieces.stream() - .filter(piece -> receivingStatuses.contains(piece.getReceivingStatus())) - .count(); + private Future updateOrderStatus(PoLine poLine, Map okapiHeaders, RequestContext requestContext) { + var messageContent = JsonObject.of( + OKAPI_HEADERS, okapiHeaders, + EVENT_PAYLOAD, JsonArray.of(JsonObject.of(ORDER_ID, poLine.getPurchaseOrderId())) + ); + HelperUtils.sendEvent(MessageAddress.RECEIVE_ORDER_STATUS_UPDATE, messageContent, requestContext); + return Future.succeededFuture(); } - Future> getPieces(String endpoint, RequestContext requestContext) { - return new RestClient().get(endpoint, PieceCollection.class, requestContext) - .map(PieceCollection::getPieces); - } } diff --git a/src/main/java/org/folio/orders/events/utils/EventUtils.java b/src/main/java/org/folio/orders/events/utils/EventUtils.java new file mode 100644 index 000000000..99dcebce6 --- /dev/null +++ b/src/main/java/org/folio/orders/events/utils/EventUtils.java @@ -0,0 +1,19 @@ +package org.folio.orders.events.utils; + +import io.vertx.core.json.JsonObject; + +public class EventUtils { + + public static final String POL_UPDATE_FIELD = "poLineIdUpdate"; + + public static JsonObject createPoLineUpdateEvent(String poLineId) { + return JsonObject.of(POL_UPDATE_FIELD, poLineId); + } + + public static String getPoLineId(JsonObject eventPayload) { + return eventPayload.getString(POL_UPDATE_FIELD); + } + + private EventUtils() {} + +} diff --git a/src/main/java/org/folio/orders/utils/FutureUtils.java b/src/main/java/org/folio/orders/utils/FutureUtils.java new file mode 100644 index 000000000..2abbf9e62 --- /dev/null +++ b/src/main/java/org/folio/orders/utils/FutureUtils.java @@ -0,0 +1,26 @@ +package org.folio.orders.utils; + +import io.vertx.core.Future; + +import java.util.concurrent.Callable; + +public class FutureUtils { + + public static Future asFuture(Runnable runnable) { + return asFuture(() -> { + runnable.run(); + return null; + }); + } + + public static Future asFuture(Callable callable) { + try { + return Future.succeededFuture(callable.call()); + } catch (Exception e) { + return Future.failedFuture(e); + } + } + + private FutureUtils() {} + +} diff --git a/src/main/java/org/folio/rest/impl/PiecesAPI.java b/src/main/java/org/folio/rest/impl/PiecesAPI.java index 550fd6176..d79527686 100644 --- a/src/main/java/org/folio/rest/impl/PiecesAPI.java +++ b/src/main/java/org/folio/rest/impl/PiecesAPI.java @@ -13,7 +13,9 @@ import org.folio.rest.annotations.Validate; import org.folio.rest.core.models.RequestContext; import org.folio.rest.jaxrs.model.Piece; +import org.folio.rest.jaxrs.model.PieceBatchStatusCollection; import org.folio.rest.jaxrs.resource.OrdersPieces; +import org.folio.rest.jaxrs.resource.OrdersPiecesBatch; import org.folio.rest.jaxrs.resource.OrdersPiecesRequests; import org.folio.service.CirculationRequestsRetriever; import org.folio.service.pieces.PieceStorageService; @@ -29,7 +31,7 @@ import io.vertx.core.Vertx; import io.vertx.core.json.JsonObject; -public class PiecesAPI extends BaseApi implements OrdersPieces, OrdersPiecesRequests { +public class PiecesAPI extends BaseApi implements OrdersPieces, OrdersPiecesRequests, OrdersPiecesBatch { private static final Logger logger = LogManager.getLogger(); @Autowired @@ -62,9 +64,8 @@ public void postOrdersPieces(boolean createItem, Piece entity, Map> asyncResultHandler, Context vertxContext) { pieceCreateFlowManager.createPiece(entity, createItem, new RequestContext(vertxContext, okapiHeaders)) .onSuccess(piece -> { - if (logger.isInfoEnabled()) { - logger.debug("Successfully created piece: {}", JsonObject.mapFrom(piece) - .encodePrettily()); + if (logger.isDebugEnabled()) { + logger.debug("Successfully created piece: {}", JsonObject.mapFrom(piece).encodePrettily()); } asyncResultHandler.handle(succeededFuture(buildCreatedResponse(piece))); }) @@ -104,10 +105,17 @@ public void deleteOrdersPiecesById(String pieceId, boolean deleteHolding, Map pieceIds, String status, Map okapiHeaders, - Handler> asyncResultHandler, Context vertxContext) { + Handler> asyncResultHandler, Context vertxContext) { circulationRequestsRetriever.getRequesterIdsToRequestsByPieceIds(pieceIds, status, new RequestContext(vertxContext, okapiHeaders)) .onSuccess(requests -> asyncResultHandler.handle(succeededFuture(buildOkResponse(requests)))) .onFailure(fail -> handleErrorResponse(asyncResultHandler, fail)); } + @Override + public void putOrdersPiecesBatchStatus(PieceBatchStatusCollection pieceBatchStatusCollection, Map okapiHeaders, + Handler> asyncResultHandler, Context vertxContext) { + pieceUpdateFlowManager.updatePiecesStatuses(pieceBatchStatusCollection.getPieceIds(), pieceBatchStatusCollection.getReceivingStatus(), new RequestContext(vertxContext, okapiHeaders)) + .onSuccess(v -> asyncResultHandler.handle(succeededFuture(buildNoContentResponse()))) + .onFailure(t -> handleErrorResponse(asyncResultHandler, t)); + } } diff --git a/src/main/java/org/folio/service/orders/flows/update/open/OpenCompositeOrderPieceService.java b/src/main/java/org/folio/service/orders/flows/update/open/OpenCompositeOrderPieceService.java index fc90fd041..d5cf2777d 100644 --- a/src/main/java/org/folio/service/orders/flows/update/open/OpenCompositeOrderPieceService.java +++ b/src/main/java/org/folio/service/orders/flows/update/open/OpenCompositeOrderPieceService.java @@ -1,17 +1,17 @@ package org.folio.service.orders.flows.update.open; +import static org.folio.orders.events.utils.EventUtils.createPoLineUpdateEvent; import static org.folio.orders.utils.HelperUtils.calculateInventoryItemsQuantity; import static org.folio.orders.utils.HelperUtils.collectResultsOnSuccess; import static org.folio.orders.utils.RequestContextUtil.createContextWithNewTenantId; +import static org.folio.service.pieces.PieceUtil.updatePieceStatus; import java.util.ArrayList; -import java.util.Date; import java.util.List; import java.util.Optional; import java.util.concurrent.CompletionException; import io.vertx.core.Future; -import io.vertx.core.json.JsonObject; import org.apache.commons.collections4.CollectionUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -124,18 +124,12 @@ public Future updatePiece(Piece piece, RequestContext requestContext) { .compose(v -> inventoryItemManager.updateItemWithPieceFields(piece, requestContext)) .compose(vVoid -> pieceStorageService.getPieceById(piece.getId(), requestContext)) .compose(pieceStorage -> { - var receivingStatusUpdate = piece.getReceivingStatus(); - var receivingStatusStorage = pieceStorage.getReceivingStatus(); - boolean isReceivingStatusChanged = !receivingStatusStorage.equals(receivingStatusUpdate); - if (isReceivingStatusChanged) { - piece.setStatusUpdatedDate(new Date()); - } + var isReceivingStatusChanged = updatePieceStatus(piece, pieceStorage.getReceivingStatus(), piece.getReceivingStatus()); return pieceStorageService.updatePiece(piece, requestContext) .compose(v -> { - logger.debug("updatePiece:: receivingStatusStorage - {}, receivingStatusUpdate - {}", receivingStatusStorage, receivingStatusUpdate); + logger.debug("updatePiece:: Status updated from: {} to {}", pieceStorage.getReceivingStatus(), piece.getReceivingStatus()); if (isReceivingStatusChanged) { - var messageToEventBus = JsonObject.of("poLineIdUpdate", piece.getPoLineId()); - receiptStatusPublisher.sendEvent(MessageAddress.RECEIPT_STATUS, messageToEventBus, requestContext); + receiptStatusPublisher.sendEvent(MessageAddress.RECEIPT_STATUS, createPoLineUpdateEvent(piece.getPoLineId()), requestContext); } return Future.succeededFuture(); }) diff --git a/src/main/java/org/folio/service/orders/utils/StatusUtils.java b/src/main/java/org/folio/service/orders/utils/StatusUtils.java index d598778b3..cd7b57241 100644 --- a/src/main/java/org/folio/service/orders/utils/StatusUtils.java +++ b/src/main/java/org/folio/service/orders/utils/StatusUtils.java @@ -1,19 +1,29 @@ package org.folio.service.orders.utils; +import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.StringUtils; import org.folio.rest.jaxrs.model.CloseReason; import org.folio.rest.jaxrs.model.CompositePoLine; +import org.folio.rest.jaxrs.model.Piece; import org.folio.rest.jaxrs.model.PoLine; import org.folio.rest.jaxrs.model.PoLine.PaymentStatus; import org.folio.rest.jaxrs.model.PoLine.ReceiptStatus; import org.folio.rest.jaxrs.model.PurchaseOrder; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; +import static org.folio.helper.CheckinReceivePiecesHelper.EXPECTED_STATUSES; +import static org.folio.helper.CheckinReceivePiecesHelper.RECEIVED_STATUSES; import static org.folio.orders.utils.HelperUtils.REASON_CANCELLED; import static org.folio.orders.utils.HelperUtils.REASON_COMPLETE; +import static org.folio.rest.jaxrs.model.PoLine.ReceiptStatus.AWAITING_RECEIPT; +import static org.folio.rest.jaxrs.model.PoLine.ReceiptStatus.FULLY_RECEIVED; +import static org.folio.rest.jaxrs.model.PoLine.ReceiptStatus.PARTIALLY_RECEIVED; +@Log4j2 public class StatusUtils { private static final Set resolutionPaymentStatus = Set.of(PaymentStatus.CANCELLED.value(), PaymentStatus.PAYMENT_NOT_REQUIRED.value(), PaymentStatus.FULLY_PAID.value()); @@ -106,6 +116,36 @@ private static boolean closeOrder(PurchaseOrder purchaseOrder, String reason) { return true; } + public static PoLine.ReceiptStatus calculatePoLineReceiptStatus(String poLineId, List piecesFromStorage) { + return calculatePoLineReceiptStatus(poLineId, piecesFromStorage, List.of()); + } + + public static PoLine.ReceiptStatus calculatePoLineReceiptStatus(String poLineId, List piecesFromStorage, List piecesToUpdate) { + // 1. Get map of all persistent piece statuses + var pieceStatues = piecesFromStorage.stream().collect(Collectors.toMap(Piece::getId, Piece::getReceivingStatus)); + // 2. Update new piece statuses (if any) + piecesToUpdate.forEach(piece -> pieceStatues.put(piece.getId(), piece.getReceivingStatus())); + // 3. Calculate receipt status + return calculatePoLineReceiptStatus(poLineId, pieceStatues); + } + + private static PoLine.ReceiptStatus calculatePoLineReceiptStatus(String poLineId, Map pieceStatuses) { + // Count received and expected statuses + long receivedQuantity = pieceStatuses.values().stream().filter(RECEIVED_STATUSES::contains).count(); + long expectedQuantity = pieceStatuses.values().stream().filter(EXPECTED_STATUSES::contains).count(); + + if (expectedQuantity == 0) { + log.info("calculatePoLineReceiptStatus:: PoLine with id: '{}', status: Fully Received", poLineId); + return FULLY_RECEIVED; + } + if (receivedQuantity > 0) { + log.info("calculatePoLineReceiptStatus:: PoLine with id: '{}', status: Partially Received. Successfully Received pieces: {}", poLineId, receivedQuantity); + return PARTIALLY_RECEIVED; + } + log.info("calculatePoLineReceiptStatus:: PoLine with id: '{}', status: Awaiting Receipt. Pieces were rolled-back to Expected", poLineId); + return AWAITING_RECEIPT; + } + private StatusUtils() {} } diff --git a/src/main/java/org/folio/service/pieces/PieceService.java b/src/main/java/org/folio/service/pieces/PieceService.java index e0046db49..3203ca8f7 100644 --- a/src/main/java/org/folio/service/pieces/PieceService.java +++ b/src/main/java/org/folio/service/pieces/PieceService.java @@ -1,14 +1,13 @@ package org.folio.service.pieces; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import lombok.extern.log4j.Log4j2; import org.folio.orders.events.handlers.MessageAddress; import org.folio.rest.core.models.RequestContext; -import io.vertx.core.json.JsonObject; +import static org.folio.orders.events.utils.EventUtils.createPoLineUpdateEvent; +@Log4j2 public class PieceService { - private static final Logger logger = LogManager.getLogger(PieceService.class); private final PieceChangeReceiptStatusPublisher receiptStatusPublisher; @@ -16,11 +15,10 @@ public PieceService(PieceChangeReceiptStatusPublisher receiptStatusPublisher) { this.receiptStatusPublisher = receiptStatusPublisher; } - public void receiptConsistencyPiecePoLine(JsonObject jsonObj, RequestContext requestContext) { - logger.debug("Sending event to verify receipt status"); - - receiptStatusPublisher.sendEvent(MessageAddress.RECEIPT_STATUS, jsonObj, requestContext); - - logger.debug("Event to verify receipt status - sent"); + public void receiptConsistencyPiecePoLine(String poLineId, RequestContext requestContext) { + log.debug("Sending event to verify receipt status for poLineId: {}", poLineId); + receiptStatusPublisher.sendEvent(MessageAddress.RECEIPT_STATUS, createPoLineUpdateEvent(poLineId), requestContext); + log.debug("Event to verify receipt status is sent for poLineId: {}", poLineId); } + } diff --git a/src/main/java/org/folio/service/pieces/PieceUtil.java b/src/main/java/org/folio/service/pieces/PieceUtil.java index d0f02ff19..e97d67103 100644 --- a/src/main/java/org/folio/service/pieces/PieceUtil.java +++ b/src/main/java/org/folio/service/pieces/PieceUtil.java @@ -2,6 +2,7 @@ import static org.folio.rest.jaxrs.model.CompositePoLine.OrderFormat.OTHER; +import java.util.Date; import java.util.EnumMap; import java.util.List; import java.util.Map; @@ -68,7 +69,17 @@ private static EnumMap calculateElectronicPiecesQuantityW } private static boolean isLocationMatch(Piece piece, Location loc) { - return (Objects.nonNull(piece.getLocationId()) && piece.getLocationId().equals(loc.getLocationId())) || - (Objects.nonNull(piece.getHoldingId()) && piece.getHoldingId().equals(loc.getHoldingId())); + return (Objects.nonNull(piece.getLocationId()) && piece.getLocationId().equals(loc.getLocationId())) + || (Objects.nonNull(piece.getHoldingId()) && piece.getHoldingId().equals(loc.getHoldingId())); } + + public static boolean updatePieceStatus(Piece piece, Piece.ReceivingStatus oldStatus, Piece.ReceivingStatus newStatus) { + var isStatusChanged = !oldStatus.equals(newStatus); + if (isStatusChanged) { + piece.setStatusUpdatedDate(new Date()); + } + piece.setReceivingStatus(newStatus); + return isStatusChanged; + } + } diff --git a/src/main/java/org/folio/service/pieces/flows/update/PieceUpdateFlowManager.java b/src/main/java/org/folio/service/pieces/flows/update/PieceUpdateFlowManager.java index 84da4a84c..3c28b8831 100644 --- a/src/main/java/org/folio/service/pieces/flows/update/PieceUpdateFlowManager.java +++ b/src/main/java/org/folio/service/pieces/flows/update/PieceUpdateFlowManager.java @@ -1,28 +1,28 @@ package org.folio.service.pieces.flows.update; -import static org.folio.helper.CheckinReceivePiecesHelper.EXPECTED_STATUSES; -import static org.folio.helper.CheckinReceivePiecesHelper.RECEIVED_STATUSES; +import static org.folio.orders.utils.FutureUtils.asFuture; import static org.folio.orders.utils.ProtectedOperationType.UPDATE; -import static org.folio.rest.jaxrs.model.CompositePoLine.ReceiptStatus.AWAITING_RECEIPT; -import static org.folio.rest.jaxrs.model.CompositePoLine.ReceiptStatus.FULLY_RECEIVED; -import static org.folio.rest.jaxrs.model.CompositePoLine.ReceiptStatus.PARTIALLY_RECEIVED; +import static org.folio.service.orders.utils.StatusUtils.calculatePoLineReceiptStatus; +import static org.folio.service.pieces.PieceUtil.updatePieceStatus; -import java.util.Date; -import java.util.HashMap; import java.util.List; -import java.util.Map; +import java.util.stream.Collectors; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import lombok.extern.log4j.Log4j2; +import org.folio.models.pieces.BasePieceFlowHolder; +import org.folio.models.pieces.PieceBatchStatusUpdateHolder; import org.folio.models.pieces.PieceUpdateHolder; +import org.folio.okapi.common.GenericCompositeFuture; +import org.folio.orders.utils.HelperUtils; import org.folio.orders.utils.PoLineCommonUtil; +import org.folio.orders.utils.ProtectedOperationType; import org.folio.rest.core.models.RequestContext; import org.folio.rest.jaxrs.model.CompositePoLine; -import org.folio.rest.jaxrs.model.CompositePurchaseOrder; import org.folio.rest.jaxrs.model.CompositePurchaseOrder.OrderType; import org.folio.rest.jaxrs.model.CompositePurchaseOrder.WorkflowStatus; import org.folio.rest.jaxrs.model.Location; import org.folio.rest.jaxrs.model.Piece; +import org.folio.rest.jaxrs.model.PieceBatchStatusCollection; import org.folio.service.ProtectionService; import org.folio.service.orders.PurchaseOrderLineService; import org.folio.service.pieces.PieceService; @@ -32,13 +32,14 @@ import org.folio.service.pieces.flows.DefaultPieceFlowsValidator; import io.vertx.core.Future; -import io.vertx.core.json.JsonObject; +import org.folio.service.titles.TitlesService; +@Log4j2 public class PieceUpdateFlowManager { - private static final Logger logger = LogManager.getLogger(PieceUpdateFlowManager.class); private final PieceStorageService pieceStorageService; private final PieceService pieceService; + private final TitlesService titlesService; private final ProtectionService protectionService; private final PieceUpdateFlowPoLineService updatePoLineService; private final PieceUpdateFlowInventoryManager pieceUpdateFlowInventoryManager; @@ -46,12 +47,13 @@ public class PieceUpdateFlowManager { private final DefaultPieceFlowsValidator defaultPieceFlowsValidator; private final PurchaseOrderLineService purchaseOrderLineService; - public PieceUpdateFlowManager(PieceStorageService pieceStorageService, PieceService pieceService, ProtectionService protectionService, - PieceUpdateFlowPoLineService updatePoLineService, PieceUpdateFlowInventoryManager pieceUpdateFlowInventoryManager, - BasePieceFlowHolderBuilder basePieceFlowHolderBuilder, DefaultPieceFlowsValidator defaultPieceFlowsValidator, - PurchaseOrderLineService purchaseOrderLineService) { + public PieceUpdateFlowManager(PieceStorageService pieceStorageService, PieceService pieceService, TitlesService titlesService, ProtectionService protectionService, + PieceUpdateFlowPoLineService updatePoLineService, PieceUpdateFlowInventoryManager pieceUpdateFlowInventoryManager, + BasePieceFlowHolderBuilder basePieceFlowHolderBuilder, DefaultPieceFlowsValidator defaultPieceFlowsValidator, + PurchaseOrderLineService purchaseOrderLineService) { this.pieceStorageService = pieceStorageService; this.pieceService = pieceService; + this.titlesService = titlesService; this.protectionService = protectionService; this.updatePoLineService = updatePoLineService; this.pieceUpdateFlowInventoryManager = pieceUpdateFlowInventoryManager; @@ -74,85 +76,97 @@ public Future updatePiece(Piece pieceToUpdate, boolean createItem, boolean .map(holder::withPieceFromStorage) .compose(aHolder -> basePieceFlowHolderBuilder.updateHolderWithOrderInformation(holder, requestContext)) .compose(aHolder -> basePieceFlowHolderBuilder.updateHolderWithTitleInformation(holder, requestContext)) - .map(v -> { - defaultPieceFlowsValidator.isPieceRequestValid(pieceToUpdate, holder.getOriginPoLine(), createItem); - return null; - }) + .compose(v -> asFuture(() -> defaultPieceFlowsValidator.isPieceRequestValid(pieceToUpdate, holder.getOriginPoLine(), createItem))) .compose(title -> protectionService.isOperationRestricted(holder.getTitle().getAcqUnitIds(), UPDATE, requestContext)) .compose(v -> pieceUpdateFlowInventoryManager.processInventory(holder, requestContext)) .compose(v -> updatePoLine(holder, requestContext)) - .map(v -> { - Piece.ReceivingStatus receivingStatusStorage = holder.getPieceFromStorage().getReceivingStatus(); - Piece.ReceivingStatus receivingStatusUpdate = holder.getPieceToUpdate().getReceivingStatus(); - logger.debug("receivingStatusStorage -- {}", receivingStatusStorage); - logger.debug("receivingStatusUpdate -- {}", receivingStatusUpdate); - if (receivingStatusStorage.compareTo(receivingStatusUpdate) != 0) { - holder.getPieceToUpdate().setStatusUpdatedDate(new Date()); - return true; - } - return false; - }) - .compose(verifyReceiptStatus -> pieceStorageService.updatePiece(holder.getPieceToUpdate(), requestContext) - .map(verifyReceiptStatus)) - .map(verifyReceiptStatus -> { + .map(v -> updatePieceStatus(holder.getPieceToUpdate(), holder.getPieceFromStorage().getReceivingStatus(), holder.getPieceToUpdate().getReceivingStatus())) + .compose(verifyReceiptStatus -> pieceStorageService.updatePiece(holder.getPieceToUpdate(), requestContext).map(verifyReceiptStatus)) + .compose(verifyReceiptStatus -> asFuture(() -> { if (Boolean.TRUE.equals(verifyReceiptStatus)) { - JsonObject messageToEventBus = new JsonObject(); - messageToEventBus.put("poLineIdUpdate", holder.getPieceToUpdate().getPoLineId()); - pieceService.receiptConsistencyPiecePoLine(messageToEventBus, requestContext); + pieceService.receiptConsistencyPiecePoLine(holder.getPieceToUpdate().getPoLineId(), requestContext); } - return null; - }) - .onFailure(t -> logger.error("User to update piece with id={}", holder.getPieceToUpdate().getId(), t)) + })) + .onFailure(t -> log.error("User to update piece with id={}", holder.getPieceToUpdate().getId(), t)) + .mapEmpty(); + } + + public Future updatePiecesStatuses(List pieceIds, PieceBatchStatusCollection.ReceivingStatus receivingStatus, RequestContext requestContext) { + var newStatus = Piece.ReceivingStatus.fromValue(receivingStatus.value()); + return pieceStorageService.getPiecesByIds(pieceIds, requestContext) + .compose(pieces -> isOperationRestricted(pieces, requestContext)) + .map(pieces -> pieces.stream().collect(Collectors.groupingBy(Piece::getPoLineId))) + .map(piecesByPoLineId -> piecesByPoLineId.entrySet().stream() + .map(entry -> new PieceBatchStatusUpdateHolder(newStatus, entry.getValue(), entry.getKey())) + .map(holder -> basePieceFlowHolderBuilder.updateHolderWithOrderInformation(holder, requestContext) + .compose(v -> updatePoLine(holder, requestContext)) + .compose(v -> updatePiecesStatusesByPoLine(holder, requestContext))) + .toList()) + .compose(HelperUtils::collectResultsOnSuccess) + .onSuccess(v -> log.info("Pieces statuses are updated for pieceIds: {} to status: {}", pieceIds, receivingStatus)) + .onFailure(t -> log.error("Failed to update pieces statuses for pieceIds: {} to status: {}", pieceIds, receivingStatus, t)) .mapEmpty(); } protected Future updatePoLine(PieceUpdateHolder holder, RequestContext requestContext) { - CompositePoLine originPoLine = holder.getOriginPoLine(); + return updatePoLine(holder, List.of(holder.getPieceToUpdate()), requestContext) + .compose(v -> !Boolean.TRUE.equals(holder.getOriginPoLine().getIsPackage()) && !Boolean.TRUE.equals(holder.getOriginPoLine().getCheckinItems()) + ? updatePoLineService.updatePoLine(holder, requestContext) + : Future.succeededFuture()); + } + protected Future updatePoLine(PieceBatchStatusUpdateHolder holder, RequestContext requestContext) { + return updatePoLine(holder, holder.getPieces(), requestContext); + } + + private Future updatePoLine(T holder, List piecesToUpdate, RequestContext requestContext) { + var originPurchaseOrder = holder.getOriginPurchaseOrder(); + if (originPurchaseOrder.getOrderType() != OrderType.ONE_TIME || originPurchaseOrder.getWorkflowStatus() != WorkflowStatus.OPEN) { + return Future.succeededFuture(); + } + + var originPoLine = holder.getOriginPoLine(); + var poLineToSave = holder.getPoLineToSave(); + var pieceIds = piecesToUpdate.stream().map(Piece::getId).toList(); return pieceStorageService.getPiecesByLineId(originPoLine.getId(), requestContext) .compose(pieces -> { - CompositePurchaseOrder order = holder.getOriginPurchaseOrder(); - if (order.getOrderType() != OrderType.ONE_TIME || order.getWorkflowStatus() != WorkflowStatus.OPEN) { - return Future.succeededFuture(); - } - List piecesToUpdate = List.of(holder.getPieceToUpdate()); - CompositePoLine poLineToSave = holder.getPoLineToSave(); if (PoLineCommonUtil.isCancelledOrOngoingStatus(PoLineCommonUtil.convertToPoLine(poLineToSave))) { - logger.info("updatePoLine:: Skipping updating POL '{}' status for CANCELLED or ONGOING po lines", poLineToSave.getId()); + log.info("updatePoLine:: Skip updating PoLine: '{}' with status: '{}'", poLineToSave.getId(), poLineToSave.getReceiptStatus()); } else { - poLineToSave.setReceiptStatus(calculatePoLineReceiptStatus(originPoLine, pieces, piecesToUpdate)); + var newStatus = calculatePoLineReceiptStatus(poLineToSave.getId(), pieces, piecesToUpdate); + poLineToSave.setReceiptStatus(CompositePoLine.ReceiptStatus.fromValue(newStatus.value())); } - List locations = PieceUtil.findOrderPieceLineLocation(holder.getPieceToUpdate(), poLineToSave); + var locations = getPieceLocations(piecesToUpdate, poLineToSave); return purchaseOrderLineService.saveOrderLine(poLineToSave, locations, requestContext); - }).compose(v -> { - if (!Boolean.TRUE.equals(originPoLine.getIsPackage()) && - !Boolean.TRUE.equals(originPoLine.getCheckinItems())) { - return updatePoLineService.updatePoLine(holder, requestContext); - } - return Future.succeededFuture(); }) - .onSuccess(aVoid -> logger.info("updatePoLine:: Po line with id: {} is updated for pieceId: {}", - originPoLine.getId(), holder.getPieceToUpdate().getId())) - .onFailure(t -> logger.error("Failed to update PO line with id: {} for pieceId: {}", - originPoLine.getId(), holder.getPieceToUpdate().getId(), t)); + .onSuccess(v -> log.info("updatePoLine:: PoLine with id: '{}' is updated for pieceIds: {}", originPoLine.getId(), pieceIds)) + .onFailure(t -> log.error("Failed to update PO line with id: '{}' for pieceIds: {}", originPoLine.getId(), pieceIds, t)); } - CompositePoLine.ReceiptStatus calculatePoLineReceiptStatus(CompositePoLine poLine, List fromStorage, List toUpdate) { - - // 1. collect all piece statuses - Map map = new HashMap<>(); - fromStorage.forEach(piece -> map.put(piece.getId(), piece.getReceivingStatus())); - toUpdate.forEach(piece -> map.put(piece.getId(), piece.getReceivingStatus())); - - // 2. count received and expected statuses - long receivedQuantity = map.values().stream().filter(RECEIVED_STATUSES::contains).count(); - long expectedQuantity = map.values().stream().filter(EXPECTED_STATUSES::contains).count(); + private Future updatePiecesStatusesByPoLine(PieceBatchStatusUpdateHolder holder, RequestContext requestContext) { + var isAnyPiecesUpdated = holder.getPieces().stream().anyMatch(piece -> updatePieceStatus(piece, piece.getReceivingStatus(), holder.getReceivingStatus())); + if (!isAnyPiecesUpdated) { + return Future.succeededFuture(); + } + var updates = holder.getPieces().stream().map(piece -> pieceStorageService.updatePiece(piece, requestContext)).toList(); + return HelperUtils.collectResultsOnSuccess(updates) + .compose(v -> asFuture(() -> pieceService.receiptConsistencyPiecePoLine(holder.getOrderLineId(), requestContext))); + } - logger.info("calculatePoLineReceiptStatus:: POL: {}, received: {}, expected: {}", - poLine.getId(), receivedQuantity, expectedQuantity); + private List getPieceLocations(List pieces, CompositePoLine poLine) { + return pieces.stream() + .flatMap(pieceToUpdate -> PieceUtil.findOrderPieceLineLocation(pieceToUpdate, poLine).stream()) + .toList(); + } - return expectedQuantity == 0 ? FULLY_RECEIVED : - receivedQuantity > 0 ? PARTIALLY_RECEIVED : AWAITING_RECEIPT; + protected Future> isOperationRestricted(List pieces, RequestContext requestContext) { + var pieceIds = pieces.stream().map(Piece::getId).toList(); + return titlesService.getTitlesByPieceIds(pieceIds, requestContext) + .map(titles -> titles.stream() + .map(title -> protectionService.isOperationRestricted(title.getAcqUnitIds(), ProtectedOperationType.UPDATE, requestContext)) + .toList()) + .map(GenericCompositeFuture::all) + .map(pieces); } } diff --git a/src/test/java/org/folio/orders/events/handlers/HandlersTestHelper.java b/src/test/java/org/folio/orders/events/handlers/HandlersTestHelper.java index 50744e2b0..d331d60ad 100644 --- a/src/test/java/org/folio/orders/events/handlers/HandlersTestHelper.java +++ b/src/test/java/org/folio/orders/events/handlers/HandlersTestHelper.java @@ -4,6 +4,7 @@ import static java.util.concurrent.TimeUnit.SECONDS; import static org.awaitility.Awaitility.await; import static org.folio.helper.BaseHelper.EVENT_PAYLOAD; +import static org.folio.orders.events.utils.EventUtils.POL_UPDATE_FIELD; import static org.folio.rest.impl.EventBusContextConfiguration.eventMessages; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; @@ -71,7 +72,7 @@ public static void verifyReceiptStatusUpdateEvent(int msgQty) { assertThat(message.headers(), not(emptyIterable())); assertThat(message.body(), notNullValue()); assertThat(message.body() - .getString("poLineIdUpdate"), not(is(emptyOrNullString()))); + .getString(POL_UPDATE_FIELD), not(is(emptyOrNullString()))); assertThat(message.body() .getString(HelperUtils.LANG), not(is(emptyOrNullString()))); } diff --git a/src/test/java/org/folio/orders/events/handlers/ReceiptStatusConsistencyTest.java b/src/test/java/org/folio/orders/events/handlers/ReceiptStatusConsistencyTest.java index 109bc1433..aa2ac5a03 100644 --- a/src/test/java/org/folio/orders/events/handlers/ReceiptStatusConsistencyTest.java +++ b/src/test/java/org/folio/orders/events/handlers/ReceiptStatusConsistencyTest.java @@ -5,6 +5,7 @@ import static org.folio.TestConfig.isVerticleNotDeployed; import static org.folio.TestUtils.checkVertxContextCompletion; import static org.folio.TestUtils.getMockAsJson; +import static org.folio.orders.events.utils.EventUtils.POL_UPDATE_FIELD; import static org.folio.rest.impl.MockServer.POLINES_COLLECTION; import static org.folio.rest.impl.MockServer.PO_LINES_MOCK_DATA_PATH; import static org.folio.rest.impl.MockServer.getPieceSearches; @@ -33,6 +34,7 @@ import org.folio.rest.impl.MockServer; import org.folio.rest.jaxrs.model.CompositePoLine; import org.folio.service.orders.PurchaseOrderLineService; +import org.folio.service.pieces.PieceStorageService; import org.folio.spring.SpringContextUtil; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; @@ -67,6 +69,8 @@ public class ReceiptStatusConsistencyTest { @Autowired private PurchaseOrderLineService purchaseOrderLineService; + @Autowired + private PieceStorageService pieceStorageService; @BeforeAll static void before() throws InterruptedException, ExecutionException, TimeoutException { @@ -82,7 +86,7 @@ static void before() throws InterruptedException, ExecutionException, TimeoutExc @BeforeEach void setUp() { SpringContextUtil.autowireDependencies(this, vertx.getOrCreateContext()); - vertx.eventBus().consumer(TEST_ADDRESS, new ReceiptStatusConsistency(vertx, purchaseOrderLineService)); + vertx.eventBus().consumer(TEST_ADDRESS, new ReceiptStatusConsistency(vertx, pieceStorageService, purchaseOrderLineService)); } @AfterEach @@ -224,7 +228,7 @@ void testPieceReceiptStatusFailureWhenNoMatchingPoLineForPiece(VertxTestContext private JsonObject createBody(String poLineId) { JsonObject jsonObj = new JsonObject(); - jsonObj.put("poLineIdUpdate", poLineId); + jsonObj.put(POL_UPDATE_FIELD, poLineId); return jsonObj; } diff --git a/src/test/java/org/folio/orders/utils/StatusUtilsTest.java b/src/test/java/org/folio/orders/utils/StatusUtilsTest.java index 6cba2a5d4..540b8ff8f 100644 --- a/src/test/java/org/folio/orders/utils/StatusUtilsTest.java +++ b/src/test/java/org/folio/orders/utils/StatusUtilsTest.java @@ -2,19 +2,26 @@ import org.folio.CopilotGenerated; import org.folio.rest.jaxrs.model.CompositePoLine; +import org.folio.rest.jaxrs.model.Piece; import org.folio.rest.jaxrs.model.PoLine; import org.folio.rest.jaxrs.model.PurchaseOrder; import org.folio.service.orders.utils.StatusUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.util.Arrays; import java.util.List; +import java.util.UUID; +import static org.folio.rest.jaxrs.model.Piece.ReceivingStatus.EXPECTED; +import static org.folio.rest.jaxrs.model.Piece.ReceivingStatus.RECEIVED; +import static org.folio.rest.jaxrs.model.Piece.ReceivingStatus.UNRECEIVABLE; +import static org.folio.service.orders.utils.StatusUtils.calculatePoLineReceiptStatus; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -@CopilotGenerated +@CopilotGenerated(partiallyGenerated = true) public class StatusUtilsTest { private PurchaseOrder purchaseOrder; @@ -115,4 +122,67 @@ void changeOrderStatusForPoLineUpdate_shouldNotReopenOrder_whenAllPoLinesComplet assertFalse(StatusUtils.changeOrderStatusForPoLineUpdate(purchaseOrder, List.of(poLine1))); assertEquals(PurchaseOrder.WorkflowStatus.CLOSED, purchaseOrder.getWorkflowStatus()); } + + @Test + void testCalculatePoLineReceiptStatusWhenReceiveLast() { + // given + String poLineId = UUID.randomUUID().toString(); + List fromStorage = givenPieces(EXPECTED, RECEIVED, UNRECEIVABLE); + List update = List.of(new Piece().withId(fromStorage.get(0).getId()).withReceivingStatus(RECEIVED)); + + // when + var receiptStatus = calculatePoLineReceiptStatus(poLineId, fromStorage, update); + + // then + assertEquals(PoLine.ReceiptStatus.FULLY_RECEIVED, receiptStatus); + } + + @Test + void testCalculatePoLineReceiptStatusWhenExpectLast() { + // given + String poLineId = UUID.randomUUID().toString(); + List fromStorage = givenPieces(RECEIVED, RECEIVED, UNRECEIVABLE); + List update = List.of(new Piece().withId(fromStorage.get(0).getId()).withReceivingStatus(EXPECTED)); + + // when + var receiptStatus = calculatePoLineReceiptStatus(poLineId, fromStorage, update); + + // then + assertEquals(PoLine.ReceiptStatus.PARTIALLY_RECEIVED, receiptStatus); + } + + @Test + void testCalculatePoLineReceiptStatusWhenExpectAll() { + // given + String poLineId = UUID.randomUUID().toString(); + List fromStorage = givenPieces(RECEIVED); + List update = List.of(new Piece().withId(fromStorage.get(0).getId()).withReceivingStatus(EXPECTED)); + + // when + var receiptStatus = calculatePoLineReceiptStatus(poLineId, fromStorage, update); + + // then + assertEquals(PoLine.ReceiptStatus.AWAITING_RECEIPT, receiptStatus); + } + + @Test + void testCalculatePoLineReceiptStatusWhenReceivePart() { + // given + String poLineId = UUID.randomUUID().toString(); + List fromStorage = givenPieces(EXPECTED, EXPECTED); + List update = List.of(new Piece().withId(fromStorage.get(0).getId()).withReceivingStatus(UNRECEIVABLE)); + + // when + var receiptStatus = calculatePoLineReceiptStatus(poLineId, fromStorage, update); + + // then + assertEquals(PoLine.ReceiptStatus.PARTIALLY_RECEIVED, receiptStatus); + } + + private static List givenPieces(Piece.ReceivingStatus... statuses) { + return Arrays.stream(statuses) + .map(status -> new Piece().withId(UUID.randomUUID().toString()).withReceivingStatus(status)) + .toList(); + } + } diff --git a/src/test/java/org/folio/service/pieces/flows/update/PieceUpdateFlowManagerTest.java b/src/test/java/org/folio/service/pieces/flows/update/PieceUpdateFlowManagerTest.java index 1b3cae601..1fbb37eeb 100644 --- a/src/test/java/org/folio/service/pieces/flows/update/PieceUpdateFlowManagerTest.java +++ b/src/test/java/org/folio/service/pieces/flows/update/PieceUpdateFlowManagerTest.java @@ -10,14 +10,12 @@ import static org.folio.TestConfig.isVerticleNotDeployed; import static org.folio.TestConstants.ID; import static org.folio.rest.jaxrs.model.Eresource.CreateInventory.INSTANCE_HOLDING_ITEM; -import static org.folio.rest.jaxrs.model.Piece.ReceivingStatus.EXPECTED; -import static org.folio.rest.jaxrs.model.Piece.ReceivingStatus.RECEIVED; -import static org.folio.rest.jaxrs.model.Piece.ReceivingStatus.UNRECEIVABLE; import static org.folio.service.inventory.InventoryHoldingManager.HOLDING_PERMANENT_LOCATION_ID; import static org.folio.service.inventory.InventoryItemManager.ITEM_STATUS; import static org.folio.service.inventory.InventoryItemManager.ITEM_STATUS_NAME; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; @@ -27,7 +25,6 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.UUID; @@ -46,17 +43,20 @@ import org.folio.rest.jaxrs.model.Eresource; import org.folio.rest.jaxrs.model.Location; import org.folio.rest.jaxrs.model.Piece; +import org.folio.rest.jaxrs.model.PieceBatchStatusCollection; import org.folio.rest.jaxrs.model.PoLine; import org.folio.rest.jaxrs.model.PurchaseOrder; import org.folio.rest.jaxrs.model.Title; import org.folio.service.ProtectionService; import org.folio.service.caches.InventoryCache; import org.folio.service.orders.PurchaseOrderLineService; +import org.folio.service.orders.PurchaseOrderStorageService; import org.folio.service.pieces.PieceService; import org.folio.service.pieces.PieceStorageService; import org.folio.service.pieces.PieceUtil; import org.folio.service.pieces.flows.BasePieceFlowHolderBuilder; import org.folio.service.pieces.flows.DefaultPieceFlowsValidator; +import org.folio.service.titles.TitlesService; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; @@ -78,14 +78,26 @@ @ExtendWith(VertxExtension.class) public class PieceUpdateFlowManagerTest { - @Autowired PieceUpdateFlowManager pieceUpdateFlowManager; - @Autowired PieceStorageService pieceStorageService; - @Autowired ProtectionService protectionService; - @Autowired PieceUpdateFlowInventoryManager pieceUpdateFlowInventoryManager; - @Autowired PieceService pieceService; - @Autowired BasePieceFlowHolderBuilder basePieceFlowHolderBuilder; - @Autowired PieceUpdateFlowPoLineService pieceUpdateFlowPoLineService; - @Autowired PurchaseOrderLineService purchaseOrderLineService; + @Autowired + PieceUpdateFlowManager pieceUpdateFlowManager; + @Autowired + PieceStorageService pieceStorageService; + @Autowired + ProtectionService protectionService; + @Autowired + PieceUpdateFlowInventoryManager pieceUpdateFlowInventoryManager; + @Autowired + PieceService pieceService; + @Autowired + TitlesService titlesService; + @Autowired + BasePieceFlowHolderBuilder basePieceFlowHolderBuilder; + @Autowired + PieceUpdateFlowPoLineService pieceUpdateFlowPoLineService; + @Autowired + PurchaseOrderStorageService purchaseOrderStorageService; + @Autowired + PurchaseOrderLineService purchaseOrderLineService; private final Context ctx = getFirstContextFromVertx(getVertx()); @Mock @@ -95,7 +107,7 @@ public class PieceUpdateFlowManagerTest { private static boolean runningOnOwn; @BeforeEach - void initMocks(){ + void initMocks() { MockitoAnnotations.openMocks(this); autowireDependencies(this); requestContext = new RequestContext(ctx, okapiHeadersMock); @@ -173,7 +185,7 @@ void shouldNotUpdateLineQuantityIfPoLineIsPackageAndShouldRunProcessInventory() doReturn(succeededFuture(null)).when(protectionService).isOperationRestricted(any(), any(ProtectedOperationType.class), eq(requestContext)); doReturn(succeededFuture(null)).when(pieceUpdateFlowInventoryManager).processInventory(any(PieceUpdateHolder.class), eq(requestContext)); - doNothing().when(pieceService).receiptConsistencyPiecePoLine(any(JsonObject.class), eq(requestContext)); + doNothing().when(pieceService).receiptConsistencyPiecePoLine(anyString(), eq(requestContext)); doReturn(succeededFuture(null)).when(pieceUpdateFlowPoLineService).updatePoLine(pieceUpdateHolderCapture.capture(), eq(requestContext)); doReturn(succeededFuture(null)) .when(purchaseOrderLineService).saveOrderLine(any(CompositePoLine.class), @@ -239,7 +251,7 @@ void shouldNotUpdateLineQuantityIfManualPieceCreateTrueAndShouldRunProcessInvent doReturn(succeededFuture(null)).when(protectionService).isOperationRestricted(any(), any(ProtectedOperationType.class), eq(requestContext)); doReturn(succeededFuture(null)).when(pieceUpdateFlowInventoryManager).processInventory(any(PieceUpdateHolder.class), eq(requestContext)); - doNothing().when(pieceService).receiptConsistencyPiecePoLine(any(JsonObject.class), eq(requestContext)); + doNothing().when(pieceService).receiptConsistencyPiecePoLine(anyString(), eq(requestContext)); doReturn(succeededFuture(null)).when(pieceUpdateFlowPoLineService).updatePoLine(pieceUpdateHolderCapture.capture(), eq(requestContext)); //When pieceUpdateFlowManager.updatePiece(pieceToUpdate, true, true, requestContext).result(); @@ -286,7 +298,7 @@ void shouldUpdateLineQuantityIfPoLineIsNotPackageAndHoldingReferenceChangedAndSh final ArgumentCaptor pieceToUpdateCapture = ArgumentCaptor.forClass(Piece.class); doReturn(succeededFuture(null)).when(pieceStorageService).updatePiece(pieceToUpdateCapture.capture(), eq(requestContext)); givenPoLineHasPieces(lineId, List.of()); - doNothing().when(pieceService).receiptConsistencyPiecePoLine(any(JsonObject.class), eq(requestContext)); + doNothing().when(pieceService).receiptConsistencyPiecePoLine(anyString(), eq(requestContext)); final ArgumentCaptor pieceUpdateHolderCapture = ArgumentCaptor.forClass(PieceUpdateHolder.class); doAnswer((Answer>) invocation -> { @@ -318,65 +330,29 @@ void shouldUpdateLineQuantityIfPoLineIsNotPackageAndHoldingReferenceChangedAndSh } @Test - void poLineStatusWhenReceiveLast() { - // given - CompositePoLine poLine = new CompositePoLine().withId(UUID.randomUUID().toString()); - List fromStorage = givenPieces(EXPECTED, RECEIVED, UNRECEIVABLE); - List update = List.of(new Piece().withId(fromStorage.get(0).getId()).withReceivingStatus(RECEIVED)); - - // when - var receiptStatus = pieceUpdateFlowManager.calculatePoLineReceiptStatus(poLine, fromStorage, update); - - // then - assertEquals(CompositePoLine.ReceiptStatus.FULLY_RECEIVED, receiptStatus); - } - - @Test - void poLineStatusWhenExpectLast() { - // given - CompositePoLine poLine = new CompositePoLine().withId(UUID.randomUUID().toString()); - List fromStorage = givenPieces(RECEIVED, RECEIVED, UNRECEIVABLE); - List update = List.of(new Piece().withId(fromStorage.get(0).getId()).withReceivingStatus(EXPECTED)); - - // when - var receiptStatus = pieceUpdateFlowManager.calculatePoLineReceiptStatus(poLine, fromStorage, update); - - // then - assertEquals(CompositePoLine.ReceiptStatus.PARTIALLY_RECEIVED, receiptStatus); - } - - @Test - void poLineStatusWhenExpectAll() { - // given - CompositePoLine poLine = new CompositePoLine().withId(UUID.randomUUID().toString()); - List fromStorage = givenPieces(RECEIVED); - List update = List.of(new Piece().withId(fromStorage.get(0).getId()).withReceivingStatus(EXPECTED)); - - // when - var receiptStatus = pieceUpdateFlowManager.calculatePoLineReceiptStatus(poLine, fromStorage, update); - - // then - assertEquals(CompositePoLine.ReceiptStatus.AWAITING_RECEIPT, receiptStatus); - } - - @Test - void poLineStatusWhenReceivePart() { - // given - CompositePoLine poLine = new CompositePoLine().withId(UUID.randomUUID().toString()); - List fromStorage = givenPieces(EXPECTED, EXPECTED); - List update = List.of(new Piece().withId(fromStorage.get(0).getId()).withReceivingStatus(UNRECEIVABLE)); - - // when - var receiptStatus = pieceUpdateFlowManager.calculatePoLineReceiptStatus(poLine, fromStorage, update); - - // then - assertEquals(CompositePoLine.ReceiptStatus.PARTIALLY_RECEIVED, receiptStatus); - } - - private static List givenPieces(Piece.ReceivingStatus... statuses) { - return Arrays.stream(statuses).map(status -> - new Piece().withId(UUID.randomUUID().toString()).withReceivingStatus(status) - ).toList(); + void shouldUpdatePiecesStatusesSuccessfully() { + List pieceIds = List.of(UUID.randomUUID().toString(), UUID.randomUUID().toString()); + PieceBatchStatusCollection.ReceivingStatus receivingStatus = PieceBatchStatusCollection.ReceivingStatus.RECEIVED; + Title title = new Title().withId(UUID.randomUUID().toString()).withAcqUnitIds(List.of(UUID.randomUUID().toString())); + PurchaseOrder purchaseOrder = new PurchaseOrder().withId(UUID.randomUUID().toString()); + PoLine poLine1 = new PoLine().withId(UUID.randomUUID().toString()).withOrderFormat(PoLine.OrderFormat.P_E_MIX).withPurchaseOrderId(purchaseOrder.getId()); + PoLine poLine2 = new PoLine().withId(UUID.randomUUID().toString()).withOrderFormat(PoLine.OrderFormat.P_E_MIX).withPurchaseOrderId(purchaseOrder.getId()); + Piece piece1 = new Piece().withId(pieceIds.get(0)).withPoLineId(poLine1.getId()).withTitleId(title.getId()); + Piece piece2 = new Piece().withId(pieceIds.get(1)).withPoLineId(poLine2.getId()).withTitleId(title.getId()); + + doReturn(succeededFuture(List.of(piece1, piece2))).when(pieceStorageService).getPiecesByIds(pieceIds, requestContext); + doReturn(succeededFuture(List.of(title))).when(titlesService).getTitlesByPieceIds(pieceIds, requestContext); + doReturn(succeededFuture()).when(protectionService).isOperationRestricted(title.getAcqUnitIds(), ProtectedOperationType.UPDATE, requestContext); + doReturn(succeededFuture(poLine1)).when(purchaseOrderLineService).getOrderLineById(poLine1.getId(), requestContext); + doReturn(succeededFuture(poLine2)).when(purchaseOrderLineService).getOrderLineById(poLine2.getId(), requestContext); + doReturn(succeededFuture(purchaseOrder)).when(purchaseOrderStorageService).getPurchaseOrderById(purchaseOrder.getId(), requestContext); + doReturn(succeededFuture()).when(pieceUpdateFlowPoLineService).updatePoLine(any(), eq(requestContext)); + doReturn(succeededFuture()).when(pieceStorageService).updatePiece(any(), eq(requestContext)); + doNothing().when(pieceService).receiptConsistencyPiecePoLine(anyString(), eq(requestContext)); + + Future result = pieceUpdateFlowManager.updatePiecesStatuses(pieceIds, receivingStatus, requestContext); + + assertTrue(result.succeeded()); } private void givenPoLineHasPieces(String lineId, List pieces) { @@ -386,46 +362,74 @@ private void givenPoLineHasPieces(String lineId, List pieces) { } private static class ContextConfiguration { - @Bean PieceStorageService pieceStorageService() { + @Bean + PieceStorageService pieceStorageService() { return mock(PieceStorageService.class); } - @Bean ProtectionService protectionService() { + + @Bean + ProtectionService protectionService() { return mock(ProtectionService.class); } - @Bean PieceService pieceService() { + + @Bean + PieceService pieceService() { return mock(PieceService.class); } - @Bean PieceUpdateFlowInventoryManager pieceUpdateFlowInventoryManager() { + + @Bean + TitlesService titlesService() { + return mock(TitlesService.class); + } + + @Bean + PieceUpdateFlowInventoryManager pieceUpdateFlowInventoryManager() { return mock(PieceUpdateFlowInventoryManager.class); } - @Bean BasePieceFlowHolderBuilder basePieceFlowHolderBuilder() { - return mock(BasePieceFlowHolderBuilder.class); + + @Bean + BasePieceFlowHolderBuilder basePieceFlowHolderBuilder(PurchaseOrderStorageService purchaseOrderStorageService, + PurchaseOrderLineService purchaseOrderLineService, + TitlesService titlesService) { + return spy(new BasePieceFlowHolderBuilder(purchaseOrderStorageService, purchaseOrderLineService, titlesService)); } - @Bean PieceUpdateFlowPoLineService pieceUpdateFlowPoLineService() { - return mock(PieceUpdateFlowPoLineService.class); + + @Bean + PieceUpdateFlowPoLineService pieceUpdateFlowPoLineService() { + return mock(PieceUpdateFlowPoLineService.class); } - @Bean DefaultPieceFlowsValidator defaultPieceFlowsValidator() { + + @Bean + DefaultPieceFlowsValidator defaultPieceFlowsValidator() { return spy(new DefaultPieceFlowsValidator()); } - @Bean RestClient restClient() { + @Bean + RestClient restClient() { return mock(RestClient.class); } - @Bean InventoryCache inventoryCache() { + @Bean + InventoryCache inventoryCache() { return mock(InventoryCache.class); } - @Bean PurchaseOrderLineService purchaseOrderLineService() { + @Bean + PurchaseOrderStorageService purchaseOrderStorageService() { + return mock(PurchaseOrderStorageService.class); + } + + @Bean + PurchaseOrderLineService purchaseOrderLineService() { return mock(PurchaseOrderLineService.class); } @Bean - PieceUpdateFlowManager pieceUpdateFlowManager(PieceStorageService pieceStorageService, PieceService pieceService, + PieceUpdateFlowManager pieceUpdateFlowManager(PieceStorageService pieceStorageService, PieceService pieceService, TitlesService titlesService, ProtectionService protectionService, PieceUpdateFlowPoLineService pieceUpdateFlowPoLineService, PieceUpdateFlowInventoryManager pieceUpdateFlowInventoryManager, BasePieceFlowHolderBuilder basePieceFlowHolderBuilder, DefaultPieceFlowsValidator defaultPieceFlowsValidator, PurchaseOrderLineService purchaseOrderLineService) { - return new PieceUpdateFlowManager(pieceStorageService, pieceService, protectionService, + return new PieceUpdateFlowManager(pieceStorageService, pieceService, titlesService, protectionService, pieceUpdateFlowPoLineService, pieceUpdateFlowInventoryManager, basePieceFlowHolderBuilder, defaultPieceFlowsValidator, purchaseOrderLineService); }