diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 6b6378e50a..200c6a0750 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -69,6 +69,7 @@ "circulation.search-slips.get" ], "modulePermissions": [ + "modperms.circulation.search-slips.get" ] } ] @@ -1556,7 +1557,8 @@ "circulation.requests.hold-shelf-clearance-report.get", "circulation.requests.allowed-service-points.get", "circulation.inventory.items-in-transit-report.get", - "circulation.pick-slips.get" + "circulation.pick-slips.get", + "circulation.search-slips.get" ] }, { @@ -2320,6 +2322,21 @@ ], "visible": false }, + { + "permissionName": "modperms.circulation.search-slips.get", + "displayName": "module permissions for one op", + "description": "to reduce X-Okapi-Token size", + "subPermissions": [ + "circulation.internal.fetch-items", + "circulation-storage.requests.item.get", + "circulation-storage.requests.collection.get", + "users.item.get", + "users.collection.get", + "addresstypes.item.get", + "addresstypes.collection.get" + ], + "visible": false + }, { "permissionName": "circulation.internal.fetch-items", "displayName" : "Fetch item(s)", diff --git a/ramls/staff-slips.raml b/ramls/staff-slips.raml index 31b5c699f3..45c1a7a023 100644 --- a/ramls/staff-slips.raml +++ b/ramls/staff-slips.raml @@ -1,5 +1,5 @@ #%RAML 1.0 -title: Stuff Slips +title: Staff Slips version: v0.3 protocols: [ HTTP, HTTPS ] baseUri: http://localhost:9130 @@ -9,7 +9,7 @@ documentation: content: API for staff slips generation types: - stuff-slips: !include staff-slips-response.json + staff-slips: !include staff-slips-response.json traits: language: !include raml-util/traits/language.raml @@ -23,10 +23,10 @@ resourceTypes: type: collection-get: exampleCollection: !include examples/staff-slips-response.json - schemaCollection: stuff-slips + schemaCollection: staff-slips /search-slips: /{servicePointId}: type: collection-get: exampleCollection: !include examples/staff-slips-response.json - schemaCollection: stuff-slips + schemaCollection: staff-slips diff --git a/src/main/java/org/folio/circulation/domain/RequestTypeItemStatusWhiteList.java b/src/main/java/org/folio/circulation/domain/RequestTypeItemStatusWhiteList.java index 50fa6ff5d5..59b30f0aa1 100644 --- a/src/main/java/org/folio/circulation/domain/RequestTypeItemStatusWhiteList.java +++ b/src/main/java/org/folio/circulation/domain/RequestTypeItemStatusWhiteList.java @@ -25,6 +25,7 @@ import java.util.Arrays; import java.util.EnumMap; import java.util.List; +import java.util.Map; public class RequestTypeItemStatusWhiteList { private static EnumMap recallRules; @@ -161,4 +162,13 @@ public static List getRequestTypesAllowedForItemStatus(ItemStatus i .filter(requestType -> requestsRulesMap.get(requestType).get(itemStatus)) .toList(); } + + public static List getItemStatusesAllowedForRequestType(RequestType requestType) { + return requestsRulesMap.get(requestType) + .entrySet() + .stream() + .filter(entry -> Boolean.TRUE.equals(entry.getValue())) + .map(Map.Entry::getKey) + .toList(); + } } diff --git a/src/main/java/org/folio/circulation/resources/PickSlipsResource.java b/src/main/java/org/folio/circulation/resources/PickSlipsResource.java index 910cccd773..94fa718aae 100644 --- a/src/main/java/org/folio/circulation/resources/PickSlipsResource.java +++ b/src/main/java/org/folio/circulation/resources/PickSlipsResource.java @@ -1,142 +1,13 @@ package org.folio.circulation.resources; -import static java.util.Collections.emptyList; -import static java.util.concurrent.CompletableFuture.completedFuture; -import static java.util.stream.Collectors.toSet; -import static org.folio.circulation.support.fetching.MultipleCqlIndexValuesCriteria.byIndex; -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.multipleRecordsAsString; - -import java.lang.invoke.MethodHandles; -import java.util.Collection; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; - -import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.folio.circulation.domain.Item; -import org.folio.circulation.domain.ItemStatus; -import org.folio.circulation.domain.Location; -import org.folio.circulation.domain.MultipleRecords; -import org.folio.circulation.domain.Request; -import org.folio.circulation.domain.RequestStatus; -import org.folio.circulation.domain.RequestType; -import org.folio.circulation.domain.notice.TemplateContextUtil; -import org.folio.circulation.infrastructure.storage.ServicePointRepository; -import org.folio.circulation.infrastructure.storage.inventory.ItemRepository; -import org.folio.circulation.infrastructure.storage.inventory.LocationRepository; -import org.folio.circulation.infrastructure.storage.users.AddressTypeRepository; -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.support.Clients; -import org.folio.circulation.support.RouteRegistration; -import org.folio.circulation.support.http.client.CqlQuery; -import org.folio.circulation.support.http.server.JsonHttpResponse; -import org.folio.circulation.support.http.server.WebContext; -import org.folio.circulation.support.results.Result; +import static org.folio.circulation.domain.ItemStatus.PAGED; +import static org.folio.circulation.domain.RequestType.PAGE; import io.vertx.core.http.HttpClient; -import io.vertx.ext.web.Router; -import io.vertx.ext.web.RoutingContext; 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; - public PickSlipsResource(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); - final Clients clients = Clients.create(context, client); - - final var userRepository = new UserRepository(clients); - final var itemRepository = new ItemRepository(clients); - final AddressTypeRepository addressTypeRepository = new AddressTypeRepository(clients); - final ServicePointRepository servicePointRepository = new ServicePointRepository(clients); - final PatronGroupRepository patronGroupRepository = new PatronGroupRepository(clients); - final DepartmentRepository departmentRepository = new DepartmentRepository(clients); - final UUID servicePointId = UUID.fromString( - routingContext.request().getParam(SERVICE_POINT_ID_PARAM)); - - fetchLocationsForServicePoint(servicePointId, clients) - .thenComposeAsync(r -> r.after(locations -> fetchPagedItemsForLocations(locations, - itemRepository, LocationRepository.using(clients, servicePointRepository)))) - .thenComposeAsync(r -> r.after(items -> fetchOpenPageRequestsForItems(items, clients))) - .thenComposeAsync(r -> r.after(userRepository::findUsersForRequests)) - .thenComposeAsync(result -> result.after(patronGroupRepository::findPatronGroupsForRequestsUsers)) - .thenComposeAsync(r -> r.after(departmentRepository::findDepartmentsForRequestUsers)) - .thenComposeAsync(r -> r.after(addressTypeRepository::findAddressTypesForRequests)) - .thenComposeAsync(r -> r.after(servicePointRepository::findServicePointsForRequests)) - .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>> fetchPagedItemsForLocations( - MultipleRecords multipleLocations, - ItemRepository itemRepository, LocationRepository locationRepository) { - - log.debug("fetchPagedItemsForLocations:: parameters multipleLocations: {}", - () -> multipleRecordsAsString(multipleLocations)); - Collection locations = multipleLocations.getRecords(); - - Set locationIds = locations.stream() - .map(Location::getId) - .filter(StringUtils::isNoneBlank) - .collect(toSet()); - - if (locationIds.isEmpty()) { - log.info("fetchPagedItemsForLocations:: locationIds is empty"); - - return completedFuture(succeeded(emptyList())); - } - - Result statusQuery = exactMatch(STATUS_NAME_KEY, ItemStatus.PAGED.getValue()); - - return itemRepository.findByIndexNameAndQuery(locationIds, EFFECTIVE_LOCATION_ID_KEY, statusQuery) - .thenComposeAsync(r -> r.after(items -> fetchLocationDetailsForItems(items, locations, - locationRepository))); - } - - private CompletableFuture>> fetchOpenPageRequestsForItems( - Collection items, Clients clients) { - - Set itemIds = items.stream() - .map(Item::getItemId) - .filter(StringUtils::isNoneBlank) - .collect(toSet()); - - if (itemIds.isEmpty()) { - log.info("fetchOpenPageRequestsForItems:: itemIds is empty"); - - return completedFuture(succeeded(MultipleRecords.empty())); - } - - final Result typeQuery = exactMatch(REQUEST_TYPE_KEY, RequestType.PAGE.getValue()); - final Result statusQuery = exactMatch(STATUS_KEY, RequestStatus.OPEN_NOT_YET_FILLED.getValue()); - final Result statusAndTypeQuery = typeQuery.combine(statusQuery, CqlQuery::and); - - return findWithMultipleCqlIndexValues(clients.requestsStorage(), REQUESTS_KEY, Request::from) - .find(byIndex(ITEM_ID_KEY, itemIds).withQuery(statusAndTypeQuery)) - .thenApply(flatMapResult(requests -> matchItemsToRequests(requests, items))); + super(rootPath, client, PAGE, PAGED, "pickSlips"); } } diff --git a/src/main/java/org/folio/circulation/resources/SearchSlipsResource.java b/src/main/java/org/folio/circulation/resources/SearchSlipsResource.java index 8fe58e5512..1b0e7362e8 100644 --- a/src/main/java/org/folio/circulation/resources/SearchSlipsResource.java +++ b/src/main/java/org/folio/circulation/resources/SearchSlipsResource.java @@ -1,46 +1,13 @@ 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.util.concurrent.CompletableFuture; - -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 static org.folio.circulation.domain.RequestType.HOLD; +import static org.folio.circulation.domain.RequestTypeItemStatusWhiteList.getItemStatusesAllowedForRequestType; 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 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()); + super(rootPath, client, HOLD, getItemStatusesAllowedForRequestType(HOLD), "searchSlips"); } } diff --git a/src/main/java/org/folio/circulation/resources/SlipsResource.java b/src/main/java/org/folio/circulation/resources/SlipsResource.java index c8d8faead2..e29aa892f6 100644 --- a/src/main/java/org/folio/circulation/resources/SlipsResource.java +++ b/src/main/java/org/folio/circulation/resources/SlipsResource.java @@ -5,8 +5,11 @@ 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.http.client.CqlQuery.exactMatchAny; 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; @@ -20,46 +23,109 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.folio.circulation.domain.Item; +import org.folio.circulation.domain.ItemStatus; import org.folio.circulation.domain.Location; import org.folio.circulation.domain.MultipleRecords; import org.folio.circulation.domain.Request; +import org.folio.circulation.domain.RequestStatus; +import org.folio.circulation.domain.RequestType; import org.folio.circulation.domain.notice.TemplateContextUtil; +import org.folio.circulation.infrastructure.storage.ServicePointRepository; +import org.folio.circulation.infrastructure.storage.inventory.ItemRepository; import org.folio.circulation.infrastructure.storage.inventory.LocationRepository; +import org.folio.circulation.infrastructure.storage.users.AddressTypeRepository; +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 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()); - - - protected SlipsResource(HttpClient client) { + private static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); + private static final PageLimit LOCATIONS_LIMIT = PageLimit.oneThousand(); + private static final String LOCATIONS_KEY = "locations"; + private static final String STATUS_KEY = "status"; + private static final String REQUESTS_KEY = "requests"; + private static final String ITEM_ID_KEY = "itemId"; + 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 final String rootPath; + private final RequestType requestType; + private final Collection itemStatuses; + private final String jsonCollectionName; + + public SlipsResource(String rootPath, HttpClient client, RequestType requestType, + ItemStatus itemStatus, String jsonCollectionName) { + + this(rootPath, client, requestType, List.of(itemStatus), jsonCollectionName); + } + + public SlipsResource(String rootPath, HttpClient client, RequestType requestType, + Collection itemStatuses, String jsonCollectionName) { + super(client); + this.rootPath = rootPath; + this.requestType = requestType; + this.itemStatuses = itemStatuses; + this.jsonCollectionName = jsonCollectionName; + } + + @Override + public void register(Router router) { + RouteRegistration routeRegistration = new RouteRegistration(rootPath, router); + routeRegistration.getMany(this::getMany); } - protected abstract void getMany(RoutingContext routingContext); + private void getMany(RoutingContext routingContext) { + final WebContext context = new WebContext(routingContext); + final Clients clients = Clients.create(context, client); + + final var userRepository = new UserRepository(clients); + final var itemRepository = new ItemRepository(clients); + final AddressTypeRepository addressTypeRepository = new AddressTypeRepository(clients); + final ServicePointRepository servicePointRepository = new ServicePointRepository(clients); + final PatronGroupRepository patronGroupRepository = new PatronGroupRepository(clients); + final DepartmentRepository departmentRepository = new DepartmentRepository(clients); + final UUID servicePointId = UUID.fromString( + routingContext.request().getParam(SERVICE_POINT_ID_PARAM)); + + fetchLocationsForServicePoint(servicePointId, clients) + .thenComposeAsync(r -> r.after(locations -> fetchItemsForLocations(locations, + itemRepository, LocationRepository.using(clients, servicePointRepository)))) + .thenComposeAsync(r -> r.after(items -> fetchRequests(items, clients))) + .thenComposeAsync(r -> r.after(userRepository::findUsersForRequests)) + .thenComposeAsync(result -> result.after(patronGroupRepository::findPatronGroupsForRequestsUsers)) + .thenComposeAsync(r -> r.after(departmentRepository::findDepartmentsForRequestUsers)) + .thenComposeAsync(r -> r.after(addressTypeRepository::findAddressTypesForRequests)) + .thenComposeAsync(r -> r.after(servicePointRepository::findServicePointsForRequests)) + .thenApply(flatMapResult(this::mapResultToJson)) + .thenComposeAsync(r -> r.combineAfter(() -> servicePointRepository.getServicePointById(servicePointId), + TemplateContextUtil::addPrimaryServicePointNameToStaffSlipContext)) + .thenApply(r -> r.map(JsonHttpResponse::ok)) + .thenAccept(context::writeResultToHttpResponse); + } - protected CompletableFuture>> fetchLocationsForServicePoint( + private CompletableFuture>> fetchLocationsForServicePoint( UUID servicePointId, Clients clients) { log.debug("fetchLocationsForServicePoint:: parameters servicePointId: {}", servicePointId); @@ -68,7 +134,60 @@ protected CompletableFuture>> fetchLocationsFor .findByQuery(exactMatch(PRIMARY_SERVICE_POINT_KEY, servicePointId.toString()), LOCATIONS_LIMIT); } - protected CompletableFuture>> fetchLocationDetailsForItems( + private CompletableFuture>> fetchItemsForLocations( + MultipleRecords multipleLocations, + ItemRepository itemRepository, LocationRepository locationRepository) { + + log.debug("fetchPagedItemsForLocations:: parameters multipleLocations: {}", + () -> multipleRecordsAsString(multipleLocations)); + Collection locations = multipleLocations.getRecords(); + + Set locationIds = locations.stream() + .map(Location::getId) + .filter(StringUtils::isNoneBlank) + .collect(toSet()); + + if (locationIds.isEmpty()) { + log.info("fetchPagedItemsForLocations:: locationIds is empty"); + + return completedFuture(succeeded(emptyList())); + } + + List itemStatusValues = itemStatuses.stream() + .map(ItemStatus::getValue) + .toList(); + + Result statusQuery = exactMatchAny(STATUS_NAME_KEY, itemStatusValues); + + return itemRepository.findByIndexNameAndQuery(locationIds, EFFECTIVE_LOCATION_ID_KEY, statusQuery) + .thenComposeAsync(r -> r.after(items -> fetchLocationDetailsForItems(items, locations, + locationRepository))); + } + + private CompletableFuture>> fetchRequests( + Collection items, Clients clients) { + + Set itemIds = items.stream() + .map(Item::getItemId) + .filter(StringUtils::isNoneBlank) + .collect(toSet()); + + if (itemIds.isEmpty()) { + log.info("fetchOpenPageRequestsForItems:: itemIds is empty"); + + return completedFuture(succeeded(MultipleRecords.empty())); + } + + final Result typeQuery = exactMatch(REQUEST_TYPE_KEY, requestType.getValue()); + final Result statusQuery = exactMatch(STATUS_KEY, RequestStatus.OPEN_NOT_YET_FILLED.getValue()); + final Result statusAndTypeQuery = typeQuery.combine(statusQuery, CqlQuery::and); + + return findWithMultipleCqlIndexValues(clients.requestsStorage(), REQUESTS_KEY, Request::from) + .find(byIndex(ITEM_ID_KEY, itemIds).withQuery(statusAndTypeQuery)) + .thenApply(flatMapResult(requests -> matchItemsToRequests(requests, items))); + } + + private CompletableFuture>> fetchLocationDetailsForItems( MultipleRecords items, Collection locationsForServicePoint, LocationRepository locationRepository) { @@ -93,7 +212,7 @@ protected CompletableFuture>> fetchLocationDetailsForIte .thenApply(flatMapResult(locations -> matchLocationsToItems(items, locations))); } - protected Result> matchLocationsToItems( + private Result> matchLocationsToItems( MultipleRecords items, Collection locations) { log.debug("matchLocationsToItems:: parameters items: {}, locations: {}", @@ -108,7 +227,7 @@ protected Result> matchLocationsToItems( .getRecords()); } - protected Result> matchItemsToRequests( + private Result> matchItemsToRequests( MultipleRecords requests, Collection items) { Map itemMap = items.stream() @@ -118,15 +237,13 @@ protected Result> matchItemsToRequests( itemMap.getOrDefault(request.getItemId(), null)))); } - protected Result mapResultToJson(MultipleRecords requests, - String slipsKey) { - + private Result mapResultToJson(MultipleRecords requests) { log.debug("mapResultToJson:: parameters requests: {}", () -> multipleRecordsAsString(requests)); List representations = requests.getRecords().stream() .map(TemplateContextUtil::createStaffSlipContext) .toList(); JsonObject jsonRepresentations = new JsonObject() - .put(slipsKey, representations) + .put(jsonCollectionName, representations) .put(TOTAL_RECORDS_KEY, representations.size()); return succeeded(jsonRepresentations);