diff --git a/src/main/java/org/folio/circulation/domain/RequestStatus.java b/src/main/java/org/folio/circulation/domain/RequestStatus.java index a1c6721dcc..1d747a7eeb 100644 --- a/src/main/java/org/folio/circulation/domain/RequestStatus.java +++ b/src/main/java/org/folio/circulation/domain/RequestStatus.java @@ -30,6 +30,9 @@ public enum RequestStatus { private static final EnumSet OPEN_STATUSES = EnumSet.of( OPEN_NOT_YET_FILLED, OPEN_AWAITING_PICKUP, OPEN_IN_TRANSIT, OPEN_AWAITING_DELIVERY); + private static final EnumSet CLOSED_STATUSES = EnumSet.of( + CLOSED_FILLED, CLOSED_CANCELLED, CLOSED_UNFILLED, CLOSED_PICKUP_EXPIRED); + private final String value; public static String invalidStatusErrorMessage() { @@ -60,6 +63,11 @@ public static List openStates() { .collect(Collectors.toList()); } + public static List closedStates() { + return CLOSED_STATUSES.stream().map(RequestStatus::getValue) + .toList(); + } + public boolean isValid() { return this != NONE; } diff --git a/src/main/java/org/folio/circulation/infrastructure/storage/requests/AnonymizeStorageRequestsRepository.java b/src/main/java/org/folio/circulation/infrastructure/storage/requests/AnonymizeStorageRequestsRepository.java new file mode 100644 index 0000000000..4e85411b35 --- /dev/null +++ b/src/main/java/org/folio/circulation/infrastructure/storage/requests/AnonymizeStorageRequestsRepository.java @@ -0,0 +1,53 @@ +package org.folio.circulation.infrastructure.storage.requests; + +import static java.util.concurrent.CompletableFuture.completedFuture; +import static org.folio.circulation.support.http.ResponseMapping.forwardOnFailure; +import static org.folio.circulation.support.http.ResponseMapping.mapUsingJson; +import static org.folio.circulation.support.json.JsonStringArrayPropertyFetcher.toStream; +import static org.folio.circulation.support.results.Result.succeeded; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +import org.folio.circulation.domain.anonymization.RequestAnonymizationRecords; +import org.folio.circulation.support.Clients; +import org.folio.circulation.support.CollectionResourceClient; +import org.folio.circulation.support.http.client.Response; +import org.folio.circulation.support.http.client.ResponseInterpreter; +import org.folio.circulation.support.results.Result; +import org.folio.circulation.storage.RequestBatch; + +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; + +public class AnonymizeStorageRequestsRepository { + private final CollectionResourceClient requestStorageClient; + + public AnonymizeStorageRequestsRepository(Clients clients) { + requestStorageClient = clients.requestsBatchStorage(); + } + + private static ResponseInterpreter + createStorageRequestResponseInterpreter(RequestAnonymizationRecords records) { + + Function> mapper = mapUsingJson( + response -> records.withAnonymizedRequests( + toStream(response, "anonymizedRequests").toList())); + + return new ResponseInterpreter() + .on(201, Result.of(() -> records)) + .otherwise(forwardOnFailure()); + } + + public CompletableFuture> + postAnonymizeStorageRequests(RequestAnonymizationRecords records) { + + if (records.getAnonymizedRequestIds().isEmpty()) { + return completedFuture(succeeded(records)); + } + RequestBatch requestBatch = new RequestBatch(records.getAnonymizedRequests()); + + return requestStorageClient.post(requestBatch.toJson()) + .thenApply(createStorageRequestResponseInterpreter(records)::flatMap); + } +} diff --git a/src/main/java/org/folio/circulation/infrastructure/storage/requests/RequestRepository.java b/src/main/java/org/folio/circulation/infrastructure/storage/requests/RequestRepository.java index 566bb5059b..2e73330228 100644 --- a/src/main/java/org/folio/circulation/infrastructure/storage/requests/RequestRepository.java +++ b/src/main/java/org/folio/circulation/infrastructure/storage/requests/RequestRepository.java @@ -3,11 +3,11 @@ import static java.util.Objects.isNull; import static java.util.function.Function.identity; import static org.folio.circulation.domain.RequestStatus.openStates; +import static org.folio.circulation.domain.RequestStatus.closedStates; import static org.folio.circulation.support.CqlSortBy.ascending; import static org.folio.circulation.support.fetching.RecordFetching.findWithMultipleCqlIndexValues; import static org.folio.circulation.support.http.ResponseMapping.forwardOnFailure; import static org.folio.circulation.support.http.ResponseMapping.mapUsingJson; -import static org.folio.circulation.support.http.client.CqlQuery.exactMatchAny; import static org.folio.circulation.support.results.Result.failed; import static org.folio.circulation.support.results.Result.of; import static org.folio.circulation.support.results.Result.ofAsync; @@ -17,6 +17,8 @@ import static org.folio.circulation.support.utils.LogUtil.collectionAsString; import static org.folio.circulation.support.utils.LogUtil.multipleRecordsAsString; import static org.folio.circulation.support.utils.LogUtil.resultAsString; +import static org.folio.circulation.support.http.client.CqlQuery.exactMatch; +import static org.folio.circulation.support.http.client.CqlQuery.exactMatchAny; import java.lang.invoke.MethodHandles; import java.util.Collection; @@ -317,6 +319,36 @@ public CompletableFuture>> fetchRequests(Collection>> findRequestsToAnonymize( + PageLimit pageLimit) { + + log.debug("findRequestsToAnonymize:: parameters pageLimit: {}", pageLimit); + + Result cqlQuery = exactMatchAny("status", closedStates()) + .combine(CqlQuery.hasValue("requesterId"), CqlQuery::and); + + return queryRequestStorage(cqlQuery, pageLimit); + } + + public CompletableFuture>> findClosedRequests( + String userId, PageLimit pageLimit) { + + log.debug("findClosedRequests:: parameters userId: {}, pageLimit: {}", userId, pageLimit); + + Result userQuery = exactMatch("requesterId", userId); + Result statusQuery = exactMatchAny("status", closedStates()); + + return queryRequestStorage(statusQuery.combine(userQuery, CqlQuery::and), pageLimit); + } + + private CompletableFuture>> queryRequestStorage( + Result cqlQuery, PageLimit pageLimit) { + + return cqlQuery + .after(q -> requestsStorageClient.getMany(q, pageLimit)) + .thenApply(result -> result.next(this::mapResponseToRequests)); + } + private CompletableFuture> fetchRequester(Result result) { log.debug("fetchRequester:: parameters result: {}", ()-> resultAsString(result)); return result.combineAfter(request -> diff --git a/src/test/java/org/folio/circulation/domain/RequestStatusTest.java b/src/test/java/org/folio/circulation/domain/RequestStatusTest.java new file mode 100644 index 0000000000..433912a1cf --- /dev/null +++ b/src/test/java/org/folio/circulation/domain/RequestStatusTest.java @@ -0,0 +1,32 @@ +package org.folio.circulation.domain; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +class RequestStatusTest { + + @Test + void closedStatesReturnsAllClosedStatuses() { + List closedStates = RequestStatus.closedStates(); + + assertEquals(4, closedStates.size()); + assertTrue(closedStates.contains("Closed - Filled")); + assertTrue(closedStates.contains("Closed - Cancelled")); + assertTrue(closedStates.contains("Closed - Unfilled")); + assertTrue(closedStates.contains("Closed - Pickup expired")); + } + + @Test + void closedStatesDoesNotContainOpenStatuses() { + List closedStates = RequestStatus.closedStates(); + + assertTrue(!closedStates.contains("Open - Not yet filled")); + assertTrue(!closedStates.contains("Open - Awaiting pickup")); + assertTrue(!closedStates.contains("Open - In transit")); + assertTrue(!closedStates.contains("Open - Awaiting delivery")); + } +} diff --git a/src/test/java/org/folio/circulation/infrastructure/storage/requests/AnonymizeStorageRequestsRepositoryTest.java b/src/test/java/org/folio/circulation/infrastructure/storage/requests/AnonymizeStorageRequestsRepositoryTest.java new file mode 100644 index 0000000000..859bc98d50 --- /dev/null +++ b/src/test/java/org/folio/circulation/infrastructure/storage/requests/AnonymizeStorageRequestsRepositoryTest.java @@ -0,0 +1,104 @@ +package org.folio.circulation.infrastructure.storage.requests; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.Collections; +import java.util.concurrent.CompletableFuture; + +import org.folio.circulation.domain.anonymization.RequestAnonymizationRecords; +import org.folio.circulation.support.Clients; +import org.folio.circulation.support.CollectionResourceClient; +import org.folio.circulation.support.http.client.Response; +import org.folio.circulation.support.results.Result; +import org.junit.jupiter.api.Test; + +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; + +class AnonymizeStorageRequestsRepositoryTest { + + @Test + void shouldReturnSuccessWhenRequestIdsAreEmpty() { + Clients clients = mock(Clients.class); + CollectionResourceClient client = mock(CollectionResourceClient.class); + + when(clients.requestsBatchStorage()).thenReturn(client); + + AnonymizeStorageRequestsRepository repository = + new AnonymizeStorageRequestsRepository(clients); + + RequestAnonymizationRecords emptyRecords = new RequestAnonymizationRecords(); + + Result result = + repository.postAnonymizeStorageRequests(emptyRecords).join(); + + assertTrue(result.succeeded()); + assertEquals(0, result.value().getAnonymizedRequestIds().size()); + verify(client, times(0)).post(any(JsonObject.class)); + } + + @Test + void shouldPostToStorageWhenRequestIdsExist() { + Clients clients = mock(Clients.class); + CollectionResourceClient client = mock(CollectionResourceClient.class); + + when(clients.requestsBatchStorage()).thenReturn(client); + + Response response = mock(Response.class); + when(response.getStatusCode()).thenReturn(201); + + JsonObject responseBody = new JsonObject(); + + when(response.getJson()).thenReturn(responseBody); + when(client.post(any(JsonObject.class))) + .thenReturn(CompletableFuture.completedFuture(Result.succeeded(response))); + + AnonymizeStorageRequestsRepository repository = + new AnonymizeStorageRequestsRepository(clients); + + RequestAnonymizationRecords records = new RequestAnonymizationRecords() + .withAnonymizedRequests(Arrays.asList("request-id-1", "request-id-2")); + + Result result = + repository.postAnonymizeStorageRequests(records).join(); + + assertTrue(result.succeeded()); + assertNotNull(result.value()); + verify(client, times(1)).post(any(JsonObject.class)); + } + + @Test + void shouldCreateCorrectPayload() { + Clients clients = mock(Clients.class); + CollectionResourceClient client = mock(CollectionResourceClient.class); + + when(clients.requestsBatchStorage()).thenReturn(client); + + Response response = mock(Response.class); + when(response.getStatusCode()).thenReturn(201); + when(response.getJson()).thenReturn(new JsonObject()); + + when(client.post(any(JsonObject.class))) + .thenReturn(CompletableFuture.completedFuture(Result.succeeded(response))); + + AnonymizeStorageRequestsRepository repository = + new AnonymizeStorageRequestsRepository(clients); + + RequestAnonymizationRecords records = new RequestAnonymizationRecords() + .withAnonymizedRequests(Collections.singletonList("request-1")); + + Result result = + repository.postAnonymizeStorageRequests(records).join(); + + assertTrue(result.succeeded()); + verify(client).post(any(JsonObject.class)); + } +} diff --git a/src/test/java/org/folio/circulation/infrastructure/storage/requests/RequestRepositoryTest.java b/src/test/java/org/folio/circulation/infrastructure/storage/requests/RequestRepositoryTest.java new file mode 100644 index 0000000000..3a17cb17e5 --- /dev/null +++ b/src/test/java/org/folio/circulation/infrastructure/storage/requests/RequestRepositoryTest.java @@ -0,0 +1,109 @@ +package org.folio.circulation.infrastructure.storage.requests; + +import static org.junit.jupiter.api.Assertions.assertEquals; +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.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.concurrent.CompletableFuture; + +import org.folio.circulation.domain.MultipleRecords; +import org.folio.circulation.domain.Request; +import org.folio.circulation.support.Clients; +import org.folio.circulation.support.CollectionResourceClient; +import org.folio.circulation.support.http.client.CqlQuery; +import org.folio.circulation.support.http.client.PageLimit; +import org.folio.circulation.support.http.client.Response; +import org.folio.circulation.support.results.Result; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; + +class RequestRepositoryTest { + + private RequestRepository repository; + private CollectionResourceClient requestsStorageClient; + + @BeforeEach + void setUp() { + Clients clients = mock(Clients.class); + requestsStorageClient = mock(CollectionResourceClient.class); + + when(clients.requestsStorage()).thenReturn(requestsStorageClient); + when(clients.requestsBatchStorage()).thenReturn(mock(CollectionResourceClient.class)); + when(clients.cancellationReasonStorage()).thenReturn(mock(CollectionResourceClient.class)); + + repository = new RequestRepository(clients); + } + + @Test + void findRequestsToAnonymizeQueriesClosedRequests() { + Response response = createMockResponse(); + when(requestsStorageClient.getMany(any(CqlQuery.class), any(PageLimit.class))) + .thenReturn(CompletableFuture.completedFuture(Result.succeeded(response))); + + PageLimit pageLimit = PageLimit.limit(100); + Result> result = + repository.findRequestsToAnonymize(pageLimit).join(); + + assertTrue(result.succeeded()); + verify(requestsStorageClient).getMany(any(CqlQuery.class), eq(pageLimit)); + } + + @Test + void findClosedRequestsQueriesForSpecificUser() { + Response response = createMockResponse(); + when(requestsStorageClient.getMany(any(CqlQuery.class), any(PageLimit.class))) + .thenReturn(CompletableFuture.completedFuture(Result.succeeded(response))); + + String userId = "user-123"; + PageLimit pageLimit = PageLimit.limit(50); + + Result> result = + repository.findClosedRequests(userId, pageLimit).join(); + + assertTrue(result.succeeded()); + verify(requestsStorageClient).getMany(any(CqlQuery.class), eq(pageLimit)); + } + + @Test + void findRequestsToAnonymizeReturnsEmptyWhenNoRequests() { + Response response = createMockResponse(); + when(requestsStorageClient.getMany(any(CqlQuery.class), any(PageLimit.class))) + .thenReturn(CompletableFuture.completedFuture(Result.succeeded(response))); + + Result> result = + repository.findRequestsToAnonymize(PageLimit.limit(10)).join(); + + assertTrue(result.succeeded()); + assertEquals(0, result.value().getTotalRecords()); + } + + @Test + void findClosedRequestsReturnsEmptyWhenNoRequestsForUser() { + Response response = createMockResponse(); + when(requestsStorageClient.getMany(any(CqlQuery.class), any(PageLimit.class))) + .thenReturn(CompletableFuture.completedFuture(Result.succeeded(response))); + + Result> result = + repository.findClosedRequests("user-456", PageLimit.limit(10)).join(); + + assertTrue(result.succeeded()); + assertEquals(0, result.value().getTotalRecords()); + } + + private Response createMockResponse() { + Response response = mock(Response.class); + JsonObject body = new JsonObject() + .put("requests", new JsonArray()) + .put("totalRecords", 0); + when(response.getJson()).thenReturn(body); + when(response.getStatusCode()).thenReturn(200); + return response; + } +}