From 86e6516fe70b19badd58fccdc468f9dc3bdeef56 Mon Sep 17 00:00:00 2001 From: MagzhanArtykov Date: Mon, 6 Nov 2023 16:32:21 +0600 Subject: [PATCH 1/2] CIRC-1907 Check in/Check out for the virtual item --- descriptors/ModuleDescriptor-template.json | 10 +++ .../org/folio/circulation/domain/Item.java | 6 +- .../storage/inventory/ItemRepository.java | 32 +++++++++- .../folio/circulation/support/Clients.java | 12 ++++ .../support/CollectionResourceClient.java | 8 +++ .../support/SingleRecordFetcher.java | 16 ++++- .../inventory/ItemRepositoryTests.java | 62 +++++++++++++++++-- 7 files changed, 135 insertions(+), 11 deletions(-) diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index e80d8582c3..a2d5cc7a77 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -1249,6 +1249,12 @@ "version": "1.0" } ], + "optional": [ + { + "id": "circulation-item", + "version": "1.0" + } + ], "permissionSets": [ { "permissionName": "circulation.requests.queue.reorder.collection.post", @@ -1669,6 +1675,7 @@ "circulation.rules.loan-policy.get", "circulation.rules.request-policy.get", "inventory-storage.items.item.put", + "circulation-item-storage.items.item.put", "circulation.internal.fetch-items", "users.item.get", "users.collection.get", @@ -1716,6 +1723,7 @@ "circulation.rules.loan-policy.get", "circulation.rules.request-policy.get", "inventory-storage.items.item.put", + "circulation-item-storage.items.item.put", "circulation.internal.fetch-items", "users.item.get", "users.collection.get", @@ -2294,6 +2302,8 @@ "description" : "Internal permission set for fetching item(s)", "subPermissions": [ "inventory-storage.items.item.get", + "circulation-item-storage.items.item.get", + "circulation-item-storage.items.collection.get", "inventory-storage.items.collection.get", "inventory-storage.holdings.item.get", "inventory-storage.holdings.collection.get", diff --git a/src/main/java/org/folio/circulation/domain/Item.java b/src/main/java/org/folio/circulation/domain/Item.java index 3c7277a0ac..95e8ead468 100644 --- a/src/main/java/org/folio/circulation/domain/Item.java +++ b/src/main/java/org/folio/circulation/domain/Item.java @@ -1,6 +1,5 @@ package org.folio.circulation.domain; -import static java.lang.String.format; import static org.apache.commons.lang3.StringUtils.firstNonBlank; import static org.folio.circulation.domain.ItemStatus.AVAILABLE; import static org.folio.circulation.domain.ItemStatus.AWAITING_PICKUP; @@ -11,6 +10,7 @@ import static org.folio.circulation.domain.ItemStatus.MISSING; import static org.folio.circulation.domain.ItemStatus.PAGED; import static org.folio.circulation.domain.representations.ItemProperties.STATUS_PROPERTY; +import static org.folio.circulation.support.json.JsonPropertyFetcher.getBooleanProperty; import static org.folio.circulation.support.json.JsonPropertyFetcher.getNestedStringProperty; import static org.folio.circulation.support.json.JsonPropertyWriter.write; @@ -398,4 +398,8 @@ public Item withInTransitDestinationServicePoint(ServicePoint servicePoint) { this.permanentLocation, servicePoint, this.changed, this.holdings, this.instance, this.materialType, this.loanType, this.description); } + + public boolean isDcbItem(){ + return getBooleanProperty(itemRepresentation, "dcbItem"); + } } diff --git a/src/main/java/org/folio/circulation/infrastructure/storage/inventory/ItemRepository.java b/src/main/java/org/folio/circulation/infrastructure/storage/inventory/ItemRepository.java index ec66375497..9f8406a359 100644 --- a/src/main/java/org/folio/circulation/infrastructure/storage/inventory/ItemRepository.java +++ b/src/main/java/org/folio/circulation/infrastructure/storage/inventory/ItemRepository.java @@ -14,6 +14,7 @@ import static org.folio.circulation.support.json.JsonPropertyWriter.remove; import static org.folio.circulation.support.json.JsonPropertyWriter.write; import static org.folio.circulation.support.results.AsynchronousResultBindings.combineAfter; +import static org.folio.circulation.support.results.MappingFunctions.when; import static org.folio.circulation.support.results.Result.ofAsync; import static org.folio.circulation.support.results.Result.succeeded; import static org.folio.circulation.support.results.ResultBinding.mapResult; @@ -22,6 +23,7 @@ import java.lang.invoke.MethodHandles; import java.util.Collection; import java.util.HashSet; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.function.BiFunction; import java.util.function.Function; @@ -61,6 +63,7 @@ public class ItemRepository { private final InstanceRepository instanceRepository; private final HoldingsRepository holdingsRepository; private final LoanTypeRepository loanTypeRepository; + private final CollectionResourceClient circulationItemClient; private final IdentityMap identityMap = new IdentityMap( item -> getProperty(item, "id")); @@ -69,7 +72,7 @@ public ItemRepository(Clients clients) { new ServicePointRepository(clients)), new MaterialTypeRepository(clients), new InstanceRepository(clients), new HoldingsRepository(clients.holdingsStorage()), - new LoanTypeRepository(clients.loanTypesStorage())); + new LoanTypeRepository(clients.loanTypesStorage()), clients.circulationItemClient()); } public CompletableFuture> fetchFor(ItemRelatedRecord itemRelatedRecord) { @@ -115,6 +118,11 @@ public CompletableFuture> updateItem(Item item) { write(updatedItemRepresentation, LAST_CHECK_IN, lastCheckIn.toJson()); } + if (item.isDcbItem()) { + return circulationItemClient.put(item.getItemId(), updatedItemRepresentation) + .thenApply(noContentRecordInterpreter(item)::flatMap) + .thenCompose(x -> ofAsync(() -> item)); + } return itemsClient.put(item.getItemId(), updatedItemRepresentation) .thenApply(noContentRecordInterpreter(item)::flatMap) .thenCompose(x -> ofAsync(() -> item)); @@ -141,14 +149,36 @@ private CompletableFuture> getAvailableItem( public CompletableFuture> fetchByBarcode(String barcode) { return fetchItemByBarcode(barcode) + .thenComposeAsync(itemResult -> itemResult.after(when(item -> ofAsync(item::isNotFound), + item -> fetchCirculationItemByBarcode(barcode), item -> completedFuture(itemResult)))) .thenComposeAsync(this::fetchItemRelatedRecords); } + private CompletableFuture> fetchCirculationItemByBarcode(String barcode) { + final var mapper = new ItemMapper(); + + return SingleRecordFetcher.jsonOrNull(circulationItemClient, "item") + .fetchWithQueryStringParameters(Map.of("barcode", barcode)) + .thenApply(mapResult(identityMap::add)) + .thenApply(r -> r.map(mapper::toDomain)); + } + public CompletableFuture> fetchById(String itemId) { return fetchItem(itemId) + .thenComposeAsync(itemResult -> itemResult.after(when(item -> ofAsync(item::isNotFound), + item -> fetchCirculationItem(itemId), item -> completedFuture(itemResult)))) .thenComposeAsync(this::fetchItemRelatedRecords); } + private CompletableFuture> fetchCirculationItem(String id) { + final var mapper = new ItemMapper(); + + return SingleRecordFetcher.jsonOrNull(circulationItemClient, "item") + .fetch(id) + .thenApply(mapResult(identityMap::add)) + .thenApply(r -> r.map(mapper::toDomain)); + } + private CompletableFuture>> fetchLocations( Result> result) { diff --git a/src/main/java/org/folio/circulation/support/Clients.java b/src/main/java/org/folio/circulation/support/Clients.java index af5d877dea..c859a08abb 100644 --- a/src/main/java/org/folio/circulation/support/Clients.java +++ b/src/main/java/org/folio/circulation/support/Clients.java @@ -66,6 +66,7 @@ public class Clients { private final CollectionResourceClient actualCostFeeFineCancelClient; private final CollectionResourceClient departmentClient; private final CollectionResourceClient checkOutLockStorageClient; + private final CollectionResourceClient circulationItemClient; private final GetManyRecordsClient settingsStorageClient; public static Clients create(WebContext context, HttpClient httpClient) { @@ -132,6 +133,7 @@ private Clients(OkapiHttpClient client, WebContext context) { departmentClient = createDepartmentClient(client, context); checkOutLockStorageClient = createCheckoutLockClient(client, context); settingsStorageClient = createSettingsStorageClient(client, context); + circulationItemClient = createCirculationItemClient(client, context); } catch(MalformedURLException e) { throw new InvalidOkapiLocationException(context.getOkapiLocation(), e); @@ -362,6 +364,10 @@ public GetManyRecordsClient settingsStorageClient() { return settingsStorageClient; } + public CollectionResourceClient circulationItemClient() { + return circulationItemClient; + } + private static CollectionResourceClient getCollectionResourceClient( OkapiHttpClient client, WebContext context, String path) @@ -777,6 +783,12 @@ private CollectionResourceClient createCheckoutLockClient( return getCollectionResourceClient(client, context, "/check-out-lock-storage"); } + private CollectionResourceClient createCirculationItemClient( + OkapiHttpClient client, WebContext context) throws MalformedURLException { + + return getCollectionResourceClient(client, context, "/circulation-item"); + } + private GetManyRecordsClient createSettingsStorageClient( OkapiHttpClient client, WebContext context) throws MalformedURLException { diff --git a/src/main/java/org/folio/circulation/support/CollectionResourceClient.java b/src/main/java/org/folio/circulation/support/CollectionResourceClient.java index 6d90dcd27c..a5c827108c 100644 --- a/src/main/java/org/folio/circulation/support/CollectionResourceClient.java +++ b/src/main/java/org/folio/circulation/support/CollectionResourceClient.java @@ -5,6 +5,7 @@ import static org.folio.circulation.support.http.client.Offset.noOffset; import java.net.URL; +import java.util.Map; import java.util.concurrent.CompletableFuture; import org.folio.circulation.support.http.client.CqlQuery; @@ -87,6 +88,13 @@ public CompletableFuture> getManyWithRawQueryStringParameters( return client.get(url); } + public CompletableFuture> getManyWithQueryStringParameters(Map queryParameters) { + return getManyWithRawQueryStringParameters(queryParameters.entrySet().stream() + .map(entry -> entry.getKey() + "=" + entry.getValue()) + .reduce((a, b) -> a + "&" + b) + .orElse("")); + } + @Override public CompletableFuture> getMany(CqlQuery cqlQuery, PageLimit pageLimit) { diff --git a/src/main/java/org/folio/circulation/support/SingleRecordFetcher.java b/src/main/java/org/folio/circulation/support/SingleRecordFetcher.java index 14f1f3a013..3b7b71f210 100644 --- a/src/main/java/org/folio/circulation/support/SingleRecordFetcher.java +++ b/src/main/java/org/folio/circulation/support/SingleRecordFetcher.java @@ -7,8 +7,10 @@ import static org.folio.circulation.support.logging.LogMessageSanitizer.sanitizeLogParameter; 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.mapAsString; import java.lang.invoke.MethodHandles; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.function.Function; @@ -60,9 +62,7 @@ public static SingleRecordFetcher jsonOrNull( } public CompletableFuture> fetch(String id) { - if (log.isInfoEnabled()) { - log.info("Fetching {} with ID: {}", recordType, sanitizeLogParameter(id)); - } + log.info("Fetching {} with ID: {}", () -> recordType, () -> sanitizeLogParameter(id)); requireNonNull(id, format("Cannot fetch single %s with null ID", recordType)); @@ -70,4 +70,14 @@ public CompletableFuture> fetch(String id) { .thenApply(flatMapResult(interpreter::apply)) .exceptionally(CommonFailures::failedDueToServerError); } + + public CompletableFuture> fetchWithQueryStringParameters(Map queryParameters) { + log.info("Fetching {} with query parameters: {}", () -> recordType, () -> mapAsString(queryParameters)); + + requireNonNull(queryParameters, format("Cannot fetch %s with null parameters", recordType)); + + return client.getManyWithQueryStringParameters(queryParameters) + .thenApply(flatMapResult(interpreter::apply)) + .exceptionally(CommonFailures::failedDueToServerError); + } } diff --git a/src/test/java/org/folio/circulation/infrastructure/storage/inventory/ItemRepositoryTests.java b/src/test/java/org/folio/circulation/infrastructure/storage/inventory/ItemRepositoryTests.java index f62889fa7d..d389843e87 100644 --- a/src/test/java/org/folio/circulation/infrastructure/storage/inventory/ItemRepositoryTests.java +++ b/src/test/java/org/folio/circulation/infrastructure/storage/inventory/ItemRepositoryTests.java @@ -17,6 +17,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; +import io.vertx.core.json.JsonArray; import org.folio.circulation.domain.Holdings; import org.folio.circulation.domain.Instance; import org.folio.circulation.domain.Item; @@ -36,7 +37,7 @@ class ItemRepositoryTests { @Test void canUpdateAnItemThatHasBeenFetched() { final var itemsClient = mock(CollectionResourceClient.class); - final var repository = createRepository(itemsClient); + final var repository = createRepository(itemsClient, null); final var itemId = UUID.randomUUID().toString(); @@ -62,7 +63,7 @@ void canUpdateAnItemThatHasBeenFetched() { @Test void cannotUpdateAnItemThatHasNotBeenFetched() { - final var repository = createRepository(null); + final var repository = createRepository(null, null); final var notFetchedItem = dummyItem(); @@ -74,7 +75,7 @@ void cannotUpdateAnItemThatHasNotBeenFetched() { @Test void nullItemIsNotUpdated() { - final var repository = createRepository(null); + final var repository = createRepository(null, null); final var updateResult = get(repository.updateItem(null)); @@ -82,12 +83,61 @@ void nullItemIsNotUpdated() { assertThat(updateResult.value(), is(nullValue())); } + @Test + void returnCirculationItemWhenNotFound() { + final var itemsClient = mock(CollectionResourceClient.class); + final var circulationItemsClient = mock(CollectionResourceClient.class); + final var repository = createRepository(itemsClient, circulationItemsClient); + final var itemId = UUID.randomUUID().toString(); + + final var circulationItemJson = new JsonObject() + .put("id", itemId) + .put("holdingsRecordId", UUID.randomUUID()) + .put("effectiveLocationId", UUID.randomUUID()).toString(); + final var emptyResult = new JsonObject() + .put("items", new JsonArray()).toString(); + + when(itemsClient.getMany(any(), any())).thenReturn(ofAsync( + () -> new Response(200, emptyResult, "application/json"))); + when(circulationItemsClient.getManyWithQueryStringParameters(any())).thenReturn(ofAsync( + () -> new Response(200, circulationItemJson, "application/json"))); + + assertThat(get(repository.fetchByBarcode(itemId)).value().getItemId(), is(itemId)); + } + + @Test + void canUpdateCirculationItemThatHasBeenFetched(){ + final var itemsClient = mock(CollectionResourceClient.class); + final var circulationItemsClient = mock(CollectionResourceClient.class); + final var repository = createRepository(itemsClient, circulationItemsClient); + final var itemId = UUID.randomUUID().toString(); + + final var circulationItemJson = new JsonObject() + .put("id", itemId) + .put("holdingsRecordId", UUID.randomUUID()) + .put("effectiveLocationId", UUID.randomUUID()) + .put("dcbItem", true); + final var emptyResult = new JsonObject() + .put("items", new JsonArray()).toString(); + + mockedClientGet(itemsClient, circulationItemJson.encodePrettily()); + when(itemsClient.get(anyString())).thenReturn(ofAsync( + () -> new Response(200, emptyResult, "application/json"))); + when(circulationItemsClient.put(any(), any())).thenReturn(ofAsync( + () -> new Response(204, circulationItemJson.toString(), "application/json"))); + + final var fetchedItem = get(repository.fetchById(itemId)).value(); + final var updateResult = get(repository.updateItem(fetchedItem)); + + assertThat(updateResult, succeeded()); + } + private void mockedClientGet(CollectionResourceClient client, String body) { - when(client.get(anyString())).thenReturn(Result.ofAsync( + when(client.get(anyString())).thenReturn(ofAsync( () -> new Response(200, body, "application/json"))); } - private ItemRepository createRepository(CollectionResourceClient itemsClient) { + private ItemRepository createRepository(CollectionResourceClient itemsClient, CollectionResourceClient circulationItemClient) { final var locationRepository = mock(LocationRepository.class); final var materialTypeRepository = mock(MaterialTypeRepository.class); final var instanceRepository = mock(InstanceRepository.class); @@ -108,7 +158,7 @@ private ItemRepository createRepository(CollectionResourceClient itemsClient) { return new ItemRepository(itemsClient, locationRepository, materialTypeRepository, instanceRepository, - holdingsRepository, loanTypeRepository); + holdingsRepository, loanTypeRepository, circulationItemClient); } private Item dummyItem() { From 8bcc0581ad2af88b92f915b990576cefdc5f19b6 Mon Sep 17 00:00:00 2001 From: MagzhanArtykov Date: Mon, 6 Nov 2023 16:52:36 +0600 Subject: [PATCH 2/2] CIRC-1907 Check in/Check out for the virtual item --- .../infrastructure/storage/inventory/ItemRepository.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/folio/circulation/infrastructure/storage/inventory/ItemRepository.java b/src/main/java/org/folio/circulation/infrastructure/storage/inventory/ItemRepository.java index 9f8406a359..9c975a08e2 100644 --- a/src/main/java/org/folio/circulation/infrastructure/storage/inventory/ItemRepository.java +++ b/src/main/java/org/folio/circulation/infrastructure/storage/inventory/ItemRepository.java @@ -118,12 +118,8 @@ public CompletableFuture> updateItem(Item item) { write(updatedItemRepresentation, LAST_CHECK_IN, lastCheckIn.toJson()); } - if (item.isDcbItem()) { - return circulationItemClient.put(item.getItemId(), updatedItemRepresentation) - .thenApply(noContentRecordInterpreter(item)::flatMap) - .thenCompose(x -> ofAsync(() -> item)); - } - return itemsClient.put(item.getItemId(), updatedItemRepresentation) + return (item.isDcbItem() ? circulationItemClient : itemsClient) + .put(item.getItemId(), updatedItemRepresentation) .thenApply(noContentRecordInterpreter(item)::flatMap) .thenCompose(x -> ofAsync(() -> item)); }