From 7a721911ab88efc20f4ebaddddfc15fa0a795ca3 Mon Sep 17 00:00:00 2001 From: Roman_Barannyk Date: Tue, 31 Oct 2023 16:12:11 +0200 Subject: [PATCH] CIRC-1932 implement search-slips endpoint skeleton --- descriptors/ModuleDescriptor-template.json | 17 +++ ramls/search-slips.raml | 26 ++++ .../circulation/CirculationVerticle.java | 3 + .../resources/PickSlipsResource.java | 109 +------------- .../resources/SearchSlipsResource.java | 50 +++++++ .../circulation/resources/SlipsResource.java | 135 ++++++++++++++++++ .../java/api/requests/SearchSlipsTests.java | 32 +++++ .../java/api/support/http/InterfaceUrls.java | 4 + .../java/api/support/http/ResourceClient.java | 4 + 9 files changed, 276 insertions(+), 104 deletions(-) create mode 100644 ramls/search-slips.raml create mode 100644 src/main/java/org/folio/circulation/resources/SearchSlipsResource.java create mode 100644 src/main/java/org/folio/circulation/resources/SlipsResource.java create mode 100644 src/test/java/api/requests/SearchSlipsTests.java diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index fc326f66d3..761ecabaa6 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -56,6 +56,23 @@ } ] }, + { + "id": "search-slips", + "version": "0.1", + "handlers": [ + { + "methods": [ + "GET" + ], + "pathPattern": "/circulation/search-slips/{servicePointId}", + "permissionsRequired": [ + "circulation.search-slips.get" + ], + "modulePermissions": [ + ] + } + ] + }, { "id": "request-move", "version": "0.7", diff --git a/ramls/search-slips.raml b/ramls/search-slips.raml new file mode 100644 index 0000000000..f278335d00 --- /dev/null +++ b/ramls/search-slips.raml @@ -0,0 +1,26 @@ +#%RAML 1.0 +title: Pick Slips +version: v0.3 +protocols: [ HTTP, HTTPS ] +baseUri: http://localhost:9130 + +documentation: + - title: API for fetching current search slips + content: API for search slips generation + +types: + search-slips: !include pick-slips-response.json + +traits: + language: !include raml-util/traits/language.raml + +resourceTypes: + collection-get: !include raml-util/rtypes/collection-get.raml + +/circulation: + /search-slips: + /{servicePointId}: + type: + collection-get: + exampleCollection: !include examples/pick-slips-response.json + schemaCollection: search-slips diff --git a/src/main/java/org/folio/circulation/CirculationVerticle.java b/src/main/java/org/folio/circulation/CirculationVerticle.java index fe3882dbeb..0c3a88cc75 100644 --- a/src/main/java/org/folio/circulation/CirculationVerticle.java +++ b/src/main/java/org/folio/circulation/CirculationVerticle.java @@ -38,6 +38,7 @@ import org.folio.circulation.resources.RequestQueueResource; import org.folio.circulation.resources.RequestScheduledNoticeProcessingResource; import org.folio.circulation.resources.ScheduledAnonymizationProcessingResource; +import org.folio.circulation.resources.SearchSlipsResource; import org.folio.circulation.resources.TenantActivationResource; import org.folio.circulation.resources.agedtolost.ScheduledAgeToLostFeeChargingResource; import org.folio.circulation.resources.agedtolost.ScheduledAgeToLostResource; @@ -100,6 +101,8 @@ public void start(Promise startFuture) { .register(router); new PickSlipsResource("/circulation/pick-slips/:servicePointId", client) .register(router); + new SearchSlipsResource("/circulation/search-slips/:servicePointId", client) + .register(router); new CirculationRulesResource("/circulation/rules", client) .register(router); diff --git a/src/main/java/org/folio/circulation/resources/PickSlipsResource.java b/src/main/java/org/folio/circulation/resources/PickSlipsResource.java index 8c2cbafaf4..910cccd773 100644 --- a/src/main/java/org/folio/circulation/resources/PickSlipsResource.java +++ b/src/main/java/org/folio/circulation/resources/PickSlipsResource.java @@ -2,26 +2,19 @@ import static java.util.Collections.emptyList; import static java.util.concurrent.CompletableFuture.completedFuture; -import static java.util.function.Function.identity; -import static java.util.stream.Collectors.toMap; import static java.util.stream.Collectors.toSet; import static org.folio.circulation.support.fetching.MultipleCqlIndexValuesCriteria.byIndex; -import static org.folio.circulation.support.fetching.RecordFetching.findWithCqlQuery; import static org.folio.circulation.support.fetching.RecordFetching.findWithMultipleCqlIndexValues; import static org.folio.circulation.support.http.client.CqlQuery.exactMatch; import static org.folio.circulation.support.results.Result.succeeded; import static org.folio.circulation.support.results.ResultBinding.flatMapResult; -import static org.folio.circulation.support.utils.LogUtil.collectionAsString; import static org.folio.circulation.support.utils.LogUtil.multipleRecordsAsString; import java.lang.invoke.MethodHandles; import java.util.Collection; -import java.util.List; -import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; -import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; @@ -41,36 +34,20 @@ import org.folio.circulation.infrastructure.storage.users.DepartmentRepository; import org.folio.circulation.infrastructure.storage.users.PatronGroupRepository; import org.folio.circulation.infrastructure.storage.users.UserRepository; -import org.folio.circulation.storage.mappers.LocationMapper; import org.folio.circulation.support.Clients; import org.folio.circulation.support.RouteRegistration; import org.folio.circulation.support.http.client.CqlQuery; -import org.folio.circulation.support.http.client.PageLimit; import org.folio.circulation.support.http.server.JsonHttpResponse; import org.folio.circulation.support.http.server.WebContext; import org.folio.circulation.support.results.Result; import io.vertx.core.http.HttpClient; -import io.vertx.core.json.JsonObject; import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; -public class PickSlipsResource extends Resource { - private static final String STATUS_KEY = "status"; - private static final String ITEM_ID_KEY = "itemId"; - private static final String REQUESTS_KEY = "requests"; - private static final String LOCATIONS_KEY = "locations"; - private static final String PICK_SLIPS_KEY = "pickSlips"; - private static final String STATUS_NAME_KEY = "status.name"; - private static final String REQUEST_TYPE_KEY = "requestType"; - private static final String TOTAL_RECORDS_KEY = "totalRecords"; - private static final String SERVICE_POINT_ID_PARAM = "servicePointId"; - private static final String EFFECTIVE_LOCATION_ID_KEY = "effectiveLocationId"; - private static final String PRIMARY_SERVICE_POINT_KEY = "primaryServicePoint"; - - private static final PageLimit LOCATIONS_LIMIT = PageLimit.oneThousand(); +public class PickSlipsResource extends SlipsResource { private static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); - + private static final String PICK_SLIPS_KEY = "pickSlips"; private final String rootPath; @@ -85,8 +62,7 @@ public void register(Router router) { routeRegistration.getMany(this::getMany); } - - private void getMany(RoutingContext routingContext) { + protected void getMany(RoutingContext routingContext) { final WebContext context = new WebContext(routingContext); final Clients clients = Clients.create(context, client); @@ -108,22 +84,13 @@ private void getMany(RoutingContext routingContext) { .thenComposeAsync(r -> r.after(departmentRepository::findDepartmentsForRequestUsers)) .thenComposeAsync(r -> r.after(addressTypeRepository::findAddressTypesForRequests)) .thenComposeAsync(r -> r.after(servicePointRepository::findServicePointsForRequests)) - .thenApply(flatMapResult(this::mapResultToJson)) + .thenApply(flatMapResult(requests -> mapResultToJson(requests, PICK_SLIPS_KEY))) .thenComposeAsync(r -> r.combineAfter(() -> servicePointRepository.getServicePointById(servicePointId), TemplateContextUtil::addPrimaryServicePointNameToStaffSlipContext)) .thenApply(r -> r.map(JsonHttpResponse::ok)) .thenAccept(context::writeResultToHttpResponse); } - private CompletableFuture>> fetchLocationsForServicePoint( - UUID servicePointId, Clients clients) { - - log.debug("fetchLocationsForServicePoint:: parameters servicePointId: {}", servicePointId); - - return findWithCqlQuery(clients.locationsStorage(), LOCATIONS_KEY, new LocationMapper()::toDomain) - .findByQuery(exactMatch(PRIMARY_SERVICE_POINT_KEY, servicePointId.toString()), LOCATIONS_LIMIT); - } - private CompletableFuture>> fetchPagedItemsForLocations( MultipleRecords multipleLocations, ItemRepository itemRepository, LocationRepository locationRepository) { @@ -150,48 +117,6 @@ private CompletableFuture>> fetchPagedItemsForLocations( locationRepository))); } - private CompletableFuture>> fetchLocationDetailsForItems( - MultipleRecords items, Collection locationsForServicePoint, - LocationRepository locationRepository) { - - log.debug("fetchLocationDetailsForItems:: parameters items: {}", - () -> multipleRecordsAsString(items)); - - Set locationIdsFromItems = items.toKeys(Item::getEffectiveLocationId); - - Set locationsForItems = locationsForServicePoint.stream() - .filter(location -> locationIdsFromItems.contains(location.getId())) - .collect(toSet()); - - if (locationsForItems.isEmpty()) { - log.info("fetchLocationDetailsForItems:: locationsForItems is empty"); - - return completedFuture(succeeded(emptyList())); - } - - return completedFuture(succeeded(locationsForItems)) - .thenComposeAsync(r -> r.after(locationRepository::fetchLibraries)) - .thenComposeAsync(r -> r.after(locationRepository::fetchInstitutions)) - .thenComposeAsync(r -> r.after(locationRepository::fetchCampuses)) - .thenApply(flatMapResult(locations -> matchLocationsToItems(items, locations))); - } - - private Result> matchLocationsToItems( - MultipleRecords items, Collection locations) { - - log.debug("matchLocationsToItems:: parameters items: {}, locations: {}", - () -> multipleRecordsAsString(items), () -> collectionAsString(locations)); - - Map locationsMap = locations.stream() - .collect(toMap(Location::getId, identity())); - - return succeeded( - items.mapRecords(item -> item.withLocation( - locationsMap.getOrDefault(item.getEffectiveLocationId(), - Location.unknown(item.getEffectiveLocationId())))) - .getRecords()); - } - private CompletableFuture>> fetchOpenPageRequestsForItems( Collection items, Clients clients) { @@ -200,7 +125,7 @@ private CompletableFuture>> fetchOpenPageRequest .filter(StringUtils::isNoneBlank) .collect(toSet()); - if(itemIds.isEmpty()) { + if (itemIds.isEmpty()) { log.info("fetchOpenPageRequestsForItems:: itemIds is empty"); return completedFuture(succeeded(MultipleRecords.empty())); @@ -214,28 +139,4 @@ private CompletableFuture>> fetchOpenPageRequest .find(byIndex(ITEM_ID_KEY, itemIds).withQuery(statusAndTypeQuery)) .thenApply(flatMapResult(requests -> matchItemsToRequests(requests, items))); } - - private Result> matchItemsToRequests( - MultipleRecords requests, Collection items) { - - Map itemMap = items.stream() - .collect(toMap(Item::getItemId, identity())); - - return succeeded( - requests.mapRecords(request -> - request.withItem(itemMap.getOrDefault(request.getItemId(), null)) - )); - } - - private Result mapResultToJson(MultipleRecords requests) { - log.debug("mapResultToJson:: parameters requests: {}", () -> multipleRecordsAsString(requests)); - List representations = requests.getRecords().stream() - .map(TemplateContextUtil::createStaffSlipContext) - .collect(Collectors.toList()); - JsonObject jsonRepresentations = new JsonObject() - .put(PICK_SLIPS_KEY, representations) - .put(TOTAL_RECORDS_KEY, representations.size()); - - return succeeded(jsonRepresentations); - } } diff --git a/src/main/java/org/folio/circulation/resources/SearchSlipsResource.java b/src/main/java/org/folio/circulation/resources/SearchSlipsResource.java new file mode 100644 index 0000000000..2a543ea942 --- /dev/null +++ b/src/main/java/org/folio/circulation/resources/SearchSlipsResource.java @@ -0,0 +1,50 @@ +package org.folio.circulation.resources; + +import static org.folio.circulation.support.results.Result.ofAsync; +import static org.folio.circulation.support.results.ResultBinding.flatMapResult; + +import java.lang.invoke.MethodHandles; +import java.util.concurrent.CompletableFuture; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.folio.circulation.domain.MultipleRecords; +import org.folio.circulation.domain.Request; +import org.folio.circulation.support.RouteRegistration; +import org.folio.circulation.support.http.server.JsonHttpResponse; +import org.folio.circulation.support.http.server.WebContext; +import org.folio.circulation.support.results.Result; + +import io.vertx.core.http.HttpClient; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; + +public class SearchSlipsResource extends SlipsResource { + private static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); + private static final String SEARCH_SLIPS_KEY = "searchSlips"; + private final String rootPath; + + public SearchSlipsResource(String rootPath, HttpClient client) { + super(client); + this.rootPath = rootPath; + } + + @Override + public void register(Router router) { + RouteRegistration routeRegistration = new RouteRegistration(rootPath, router); + routeRegistration.getMany(this::getMany); + } + + protected void getMany(RoutingContext routingContext) { + final WebContext context = new WebContext(routingContext); + + fetchHoldRequests() + .thenApply(flatMapResult(requests -> mapResultToJson(requests, SEARCH_SLIPS_KEY))) + .thenApply(r -> r.map(JsonHttpResponse::ok)) + .thenAccept(context::writeResultToHttpResponse); + } + + private CompletableFuture>> fetchHoldRequests() { + return ofAsync(MultipleRecords.empty()); + } +} diff --git a/src/main/java/org/folio/circulation/resources/SlipsResource.java b/src/main/java/org/folio/circulation/resources/SlipsResource.java new file mode 100644 index 0000000000..64826dad19 --- /dev/null +++ b/src/main/java/org/folio/circulation/resources/SlipsResource.java @@ -0,0 +1,135 @@ +package org.folio.circulation.resources; + +import static java.util.Collections.emptyList; +import static java.util.concurrent.CompletableFuture.completedFuture; +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.toMap; +import static java.util.stream.Collectors.toSet; +import static org.folio.circulation.support.fetching.RecordFetching.findWithCqlQuery; +import static org.folio.circulation.support.http.client.CqlQuery.exactMatch; +import static org.folio.circulation.support.results.Result.succeeded; +import static org.folio.circulation.support.results.ResultBinding.flatMapResult; +import static org.folio.circulation.support.utils.LogUtil.collectionAsString; +import static org.folio.circulation.support.utils.LogUtil.multipleRecordsAsString; + +import java.lang.invoke.MethodHandles; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.folio.circulation.domain.Item; +import org.folio.circulation.domain.Location; +import org.folio.circulation.domain.MultipleRecords; +import org.folio.circulation.domain.Request; +import org.folio.circulation.domain.notice.TemplateContextUtil; +import org.folio.circulation.infrastructure.storage.inventory.LocationRepository; +import org.folio.circulation.storage.mappers.LocationMapper; +import org.folio.circulation.support.Clients; +import org.folio.circulation.support.http.client.PageLimit; +import org.folio.circulation.support.results.Result; + +import io.vertx.core.http.HttpClient; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; + +public abstract class SlipsResource extends Resource { + protected static final String LOCATIONS_KEY = "locations"; + protected static final String STATUS_KEY = "status"; + protected static final String REQUESTS_KEY = "requests"; + protected static final String ITEM_ID_KEY = "itemId"; + protected static final String STATUS_NAME_KEY = "status.name"; + protected static final String REQUEST_TYPE_KEY = "requestType"; + protected static final String TOTAL_RECORDS_KEY = "totalRecords"; + protected static final String SERVICE_POINT_ID_PARAM = "servicePointId"; + protected static final String EFFECTIVE_LOCATION_ID_KEY = "effectiveLocationId"; + protected static final String PRIMARY_SERVICE_POINT_KEY = "primaryServicePoint"; + + protected static final PageLimit LOCATIONS_LIMIT = PageLimit.oneThousand(); + protected static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); + + + public SlipsResource(HttpClient client) { + super(client); + } + + protected abstract void getMany(RoutingContext routingContext); + + protected CompletableFuture>> fetchLocationsForServicePoint( + UUID servicePointId, Clients clients) { + + log.debug("fetchLocationsForServicePoint:: parameters servicePointId: {}", servicePointId); + + return findWithCqlQuery(clients.locationsStorage(), LOCATIONS_KEY, new LocationMapper()::toDomain) + .findByQuery(exactMatch(PRIMARY_SERVICE_POINT_KEY, servicePointId.toString()), LOCATIONS_LIMIT); + } + + protected CompletableFuture>> fetchLocationDetailsForItems( + MultipleRecords items, Collection locationsForServicePoint, + LocationRepository locationRepository) { + + log.debug("fetchLocationDetailsForItems:: parameters items: {}", + () -> multipleRecordsAsString(items)); + + Set locationIdsFromItems = items.toKeys(Item::getEffectiveLocationId); + Set locationsForItems = locationsForServicePoint.stream() + .filter(location -> locationIdsFromItems.contains(location.getId())) + .collect(toSet()); + + if (locationsForItems.isEmpty()) { + log.info("fetchLocationDetailsForItems:: locationsForItems is empty"); + + return completedFuture(succeeded(emptyList())); + } + + return completedFuture(succeeded(locationsForItems)) + .thenComposeAsync(r -> r.after(locationRepository::fetchLibraries)) + .thenComposeAsync(r -> r.after(locationRepository::fetchInstitutions)) + .thenComposeAsync(r -> r.after(locationRepository::fetchCampuses)) + .thenApply(flatMapResult(locations -> matchLocationsToItems(items, locations))); + } + + protected Result> matchLocationsToItems( + MultipleRecords items, Collection locations) { + + log.debug("matchLocationsToItems:: parameters items: {}, locations: {}", + () -> multipleRecordsAsString(items), () -> collectionAsString(locations)); + + Map locationsMap = locations.stream() + .collect(toMap(Location::getId, identity())); + + return succeeded(items.mapRecords(item -> item.withLocation( + locationsMap.getOrDefault(item.getEffectiveLocationId(), + Location.unknown(item.getEffectiveLocationId())))) + .getRecords()); + } + + protected Result> matchItemsToRequests( + MultipleRecords requests, Collection items) { + + Map itemMap = items.stream() + .collect(toMap(Item::getItemId, identity())); + + return succeeded(requests.mapRecords(request -> request.withItem( + itemMap.getOrDefault(request.getItemId(), null)))); + } + + protected Result mapResultToJson(MultipleRecords requests, + String slipsKey) { + + log.debug("mapResultToJson:: parameters requests: {}", () -> multipleRecordsAsString(requests)); + List representations = requests.getRecords().stream() + .map(TemplateContextUtil::createStaffSlipContext) + .collect(Collectors.toList()); + JsonObject jsonRepresentations = new JsonObject() + .put(slipsKey, representations) + .put(TOTAL_RECORDS_KEY, representations.size()); + + return succeeded(jsonRepresentations); + } +} diff --git a/src/test/java/api/requests/SearchSlipsTests.java b/src/test/java/api/requests/SearchSlipsTests.java new file mode 100644 index 0000000000..ed357bcca2 --- /dev/null +++ b/src/test/java/api/requests/SearchSlipsTests.java @@ -0,0 +1,32 @@ +package api.requests; + +import static java.net.HttpURLConnection.HTTP_OK; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; + +import java.util.UUID; + +import org.folio.circulation.support.http.client.Response; +import org.junit.jupiter.api.Test; + +import api.support.APITests; +import api.support.http.ResourceClient; +import io.vertx.core.json.JsonObject; + +class SearchSlipsTests extends APITests { + private static final String TOTAL_RECORDS = "totalRecords"; + private static final String SEARCH_SLIPS_KEY = "searchSlips"; + + @Test + void responseShouldHaveEmptyListOfSearchSlipsRecords() { + Response response = ResourceClient.forSearchSlips().getById(UUID.randomUUID()); + assertThat(response.getStatusCode(), is(HTTP_OK)); + assertResponseHasItems(response, 0); + } + + private void assertResponseHasItems(Response response, int itemsCount) { + JsonObject responseJson = response.getJson(); + assertThat(responseJson.getJsonArray(SEARCH_SLIPS_KEY).size(), is(itemsCount)); + assertThat(responseJson.getInteger(TOTAL_RECORDS), is(itemsCount)); + } +} diff --git a/src/test/java/api/support/http/InterfaceUrls.java b/src/test/java/api/support/http/InterfaceUrls.java index 01bfe547fd..b4b5c0ec78 100644 --- a/src/test/java/api/support/http/InterfaceUrls.java +++ b/src/test/java/api/support/http/InterfaceUrls.java @@ -129,6 +129,10 @@ static URL pickSlipsUrl(String servicePointId) { return circulationModuleUrl("/circulation/pick-slips" + servicePointId); } + static URL searchSlipsUrl(String servicePointId) { + return circulationModuleUrl("/circulation/search-slips" + servicePointId); + } + public static URL requestQueueUrl(UUID itemId) { return requestsUrl(String.format("/queue/item/%s", itemId)); } diff --git a/src/test/java/api/support/http/ResourceClient.java b/src/test/java/api/support/http/ResourceClient.java index 02f4f3740a..fc947fe16d 100644 --- a/src/test/java/api/support/http/ResourceClient.java +++ b/src/test/java/api/support/http/ResourceClient.java @@ -53,6 +53,10 @@ public static ResourceClient forPickSlips() { return new ResourceClient(InterfaceUrls::pickSlipsUrl, "pickSlips"); } + public static ResourceClient forSearchSlips() { + return new ResourceClient(InterfaceUrls::searchSlipsUrl, "searchSlips"); + } + public static ResourceClient forLoans() { return new ResourceClient(InterfaceUrls::loansUrl, "loans"); }