diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 194061617..e26debc38 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -1003,6 +1003,19 @@ } ] }, + { + "id": "orders.routing-list", + "version": "1.0", + "handlers": [ + { + "methods": ["POST"], + "pathPattern": "/orders/routing-lists/{id}/process-template", + "permissionsRequired": [ + "orders.routing-list.item.post" + ] + } + ] + }, { "id": "_jsonSchemas", "version": "1.0", diff --git a/ramls/routing-lists.raml b/ramls/routing-lists.raml index 2f2aa02c1..3593957bf 100644 --- a/ramls/routing-lists.raml +++ b/ramls/routing-lists.raml @@ -8,8 +8,8 @@ documentation: content: CRUD API to manage routing lists. types: - title: !include acq-models/mod-orders-storage/schemas/routing_list.json - title_collection: !include acq-models/mod-orders-storage/schemas/routing_list_collection.json + 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}$ @@ -19,8 +19,8 @@ traits: searchable: !include raml-util/traits/searchable.raml resourceTypes: - collection: !include raml-util/rtypes/collection.raml - collection-item: !include raml-util/rtypes/item-collection.raml + collection: !include rtypes/collection-with-json-response.raml + collection-item: !include rtypes/item-collection-with-json-response.raml /orders/routing-lists: type: @@ -40,9 +40,5 @@ resourceTypes: id: description: The UUID of a Title type: UUID - type: - collection-item: - exampleItem: !include acq-models/mod-orders-storage/examples/routing_list_get.sample - schema: routing_list post: description: Execute mod-template-engine to process templates with replaced token placeholders [update] diff --git a/src/main/java/org/folio/models/template/TemplateProcessingRequest.java b/src/main/java/org/folio/models/template/TemplateProcessingRequest.java new file mode 100644 index 000000000..51dcd575f --- /dev/null +++ b/src/main/java/org/folio/models/template/TemplateProcessingRequest.java @@ -0,0 +1,66 @@ +package org.folio.models.template; + +import java.util.List; +import java.util.UUID; + +import lombok.Getter; + +@Getter +public class TemplateProcessingRequest { + private UUID templateId; + private String lang; + private String outputFormat; + private Context 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; + } + + @Getter + public static class Context { + private List users; + private List items; + + public Context withUsers(List users) { + this.users = users; + return this; + } + + } + + @Getter + public static class User { + private String name; + public User withName(String name) { + this.name = name; + return this; + } + } + + @Getter + public static class Item { + private String name; + + public Item setName(String name) { + this.name = name; + return this; + } + } +} + diff --git a/src/main/java/org/folio/orders/utils/ResourcePathResolver.java b/src/main/java/org/folio/orders/utils/ResourcePathResolver.java index 50b3da277..c03ffcdfc 100644 --- a/src/main/java/org/folio/orders/utils/ResourcePathResolver.java +++ b/src/main/java/org/folio/orders/utils/ResourcePathResolver.java @@ -28,6 +28,9 @@ 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 ROUTING_LISTS = "routing-lists"; + public static final String TEMPLATE_REQUEST = "template-request"; + public static final String USERS = "users"; public static final String FUNDS = "finance.funds"; public static final String BUDGETS = "finance.budgets"; public static final String LEDGERS = "finance.ledgers"; @@ -66,6 +69,8 @@ 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(ROUTING_LISTS, "/orders-storage/routing-lists"); + 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,7 @@ 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"); 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..14d846ca5 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 { diff --git a/src/main/java/org/folio/rest/impl/RoutingListAPI.java b/src/main/java/org/folio/rest/impl/RoutingListAPI.java new file mode 100644 index 000000000..145bf7234 --- /dev/null +++ b/src/main/java/org/folio/rest/impl/RoutingListAPI.java @@ -0,0 +1,39 @@ +package org.folio.rest.impl; + +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 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.routinglist.RoutingListService; +import org.springframework.beans.factory.annotation.Autowired; + +public class RoutingListAPI extends BaseApi implements OrdersRoutingLists { + + @Autowired + private RoutingListService routingListService; + + @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 postOrdersRoutingListsProcessTemplateById(String id, Map okapiHeaders, + Handler> asyncResultHandler, Context vertxContext) { + routingListService.processTemplateEngine(id, new RequestContext(vertxContext, okapiHeaders)) + .onFailure(t -> handleErrorResponse(asyncResultHandler, t)); + } +} diff --git a/src/main/java/org/folio/service/UserService.java b/src/main/java/org/folio/service/UserService.java index d19d99f65..017c8e355 100644 --- a/src/main/java/org/folio/service/UserService.java +++ b/src/main/java/org/folio/service/UserService.java @@ -1,12 +1,37 @@ package org.folio.service; +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.RestVerticle.OKAPI_USERID_HEADER; +import java.util.List; import java.util.Map; +import io.vertx.core.Future; +import io.vertx.core.json.JsonObject; +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 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 requestEntry = new RequestEntry(USERS) + .withOffset(0) + .withLimit(Integer.MAX_VALUE) + .withQuery(convertIdsToCqlQuery(userIds, "sourceInvoiceId")); + + return restClient.get(requestEntry, JsonObject.class, requestContext); + } } diff --git a/src/main/java/org/folio/service/routinglist/RoutingListService.java b/src/main/java/org/folio/service/routinglist/RoutingListService.java new file mode 100644 index 000000000..c6e10fd29 --- /dev/null +++ b/src/main/java/org/folio/service/routinglist/RoutingListService.java @@ -0,0 +1,76 @@ +package org.folio.service.routinglist; + +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.List; +import java.util.UUID; + +import io.vertx.core.Future; +import io.vertx.core.json.JsonObject; +import lombok.extern.log4j.Log4j2; +import org.folio.models.template.TemplateProcessingRequest; +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.folio.service.UserService; + +@Log4j2 +public class RoutingListService { + + private static final String ENDPOINT = resourcesPath(ROUTING_LISTS); + private static final String BY_ID_ENDPOINT = ENDPOINT + "/{id}"; + private final RestClient restClient; + private final UserService userService; + + public RoutingListService(RestClient restClient, UserService userService) { + this.restClient = restClient; + this.userService = userService; + } + + public Future processTemplateEngine(String id, RequestContext requestContext) { + return getRoutingListById(id, requestContext) + .compose(routingList -> fetchUsersAndCreateTemplate(routingList, requestContext)) + .compose(templateProcessingRequest -> postTemplateRequest(templateProcessingRequest, requestContext)); + } + + public Future getRoutingListById(String routingListId, RequestContext requestContext) { + var requestEntry = new RequestEntry(BY_ID_ENDPOINT).withId(routingListId); + return restClient.get(requestEntry, RoutingList.class, requestContext); + } + + private Future fetchUsersAndCreateTemplate(RoutingList routingList, RequestContext requestContext) { + return userService.getUsersByIds(routingList.getUserIds(), requestContext) + .map(users -> createTemplateRequest(routingList, users)); + } + + private TemplateProcessingRequest createTemplateRequest(RoutingList routingList, JsonObject users) { + var templateRequest =createBaseTemplateRequest(); + var userListForContext = createUserListForContext(users); + var context = new TemplateProcessingRequest.Context().withUsers(userListForContext); + templateRequest.withContext(context); + return templateRequest; + } + + private TemplateProcessingRequest createBaseTemplateRequest() { + return new TemplateProcessingRequest() + .withTemplateId(UUID.randomUUID()) + .withLang("en") + .withOutputFormat("text/plain"); + } + + private List createUserListForContext(JsonObject users) { + return users.getJsonArray("users").stream() + .map(JsonObject.class::cast) + .map(user -> new TemplateProcessingRequest.User() + .withName(user.getJsonObject("personal").getString("firstName")) + ) + .toList(); + } + + private Future postTemplateRequest(TemplateProcessingRequest templateProcessingRequest, RequestContext requestContext) { + return restClient.post(TEMPLATE_REQUEST, JsonObject.mapFrom(templateProcessingRequest), JsonObject.class, requestContext); + } +} 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/service/routinglist/RoutingListServiceTest.java b/src/test/java/org/folio/service/routinglist/RoutingListServiceTest.java new file mode 100644 index 000000000..23b620f77 --- /dev/null +++ b/src/test/java/org/folio/service/routinglist/RoutingListServiceTest.java @@ -0,0 +1,66 @@ +package org.folio.service.routinglist; + +import static io.vertx.core.Future.succeededFuture; +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.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; + +import java.io.IOException; + +import io.vertx.core.Future; +import io.vertx.core.json.JsonObject; +import io.vertx.junit5.VertxExtension; +import io.vertx.junit5.VertxTestContext; +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.folio.service.UserService; +import org.junit.jupiter.api.Assertions; +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 { + + private static final String ROUTING_LIST_ID = "eee951de-ea49-400a-96e8-705ae5a1e1e8"; + @InjectMocks + RoutingListService routingListService; + @Mock + private RestClient restClient; + @Mock + private UserService userService; + @Mock + private RequestContext requestContextMock; + + @BeforeEach + public void initMocks() { + MockitoAnnotations.openMocks(this); + } + + @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")); + + doReturn(succeededFuture(routingList)).when(restClient).get(any(RequestEntry.class), eq(RoutingList.class), any(RequestContext.class)); + doReturn(succeededFuture(users)).when(userService).getUsersByIds(eq(routingList.getUserIds()), any(RequestContext.class)); + doReturn(succeededFuture(new JsonObject())).when(restClient).post(anyString(), any(), eq(JsonObject.class), any()); + + Future future = routingListService.processTemplateEngine(ROUTING_LIST_ID, requestContextMock); + + vertxTestContext.assertComplete(future) + .onComplete(result -> { + Assertions.assertTrue(result.succeeded()); + vertxTestContext.completeNow(); + }); + } +} 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..d89742eb8 --- /dev/null +++ b/src/test/resources/mockdata/routingLists/eee951de-ea49-400a-96e8-705ae5a1e1e8.json @@ -0,0 +1,11 @@ +{ + "id": "c0d13648-347b-4ac9-8c2f-5bc47248b87e", + "_version": 1, + "name": "Mars expedition", + "notes": "Future note", + "userIds": [ + "d926d900-e27d-46d6-bba8-31e9d5c2cf44", + "077274ad-6b4f-4c28-9779-6d381e7a1ca1" + ], + "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..2473888ff --- /dev/null +++ b/src/test/resources/mockdata/users/user_collection.json @@ -0,0 +1,41 @@ +{ + "users": [ + { + "username": "mockuser8", + "id": "d926d900-e27d-46d6-bba8-31e9d5c2cf44", + "active": true, + "type": "staff", + "meta": { + "creation_date": "2016-11-05T0723", + "last_login_date": "" + }, + "personal": { + "firstName": "Albert", + "lastName": "Einstein", + "email": "albert@si.edu", + "phone": "927-306-2327" + } + }, + { + "username": "mockuser9", + "id": "077274ad-6b4f-4c28-9779-6d381e7a1ca1", + "active": true, + "type": "staff", + "meta": { + "creation_date": "2016-11-05T0723", + "last_login_date": "" + }, + "personal": { + "lastName": "Mockerson", + "middleName": "M.", + "firstName": "Mickey", + "email": "mock@biglibrary.org", + "phone": "2125551212", + "mobilePhone": "112233" + }, + "externalSystemId": "123", + "barcode": "test123" + } + ], + "totalRecords": 2 +}