diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json
index 42a4ca62d..46e90f423 100644
--- a/descriptors/ModuleDescriptor-template.json
+++ b/descriptors/ModuleDescriptor-template.json
@@ -998,6 +998,22 @@
}
]
},
+ {
+ "id": "orders.routing-list",
+ "version": "1.0",
+ "handlers": [
+ {
+ "methods": ["GET"],
+ "pathPattern": "/orders/routing-lists/{id}/template",
+ "permissionsRequired": ["orders.routing-list-template.item.get"],
+ "modulePermissions": [
+ "orders-storage.routing-lists.item.get",
+ "users.collection.get",
+ "template-request.post"
+ ]
+ }
+ ]
+ },
{
"id": "_jsonSchemas",
"version": "1.0",
@@ -1198,6 +1214,14 @@
{
"id": "user-tenants",
"version": "1.0"
+ },
+ {
+ "id": "users",
+ "version": "16.0"
+ },
+ {
+ "id": "template-engine",
+ "version": "2.2"
}
],
"optional": [
@@ -1705,6 +1729,11 @@
"displayName" : "orders holding-summary get",
"description" : "Holding summary"
},
+ {
+ "permissionName": "orders.routing-list-template.item.get",
+ "displayName" : "orders routing-list-template item get",
+ "description" : "Orders routing-list-template item get"
+ },
{
"permissionName": "orders.all",
"displayName": "orders - all permissions",
@@ -1739,8 +1768,8 @@
"orders.rollover.item.post",
"orders.holding-summary.collection.get",
"orders.acquisition-methods.all",
- "orders.export-history.all"
-
+ "orders.export-history.all",
+ "orders.routing-list-template.item.get"
]
},
{
diff --git a/ramls/routing-lists.raml b/ramls/routing-lists.raml
new file mode 100644
index 000000000..b98725dad
--- /dev/null
+++ b/ramls/routing-lists.raml
@@ -0,0 +1,47 @@
+#%RAML 1.0
+title: "RoutingList"
+baseUri: https://github.com/folio-org/mod-orders
+version: v1.0
+
+documentation:
+ - title: Routing lists
+ content: CRUD API to manage 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}$
+
+traits:
+ pageable: !include raml-util/traits/pageable.raml
+ searchable: !include raml-util/traits/searchable.raml
+
+resourceTypes:
+ collection: !include rtypes/collection-with-json-response.raml
+ collection-item: !include rtypes/item-collection-with-json-response.raml
+
+/orders/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
+ get:
+ description: Get routing lists
+ is: [
+ searchable: {description: "with valid searchable fields: for example routing list", example: "[\"routing_list\", \"ROUTING_LIST\", \"=\"]"},
+ pageable
+ ]
+ post:
+ description: Create routing lists
+
+ /{id}/template:
+ uriParameters:
+ id:
+ description: The UUID of a Title
+ 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 584837b4b..6c1a45321 100644
--- a/src/main/java/org/folio/config/ApplicationConfig.java
+++ b/src/main/java/org/folio/config/ApplicationConfig.java
@@ -26,6 +26,7 @@
import org.folio.service.ReasonForClosureService;
import org.folio.service.SuffixService;
import org.folio.service.TagService;
+import org.folio.service.UserService;
import org.folio.service.caches.ConfigurationEntriesCache;
import org.folio.service.caches.InventoryCache;
import org.folio.service.configuration.ConfigurationEntriesService;
@@ -111,6 +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.titles.TitleValidationService;
import org.folio.service.titles.TitlesService;
import org.springframework.beans.factory.annotation.Qualifier;
@@ -434,6 +436,16 @@ CompositeOrderDynamicDataPopulateService combinedPopulateService(CompositeOrderR
return new CombinedOrderDataPopulateService(compositeOrderRetrieveHolderBuilder, populateServices);
}
+ @Bean
+ RoutingListService routingListService(RestClient restClient, UserService userService) {
+ return new RoutingListService(restClient, userService);
+ }
+
+ @Bean
+ UserService userService(RestClient restClient) {
+ return new UserService(restClient);
+ }
+
@Bean
TitlesService titlesService(RestClient restClient, ProtectionService protectionService, InventoryManager inventoryManager) {
return new TitlesService(restClient, protectionService, inventoryManager);
diff --git a/src/main/java/org/folio/models/TemplateProcessingRequest.java b/src/main/java/org/folio/models/TemplateProcessingRequest.java
new file mode 100644
index 000000000..7646ecfc6
--- /dev/null
+++ b/src/main/java/org/folio/models/TemplateProcessingRequest.java
@@ -0,0 +1,135 @@
+package org.folio.models;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import org.folio.rest.jaxrs.model.RoutingList;
+
+public class TemplateProcessingRequest {
+ @JsonProperty
+ private UUID templateId;
+ @JsonProperty
+ private String lang;
+ @JsonProperty
+ private String outputFormat;
+ @JsonProperty
+ private Context context;
+
+ public UUID getTemplateId() {
+ return templateId;
+ }
+
+ public String getLang() {
+ return lang;
+ }
+
+ public String getOutputFormat() {
+ return outputFormat;
+ }
+
+ public Context getContext() {
+ return context;
+ }
+
+ public TemplateProcessingRequest withTemplateId(UUID templateId) {
+ this.templateId = templateId;
+ return this;
+ }
+
+ public TemplateProcessingRequest withLang(String lang) {
+ this.lang = lang;
+ return this;
+ }
+
+ public TemplateProcessingRequest withOutputFormat(String outputFormat) {
+ this.outputFormat = outputFormat;
+ return this;
+ }
+
+ public TemplateProcessingRequest withContext(Context context) {
+ this.context = context;
+ return this;
+ }
+
+ public static class Context {
+ @JsonProperty
+ @JsonInclude(JsonInclude.Include.NON_EMPTY)
+ private RoutingList routingList;
+ @JsonProperty
+ private List users;
+
+ public RoutingList getRoutingList() {
+ return routingList;
+ }
+
+ public List getUsers() {
+ return users;
+ }
+
+ public Context withRoutingList(RoutingList routingList) {
+ this.routingList = routingList;
+ return this;
+ }
+
+ public Context withUsers(List users) {
+ this.users = users;
+ return this;
+ }
+ }
+
+ public static class User {
+ @JsonProperty
+ private String lastName;
+ @JsonProperty
+ private String firstName;
+ @JsonProperty
+ private String routingAddress;
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public String getRoutingAddress() {
+ return routingAddress;
+ }
+
+ public User withLastName(String lastName) {
+ this.lastName = lastName;
+ return this;
+ }
+
+ public User withFirstName(String firstName) {
+ this.firstName = firstName;
+ return this;
+ }
+
+ public User withRoutingAddress(String routingAddress) {
+ this.routingAddress = routingAddress;
+ return this;
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof TemplateProcessingRequest that)) return false;
+ return Objects.equals(templateId, that.templateId)
+ && Objects.equals(lang, that.lang)
+ && Objects.equals(outputFormat, that.outputFormat)
+ && Objects.equals(context, that.context);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(templateId, lang, outputFormat, context);
+ }
+}
+
diff --git a/src/main/java/org/folio/models/UserCollection.java b/src/main/java/org/folio/models/UserCollection.java
new file mode 100644
index 000000000..889fbe31b
--- /dev/null
+++ b/src/main/java/org/folio/models/UserCollection.java
@@ -0,0 +1,76 @@
+package org.folio.models;
+
+import java.util.List;
+import java.util.UUID;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class UserCollection {
+ @JsonProperty
+ private List users;
+ @JsonProperty
+ private int totalRecords;
+
+ public List getUsers() {
+ return users;
+ }
+ public int getTotalRecords() {
+ return totalRecords;
+ }
+
+ public UserCollection withUsers(List users) {
+ this.users = users;
+ return this;
+ }
+
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public static class User {
+ @JsonProperty
+ private UUID id;
+ @JsonProperty
+ private Personal personal;
+
+ public UUID getId () {
+ return id;
+ }
+ public Personal getPersonal () {
+ return personal;
+ }
+
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public static class Personal {
+ @JsonProperty
+ private String firstName;
+ @JsonProperty
+ private String lastName;
+ @JsonProperty
+ private List addresses;
+
+ public String getFirstName() {
+ return firstName;
+ }
+ public String getLastName() {
+ return lastName;
+ }
+ public List getAddresses() {
+ return addresses;
+ }
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public static class Address {
+ @JsonProperty
+ private String addressLine1;
+ @JsonProperty
+ private String addressTypeId;
+
+ public String getAddressLine1() {
+ return addressLine1;
+ }
+ public String getAddressTypeId() {
+ return addressTypeId;
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/folio/orders/utils/ResourcePathResolver.java b/src/main/java/org/folio/orders/utils/ResourcePathResolver.java
index 50b3da277..4e29b8e7a 100644
--- a/src/main/java/org/folio/orders/utils/ResourcePathResolver.java
+++ b/src/main/java/org/folio/orders/utils/ResourcePathResolver.java
@@ -28,6 +28,7 @@ private ResourcePathResolver() {
public static final String PAYMENT_STATUS = "paymentStatus";
public static final String ORDER_TEMPLATES = "orderTemplates";
public static final String TITLES = "titles";
+ public static final String TEMPLATE_REQUEST = "templateRequest";
public static final String FUNDS = "finance.funds";
public static final String BUDGETS = "finance.budgets";
public static final String LEDGERS = "finance.ledgers";
@@ -47,6 +48,9 @@ private ResourcePathResolver() {
public static final String ORDER_INVOICE_RELATIONSHIP = "order-invoice-relationship";
public static final String EXPORT_HISTORY = "export-history";
public static final String TAGS = "tags";
+ public static final String ROUTING_LISTS = "routingLists";
+ public static final String ORDER_SETTINGS = "orderSettings";
+ public static final String USERS = "users";
private static final Map SUB_OBJECT_ITEM_APIS;
private static final Map SUB_OBJECT_COLLECTION_APIS;
@@ -66,6 +70,7 @@ private ResourcePathResolver() {
apis.put(RECEIVING_HISTORY, "/orders-storage/receiving-history");
apis.put(PO_LINE_NUMBER, "/orders-storage/po-line-number");
apis.put(ORDER_TEMPLATES, "/orders-storage/order-templates");
+ apis.put(TEMPLATE_REQUEST, "/template-request");
apis.put(FUNDS, "/finance/funds");
apis.put(BUDGETS, "/finance/budgets");
apis.put(LEDGERS, "/finance-storage/ledgers");
@@ -86,6 +91,9 @@ private ResourcePathResolver() {
apis.put(ORDER_INVOICE_RELATIONSHIP, "/orders-storage/order-invoice-relns");
apis.put(EXPORT_HISTORY, "/orders-storage/export-history");
apis.put(TAGS, "/tags");
+ apis.put(USERS, "/users");
+ apis.put(ORDER_SETTINGS, "/orders-storage/settings");
+ apis.put(ROUTING_LISTS, "/orders-storage/routing-lists");
SUB_OBJECT_COLLECTION_APIS = Collections.unmodifiableMap(apis);
SUB_OBJECT_ITEM_APIS = Collections.unmodifiableMap(
diff --git a/src/main/java/org/folio/rest/core/RestClient.java b/src/main/java/org/folio/rest/core/RestClient.java
index 17a787c98..d95ac0f29 100644
--- a/src/main/java/org/folio/rest/core/RestClient.java
+++ b/src/main/java/org/folio/rest/core/RestClient.java
@@ -10,13 +10,6 @@
import java.util.Map;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-import org.folio.okapi.common.WebClientFactory;
-import org.folio.rest.core.exceptions.HttpException;
-import org.folio.rest.core.models.RequestContext;
-import org.folio.rest.core.models.RequestEntry;
-
import io.vertx.core.Context;
import io.vertx.core.Future;
import io.vertx.core.MultiMap;
@@ -29,6 +22,12 @@
import io.vertx.ext.web.client.WebClientOptions;
import io.vertx.ext.web.client.predicate.ErrorConverter;
import io.vertx.ext.web.client.predicate.ResponsePredicate;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.folio.okapi.common.WebClientFactory;
+import org.folio.rest.core.exceptions.HttpException;
+import org.folio.rest.core.models.RequestContext;
+import org.folio.rest.core.models.RequestEntry;
public class RestClient {
@@ -243,12 +242,14 @@ public Future getAsJsonObject(RequestEntry requestEntry, RequestCont
public String extractRecordId(HttpResponse response) {
JsonObject body = response.bodyAsJsonObject();
- String id;
+ String id = "";
if (body != null && !body.isEmpty() && body.containsKey(ID)) {
id = body.getString(ID);
} else {
String location = response.getHeader(LOCATION);
- id = location.substring(location.lastIndexOf('/') + 1);
+ if (location != null) {
+ id = location.substring(location.lastIndexOf('/') + 1);
+ }
}
return id;
}
diff --git a/src/main/java/org/folio/rest/impl/RoutingListsAPI.java b/src/main/java/org/folio/rest/impl/RoutingListsAPI.java
new file mode 100644
index 000000000..23a61328d
--- /dev/null
+++ b/src/main/java/org/folio/rest/impl/RoutingListsAPI.java
@@ -0,0 +1,48 @@
+package org.folio.rest.impl;
+
+import static io.vertx.core.Future.succeededFuture;
+
+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.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.spring.SpringContextUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+
+public class RoutingListsAPI extends BaseApi implements OrdersRoutingLists {
+
+ @Autowired
+ private RoutingListService routingListService;
+
+ public RoutingListsAPI() {
+ SpringContextUtil.autowireDependencies(this, Vertx.currentContext());
+ }
+
+ @Override
+ public void getOrdersRoutingLists(String query, String totalRecords, int offset, int limit, Map okapiHeaders,
+ Handler> asyncResultHandler, Context vertxContext) {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ public void postOrdersRoutingLists(RoutingList entity, Map okapiHeaders,
+ Handler> asyncResultHandler, Context vertxContext) {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ public void getOrdersRoutingListsTemplateById(String id, Map okapiHeaders,
+ Handler> asyncResultHandler, Context vertxContext) {
+ routingListService.processTemplateRequest(id, new RequestContext(vertxContext, okapiHeaders))
+ .onSuccess(jsonObject -> asyncResultHandler.handle(succeededFuture(this.buildOkResponse(jsonObject))))
+ .onFailure(t -> handleErrorResponse(asyncResultHandler, t));
+ }
+}
diff --git a/src/main/java/org/folio/service/RoutingListService.java b/src/main/java/org/folio/service/RoutingListService.java
new file mode 100644
index 000000000..8c9b92b9e
--- /dev/null
+++ b/src/main/java/org/folio/service/RoutingListService.java
@@ -0,0 +1,139 @@
+package org.folio.service;
+
+import static org.folio.orders.utils.ResourcePathResolver.ORDER_SETTINGS;
+import static org.folio.orders.utils.ResourcePathResolver.ROUTING_LISTS;
+import static org.folio.orders.utils.ResourcePathResolver.TEMPLATE_REQUEST;
+import static org.folio.orders.utils.ResourcePathResolver.resourcesPath;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+
+import io.vertx.core.Future;
+import io.vertx.core.json.JsonObject;
+import org.apache.commons.lang3.ObjectUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.kafka.common.errors.ResourceNotFoundException;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.folio.models.TemplateProcessingRequest;
+import org.folio.models.UserCollection;
+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;
+
+public class RoutingListService {
+
+ 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 TEMPLATE_REQUEST_ENDPOINT = resourcesPath(TEMPLATE_REQUEST);
+
+ private final RestClient restClient;
+ private final UserService userService;
+
+ public RoutingListService(RestClient restClient, UserService userService) {
+ this.restClient = restClient;
+ this.userService = userService;
+ }
+
+ public Future processTemplateRequest(String routingListId, RequestContext requestContext) {
+ log.debug("processTemplateRequest: Tying to process template request for routingListId={}", routingListId);
+ return getRoutingListById(routingListId, requestContext)
+ .compose(routingList -> getUsersAndCreateTemplate(routingList, requestContext))
+ .compose(templateProcessingRequest -> postTemplateRequest(templateProcessingRequest, requestContext));
+ }
+
+ public Future getRoutingListById(String routingListId, RequestContext requestContext) {
+ var requestEntry = new RequestEntry(ROUTING_LIST_BY_ID_ENDPOINT).withId(routingListId);
+ return restClient.get(requestEntry, RoutingList.class, requestContext);
+ }
+
+ private Future getUsersAndCreateTemplate(RoutingList routingList, RequestContext requestContext) {
+ return getAddressTypeId(requestContext)
+ .compose(addressTypId -> userService.getUsersByIds(routingList.getUserIds(), requestContext)
+ .map(users -> createTemplateRequest(routingList, users, addressTypId)));
+ }
+
+ private Future getAddressTypeId(RequestContext requestContext) {
+ var requestEntry = new RequestEntry(ORDER_SETTINGS_ENDPOINT)
+ .withQuery("key=" + ROUTING_USER_ADDRESS_TYPE_ID);
+ return restClient.get(requestEntry, SettingCollection.class, 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);
+ throw new ResourceNotFoundException(String.format("Setting is not found with key=%s", ROUTING_USER_ADDRESS_TYPE_ID));
+ }
+ return settings.get(0).getValue();
+ });
+ }
+
+ private TemplateProcessingRequest createTemplateRequest(RoutingList routingList, UserCollection users, String addressTypeId) {
+ var templateRequest = createBaseTemplateRequest();
+ templateRequest.withContext(new TemplateProcessingRequest.Context()
+ .withRoutingList(fillRoutingListForContext(routingList))
+ .withUsers(fillUsersForContext(users, addressTypeId)));
+
+ log.info("createTemplateRequest:: TemplateProcessingRequest object created for routing list name: {}",
+ templateRequest.getContext().getRoutingList().getName());
+ return templateRequest;
+ }
+
+ private TemplateProcessingRequest createBaseTemplateRequest() {
+ return new TemplateProcessingRequest()
+ .withTemplateId(TEMPLATE_REQUEST_ID)
+ .withLang(TEMPLATE_REQUEST_LANG)
+ .withOutputFormat(TEMPLATE_REQUEST_OUTPUT);
+ }
+
+ private List fillUsersForContext(UserCollection userCollection, String addressTypeId) {
+ if (userCollection.getUsers().isEmpty()) {
+ return Collections.emptyList();
+ }
+ return userCollection.getUsers().stream()
+ .map(UserCollection.User::getPersonal)
+ .filter(ObjectUtils::isNotEmpty)
+ .map(personalData -> {
+ var userForContext = new TemplateProcessingRequest.User()
+ .withFirstName(personalData.getFirstName())
+ .withLastName(personalData.getLastName());
+ List addressList = personalData.getAddresses();
+ if (addressList != null && !addressList.isEmpty()) {
+ userForContext.withRoutingAddress(getUserAddress(addressList, addressTypeId));
+ }
+ return userForContext;
+ }
+ ).toList();
+ }
+
+ 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);
+ return address.getAddressLine1();
+ }
+ }
+ log.warn("getUserAddress:: Required address is not found with addressTypId={}", addressTypeId);
+ return "";
+ }
+
+ private RoutingList fillRoutingListForContext(RoutingList routingList) {
+ return new RoutingList()
+ .withName(routingList.getName())
+ .withNotes(routingList.getNotes());
+ }
+
+ 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());
+ return restClient.postJsonObject(requestEntry, JsonObject.mapFrom(templateRequest), requestContext);
+ }
+}
diff --git a/src/main/java/org/folio/service/UserService.java b/src/main/java/org/folio/service/UserService.java
index d19d99f65..97aa93b75 100644
--- a/src/main/java/org/folio/service/UserService.java
+++ b/src/main/java/org/folio/service/UserService.java
@@ -1,12 +1,55 @@
package org.folio.service;
+import static org.folio.orders.utils.HelperUtils.collectResultsOnSuccess;
+import static org.folio.orders.utils.HelperUtils.convertIdsToCqlQuery;
+import static org.folio.orders.utils.ResourcePathResolver.USERS;
+import static org.folio.orders.utils.ResourcePathResolver.resourcesPath;
+import static org.folio.rest.RestConstants.MAX_IDS_FOR_GET_RQ_15;
import static org.folio.rest.RestVerticle.OKAPI_USERID_HEADER;
+import java.util.List;
import java.util.Map;
+import io.vertx.core.Future;
+import one.util.streamex.StreamEx;
+import org.apache.commons.lang3.ObjectUtils;
+import org.folio.models.UserCollection;
+import org.folio.rest.core.RestClient;
+import org.folio.rest.core.models.RequestContext;
+import org.folio.rest.core.models.RequestEntry;
+
public class UserService {
+ private static final String USERS_ENDPOINT = resourcesPath(USERS);
+ private final RestClient restClient;
+
+ public UserService(RestClient restClient) {
+ this.restClient = restClient;
+ }
+
public static String getCurrentUserId(Map okapiHeaders) {
return okapiHeaders.get(OKAPI_USERID_HEADER);
}
+
+ public Future getUsersByIds(List userIds, RequestContext requestContext) {
+ var futures = StreamEx.ofSubLists(userIds, MAX_IDS_FOR_GET_RQ_15)
+ .map(ids -> getBatchUsersByIds(ids, requestContext))
+ .toList();
+ return collectResultsOnSuccess(futures)
+ .map(this::combineUserCollections);
+ }
+
+ private Future getBatchUsersByIds(List userIds, RequestContext requestContext) {
+ var requestEntry = new RequestEntry(USERS_ENDPOINT).withOffset(0).withLimit(Integer.MAX_VALUE)
+ .withQuery(convertIdsToCqlQuery(userIds, "id"));
+ return restClient.get(requestEntry, UserCollection.class, requestContext);
+ }
+
+ private UserCollection combineUserCollections(List userCollections) {
+ var userList = userCollections.stream()
+ .filter(ObjectUtils::isNotEmpty)
+ .flatMap(userCollection -> userCollection.getUsers().stream())
+ .toList();
+ return new UserCollection().withUsers(userList);
+ }
}
diff --git a/src/test/java/org/folio/ApiTestSuite.java b/src/test/java/org/folio/ApiTestSuite.java
index aaa6890d9..f0dac243e 100644
--- a/src/test/java/org/folio/ApiTestSuite.java
+++ b/src/test/java/org/folio/ApiTestSuite.java
@@ -33,6 +33,7 @@
import org.folio.rest.impl.PurchaseOrderLinesApiTest;
import org.folio.rest.impl.PurchaseOrdersApiTest;
import org.folio.rest.impl.ReceivingHistoryApiTest;
+import org.folio.rest.impl.RoutingListsApiTest;
import org.folio.rest.impl.TitlesApiTest;
import org.folio.rest.impl.crud.ConfigurationCrudTest;
import org.folio.rest.impl.protection.LinesProtectionTest;
@@ -43,6 +44,7 @@
import org.folio.service.ReasonForClosureServiceTest;
import org.folio.service.SuffixServiceTest;
import org.folio.service.TagServiceTest;
+import org.folio.service.UserServiceTest;
import org.folio.service.consortium.SharingInstanceServiceTest;
import org.folio.service.exchange.ManualExchangeRateProviderTest;
import org.folio.service.expenceclass.ExpenseClassValidationServiceTest;
@@ -97,6 +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.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Nested;
@@ -181,6 +184,10 @@ class OrderTemplateTestNested extends OrderTemplateTest {
class TitlesApiTestNested extends TitlesApiTest {
}
+ @Nested
+ class RoutingListsApiTestNested extends RoutingListsApiTest {
+ }
+
@Nested
class ConfigurationCrudTestNested extends ConfigurationCrudTest {
}
@@ -470,4 +477,12 @@ class FiscalYearServiceTestNested extends FiscalYearServiceTest {
@Nested
class TagServiceTestNested extends TagServiceTest {
}
+
+ @Nested
+ class RoutingListServiceTestNested extends RoutingListServiceTest {
+ }
+
+ @Nested
+ class UserServiceTestNested extends UserServiceTest {
+ }
}
diff --git a/src/test/java/org/folio/TestConstants.java b/src/test/java/org/folio/TestConstants.java
index 809f13c6e..8297479bd 100644
--- a/src/test/java/org/folio/TestConstants.java
+++ b/src/test/java/org/folio/TestConstants.java
@@ -1,5 +1,6 @@
package org.folio;
+import static org.folio.orders.utils.PermissionsUtil.OKAPI_HEADER_PERMISSIONS;
import static org.folio.rest.RestVerticle.OKAPI_HEADER_TENANT;
import static org.folio.rest.RestVerticle.OKAPI_HEADER_TOKEN;
import static org.folio.rest.RestVerticle.OKAPI_USERID_HEADER;
@@ -9,6 +10,8 @@
import java.util.UUID;
import io.restassured.http.Header;
+import io.vertx.core.json.JsonArray;
+import org.folio.orders.utils.AcqDesiredPermissions;
public final class TestConstants {
@@ -63,6 +66,7 @@ private TestConstants() {}
public static final Header NON_EXIST_LOAN_TYPE_TENANT_HEADER = new Header(OKAPI_HEADER_TENANT, NON_EXIST_LOAN_TYPE_TENANT);
public static final Header NON_EXIST_CONFIG_X_OKAPI_TENANT = new Header(OKAPI_HEADER_TENANT, "ordersimpltest");
public static final Header X_OKAPI_USER_ID = new Header(OKAPI_USERID_HEADER, "440c89e3-7f6c-578a-9ea8-310dad23605e");
+ public static final Header ALL_DESIRED_ACQ_PERMISSIONS_HEADER = new Header(OKAPI_HEADER_PERMISSIONS, new JsonArray(AcqDesiredPermissions.getValuesExceptBypass()).encode());
public static final Header X_OKAPI_USER_ID_WITH_ACQ_UNITS = new Header(OKAPI_USERID_HEADER, USER_ID_ASSIGNED_TO_ACQ_UNITS);
public static final Header X_OKAPI_TOKEN = new Header(OKAPI_HEADER_TOKEN, "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkaWt1X2FkbWluIiwidXNlcl9pZCI6ImJmZTI2MjM0LTMzNjktNTdhYS05ZjhhLWU2ZWVhY2M0YTgzYiIsImlhdCI6MTU4MzE1Nzg5OCwidGVuYW50IjoiZGlrdSJ9.Mk7u4KaCywSuYtBgCT44oGcVC0C8jUMY9KjsUnug48I");
public static final Header EXIST_CONFIG_X_OKAPI_TENANT_LIMIT_10 = new Header(OKAPI_HEADER_TENANT, "test_diku_limit_10");
@@ -85,4 +89,5 @@ private TestConstants() {}
public static final String PIECE_PATH = BASE_MOCK_DATA_PATH + "pieces/";
public static final String TILES_PATH = BASE_MOCK_DATA_PATH + "titles/";
public static final String ID_FOR_TEMPLATE_NAME_ALREADY_EXISTS = "cd0619fb-a628-4d90-be41-df8943e97768";
+ public static final String ROUTING_LIST_ID = "eee951de-ea49-400a-96e8-705ae5a1e1e8";
}
diff --git a/src/test/java/org/folio/rest/impl/MockServer.java b/src/test/java/org/folio/rest/impl/MockServer.java
index 58626f43a..dc4c9220b 100644
--- a/src/test/java/org/folio/rest/impl/MockServer.java
+++ b/src/test/java/org/folio/rest/impl/MockServer.java
@@ -242,6 +242,8 @@ 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 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";
private static final String ORDER_TEMPLATES_MOCK_DATA_PATH = BASE_MOCK_DATA_PATH + "orderTemplates/";
diff --git a/src/test/java/org/folio/rest/impl/RoutingListsApiTest.java b/src/test/java/org/folio/rest/impl/RoutingListsApiTest.java
new file mode 100644
index 000000000..401d0eb9e
--- /dev/null
+++ b/src/test/java/org/folio/rest/impl/RoutingListsApiTest.java
@@ -0,0 +1,116 @@
+package org.folio.rest.impl;
+
+import static io.vertx.core.Future.succeededFuture;
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
+import static org.folio.RestTestUtils.prepareHeaders;
+import static org.folio.RestTestUtils.verifyGet;
+import static org.folio.TestConfig.X_OKAPI_URL;
+import static org.folio.TestConfig.autowireDependencies;
+import static org.folio.TestConfig.clearServiceInteractions;
+import static org.folio.TestConfig.getFirstContextFromVertx;
+import static org.folio.TestConfig.getVertx;
+import static org.folio.TestConfig.initSpringContext;
+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.ROUTING_LIST_ID;
+import static org.folio.TestConstants.X_OKAPI_TOKEN;
+import static org.folio.TestConstants.X_OKAPI_USER_ID;
+import static org.folio.rest.RestConstants.OKAPI_URL;
+import static org.folio.rest.impl.PurchaseOrdersApiTest.X_OKAPI_TENANT;
+import static org.mockito.ArgumentMatchers.any;
+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.HashMap;
+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.rest.core.models.RequestContext;
+import org.folio.service.RoutingListService;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockitoAnnotations;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+
+public class RoutingListsApiTest {
+
+ private static final Logger logger = LogManager.getLogger();
+ private static final String TEMPLATE_PROCESSING_REQUEST_ENDPOINT = "orders/routing-lists/" + ROUTING_LIST_ID + "/template";
+ private static boolean runningOnOwn;
+ @Autowired
+ private RoutingListService routingListService;
+ private RequestContext requestContext;
+ private Context ctxMock;
+ private Map okapiHeadersMock;
+ private AutoCloseable mockitoMocks;
+
+
+ @BeforeAll
+ static void before() throws InterruptedException, ExecutionException, TimeoutException {
+ if (isVerticleNotDeployed()) {
+ ApiTestSuite.before();
+ runningOnOwn = true;
+ }
+ initSpringContext(RoutingListsApiTest.ContextConfiguration.class);
+ }
+
+
+ @BeforeEach
+ void beforeEach() {
+ mockitoMocks = MockitoAnnotations.openMocks(this);
+ autowireDependencies(this);
+ ctxMock = getFirstContextFromVertx(getVertx());
+ okapiHeadersMock = new HashMap<>();
+ okapiHeadersMock.put(OKAPI_URL, "http://localhost:" + mockPort);
+ okapiHeadersMock.put(X_OKAPI_TOKEN.getName(), X_OKAPI_TOKEN.getValue());
+ 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);
+ }
+
+ @AfterEach
+ void afterEach() throws Exception {
+ mockitoMocks.close();
+ clearServiceInteractions();
+ }
+
+ @AfterAll
+ static void after() {
+ if (runningOnOwn) {
+ ApiTestSuite.after();
+ }
+ }
+
+ @Test
+ void testProcessTemplateRequest() {
+ logger.info("=== Test Execute template processing request ===");
+
+ doReturn(succeededFuture(new JsonObject())).when(routingListService).processTemplateRequest(eq(ROUTING_LIST_ID), any(RequestContext.class));
+
+ verifyGet(TEMPLATE_PROCESSING_REQUEST_ENDPOINT, prepareHeaders(X_OKAPI_URL, EMPTY_CONFIG_X_OKAPI_TENANT),
+ APPLICATION_JSON, 200);
+
+ verify(routingListService, times(1)).processTemplateRequest(eq(ROUTING_LIST_ID), any(RequestContext.class));
+ }
+
+ static class ContextConfiguration {
+ @Bean
+ public RoutingListService routingListService() {
+ return mock(RoutingListService.class);
+ }
+ }
+}
diff --git a/src/test/java/org/folio/rest/impl/TitlesApiTest.java b/src/test/java/org/folio/rest/impl/TitlesApiTest.java
index d5b7004b7..73dc5cf8d 100644
--- a/src/test/java/org/folio/rest/impl/TitlesApiTest.java
+++ b/src/test/java/org/folio/rest/impl/TitlesApiTest.java
@@ -10,6 +10,7 @@
import static org.folio.TestConfig.clearServiceInteractions;
import static org.folio.TestConfig.initSpringContext;
import static org.folio.TestConfig.isVerticleNotDeployed;
+import static org.folio.TestConstants.ALL_DESIRED_ACQ_PERMISSIONS_HEADER;
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;
@@ -20,7 +21,6 @@
import static org.folio.TestUtils.getMinimalContentCompositePoLine;
import static org.folio.TestUtils.getMockAsJson;
import static org.folio.TestUtils.getMockData;
-import static org.folio.orders.utils.PermissionsUtil.OKAPI_HEADER_PERMISSIONS;
import static org.folio.orders.utils.ResourcePathResolver.PO_LINES_STORAGE;
import static org.folio.rest.core.exceptions.ErrorCodes.*;
import static org.folio.rest.impl.MockServer.TITLES_MOCK_DATA_PATH;
@@ -38,14 +38,12 @@
import java.util.concurrent.TimeoutException;
import io.restassured.http.Header;
-import io.vertx.core.json.JsonArray;
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.config.ApplicationConfig;
-import org.folio.orders.utils.AcqDesiredPermissions;
import org.folio.rest.acq.model.Title;
import org.folio.rest.jaxrs.model.CompositePoLine;
import org.folio.rest.jaxrs.model.Details;
@@ -72,7 +70,6 @@ public class TitlesApiTest {
public static final String SAMPLE_TITLE_ID = "9a665b22-9fe5-4c95-b4ee-837a5433c95d";
private final JsonObject titleJsonReqData = getMockAsJson(TITLES_MOCK_DATA_PATH + "title.json");
private final JsonObject packageTitleJsonReqData = getMockAsJson(TITLES_MOCK_DATA_PATH + "package_title.json");
- public static final Header ALL_DESIRED_PERMISSIONS_HEADER = new Header(OKAPI_HEADER_PERMISSIONS, new JsonArray(AcqDesiredPermissions.getValuesExceptBypass()).encode());
private static boolean runningOnOwn;
@@ -121,7 +118,7 @@ void testPostTitle() {
assertThat(postTitleRq.getId(), nullValue());
Title postTitleRs = verifyPostResponse(TITLES_ENDPOINT, JsonObject.mapFrom(postTitleRq).encode(),
- prepareHeaders(EXIST_CONFIG_X_OKAPI_TENANT_LIMIT_10, X_OKAPI_USER_ID_WITH_ACQ_UNITS, ALL_DESIRED_PERMISSIONS_HEADER), APPLICATION_JSON, HttpStatus.HTTP_CREATED.toInt()).as(Title.class);
+ prepareHeaders(EXIST_CONFIG_X_OKAPI_TENANT_LIMIT_10, X_OKAPI_USER_ID_WITH_ACQ_UNITS, ALL_DESIRED_ACQ_PERMISSIONS_HEADER), APPLICATION_JSON, HttpStatus.HTTP_CREATED.toInt()).as(Title.class);
// Title id not null
assertThat(postTitleRs.getId(), Matchers.notNullValue());
@@ -129,12 +126,12 @@ void testPostTitle() {
// Negative cases
// Unable to create title test
int status400 = HttpStatus.HTTP_BAD_REQUEST.toInt();
- verifyPostResponse(TITLES_ENDPOINT, JsonObject.mapFrom(postTitleRq).encode(), prepareHeaders(EXIST_CONFIG_X_OKAPI_TENANT_LIMIT_10, X_OKAPI_USER_ID_WITH_ACQ_UNITS, ALL_DESIRED_PERMISSIONS_HEADER,
+ verifyPostResponse(TITLES_ENDPOINT, JsonObject.mapFrom(postTitleRq).encode(), prepareHeaders(EXIST_CONFIG_X_OKAPI_TENANT_LIMIT_10, X_OKAPI_USER_ID_WITH_ACQ_UNITS, ALL_DESIRED_ACQ_PERMISSIONS_HEADER,
new Header(X_ECHO_STATUS, String.valueOf(status400))), APPLICATION_JSON, status400);
// Internal error on mod-orders-storage test
int status500 = HttpStatus.HTTP_INTERNAL_SERVER_ERROR.toInt();
- verifyPostResponse(TITLES_ENDPOINT, JsonObject.mapFrom(postTitleRq).encode(), prepareHeaders(EXIST_CONFIG_X_OKAPI_TENANT_LIMIT_10, X_OKAPI_USER_ID_WITH_ACQ_UNITS, ALL_DESIRED_PERMISSIONS_HEADER,
+ verifyPostResponse(TITLES_ENDPOINT, JsonObject.mapFrom(postTitleRq).encode(), prepareHeaders(EXIST_CONFIG_X_OKAPI_TENANT_LIMIT_10, X_OKAPI_USER_ID_WITH_ACQ_UNITS, ALL_DESIRED_ACQ_PERMISSIONS_HEADER,
new Header(X_ECHO_STATUS, String.valueOf(status500))), APPLICATION_JSON, status500);
}
@@ -163,7 +160,7 @@ void postTitleWithInvalidClaimingConfig() throws Exception {
String reqData = getMockData(TITLES_MOCK_DATA_PATH + "title_invalid_claiming_config.json");
- List errors = verifyPostResponse(TITLES_ENDPOINT, reqData, prepareHeaders(EXIST_CONFIG_X_OKAPI_TENANT_LIMIT_10, X_OKAPI_USER_ID, ALL_DESIRED_PERMISSIONS_HEADER), APPLICATION_JSON, 422)
+ List errors = verifyPostResponse(TITLES_ENDPOINT, reqData, prepareHeaders(EXIST_CONFIG_X_OKAPI_TENANT_LIMIT_10, X_OKAPI_USER_ID, ALL_DESIRED_ACQ_PERMISSIONS_HEADER), APPLICATION_JSON, 422)
.as(Errors.class)
.getErrors();
@@ -192,7 +189,7 @@ void titleShouldBePopulatedFromPackagePoLine() {
addMockEntry(PO_LINES_STORAGE, JsonObject.mapFrom(packagePoLine));
Title titleWithPackagePoLineRS = verifyPostResponse(TITLES_ENDPOINT, JsonObject.mapFrom(titleWithPackagePoLineRQ).encode(),
- prepareHeaders(EXIST_CONFIG_X_OKAPI_TENANT_LIMIT_10, X_OKAPI_USER_ID, ALL_DESIRED_PERMISSIONS_HEADER), APPLICATION_JSON, HttpStatus.HTTP_CREATED.toInt()).as(Title.class);
+ prepareHeaders(EXIST_CONFIG_X_OKAPI_TENANT_LIMIT_10, X_OKAPI_USER_ID, ALL_DESIRED_ACQ_PERMISSIONS_HEADER), APPLICATION_JSON, HttpStatus.HTTP_CREATED.toInt()).as(Title.class);
assertEquals(titleWithPackagePoLineRS.getPackageName(), packageTitleName);
assertNotNull(titleWithPackagePoLineRS.getExpectedReceiptDate());
@@ -231,7 +228,7 @@ void testPutTitlesByIdTest() {
.withAcqUnitIds(List.of(ACQ_UNIT_ID));
verifyPut(String.format(TITLES_ID_PATH, SAMPLE_TITLE_ID), JsonObject.mapFrom(reqData).encode(),
- prepareHeaders(EXIST_CONFIG_X_OKAPI_TENANT_LIMIT_10, ALL_DESIRED_PERMISSIONS_HEADER), "", 204);
+ prepareHeaders(EXIST_CONFIG_X_OKAPI_TENANT_LIMIT_10, ALL_DESIRED_ACQ_PERMISSIONS_HEADER), "", 204);
}
@Test
diff --git a/src/test/java/org/folio/service/RoutingListServiceTest.java b/src/test/java/org/folio/service/RoutingListServiceTest.java
new file mode 100644
index 000000000..fe9651a93
--- /dev/null
+++ b/src/test/java/org/folio/service/RoutingListServiceTest.java
@@ -0,0 +1,102 @@
+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/UserServiceTest.java b/src/test/java/org/folio/service/UserServiceTest.java
new file mode 100644
index 000000000..33e0ce7f3
--- /dev/null
+++ b/src/test/java/org/folio/service/UserServiceTest.java
@@ -0,0 +1,65 @@
+package org.folio.service;
+
+import static io.vertx.core.Future.succeededFuture;
+import static org.folio.TestUtils.getMockData;
+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.List;
+
+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.core.RestClient;
+import org.folio.rest.core.models.RequestContext;
+import org.folio.rest.core.models.RequestEntry;
+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 UserServiceTest {
+
+ @Mock
+ private RestClient restClient;
+ @InjectMocks
+ 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 getUsersByIds(VertxTestContext vertxTestContext) throws IOException {
+ var users = new JsonObject(getMockData(USERS_MOCK_DATA_PATH + "user_collection.json")).mapTo(UserCollection.class);
+ List userIds = users.getUsers().stream().map(user -> String.valueOf(user.getId())).toList();
+ doReturn(succeededFuture(users)).when(restClient).get(any(RequestEntry.class), eq(UserCollection.class), any());
+
+ Future future = userService.getUsersByIds(userIds, requestContextMock);
+
+ vertxTestContext.assertComplete(future)
+ .onComplete(result -> {
+ assertTrue(result.succeeded());
+ vertxTestContext.completeNow();
+ });
+ }
+}
diff --git a/src/test/resources/mockdata/routingLists/eee951de-ea49-400a-96e8-705ae5a1e1e8-expected-template-request.json b/src/test/resources/mockdata/routingLists/eee951de-ea49-400a-96e8-705ae5a1e1e8-expected-template-request.json
new file mode 100644
index 000000000..4863165c3
--- /dev/null
+++ b/src/test/resources/mockdata/routingLists/eee951de-ea49-400a-96e8-705ae5a1e1e8-expected-template-request.json
@@ -0,0 +1,24 @@
+{
+ "templateId": "9465105a-e8a1-470c-9817-142d33bc4fcd",
+ "lang": "en",
+ "outputFormat": "text/html",
+ "context": {
+ "routingList": {
+ "name": "Mars expedition",
+ "notes": "Future note",
+ "userIds": []
+ },
+ "users": [
+ {
+ "lastName": "Denesik",
+ "firstName": "Maiya",
+ "routingAddress": "144 Law"
+ },
+ {
+ "lastName": "Huels",
+ "firstName": "Lois",
+ "routingAddress": "123 Law"
+ }
+ ]
+ }
+}
diff --git a/src/test/resources/mockdata/routingLists/eee951de-ea49-400a-96e8-705ae5a1e1e8.json b/src/test/resources/mockdata/routingLists/eee951de-ea49-400a-96e8-705ae5a1e1e8.json
new file mode 100644
index 000000000..358a9642f
--- /dev/null
+++ b/src/test/resources/mockdata/routingLists/eee951de-ea49-400a-96e8-705ae5a1e1e8.json
@@ -0,0 +1,11 @@
+{
+ "id": "eee951de-ea49-400a-96e8-705ae5a1e1e8",
+ "_version": 1,
+ "name": "Mars expedition",
+ "notes": "Future note",
+ "userIds": [
+ "00bc2807-4d5b-4a27-a2b5-b7b1ba431cc4",
+ "011dc219-6b7f-4d93-ae7f-f512ed651493"
+ ],
+ "poLineId": "8343e5a0-fed8-11e8-8eb2-f2801f1b9fd1"
+}
diff --git a/src/test/resources/mockdata/users/user_collection.json b/src/test/resources/mockdata/users/user_collection.json
new file mode 100644
index 000000000..5a0b7aa23
--- /dev/null
+++ b/src/test/resources/mockdata/users/user_collection.json
@@ -0,0 +1,108 @@
+{
+ "users": [
+ {
+ "username": "sallie",
+ "id": "00bc2807-4d5b-4a27-a2b5-b7b1ba431cc4",
+ "barcode": "133143370961512",
+ "active": false,
+ "type": "patron",
+ "patronGroup": "503a81cd-6c26-400f-b620-14c08943697c",
+ "departments": [],
+ "proxyFor": [],
+ "personal": {
+ "lastName": "Denesik",
+ "firstName": "Maiya",
+ "middleName": "Noel",
+ "email": "ahmad@kertzmann-bailey-and-brekke.io",
+ "phone": "759.693.8557",
+ "mobilePhone": "(557)093-7575",
+ "dateOfBirth": "2009-07-21T00:00:00.000+00:00",
+ "addresses": [
+ {
+ "countryId": "US",
+ "addressLine1": "17691 Rodriguez Divide",
+ "city": "Birmingham",
+ "region": "CO",
+ "postalCode": "85748",
+ "addressTypeId": "93d3d88d-499b-45d0-9bc7-ac73c3a19880",
+ "primaryAddress": true
+ },
+ {
+ "city": "",
+ "region": "",
+ "countryId": "US",
+ "postalCode": "",
+ "addressLine1": "144 Law",
+ "addressTypeId": "1c4b225f-f669-4e9b-afcd-ebc0e273a34e",
+ "primaryAddress": false
+ }
+ ],
+ "preferredContactTypeId": "002"
+ },
+ "enrollmentDate": "2015-04-13T00:00:00.000+00:00",
+ "expirationDate": "2019-06-20T00:00:00.000+00:00",
+ "createdDate": "2024-01-28T16:01:21.650+00:00",
+ "updatedDate": "2024-01-28T16:01:21.650+00:00",
+ "metadata": {
+ "createdDate": "2024-01-28T16:01:21.601+00:00",
+ "createdByUserId": "631fdcc3-c7c6-48e6-b51c-8a727c5d3dd7",
+ "updatedDate": "2024-01-28T16:01:21.601+00:00",
+ "updatedByUserId": "631fdcc3-c7c6-48e6-b51c-8a727c5d3dd7"
+ }
+ },
+ {
+ "id": "011dc219-6b7f-4d93-ae7f-f512ed651493",
+ "type": "patron",
+ "active": false,
+ "barcode": "897083256223023",
+ "metadata": {
+ "createdDate": "2024-01-28T15:51:44.463Z",
+ "updatedDate": "2024-01-28T15:51:44.463Z",
+ "createdByUserId": "631fdcc3-c7c6-48e6-b51c-8a727c5d3dd7",
+ "updatedByUserId": "631fdcc3-c7c6-48e6-b51c-8a727c5d3dd7"
+ },
+ "personal": {
+ "email": "monserrat@donnelly-skiles.ge",
+ "phone": "(619)645-7533 x5934",
+ "firstName": "Lois",
+ "lastName": "Huels",
+ "addresses": [
+ {
+ "city": "Marana",
+ "region": "NH",
+ "countryId": "US",
+ "postalCode": "02013-0332",
+ "addressLine1": "69175 Haley Skyway",
+ "addressTypeId": "93d3d88d-499b-45d0-9bc7-ac73c3a19880",
+ "primaryAddress": true
+ },
+ {
+ "city": "",
+ "region": "",
+ "countryId": "US",
+ "postalCode": "",
+ "addressLine1": "123 Law",
+ "addressTypeId": "1c4b225f-f669-4e9b-afcd-ebc0e273a34e",
+ "primaryAddress": false
+ }
+ ],
+ "dateOfBirth": "1947-06-23T00:00:00.000+00:00",
+ "preferredContactTypeId": "004"
+ },
+ "proxyFor": [],
+ "username": "elmer",
+ "createdDate": "2024-01-28T15:51:44.505+00:00",
+ "departments": [],
+ "patronGroup": "3684a786-6671-4268-8ed0-9db82ebca60b",
+ "updatedDate": "2024-01-28T15:51:44.505+00:00",
+ "enrollmentDate": "2018-05-06T00:00:00.000+00:00",
+ "expirationDate": "2019-09-02T00:00:00.000+00:00"
+ }
+ ],
+ "totalRecords": 2,
+ "resultInfo": {
+ "totalRecords": 2,
+ "facets": [],
+ "diagnostics": []
+ }
+}