Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CIRC-1932] Implement search-slips endpoint skeleton #1358

Merged
merged 7 commits into from
Oct 31, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions descriptors/ModuleDescriptor-template.json
Original file line number Diff line number Diff line change
@@ -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",
File renamed without changes.
26 changes: 0 additions & 26 deletions ramls/pick-slips.raml

This file was deleted.

File renamed without changes.
32 changes: 32 additions & 0 deletions ramls/staff-slips.raml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#%RAML 1.0
title: Stuff Slips
version: v0.3
protocols: [ HTTP, HTTPS ]
baseUri: http://localhost:9130

documentation:
- title: API for fetching current staff slips
content: <b>API for staff slips generation</b>

types:
stuff-slips: !include staff-slips-response.json

traits:
language: !include raml-util/traits/language.raml

resourceTypes:
collection-get: !include raml-util/rtypes/collection-get.raml

/circulation:
/pick-slips:
/{servicePointId}:
type:
collection-get:
exampleCollection: !include examples/staff-slips-response.json
schemaCollection: stuff-slips
/search-slips:
/{servicePointId}:
type:
collection-get:
exampleCollection: !include examples/staff-slips-response.json
schemaCollection: stuff-slips
3 changes: 3 additions & 0 deletions src/main/java/org/folio/circulation/CirculationVerticle.java
Original file line number Diff line number Diff line change
@@ -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<Void> 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);
109 changes: 5 additions & 104 deletions src/main/java/org/folio/circulation/resources/PickSlipsResource.java
Original file line number Diff line number Diff line change
@@ -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<Result<MultipleRecords<Location>>> 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<Result<Collection<Item>>> fetchPagedItemsForLocations(
MultipleRecords<Location> multipleLocations,
ItemRepository itemRepository, LocationRepository locationRepository) {
@@ -150,48 +117,6 @@ private CompletableFuture<Result<Collection<Item>>> fetchPagedItemsForLocations(
locationRepository)));
}

private CompletableFuture<Result<Collection<Item>>> fetchLocationDetailsForItems(
MultipleRecords<Item> items, Collection<Location> locationsForServicePoint,
LocationRepository locationRepository) {

log.debug("fetchLocationDetailsForItems:: parameters items: {}",
() -> multipleRecordsAsString(items));

Set<String> locationIdsFromItems = items.toKeys(Item::getEffectiveLocationId);

Set<Location> 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<Collection<Item>> matchLocationsToItems(
MultipleRecords<Item> items, Collection<Location> locations) {

log.debug("matchLocationsToItems:: parameters items: {}, locations: {}",
() -> multipleRecordsAsString(items), () -> collectionAsString(locations));

Map<String, Location> 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<Result<MultipleRecords<Request>>> fetchOpenPageRequestsForItems(
Collection<Item> items, Clients clients) {

@@ -200,7 +125,7 @@ private CompletableFuture<Result<MultipleRecords<Request>>> 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<Result<MultipleRecords<Request>>> fetchOpenPageRequest
.find(byIndex(ITEM_ID_KEY, itemIds).withQuery(statusAndTypeQuery))
.thenApply(flatMapResult(requests -> matchItemsToRequests(requests, items)));
}

private Result<MultipleRecords<Request>> matchItemsToRequests(
MultipleRecords<Request> requests, Collection<Item> items) {

Map<String, Item> itemMap = items.stream()
.collect(toMap(Item::getItemId, identity()));

return succeeded(
requests.mapRecords(request ->
request.withItem(itemMap.getOrDefault(request.getItemId(), null))
));
}

private Result<JsonObject> mapResultToJson(MultipleRecords<Request> requests) {
log.debug("mapResultToJson:: parameters requests: {}", () -> multipleRecordsAsString(requests));
List<JsonObject> 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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
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 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";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This collection name can be an instance variable injected via constructor

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<Result<MultipleRecords<Request>>> fetchHoldRequests() {
return ofAsync(MultipleRecords.empty());
}
}
134 changes: 134 additions & 0 deletions src/main/java/org/folio/circulation/resources/SlipsResource.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
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 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());


protected SlipsResource(HttpClient client) {
super(client);
}

protected abstract void getMany(RoutingContext routingContext);

protected CompletableFuture<Result<MultipleRecords<Location>>> 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<Result<Collection<Item>>> fetchLocationDetailsForItems(
MultipleRecords<Item> items, Collection<Location> locationsForServicePoint,
LocationRepository locationRepository) {

log.debug("fetchLocationDetailsForItems:: parameters items: {}",
() -> multipleRecordsAsString(items));

Set<String> locationIdsFromItems = items.toKeys(Item::getEffectiveLocationId);
Set<Location> 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<Collection<Item>> matchLocationsToItems(
MultipleRecords<Item> items, Collection<Location> locations) {

log.debug("matchLocationsToItems:: parameters items: {}, locations: {}",
() -> multipleRecordsAsString(items), () -> collectionAsString(locations));

Map<String, Location> 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<MultipleRecords<Request>> matchItemsToRequests(
MultipleRecords<Request> requests, Collection<Item> items) {

Map<String, Item> itemMap = items.stream()
.collect(toMap(Item::getItemId, identity()));

return succeeded(requests.mapRecords(request -> request.withItem(
itemMap.getOrDefault(request.getItemId(), null))));
}

protected Result<JsonObject> mapResultToJson(MultipleRecords<Request> requests,
String slipsKey) {

log.debug("mapResultToJson:: parameters requests: {}", () -> multipleRecordsAsString(requests));
List<JsonObject> representations = requests.getRecords().stream()
.map(TemplateContextUtil::createStaffSlipContext)
.toList();
JsonObject jsonRepresentations = new JsonObject()
.put(slipsKey, representations)
.put(TOTAL_RECORDS_KEY, representations.size());

return succeeded(jsonRepresentations);
}
}
32 changes: 32 additions & 0 deletions src/test/java/api/requests/SearchSlipsTests.java
Original file line number Diff line number Diff line change
@@ -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_KEY = "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_KEY), is(itemsCount));
}
}
4 changes: 4 additions & 0 deletions src/test/java/api/support/http/InterfaceUrls.java
Original file line number Diff line number Diff line change
@@ -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));
}
4 changes: 4 additions & 0 deletions src/test/java/api/support/http/ResourceClient.java
Original file line number Diff line number Diff line change
@@ -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");
}