diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 375082e46..b9b45eee4 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -1003,6 +1003,36 @@ "id": "orders.routing-list", "version": "1.0", "handlers": [ + { + "methods": ["GET"], + "pathPattern": "/orders/routing-lists", + "permissionsRequired": ["orders.routing-lists.collection.get"], + "modulePermissions": ["orders-storage.routing-lists.collection.get"] + }, + { + "methods": ["POST"], + "pathPattern": "/orders/routing-lists", + "permissionsRequired": ["orders.routing-lists.item.post"], + "modulePermissions": ["orders-storage.routing-lists.item.post"] + }, + { + "methods": ["GET"], + "pathPattern": "/orders/routing-lists/{id}", + "permissionsRequired": ["orders.routing-lists.item.get"], + "modulePermissions": ["orders-storage.routing-lists.item.get"] + }, + { + "methods": ["PUT"], + "pathPattern": "/orders/routing-lists/{id}", + "permissionsRequired": ["orders.routing-lists.item.put"], + "modulePermissions": ["orders-storage.routing-lists.item.put"] + }, + { + "methods": ["DELETE"], + "pathPattern": "/orders/routing-lists/{id}", + "permissionsRequired": ["orders.routing-lists.item.delete"], + "modulePermissions": ["orders-storage.routing-lists.item.delete"] + }, { "methods": ["GET"], "pathPattern": "/orders/routing-lists/{id}/template", @@ -1731,11 +1761,49 @@ "displayName" : "orders holding-summary get", "description" : "Holding summary" }, + { + "permissionName" : "orders.routing-lists.collection.get", + "displayName" : "orders routing-list collection get", + "description" : "Orders routing-list collection get" + }, + { + "permissionName" : "orders.routing-lists.item.post", + "displayName" : "orders routing-list item post", + "description" : "Orders routing-list item post" + }, + { + "permissionName" : "orders.routing-lists.item.get", + "displayName" : "orders routing-list item get", + "description" : "Orders routing-list item get" + }, + { + "permissionName" : "orders.routing-lists.item.put", + "displayName" : "orders routing-list item put", + "description" : "Orders routing-list item put" + }, + { + "permissionName" : "orders.routing-lists.item.delete", + "displayName" : "orders routing-list item delete", + "description" : "Orders routing-list item delete" + }, { "permissionName": "orders.routing-list-template.item.get", "displayName" : "orders routing-list-template item get", "description" : "Orders routing-list-template item get" }, + { + "permissionName" : "orders.routing-lists.all", + "displayName" : "All routing list perms", + "description" : "All permissions for the routing list", + "subPermissions" : [ + "orders.routing-lists.collection.get", + "orders.routing-lists.item.post", + "orders.routing-lists.item.get", + "orders.routing-lists.item.put", + "orders.routing-lists.item.delete", + "orders.routing-list-template.item.get" + ] + }, { "permissionName": "orders.all", "displayName": "orders - all permissions", @@ -1771,7 +1839,7 @@ "orders.holding-summary.collection.get", "orders.acquisition-methods.all", "orders.export-history.all", - "orders.routing-list-template.item.get" + "orders.routing-lists.all" ] }, { diff --git a/ramls/routing-lists.raml b/ramls/routing-lists.raml index b98725dad..922e4de34 100644 --- a/ramls/routing-lists.raml +++ b/ramls/routing-lists.raml @@ -1,43 +1,84 @@ #%RAML 1.0 -title: "RoutingList" +title: Routing Lists baseUri: https://github.com/folio-org/mod-orders version: v1.0 documentation: - - title: Routing lists - content: CRUD API to manage routing lists. + - title: Routing Lists API + content: API for routing lists types: - routing_list: !include acq-models/mod-orders-storage/schemas/routing_list.json - routing_list_collection: !include acq-models/mod-orders-storage/schemas/routing_list_collection.json - UUID: - type: string - pattern: ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$ + routing-list: !include acq-models/mod-orders-storage/schemas/routing_list.json + routing-list-collection: !include acq-models/mod-orders-storage/schemas/routing_list_collection.json + error: !include raml-util/schemas/error.schema + errors: !include raml-util/schemas/errors.schema + UUID: + type: string + pattern: ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$ traits: - pageable: !include raml-util/traits/pageable.raml - searchable: !include raml-util/traits/searchable.raml + pageable: !include raml-util/traits/pageable.raml + searchable: !include raml-util/traits/searchable.raml + validate: !include raml-util/traits/validation.raml resourceTypes: collection: !include rtypes/collection-with-json-response.raml collection-item: !include rtypes/item-collection-with-json-response.raml /orders/routing-lists: + displayName: Routing Lists type: collection: exampleCollection: !include acq-models/mod-orders-storage/examples/routing_list_collection.sample exampleItem: !include acq-models/mod-orders-storage/examples/routing_list_get.sample - schemaCollection: routing_list_collection - schemaItem: routing_list + schemaCollection: routing-list-collection + schemaItem: routing-list get: - description: Get routing lists + description: Get a collection of routing lists is: [ - searchable: {description: "with valid searchable fields: for example routing list", example: "[\"routing_list\", \"ROUTING_LIST\", \"=\"]"}, + searchable: { description: "CQL query", example: "name=MyRoutingList" }, pageable ] post: - description: Create routing lists - + description: Create a new routing list record + is: [validate] + body: + application/json: + type: routing-list + example: + strict: false + value: !include acq-models/mod-orders-storage/examples/routing_list_get.sample + responses: + 201: + description: "Returns a newly created item, with server-controlled fields like 'id' populated" + body: + application/json: + example: !include acq-models/mod-orders-storage/examples/routing_list_get.sample + 400: + description: "Bad request, e.g. malformed request body or query parameter. Details of the error (e.g. name of the parameter or line/character number with malformed data) provided in the response." + body: + application/json: + type: error + 401: + description: "Not authorized to perform requested action" + body: + application/json: + type: error + 500: + description: "Internal server error, e.g. due to misconfiguration" + body: + application/json: + type: error + /{id}: + uriParameters: + id: + description: The UUID of a Routing List + type: UUID + description: Get, Delete or Update a specific routing list + type: + collection-item: + exampleItem: !include acq-models/mod-orders-storage/examples/routing_list_get.sample + schema: routing-list /{id}/template: uriParameters: id: @@ -45,3 +86,4 @@ resourceTypes: type: UUID get: description: Execute mod-template-engine to process templates with replaced token placeholders [update] + diff --git a/src/main/java/org/folio/config/ApplicationConfig.java b/src/main/java/org/folio/config/ApplicationConfig.java index 6c1a45321..4870506b7 100644 --- a/src/main/java/org/folio/config/ApplicationConfig.java +++ b/src/main/java/org/folio/config/ApplicationConfig.java @@ -112,7 +112,7 @@ import org.folio.service.pieces.flows.update.PieceUpdateFlowInventoryManager; import org.folio.service.pieces.flows.update.PieceUpdateFlowManager; import org.folio.service.pieces.flows.update.PieceUpdateFlowPoLineService; -import org.folio.service.RoutingListService; +import org.folio.service.routinglists.RoutingListService; import org.folio.service.titles.TitleValidationService; import org.folio.service.titles.TitlesService; import org.springframework.beans.factory.annotation.Qualifier; @@ -437,8 +437,8 @@ CompositeOrderDynamicDataPopulateService combinedPopulateService(CompositeOrderR } @Bean - RoutingListService routingListService(RestClient restClient, UserService userService) { - return new RoutingListService(restClient, userService); + RoutingListService routingListService(RestClient restClient, PurchaseOrderLineService purchaseOrderLineService, UserService userService) { + return new RoutingListService(restClient, purchaseOrderLineService, userService); } @Bean diff --git a/src/main/java/org/folio/rest/core/exceptions/ErrorCodes.java b/src/main/java/org/folio/rest/core/exceptions/ErrorCodes.java index e79a11145..238d306d2 100644 --- a/src/main/java/org/folio/rest/core/exceptions/ErrorCodes.java +++ b/src/main/java/org/folio/rest/core/exceptions/ErrorCodes.java @@ -117,7 +117,10 @@ public enum ErrorCodes { CLAIMING_CONFIG_INVALID("claimingConfigInvalid", "Claiming interval should be set and greater than 0 if claiming is active"), TEMPLATE_NAME_ALREADY_EXISTS("templateNameNotUnique", "Template name already exists"), BARCODE_IS_NOT_UNIQUE("barcodeIsNotUnique", "The barcode already exists. The barcode must be unique"), - DELETE_WITH_EXPENDED_AMOUNT("deleteWithExpendedAmount", "Cannot delete en encumbrance with an expended amount"); + DELETE_WITH_EXPENDED_AMOUNT("deleteWithExpendedAmount", "Cannot delete an encumbrance with an expended amount"), + INVALID_ROUTING_LIST_FOR_PO_LINE_FORMAT("invalidRoutingListForPoLineFormat", "Cannot create routing list for POL without 'Physical' or 'P/E Mix' order format"), + ROUTING_LIST_LIMIT_REACHED_FOR_PO_LINE("routingListLimitReachedForPoLine", "Cannot create routing list for POL as the associated lists' amount is not less than Physical copies"), + PO_LINE_NOT_FOUND_FOR_ROUTING_LIST("poLineNotFoundForRoutingList", "Cannot find a corresponding PO Line with the provided id"); private final String code; private final String description; diff --git a/src/main/java/org/folio/rest/impl/RoutingListsAPI.java b/src/main/java/org/folio/rest/impl/RoutingListsAPI.java index 23a61328d..c57a08f0a 100644 --- a/src/main/java/org/folio/rest/impl/RoutingListsAPI.java +++ b/src/main/java/org/folio/rest/impl/RoutingListsAPI.java @@ -5,18 +5,19 @@ import javax.ws.rs.core.Response; import java.util.Map; -import io.vertx.core.AsyncResult; -import io.vertx.core.Context; -import io.vertx.core.Handler; -import io.vertx.core.Vertx; -import org.apache.commons.lang.NotImplementedException; +import org.folio.rest.annotations.Validate; import org.folio.rest.core.models.RequestContext; import org.folio.rest.jaxrs.model.RoutingList; import org.folio.rest.jaxrs.resource.OrdersRoutingLists; -import org.folio.service.RoutingListService; +import org.folio.service.routinglists.RoutingListService; import org.folio.spring.SpringContextUtil; import org.springframework.beans.factory.annotation.Autowired; +import io.vertx.core.AsyncResult; +import io.vertx.core.Context; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; + public class RoutingListsAPI extends BaseApi implements OrdersRoutingLists { @Autowired @@ -27,15 +28,44 @@ public RoutingListsAPI() { } @Override - public void getOrdersRoutingLists(String query, String totalRecords, int offset, int limit, Map okapiHeaders, - Handler> asyncResultHandler, Context vertxContext) { - throw new NotImplementedException(); + @Validate + public void getOrdersRoutingLists(String query, String totalRecords, int offset, int limit, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { + routingListService.getRoutingLists(limit, offset, query, new RequestContext(vertxContext, okapiHeaders)) + .onSuccess(lists -> asyncResultHandler.handle(succeededFuture(buildOkResponse(lists)))) + .onFailure(fail -> handleErrorResponse(asyncResultHandler, fail)); + } + + @Override + @Validate + public void postOrdersRoutingLists(RoutingList entity, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { + routingListService.createRoutingList(entity, new RequestContext(vertxContext, okapiHeaders)) + .onSuccess(list -> asyncResultHandler.handle(succeededFuture(buildOkResponse(list)))) + .onFailure(fail -> handleErrorResponse(asyncResultHandler, fail)); + } + + @Override + @Validate + public void getOrdersRoutingListsById(String id, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { + routingListService.getRoutingList(id, new RequestContext(vertxContext, okapiHeaders)) + .onSuccess(list -> asyncResultHandler.handle(succeededFuture(buildOkResponse(list)))) + .onFailure(fail -> handleErrorResponse(asyncResultHandler, fail)); + } + + @Override + @Validate + public void deleteOrdersRoutingListsById(String id, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { + routingListService.deleteRoutingList(id, new RequestContext(vertxContext, okapiHeaders)) + .onSuccess(list -> asyncResultHandler.handle(succeededFuture(buildNoContentResponse()))) + .onFailure(fail -> handleErrorResponse(asyncResultHandler, fail)); + } @Override - public void postOrdersRoutingLists(RoutingList entity, Map okapiHeaders, - Handler> asyncResultHandler, Context vertxContext) { - throw new NotImplementedException(); + @Validate + public void putOrdersRoutingListsById(String id, RoutingList entity, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { + routingListService.updateRoutingList(entity, new RequestContext(vertxContext, okapiHeaders)) + .onSuccess(list -> asyncResultHandler.handle(succeededFuture(buildNoContentResponse()))) + .onFailure(fail -> handleErrorResponse(asyncResultHandler, fail)); } @Override diff --git a/src/main/java/org/folio/service/RoutingListService.java b/src/main/java/org/folio/service/routinglists/RoutingListService.java similarity index 57% rename from src/main/java/org/folio/service/RoutingListService.java rename to src/main/java/org/folio/service/routinglists/RoutingListService.java index 8c9b92b9e..6837354f2 100644 --- a/src/main/java/org/folio/service/RoutingListService.java +++ b/src/main/java/org/folio/service/routinglists/RoutingListService.java @@ -1,4 +1,4 @@ -package org.folio.service; +package org.folio.service.routinglists; import static org.folio.orders.utils.ResourcePathResolver.ORDER_SETTINGS; import static org.folio.orders.utils.ResourcePathResolver.ROUTING_LISTS; @@ -9,8 +9,7 @@ import java.util.List; import java.util.UUID; -import io.vertx.core.Future; -import io.vertx.core.json.JsonObject; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.kafka.common.errors.ResourceNotFoundException; @@ -18,34 +17,104 @@ import org.apache.logging.log4j.Logger; import org.folio.models.TemplateProcessingRequest; import org.folio.models.UserCollection; +import org.folio.rest.RestConstants; import org.folio.rest.acq.model.SettingCollection; import org.folio.rest.core.RestClient; +import org.folio.rest.core.exceptions.HttpException; import org.folio.rest.core.models.RequestContext; import org.folio.rest.core.models.RequestEntry; +import org.folio.rest.jaxrs.model.Error; +import org.folio.rest.jaxrs.model.Errors; +import org.folio.rest.jaxrs.model.PoLine; import org.folio.rest.jaxrs.model.RoutingList; +import org.folio.rest.jaxrs.model.RoutingListCollection; +import org.folio.service.UserService; +import org.folio.service.orders.PurchaseOrderLineService; +import org.folio.service.routinglists.validators.RoutingListValidatorUtil; + +import io.vertx.core.Future; +import io.vertx.core.json.JsonObject; public class RoutingListService { + private static final Logger logger = LogManager.getLogger(RoutingListService.class); - private static final Logger log = LogManager.getLogger(); - private static final UUID TEMPLATE_REQUEST_ID = UUID.fromString("9465105a-e8a1-470c-9817-142d33bc4fcd"); - private static final String TEMPLATE_REQUEST_LANG = "en"; - private static final String TEMPLATE_REQUEST_OUTPUT = "text/html"; private static final String ROUTING_LIST_ENDPOINT = resourcesPath(ROUTING_LISTS); private static final String ORDER_SETTINGS_ENDPOINT = resourcesPath(ORDER_SETTINGS); private static final String ROUTING_USER_ADDRESS_TYPE_ID = "ROUTING_USER_ADDRESS_TYPE_ID"; private static final String ROUTING_LIST_BY_ID_ENDPOINT = ROUTING_LIST_ENDPOINT + "/{id}"; + private static final String ROUTING_LIST_BY_POL_ID = "poLineId==%s"; + private static final String TEMPLATE_REQUEST_ENDPOINT = resourcesPath(TEMPLATE_REQUEST); + private static final UUID TEMPLATE_REQUEST_ID = UUID.fromString("9465105a-e8a1-470c-9817-142d33bc4fcd"); + private static final String TEMPLATE_REQUEST_LANG = "en"; + private static final String TEMPLATE_REQUEST_OUTPUT = "text/html"; - private final RestClient restClient; + private final PurchaseOrderLineService poLineService; private final UserService userService; + private final RestClient restClient; - public RoutingListService(RestClient restClient, UserService userService) { + public RoutingListService(RestClient restClient, PurchaseOrderLineService poLineService, UserService userService) { this.restClient = restClient; + this.poLineService = poLineService; this.userService = userService; } + public Future getRoutingList(String rListId, RequestContext requestContext) { + RequestEntry requestEntry = new RequestEntry(ROUTING_LIST_BY_ID_ENDPOINT).withId(rListId); + return restClient.get(requestEntry, RoutingList.class, requestContext); + } + + public Future updateRoutingList(RoutingList routingList, RequestContext requestContext) { + try { + validateRoutingList(routingList, requestContext); + } catch (HttpException e) { + return Future.failedFuture(e); + } + RequestEntry requestEntry = new RequestEntry(ROUTING_LIST_BY_ID_ENDPOINT).withId(routingList.getId()); + return restClient.put(requestEntry, routingList, requestContext); + } + + public Future deleteRoutingList(String rListId, RequestContext requestContext) { + RequestEntry requestEntry = new RequestEntry(ROUTING_LIST_BY_ID_ENDPOINT).withId(rListId); + return restClient.delete(requestEntry, requestContext); + } + + public Future createRoutingList(RoutingList routingList, RequestContext requestContext) { + try { + validateRoutingList(routingList, requestContext); + } catch (HttpException e) { + return Future.failedFuture(e); + } + RequestEntry requestEntry = new RequestEntry(ROUTING_LIST_ENDPOINT); + return restClient.post(requestEntry, routingList, RoutingList.class, requestContext); + } + + public Future getRoutingLists(int limit, int offset, String query, RequestContext requestContext) { + RequestEntry requestEntry = new RequestEntry(ROUTING_LIST_ENDPOINT) + .withLimit(limit) + .withOffset(offset) + .withQuery(query); + return restClient.get(requestEntry, RoutingListCollection.class, requestContext); + } + + private Future getRoutingListsByPoLineId(String poLineId, RequestContext requestContext) { + String query = String.format(ROUTING_LIST_BY_POL_ID, poLineId); + return getRoutingLists(Integer.MAX_VALUE, 0, query, requestContext); + } + + private void validateRoutingList(RoutingList routingList, RequestContext requestContext) throws HttpException { + RoutingListCollection routingLists = getRoutingListsByPoLineId(routingList.getPoLineId(), requestContext).result(); + PoLine poLine = poLineService.getOrderLineById(routingList.getPoLineId(), requestContext).result(); + List combinedErrors = RoutingListValidatorUtil.validateRoutingList(routingLists, poLine); + if (CollectionUtils.isNotEmpty(combinedErrors)) { + Errors errors = new Errors().withErrors(combinedErrors).withTotalRecords(combinedErrors.size()); + logger.error("Validation error: {}", JsonObject.mapFrom(errors).encodePrettily()); + throw new HttpException(RestConstants.VALIDATION_ERROR, errors); + } + } + public Future processTemplateRequest(String routingListId, RequestContext requestContext) { - log.debug("processTemplateRequest: Tying to process template request for routingListId={}", routingListId); + logger.debug("processTemplateRequest: Tying to process template request for routingListId={}", routingListId); return getRoutingListById(routingListId, requestContext) .compose(routingList -> getUsersAndCreateTemplate(routingList, requestContext)) .compose(templateProcessingRequest -> postTemplateRequest(templateProcessingRequest, requestContext)); @@ -69,7 +138,7 @@ private Future getAddressTypeId(RequestContext requestContext) { .map(settingCollection -> { var settings = settingCollection.getSettings(); if (ObjectUtils.isEmpty(settings) || StringUtils.isBlank(settings.get(0).getValue())) { - log.error("getAddressTypeId:: Setting is not found with key={}", ROUTING_USER_ADDRESS_TYPE_ID); + logger.error("getAddressTypeId:: Setting is not found with key={}", ROUTING_USER_ADDRESS_TYPE_ID); throw new ResourceNotFoundException(String.format("Setting is not found with key=%s", ROUTING_USER_ADDRESS_TYPE_ID)); } return settings.get(0).getValue(); @@ -82,7 +151,7 @@ private TemplateProcessingRequest createTemplateRequest(RoutingList routingList, .withRoutingList(fillRoutingListForContext(routingList)) .withUsers(fillUsersForContext(users, addressTypeId))); - log.info("createTemplateRequest:: TemplateProcessingRequest object created for routing list name: {}", + logger.info("createTemplateRequest:: TemplateProcessingRequest object created for routing list name: {}", templateRequest.getContext().getRoutingList().getName()); return templateRequest; } @@ -117,11 +186,11 @@ private List fillUsersForContext(UserCollection private String getUserAddress(List addressList, String addressTypeId) { for (UserCollection.User.Personal.Address address : addressList) { if (address.getAddressTypeId().equals(addressTypeId)) { - log.info("getUserAddress:: Required address with addressTypeId={} is found", addressTypeId); + logger.info("getUserAddress:: Required address with addressTypeId={} is found", addressTypeId); return address.getAddressLine1(); } } - log.warn("getUserAddress:: Required address is not found with addressTypId={}", addressTypeId); + logger.warn("getUserAddress:: Required address is not found with addressTypId={}", addressTypeId); return ""; } @@ -133,7 +202,8 @@ private RoutingList fillRoutingListForContext(RoutingList routingList) { private Future postTemplateRequest(TemplateProcessingRequest templateRequest, RequestContext requestContext) { var requestEntry = new RequestEntry(TEMPLATE_REQUEST_ENDPOINT); - log.info("postTemplateRequest:: Sending template request with routing list name={}", templateRequest.getContext().getRoutingList().getName()); + logger.info("postTemplateRequest:: Sending template request with routing list name={}", templateRequest.getContext().getRoutingList().getName()); return restClient.postJsonObject(requestEntry, JsonObject.mapFrom(templateRequest), requestContext); } + } diff --git a/src/main/java/org/folio/service/routinglists/validators/RoutingListValidatorUtil.java b/src/main/java/org/folio/service/routinglists/validators/RoutingListValidatorUtil.java new file mode 100644 index 000000000..3f68e28d9 --- /dev/null +++ b/src/main/java/org/folio/service/routinglists/validators/RoutingListValidatorUtil.java @@ -0,0 +1,41 @@ +package org.folio.service.routinglists.validators; + +import java.util.ArrayList; +import java.util.List; + +import org.folio.rest.core.exceptions.ErrorCodes; +import org.folio.rest.jaxrs.model.Error; +import org.folio.rest.jaxrs.model.Location; +import org.folio.rest.jaxrs.model.PoLine; +import org.folio.rest.jaxrs.model.RoutingListCollection; + +public class RoutingListValidatorUtil { + + private RoutingListValidatorUtil() { } + + public static List validateRoutingList(RoutingListCollection rListExisting, PoLine poLine) { + List errors = new ArrayList<>(); + if (poLine == null) { + errors.add(ErrorCodes.PO_LINE_NOT_FOUND_FOR_ROUTING_LIST); + } else if (!isPoLineFormatValid(poLine)) { + errors.add(ErrorCodes.INVALID_ROUTING_LIST_FOR_PO_LINE_FORMAT); + } else if (isRoutingListsLimitReached(rListExisting, poLine)) { + errors.add(ErrorCodes.ROUTING_LIST_LIMIT_REACHED_FOR_PO_LINE); + } + return errors.stream().map(ErrorCodes::toError).toList(); + } + + private static boolean isPoLineFormatValid(PoLine poLine) { + return poLine.getOrderFormat() == PoLine.OrderFormat.PHYSICAL_RESOURCE + || poLine.getOrderFormat() == PoLine.OrderFormat.P_E_MIX; + } + + private static boolean isRoutingListsLimitReached(RoutingListCollection rListExisting, PoLine poLine) { + return getQuantityPhysicalTotal(poLine) <= rListExisting.getTotalRecords(); + } + + private static int getQuantityPhysicalTotal(PoLine poLine) { + return poLine.getLocations().stream().mapToInt(Location::getQuantityPhysical).sum(); + } + +} diff --git a/src/test/java/org/folio/ApiTestSuite.java b/src/test/java/org/folio/ApiTestSuite.java index f0dac243e..145383176 100644 --- a/src/test/java/org/folio/ApiTestSuite.java +++ b/src/test/java/org/folio/ApiTestSuite.java @@ -99,7 +99,7 @@ import org.folio.service.pieces.flows.update.PieceUpdateFlowManagerTest; import org.folio.service.pieces.flows.update.PieceUpdateFlowPoLineServiceTest; import org.folio.service.pieces.validators.PieceValidatorUtilTest; -import org.folio.service.RoutingListServiceTest; +import org.folio.service.routinglists.RoutingListServiceTest; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Nested; diff --git a/src/test/java/org/folio/TestUtils.java b/src/test/java/org/folio/TestUtils.java index 1c5273899..ba9216596 100644 --- a/src/test/java/org/folio/TestUtils.java +++ b/src/test/java/org/folio/TestUtils.java @@ -175,6 +175,22 @@ public static CompositePoLine getMinimalContentCompositePoLine(String orderId) { .withPurchaseOrderId(orderId); } + public static PoLine getMinimalContentPoLine() { + return getMinimalContentPoLine(MIN_PO_ID); + } + + public static PoLine getMinimalContentPoLine(String orderId) { + return new PoLine().withSource(PoLine.Source.EDI) + .withId(MIN_PO_LINE_ID) + .withOrderFormat(PoLine.OrderFormat.PHYSICAL_RESOURCE) + .withAcquisitionMethod(PURCHASE_METHOD) + .withPhysical(new Physical().withMaterialType("2d1398ae-e1aa-4c7c-b9c9-15adf8cf6425")) + .withCost(new Cost().withCurrency("EUR").withQuantityPhysical(1).withListUnitPrice(10.0)) + .withLocations(Collections.singletonList(new Location().withLocationId("2a00b0be-1447-42a1-a112-124450991899").withQuantityPhysical(1).withQuantity(1))) + .withTitleOrPackage("Title") + .withPurchaseOrderId(orderId); + } + public static Title getMinimalContentTitle() { return new Title().withTitle("Test title").withId(SAMPLE_TITLE_ID); } @@ -240,4 +256,13 @@ public static void validateSavedPoLines() { poline.mapTo(PoLine.class); }); } + + public static List getLocationPhysicalCopies(int n) { + return List.of(new Location() + .withLocationId(UUID.randomUUID().toString()) + .withQuantityElectronic(0) + .withQuantityPhysical(n) + .withQuantity(n)); + } + } diff --git a/src/test/java/org/folio/rest/impl/MockServer.java b/src/test/java/org/folio/rest/impl/MockServer.java index dc4c9220b..4c9f28e6f 100644 --- a/src/test/java/org/folio/rest/impl/MockServer.java +++ b/src/test/java/org/folio/rest/impl/MockServer.java @@ -63,6 +63,7 @@ import static org.folio.orders.utils.ResourcePathResolver.FINANCE_EXCHANGE_RATE; import static org.folio.orders.utils.ResourcePathResolver.FUNDS; import static org.folio.orders.utils.ResourcePathResolver.LEDGERS; +import static org.folio.orders.utils.ResourcePathResolver.ROUTING_LISTS; import static org.folio.orders.utils.ResourcePathResolver.USER_TENANTS_ENDPOINT; import static org.folio.orders.utils.ResourcePathResolver.LEDGER_FY_ROLLOVERS; import static org.folio.orders.utils.ResourcePathResolver.LEDGER_FY_ROLLOVER_ERRORS; @@ -200,6 +201,8 @@ import org.folio.rest.jaxrs.model.PurchaseOrderCollection; import org.folio.rest.jaxrs.model.ReasonForClosure; import org.folio.rest.jaxrs.model.ReasonForClosureCollection; +import org.folio.rest.jaxrs.model.RoutingList; +import org.folio.rest.jaxrs.model.RoutingListCollection; import org.folio.rest.jaxrs.model.Suffix; import org.folio.rest.jaxrs.model.SuffixCollection; @@ -242,7 +245,7 @@ public class MockServer { private static final String HOLDINGS_SOURCE_MOCK_DATA_PATH = BASE_MOCK_DATA_PATH + "holdingsSources/"; public static final String PIECE_RECORDS_MOCK_DATA_PATH = BASE_MOCK_DATA_PATH + "pieces/"; public static final String PO_LINES_MOCK_DATA_PATH = BASE_MOCK_DATA_PATH + "lines/"; - public static final String ROUTING_LIST_MOCK_DATA_PATH = BASE_MOCK_DATA_PATH + "routingLists/"; + public static final String ROUTING_LISTS_MOCK_DATA_PATH = BASE_MOCK_DATA_PATH + "routingLists/"; public static final String USERS_MOCK_DATA_PATH = BASE_MOCK_DATA_PATH + "users/"; public static final String TITLES_MOCK_DATA_PATH = BASE_MOCK_DATA_PATH + "titles/"; private static final String ACQUISITIONS_UNITS_MOCK_DATA_PATH = BASE_MOCK_DATA_PATH + "acquisitionsUnits/units"; @@ -259,6 +262,7 @@ public class MockServer { static final String ORDER_TEMPLATES_COLLECTION = ORDER_TEMPLATES_MOCK_DATA_PATH + "/orderTemplates.json"; private static final String FUNDS_PATH = BASE_MOCK_DATA_PATH + "funds/funds.json"; private static final String TITLES_PATH = BASE_MOCK_DATA_PATH + "titles/titles.json"; + private static final String ROUTING_LISTS_PATH = BASE_MOCK_DATA_PATH + "routing-lists/routing-lists.json"; public static final String BUDGETS_PATH = BASE_MOCK_DATA_PATH + "budgets/budgets.json"; public static final String LEDGERS_PATH = BASE_MOCK_DATA_PATH + "ledgers/ledgers.json"; public static final String PATCH_ORDER_LINES_REQUEST_PATCH = BASE_MOCK_DATA_PATH + "patchOrderLines/patch.json"; @@ -546,6 +550,7 @@ private Router defineRoutes() { router.post(resourcesPath(ORDER_TEMPLATES)).handler(ctx -> handlePostGenericSubObj(ctx, ORDER_TEMPLATES)); router.post(resourcesPath(FINANCE_BATCH_TRANSACTIONS)).handler(this::handleBatchTransactions); router.post(resourcesPath(TITLES)).handler(ctx -> handlePostGenericSubObj(ctx, TITLES)); + router.post(resourcesPath(ROUTING_LISTS)).handler(ctx -> handlePostGenericSubObj(ctx, ROUTING_LISTS)); router.post(resourcesPath(ACQUISITIONS_UNITS)).handler(ctx -> handlePostGenericSubObj(ctx, ACQUISITIONS_UNITS)); router.post(resourcesPath(ACQUISITION_METHODS)).handler(ctx -> handlePostGenericSubObj(ctx, ACQUISITION_METHODS)); @@ -599,6 +604,8 @@ private Router defineRoutes() { router.get(resourcesPath(LEDGERS)).handler(this::handleGetLedgers); router.get(resourcesPath(TITLES)).handler(this::handleGetTitles); router.get(resourcePath(TITLES)).handler(this::handleGetOrderTitleById); + router.get(resourcesPath(ROUTING_LISTS)).handler(this::handleGetRoutingLists); + router.get(resourcePath(ROUTING_LISTS)).handler(this::handleGetRoutingListById); router.get(resourcesPath(REASONS_FOR_CLOSURE)).handler(ctx -> handleGetGenericSubObjs(ctx, REASONS_FOR_CLOSURE)); router.get(resourcesPath(PREFIXES)).handler(ctx -> handleGetGenericSubObjs(ctx, PREFIXES)); router.get(resourcesPath(SUFFIXES)).handler(ctx -> handleGetGenericSubObjs(ctx, SUFFIXES)); @@ -633,6 +640,7 @@ private Router defineRoutes() { router.put(resourcePath(ACQUISITIONS_MEMBERSHIPS)).handler(ctx -> handlePutGenericSubObj(ctx, ACQUISITIONS_MEMBERSHIPS)); router.put(resourcePath(ORDER_TEMPLATES)).handler(ctx -> handlePutGenericSubObj(ctx, ORDER_TEMPLATES)); router.put(resourcePath(TITLES)).handler(ctx -> handlePutGenericSubObj(ctx, TITLES)); + router.put(resourcePath(ROUTING_LISTS)).handler(ctx -> handlePutGenericSubObj(ctx, ROUTING_LISTS)); router.put(resourcePath(REASONS_FOR_CLOSURE)).handler(ctx -> handlePutGenericSubObj(ctx, REASONS_FOR_CLOSURE)); router.put(resourcePath(PREFIXES)).handler(ctx -> handlePutGenericSubObj(ctx, PREFIXES)); router.put(resourcePath(SUFFIXES)).handler(ctx -> handlePutGenericSubObj(ctx, SUFFIXES)); @@ -649,6 +657,7 @@ private Router defineRoutes() { router.delete(resourcePath(ACQUISITIONS_MEMBERSHIPS)).handler(ctx -> handleDeleteGenericSubObj(ctx, ACQUISITIONS_MEMBERSHIPS)); router.delete(resourcePath(ORDER_TEMPLATES)).handler(ctx -> handleDeleteGenericSubObj(ctx, ORDER_TEMPLATES)); router.delete(resourcePath(TITLES)).handler(ctx -> handleDeleteGenericSubObj(ctx, TITLES)); + router.delete(resourcePath(ROUTING_LISTS)).handler(ctx -> handleDeleteGenericSubObj(ctx, ROUTING_LISTS)); router.delete(resourcePath(REASONS_FOR_CLOSURE)).handler(ctx -> handleDeleteGenericSubObj(ctx, REASONS_FOR_CLOSURE)); router.delete(resourcePath(PREFIXES)).handler(ctx -> handleDeleteGenericSubObj(ctx, PREFIXES)); router.delete(resourcePath(SUFFIXES)).handler(ctx -> handleDeleteGenericSubObj(ctx, SUFFIXES)); @@ -712,6 +721,25 @@ private JsonObject getTitlesByPoLineIds(List poLineIds) { return JsonObject.mapFrom(record); } + private JsonObject getRoutingListsByPoLineId(List poLineId) { + Supplier> getFromFile = () -> { + try { + return new JsonObject(getMockData(ROUTING_LISTS_PATH)).mapTo(RoutingListCollection.class).getRoutingLists(); + } catch (IOException e) { + return Collections.emptyList(); + } + }; + + List rLists = getMockEntries(ROUTING_LISTS, RoutingList.class).orElseGet(getFromFile); + + if (!poLineId.isEmpty()) { + rLists.removeIf(item -> !item.getPoLineId().equals(poLineId.get(0))); + } + + Object record = new RoutingListCollection().withRoutingLists(rLists).withTotalRecords(rLists.size()); + return JsonObject.mapFrom(record); + } + private void handleGetFunds(RoutingContext ctx) { String query = StringUtils.trimToEmpty(ctx.request().getParam(QUERY)); addServerRqQuery(FUNDS, query); @@ -1942,6 +1970,60 @@ private void handleGetTitles(RoutingContext ctx) { } } + private void handleGetRoutingListById(RoutingContext ctx) { + logger.info("got: " + ctx.request().path()); + String id = ctx.request().getParam(ID); + logger.info("id: " + id); + + addServerRqRsData(HttpMethod.GET, ROUTING_LISTS, new JsonObject().put(ID, id)); + + if (ID_FOR_INTERNAL_SERVER_ERROR.equals(id)) { + serverResponse(ctx, 500, APPLICATION_JSON, INTERNAL_SERVER_ERROR.getReasonPhrase()); + } else { + try { + + // Attempt to find title in mock server memory + JsonObject existantTitle = getMockEntry(ROUTING_LISTS, id).orElse(null); + + // If previous step has no result then attempt to find title in stubs + if (existantTitle == null) { + RoutingList title = new JsonObject(getMockData(String.format("%s%s.json", ROUTING_LISTS_MOCK_DATA_PATH, id))).mapTo(RoutingList.class); + existantTitle = JsonObject.mapFrom(title); + } + + serverResponse(ctx, 200, APPLICATION_JSON, existantTitle.encodePrettily()); + } catch (IOException e) { + serverResponse(ctx, 404, APPLICATION_JSON, id); + } + } + } + + private void handleGetRoutingLists(RoutingContext ctx) { + String query = StringUtils.trimToEmpty(ctx.request().getParam(QUERY)); + addServerRqQuery(ROUTING_LISTS, query); + if (query.contains(ID_FOR_INTERNAL_SERVER_ERROR)) { + serverResponse(ctx, 500, APPLICATION_JSON, INTERNAL_SERVER_ERROR.getReasonPhrase()); + } else { + try { + + List ids = Collections.emptyList(); + if (query.contains("poLineId==")) { + ids = extractValuesFromQuery("poLineId", query); + } + + JsonObject collection = getRoutingListsByPoLineId(ids); + addServerRqRsData(HttpMethod.GET, ROUTING_LISTS, collection); + + ctx.response() + .setStatusCode(200) + .putHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON) + .end(collection.encodePrettily()); + } catch (Exception e) { + serverResponse(ctx, 500, APPLICATION_JSON, INTERNAL_SERVER_ERROR.getReasonPhrase()); + } + } + } + private List extractIdsFromQuery(String query) { return extractValuesFromQuery(ID, query); diff --git a/src/test/java/org/folio/rest/impl/RoutingListsApiTest.java b/src/test/java/org/folio/rest/impl/RoutingListsApiTest.java index 401d0eb9e..8a9c2ad27 100644 --- a/src/test/java/org/folio/rest/impl/RoutingListsApiTest.java +++ b/src/test/java/org/folio/rest/impl/RoutingListsApiTest.java @@ -1,9 +1,15 @@ package org.folio.rest.impl; +import static io.vertx.core.Future.failedFuture; import static io.vertx.core.Future.succeededFuture; import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import static javax.ws.rs.core.MediaType.TEXT_PLAIN; import static org.folio.RestTestUtils.prepareHeaders; +import static org.folio.RestTestUtils.verifyDeleteResponse; import static org.folio.RestTestUtils.verifyGet; +import static org.folio.RestTestUtils.verifyPostResponse; +import static org.folio.RestTestUtils.verifyPut; +import static org.folio.RestTestUtils.verifySuccessGet; import static org.folio.TestConfig.X_OKAPI_URL; import static org.folio.TestConfig.autowireDependencies; import static org.folio.TestConfig.clearServiceInteractions; @@ -13,30 +19,58 @@ import static org.folio.TestConfig.isVerticleNotDeployed; import static org.folio.TestConfig.mockPort; import static org.folio.TestConstants.EMPTY_CONFIG_X_OKAPI_TENANT; +import static org.folio.TestConstants.EXIST_CONFIG_X_OKAPI_TENANT_LIMIT_10; +import static org.folio.TestConstants.ID_BAD_FORMAT; +import static org.folio.TestConstants.ID_DOES_NOT_EXIST; import static org.folio.TestConstants.ROUTING_LIST_ID; import static org.folio.TestConstants.X_OKAPI_TOKEN; import static org.folio.TestConstants.X_OKAPI_USER_ID; +import static org.folio.TestConstants.X_OKAPI_USER_ID_WITH_ACQ_UNITS; +import static org.folio.TestUtils.getLocationPhysicalCopies; +import static org.folio.TestUtils.getMinimalContentPoLine; +import static org.folio.TestUtils.getMockAsJson; +import static org.folio.orders.utils.ResourcePathResolver.PO_LINES_STORAGE; +import static org.folio.rest.RestConstants.BAD_REQUEST; +import static org.folio.rest.RestConstants.NOT_FOUND; import static org.folio.rest.RestConstants.OKAPI_URL; +import static org.folio.rest.core.exceptions.ErrorCodes.INVALID_ROUTING_LIST_FOR_PO_LINE_FORMAT; +import static org.folio.rest.core.exceptions.ErrorCodes.PO_LINE_NOT_FOUND_FOR_ROUTING_LIST; +import static org.folio.rest.core.exceptions.ErrorCodes.ROUTING_LIST_LIMIT_REACHED_FOR_PO_LINE; +import static org.folio.rest.impl.MockServer.ROUTING_LISTS_MOCK_DATA_PATH; +import static org.folio.rest.impl.MockServer.addMockEntry; import static org.folio.rest.impl.PurchaseOrdersApiTest.X_OKAPI_TENANT; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; -import io.vertx.core.Context; -import io.vertx.core.json.JsonObject; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.folio.ApiTestSuite; +import org.folio.HttpStatus; +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.service.RoutingListService; +import org.folio.rest.jaxrs.model.Error; +import org.folio.rest.jaxrs.model.Errors; +import org.folio.rest.jaxrs.model.PoLine; +import org.folio.rest.jaxrs.model.RoutingList; +import org.folio.rest.jaxrs.model.RoutingListCollection; +import org.folio.service.routinglists.RoutingListService; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; @@ -46,18 +80,28 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; -public class RoutingListsApiTest { +import io.vertx.core.Context; +import io.vertx.core.json.JsonObject; +public class RoutingListsApiTest { private static final Logger logger = LogManager.getLogger(); + + public static final String ROUTING_LISTS_ENDPOINT = "/orders/routing-lists"; + private static final String ROUTING_LISTS_ID_PATH = ROUTING_LISTS_ENDPOINT + "/%s"; + private static final String ROUTING_LIST_UUID = "c0d13648-347b-4ac9-8c2f-5bc47248b87e"; + private static final String PO_LINE_UUID = "0009662b-8b80-4001-b704-ca10971f222d"; private static final String TEMPLATE_PROCESSING_REQUEST_ENDPOINT = "orders/routing-lists/" + ROUTING_LIST_ID + "/template"; + + private final JsonObject routingListJsonReqData = getMockAsJson(ROUTING_LISTS_MOCK_DATA_PATH + "routing-list.json"); private static boolean runningOnOwn; + @Autowired private RoutingListService routingListService; private RequestContext requestContext; private Context ctxMock; private Map okapiHeadersMock; private AutoCloseable mockitoMocks; - + private RoutingList sampleRoutingList; @BeforeAll static void before() throws InterruptedException, ExecutionException, TimeoutException { @@ -68,7 +112,6 @@ static void before() throws InterruptedException, ExecutionException, TimeoutExc initSpringContext(RoutingListsApiTest.ContextConfiguration.class); } - @BeforeEach void beforeEach() { mockitoMocks = MockitoAnnotations.openMocks(this); @@ -80,8 +123,11 @@ void beforeEach() { okapiHeadersMock.put(X_OKAPI_TENANT.getName(), X_OKAPI_TENANT.getValue()); okapiHeadersMock.put(X_OKAPI_USER_ID.getName(), X_OKAPI_USER_ID.getValue()); requestContext = new RequestContext(ctxMock, okapiHeadersMock); + sampleRoutingList = routingListJsonReqData.mapTo(RoutingList.class) + .withPoLineId(PO_LINE_UUID); } + @AfterEach void afterEach() throws Exception { mockitoMocks.close(); @@ -95,6 +141,137 @@ static void after() { } } + @Test + void testPostRoutingList() { + logger.info("=== Test POST Routing List (Create Routing List) ==="); + + PoLine poLine = getMinimalContentPoLine() + .withId(PO_LINE_UUID) + .withOrderFormat(PoLine.OrderFormat.PHYSICAL_RESOURCE) + .withLocations(getLocationPhysicalCopies(1)); + addMockEntry(PO_LINES_STORAGE, JsonObject.mapFrom(poLine)); + + doReturn(succeededFuture(sampleRoutingList)).when(routingListService).createRoutingList(eq(sampleRoutingList), any(RequestContext.class)); + + verifyPostResponse(ROUTING_LISTS_ENDPOINT, JsonObject.mapFrom(sampleRoutingList).encode(), + prepareHeaders(EXIST_CONFIG_X_OKAPI_TENANT_LIMIT_10, X_OKAPI_USER_ID_WITH_ACQ_UNITS), APPLICATION_JSON, HttpStatus.HTTP_OK.toInt()); + } + + @Test + void testPostRoutingListShouldFailForInvalidOrderType() { + logger.info("=== Test POST Routing List should fail because it's POL has invalid order type ==="); + + PoLine poLine = getMinimalContentPoLine() + .withId(PO_LINE_UUID) + .withOrderFormat(PoLine.OrderFormat.ELECTRONIC_RESOURCE); + addMockEntry(PO_LINES_STORAGE, JsonObject.mapFrom(poLine)); + + var errorsExpected = getCodesAsErrors(INVALID_ROUTING_LIST_FOR_PO_LINE_FORMAT); + doReturn(failedFuture(new HttpException(RestConstants.VALIDATION_ERROR, errorsExpected))).when(routingListService).createRoutingList(eq(sampleRoutingList), any(RequestContext.class)); + + List errors = verifyPostResponse(ROUTING_LISTS_ENDPOINT, JsonObject.mapFrom(sampleRoutingList).encode(), + prepareHeaders(EXIST_CONFIG_X_OKAPI_TENANT_LIMIT_10, X_OKAPI_USER_ID), APPLICATION_JSON, 422) + .as(Errors.class) + .getErrors(); + + assertThat(errors.get(0).getMessage(), equalTo(INVALID_ROUTING_LIST_FOR_PO_LINE_FORMAT.getDescription())); + } + + @Test + void testPostRoutingListShouldFailForLimitReached() { + logger.info("=== Test POST Routing List should fail because it's POL has reached limit of Routing Lists ==="); + + PoLine poLine = getMinimalContentPoLine() + .withId(PO_LINE_UUID) + .withOrderFormat(PoLine.OrderFormat.PHYSICAL_RESOURCE) + .withLocations(getLocationPhysicalCopies(0)); + addMockEntry(PO_LINES_STORAGE, JsonObject.mapFrom(poLine)); + + var errorsExpected = getCodesAsErrors(ROUTING_LIST_LIMIT_REACHED_FOR_PO_LINE); + doReturn(failedFuture(new HttpException(RestConstants.VALIDATION_ERROR, errorsExpected))).when(routingListService).createRoutingList(eq(sampleRoutingList), any(RequestContext.class)); + + List errors = verifyPostResponse(ROUTING_LISTS_ENDPOINT, JsonObject.mapFrom(sampleRoutingList).encode(), + prepareHeaders(EXIST_CONFIG_X_OKAPI_TENANT_LIMIT_10, X_OKAPI_USER_ID), APPLICATION_JSON, 422 + ) + .as(Errors.class) + .getErrors(); + + assertThat(errors.get(0).getMessage(), equalTo(ROUTING_LIST_LIMIT_REACHED_FOR_PO_LINE.getDescription())); + } + + @Test + void testPostRoutingListWithInvalidPoLineId() { + logger.info("=== Test POST Routing List should fail because it's POL does not exist ==="); + + var errorsExpected = getCodesAsErrors(PO_LINE_NOT_FOUND_FOR_ROUTING_LIST); + doReturn(failedFuture(new HttpException(RestConstants.VALIDATION_ERROR, errorsExpected))).when(routingListService).createRoutingList(eq(sampleRoutingList), any(RequestContext.class)); + + List errors = verifyPostResponse(ROUTING_LISTS_ENDPOINT, JsonObject.mapFrom(sampleRoutingList).encode(), + prepareHeaders(EXIST_CONFIG_X_OKAPI_TENANT_LIMIT_10, X_OKAPI_USER_ID), APPLICATION_JSON, 422 + ) + .as(Errors.class) + .getErrors(); + + assertThat(errors.get(0).getMessage(), equalTo(PO_LINE_NOT_FOUND_FOR_ROUTING_LIST.getDescription())); + } + + @Test + void testGetRoutingLists() { + logger.info("=== Test Get Routing Lists ==="); + var collection = new RoutingListCollection() + .withRoutingLists(List.of(sampleRoutingList)) + .withTotalRecords(1); + doReturn(succeededFuture(collection)).when(routingListService).getRoutingLists(anyInt(), anyInt(), any(), any(RequestContext.class)); + + final RoutingListCollection respCollection = verifySuccessGet(ROUTING_LISTS_ENDPOINT, RoutingListCollection.class); + logger.info(JsonObject.mapFrom(respCollection).encodePrettily()); + assertEquals(1, respCollection.getRoutingLists().size()); + } + + @Test + void testGetRoutingListById() { + logger.info("=== Test Get Routing List by id ==="); + doReturn(succeededFuture(sampleRoutingList)).when(routingListService).getRoutingList(eq(ROUTING_LIST_UUID), any(RequestContext.class)); + + final RoutingList resp = verifySuccessGet(String.format(ROUTING_LISTS_ID_PATH, ROUTING_LIST_UUID), RoutingList.class); + logger.info(JsonObject.mapFrom(resp).encodePrettily()); + assertEquals(ROUTING_LIST_UUID, resp.getId()); + } + + @Test + void testPutRoutingList() { + logger.info("=== Test update Routing List by id ==="); + sampleRoutingList.setNotes("new notes"); + doReturn(succeededFuture()).when(routingListService).updateRoutingList(eq(sampleRoutingList), any(RequestContext.class)); + + verifyPut(String.format(ROUTING_LISTS_ID_PATH, ROUTING_LIST_UUID), JsonObject.mapFrom(sampleRoutingList).encode(), + prepareHeaders(EXIST_CONFIG_X_OKAPI_TENANT_LIMIT_10), "", 204); + } + + @Test + void testDeleteRoutingListByIdTest() { + logger.info("=== Test delete Routing List by id ==="); + doReturn(succeededFuture()).when(routingListService).deleteRoutingList(eq(ROUTING_LIST_UUID), any(RequestContext.class)); + + verifyDeleteResponse(String.format(ROUTING_LISTS_ID_PATH, ROUTING_LIST_UUID), "", 204); + } + + @Test + void testDeleteRoutingListByIdWithInvalidFormatTest() { + logger.info("=== Test delete Routing List by id ==="); + doReturn(failedFuture(new HttpException(BAD_REQUEST, ErrorCodes.GENERIC_ERROR_CODE))).when(routingListService).deleteRoutingList(eq(ID_BAD_FORMAT), any(RequestContext.class)); + + verifyDeleteResponse(String.format(ROUTING_LISTS_ID_PATH, ID_BAD_FORMAT), TEXT_PLAIN, 400); + } + + @Test + void testDeleteNotExistentRoutingListTest() { + logger.info("=== Test delete Routing List by id ==="); + doReturn(failedFuture(new HttpException(NOT_FOUND, ErrorCodes.GENERIC_ERROR_CODE))).when(routingListService).deleteRoutingList(eq(ID_DOES_NOT_EXIST), any(RequestContext.class)); + + verifyDeleteResponse(String.format(ROUTING_LISTS_ID_PATH, ID_DOES_NOT_EXIST), APPLICATION_JSON, 404); + } + @Test void testProcessTemplateRequest() { logger.info("=== Test Execute template processing request ==="); @@ -107,10 +284,19 @@ void testProcessTemplateRequest() { verify(routingListService, times(1)).processTemplateRequest(eq(ROUTING_LIST_ID), any(RequestContext.class)); } + private Errors getCodesAsErrors(ErrorCodes... codes) { + return new Errors() + .withErrors(Arrays.stream(codes).map(ErrorCodes::toError).toList()) + .withTotalRecords(codes.length); + } + static class ContextConfiguration { + @Bean public RoutingListService routingListService() { return mock(RoutingListService.class); } + } + } diff --git a/src/test/java/org/folio/service/RoutingListServiceTest.java b/src/test/java/org/folio/service/RoutingListServiceTest.java deleted file mode 100644 index fe9651a93..000000000 --- a/src/test/java/org/folio/service/RoutingListServiceTest.java +++ /dev/null @@ -1,102 +0,0 @@ -package org.folio.service; - -import static io.vertx.core.Future.succeededFuture; -import static org.folio.TestConstants.ROUTING_LIST_ID; -import static org.folio.TestUtils.getMockData; -import static org.folio.rest.impl.MockServer.ROUTING_LIST_MOCK_DATA_PATH; -import static org.folio.rest.impl.MockServer.USERS_MOCK_DATA_PATH; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doReturn; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -import io.vertx.core.Future; -import io.vertx.core.json.JsonObject; -import io.vertx.junit5.VertxExtension; -import io.vertx.junit5.VertxTestContext; -import org.folio.models.UserCollection; -import org.folio.rest.acq.model.Setting; -import org.folio.rest.acq.model.SettingCollection; -import org.folio.rest.core.RestClient; -import org.folio.rest.core.models.RequestContext; -import org.folio.rest.core.models.RequestEntry; -import org.folio.rest.jaxrs.model.RoutingList; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -@ExtendWith(VertxExtension.class) -public class RoutingListServiceTest { - - @InjectMocks - RoutingListService routingListService; - @Mock - private RestClient restClient; - @Mock - private UserService userService; - @Mock - private RequestContext requestContextMock; - private AutoCloseable mockitoMocks; - - @BeforeEach - public void initMocks() throws Exception { - mockitoMocks = MockitoAnnotations.openMocks(this); - } - - @AfterEach - void afterEach() throws Exception { - mockitoMocks.close(); - } - - @Test - void processTemplate(VertxTestContext vertxTestContext) throws IOException { - var routingList = new JsonObject(getMockData(ROUTING_LIST_MOCK_DATA_PATH + ROUTING_LIST_ID + ".json")).mapTo(RoutingList.class); - var users = new JsonObject(getMockData(USERS_MOCK_DATA_PATH + "user_collection.json")).mapTo(UserCollection.class); - var expectedTemplateRequest = new JsonObject(getMockData(ROUTING_LIST_MOCK_DATA_PATH + ROUTING_LIST_ID + "-expected-template-request.json")); - var setting = new Setting().withId(UUID.randomUUID().toString()) - .withKey("routing-list") - .withValue("1c4b225f-f669-4e9b-afcd-ebc0e273a34e"); - var settingCollection = new SettingCollection().withSettings(List.of(setting)); - - doReturn(succeededFuture(routingList)).when(restClient).get(any(RequestEntry.class), eq(RoutingList.class), any()); - doReturn(succeededFuture(users)).when(userService).getUsersByIds(eq(routingList.getUserIds()), any()); - doReturn(succeededFuture(settingCollection)).when(restClient).get(any(RequestEntry.class), eq(SettingCollection.class), any()); - doReturn(succeededFuture(new JsonObject())).when(restClient).postJsonObject(any(RequestEntry.class), eq(expectedTemplateRequest), any()); - - Future future = routingListService.processTemplateRequest(ROUTING_LIST_ID, requestContextMock); - - vertxTestContext.assertComplete(future).onComplete(result -> { - assertTrue(result.succeeded()); - vertxTestContext.completeNow(); - }); - } - - @Test - void throwErrorWhenSettingNotFound(VertxTestContext vertxTestContext) throws IOException { - var routingList = new JsonObject(getMockData(ROUTING_LIST_MOCK_DATA_PATH + ROUTING_LIST_ID + ".json")).mapTo(RoutingList.class); - var users = new JsonObject(getMockData(USERS_MOCK_DATA_PATH + "user_collection.json")).mapTo(UserCollection.class); - - doReturn(succeededFuture(routingList)).when(restClient).get(any(RequestEntry.class), eq(RoutingList.class), any()); - doReturn(succeededFuture(users)).when(userService).getUsersByIds(eq(routingList.getUserIds()), any()); - doReturn(succeededFuture(new SettingCollection().withSettings(new ArrayList<>()))) - .when(restClient).get(any(RequestEntry.class), eq(SettingCollection.class), any()); - - Future future = routingListService.processTemplateRequest(ROUTING_LIST_ID, requestContextMock); - - vertxTestContext.assertFailure(future).onComplete(result -> { - assertTrue(result.failed()); - var exception = result.cause().getMessage(); - assertTrue(exception.contains("Setting is not found with key=ROUTING_USER_ADDRESS_TYPE_ID")); - vertxTestContext.completeNow(); - }); - } -} diff --git a/src/test/java/org/folio/service/routinglists/RoutingListServiceTest.java b/src/test/java/org/folio/service/routinglists/RoutingListServiceTest.java new file mode 100644 index 000000000..30f4b6a85 --- /dev/null +++ b/src/test/java/org/folio/service/routinglists/RoutingListServiceTest.java @@ -0,0 +1,187 @@ +package org.folio.service.routinglists; + +import static io.vertx.core.Future.succeededFuture; +import static org.folio.TestConstants.ROUTING_LIST_ID; +import static org.folio.TestUtils.getLocationPhysicalCopies; +import static org.folio.TestUtils.getMinimalContentPoLine; +import static org.folio.TestUtils.getMockAsJson; +import static org.folio.TestUtils.getMockData; +import static org.folio.rest.core.exceptions.ErrorCodes.INVALID_ROUTING_LIST_FOR_PO_LINE_FORMAT; +import static org.folio.rest.core.exceptions.ErrorCodes.ROUTING_LIST_LIMIT_REACHED_FOR_PO_LINE; +import static org.folio.rest.impl.MockServer.ROUTING_LISTS_MOCK_DATA_PATH; +import static org.folio.rest.impl.MockServer.USERS_MOCK_DATA_PATH; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import org.folio.models.UserCollection; +import org.folio.rest.acq.model.Setting; +import org.folio.rest.acq.model.SettingCollection; +import org.folio.rest.core.RestClient; +import org.folio.rest.core.models.RequestContext; +import org.folio.rest.core.models.RequestEntry; +import org.folio.rest.jaxrs.model.PoLine; +import org.folio.rest.jaxrs.model.RoutingList; +import org.folio.rest.jaxrs.model.RoutingListCollection; +import org.folio.service.UserService; +import org.folio.service.orders.PurchaseOrderLineService; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import io.vertx.core.Future; +import io.vertx.core.json.JsonObject; +import io.vertx.junit5.VertxExtension; +import io.vertx.junit5.VertxTestContext; + +@ExtendWith(VertxExtension.class) +public class RoutingListServiceTest { + + private static final String ROUTING_LIST_SAMPLE = ROUTING_LISTS_MOCK_DATA_PATH + "routing-list.json"; + + private static final String PO_LINE_UUID = "0009662b-8b80-4001-b704-ca10971f222d"; + + private PoLine samplePoLine; + private RoutingList sampleRoutingList; + + @Mock + private RestClient restClient; + + @Mock + private PurchaseOrderLineService poLineService; + + @Mock + private UserService userService; + + @Mock + private RequestContext requestContextMock; + + @InjectMocks + private RoutingListService routingListService; + + private AutoCloseable mockitoMocks; + + @BeforeEach + void before() { + mockitoMocks = MockitoAnnotations.openMocks(this); + sampleRoutingList = getMockAsJson(ROUTING_LIST_SAMPLE).mapTo(RoutingList.class); + samplePoLine = getMinimalContentPoLine() + .withId(PO_LINE_UUID) + .withOrderFormat(PoLine.OrderFormat.PHYSICAL_RESOURCE) + .withLocations(getLocationPhysicalCopies(1)); + } + + @AfterEach + void afterEach() throws Exception { + mockitoMocks.close(); + } + + @Test + void testCreateRoutingList(VertxTestContext vertxTestContext) { + doReturn(succeededFuture(getRoutingListCollection(0))).when(restClient).get(any(RequestEntry.class), eq(RoutingListCollection.class), any()); + doReturn(succeededFuture(sampleRoutingList)).when(restClient).post(any(RequestEntry.class), any(RoutingList.class), eq(RoutingList.class), any()); + doReturn(succeededFuture(samplePoLine)).when(poLineService).getOrderLineById(any(), any()); + + Future future = routingListService.createRoutingList(sampleRoutingList, requestContextMock); + vertxTestContext.assertComplete(future).onComplete(result -> { + assertTrue(result.succeeded()); + assertEquals(sampleRoutingList.getId(), result.result().getId()); + vertxTestContext.completeNow(); + }); + } + + @Test + void testCreateRoutingListWithPOLineLimitReached(VertxTestContext vertxTestContext) { + doReturn(succeededFuture(getRoutingListCollection(1))).when(restClient).get(any(RequestEntry.class), eq(RoutingListCollection.class), any()); + doReturn(succeededFuture(samplePoLine)).when(poLineService).getOrderLineById(any(), any()); + + Future future = routingListService.createRoutingList(sampleRoutingList, requestContextMock); + vertxTestContext.assertFailure(future).onComplete(result -> { + assertTrue(result.failed()); + var exception = result.cause().getMessage(); + assertTrue(exception.contains(ROUTING_LIST_LIMIT_REACHED_FOR_PO_LINE.getDescription())); + vertxTestContext.completeNow(); + }); + + } + + @Test + void testCreateRoutingListWithPOLineInvalidOrderFormat(VertxTestContext vertxTestContext) { + samplePoLine.setOrderFormat(PoLine.OrderFormat.ELECTRONIC_RESOURCE); + doReturn(succeededFuture(getRoutingListCollection(0))).when(restClient).get(any(RequestEntry.class), eq(RoutingListCollection.class), any()); + doReturn(succeededFuture(samplePoLine)).when(poLineService).getOrderLineById(any(), any()); + + Future future = routingListService.createRoutingList(sampleRoutingList, requestContextMock); + vertxTestContext.assertFailure(future).onComplete(result -> { + assertTrue(result.failed()); + var exception = result.cause().getMessage(); + assertTrue(exception.contains(INVALID_ROUTING_LIST_FOR_PO_LINE_FORMAT.getDescription())); + vertxTestContext.completeNow(); + }); + } + + @Test + void processTemplate(VertxTestContext vertxTestContext) throws IOException { + var routingList = new JsonObject(getMockData(ROUTING_LISTS_MOCK_DATA_PATH + ROUTING_LIST_ID + ".json")).mapTo(RoutingList.class); + var users = new JsonObject(getMockData(USERS_MOCK_DATA_PATH + "user_collection.json")).mapTo(UserCollection.class); + var expectedTemplateRequest = new JsonObject(getMockData(ROUTING_LISTS_MOCK_DATA_PATH + ROUTING_LIST_ID + "-expected-template-request.json")); + var setting = new Setting().withId(UUID.randomUUID().toString()) + .withKey("routing-list") + .withValue("1c4b225f-f669-4e9b-afcd-ebc0e273a34e"); + var settingCollection = new SettingCollection().withSettings(List.of(setting)); + + doReturn(succeededFuture(routingList)).when(restClient).get(any(RequestEntry.class), eq(RoutingList.class), any()); + doReturn(succeededFuture(users)).when(userService).getUsersByIds(eq(routingList.getUserIds()), any()); + doReturn(succeededFuture(settingCollection)).when(restClient).get(any(RequestEntry.class), eq(SettingCollection.class), any()); + doReturn(succeededFuture(new JsonObject())).when(restClient).postJsonObject(any(RequestEntry.class), eq(expectedTemplateRequest), any()); + + Future future = routingListService.processTemplateRequest(ROUTING_LIST_ID, requestContextMock); + + vertxTestContext.assertComplete(future).onComplete(result -> { + assertTrue(result.succeeded()); + vertxTestContext.completeNow(); + }); + } + + @Test + void throwErrorWhenSettingNotFound(VertxTestContext vertxTestContext) throws IOException { + var routingList = new JsonObject(getMockData(ROUTING_LISTS_MOCK_DATA_PATH + ROUTING_LIST_ID + ".json")).mapTo(RoutingList.class); + var users = new JsonObject(getMockData(USERS_MOCK_DATA_PATH + "user_collection.json")).mapTo(UserCollection.class); + + doReturn(succeededFuture(routingList)).when(restClient).get(any(RequestEntry.class), eq(RoutingList.class), any()); + doReturn(succeededFuture(users)).when(userService).getUsersByIds(eq(routingList.getUserIds()), any()); + doReturn(succeededFuture(new SettingCollection().withSettings(new ArrayList<>()))) + .when(restClient).get(any(RequestEntry.class), eq(SettingCollection.class), any()); + + Future future = routingListService.processTemplateRequest(ROUTING_LIST_ID, requestContextMock); + + vertxTestContext.assertFailure(future).onComplete(result -> { + assertTrue(result.failed()); + var exception = result.cause().getMessage(); + assertTrue(exception.contains("Setting is not found with key=ROUTING_USER_ADDRESS_TYPE_ID")); + vertxTestContext.completeNow(); + }); + } + + private RoutingListCollection getRoutingListCollection(int n) { + List lists = new ArrayList<>(); + for (int i = 0; i < n; i++) { + lists.add(sampleRoutingList); + } + return new RoutingListCollection() + .withRoutingLists(lists) + .withTotalRecords(n); + } + +} diff --git a/src/test/java/org/folio/service/routinglists/validators/RoutingListValidatorTest.java b/src/test/java/org/folio/service/routinglists/validators/RoutingListValidatorTest.java new file mode 100644 index 000000000..333d3bde5 --- /dev/null +++ b/src/test/java/org/folio/service/routinglists/validators/RoutingListValidatorTest.java @@ -0,0 +1,73 @@ +package org.folio.service.routinglists.validators; + +import static org.folio.TestUtils.getLocationPhysicalCopies; +import static org.folio.TestUtils.getMinimalContentPoLine; +import static org.folio.TestUtils.getMockAsJson; +import static org.folio.rest.core.exceptions.ErrorCodes.INVALID_ROUTING_LIST_FOR_PO_LINE_FORMAT; +import static org.folio.rest.core.exceptions.ErrorCodes.ROUTING_LIST_LIMIT_REACHED_FOR_PO_LINE; +import static org.folio.rest.impl.MockServer.ROUTING_LISTS_MOCK_DATA_PATH; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; +import java.util.List; + +import org.folio.rest.jaxrs.model.Error; +import org.folio.rest.jaxrs.model.PoLine; +import org.folio.rest.jaxrs.model.RoutingList; +import org.folio.rest.jaxrs.model.RoutingListCollection; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class RoutingListValidatorTest { + + private static final String ROUTING_LIST_SAMPLE = ROUTING_LISTS_MOCK_DATA_PATH + "routing-list.json"; + + private static final String PO_LINE_UUID = "0009662b-8b80-4001-b704-ca10971f222d"; + + private PoLine samplePoLine; + private RoutingList sampleRoutingList; + + @BeforeEach + void before() { + sampleRoutingList = getMockAsJson(ROUTING_LIST_SAMPLE).mapTo(RoutingList.class); + samplePoLine = getMinimalContentPoLine() + .withId(PO_LINE_UUID) + .withOrderFormat(PoLine.OrderFormat.PHYSICAL_RESOURCE) + .withLocations(getLocationPhysicalCopies(1)); + } + + @Test + void testValidateRoutingList() { + RoutingListCollection collection = getRoutingListCollection(0); + List errorList = RoutingListValidatorUtil.validateRoutingList(collection, samplePoLine); + assertEquals(errorList.size(), 0); + } + + @Test + void testValidateRoutingListWithPOLineLimitReached() { + RoutingListCollection collection = getRoutingListCollection(1); + List errors = RoutingListValidatorUtil.validateRoutingList(collection, samplePoLine); + assertEquals(errors.size(), 1); + assertEquals(errors.get(0).getMessage(), ROUTING_LIST_LIMIT_REACHED_FOR_PO_LINE.getDescription()); + } + + @Test + void testValidateRoutingListWithPOLineInvalidOrderFormat() { + samplePoLine.setOrderFormat(PoLine.OrderFormat.ELECTRONIC_RESOURCE); + RoutingListCollection collection = getRoutingListCollection(1); + List errors = RoutingListValidatorUtil.validateRoutingList(collection, samplePoLine); + assertEquals(errors.size(), 1); + assertEquals(errors.get(0).getMessage(), INVALID_ROUTING_LIST_FOR_PO_LINE_FORMAT.getDescription()); + } + + private RoutingListCollection getRoutingListCollection(int n) { + List lists = new ArrayList<>(); + for (int i = 0; i < n; i++) { + lists.add(sampleRoutingList); + } + return new RoutingListCollection() + .withRoutingLists(lists) + .withTotalRecords(n); + } + +} diff --git a/src/test/resources/mockdata/routingLists/c0d13648-347b-4ac9-8c2f-5bc47248b87e.json b/src/test/resources/mockdata/routingLists/c0d13648-347b-4ac9-8c2f-5bc47248b87e.json new file mode 100644 index 000000000..cce9d1272 --- /dev/null +++ b/src/test/resources/mockdata/routingLists/c0d13648-347b-4ac9-8c2f-5bc47248b87e.json @@ -0,0 +1,11 @@ +{ + "id": "c0d13648-347b-4ac9-8c2f-5bc47248b87e", + "_version": 1, + "name": "List name", + "notes": "Some note", + "userIds": [ + "d926d900-e27d-46d6-bba8-31e9d5c2cf44", + "077274ad-6b4f-4c28-9779-6d381e7a1ca1" + ], + "poLineId": "0009662b-8b80-4001-b704-ca10971f222d" +} diff --git a/src/test/resources/mockdata/routingLists/routing-list.json b/src/test/resources/mockdata/routingLists/routing-list.json new file mode 100644 index 000000000..cce9d1272 --- /dev/null +++ b/src/test/resources/mockdata/routingLists/routing-list.json @@ -0,0 +1,11 @@ +{ + "id": "c0d13648-347b-4ac9-8c2f-5bc47248b87e", + "_version": 1, + "name": "List name", + "notes": "Some note", + "userIds": [ + "d926d900-e27d-46d6-bba8-31e9d5c2cf44", + "077274ad-6b4f-4c28-9779-6d381e7a1ca1" + ], + "poLineId": "0009662b-8b80-4001-b704-ca10971f222d" +} diff --git a/src/test/resources/mockdata/routingLists/routing-lists.json b/src/test/resources/mockdata/routingLists/routing-lists.json new file mode 100644 index 000000000..2bb58ff0b --- /dev/null +++ b/src/test/resources/mockdata/routingLists/routing-lists.json @@ -0,0 +1,17 @@ +{ + "routingLists": + [ + { + "id": "c0d13648-347b-4ac9-8c2f-5bc47248b87e", + "_version": 1, + "name": "List name", + "notes": "Some note", + "userIds": [ + "d926d900-e27d-46d6-bba8-31e9d5c2cf44", + "077274ad-6b4f-4c28-9779-6d381e7a1ca1" + ], + "poLineId": "0009662b-8b80-4001-b704-ca10971f222d" + } + ], + "totalRecords": 1 +}