diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index cdff5cda2d..6b6378e50a 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -2326,8 +2326,9 @@ "description" : "Internal permission set for fetching item(s)", "subPermissions": [ "inventory-storage.items.item.get", - "circulation-item-storage.items.item.get", + "circulation-item-storage.item.collection.get", "circulation-item-storage.items.collection.get", + "circulation-item-storage.items.item.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/infrastructure/storage/inventory/ItemRepository.java b/src/main/java/org/folio/circulation/infrastructure/storage/inventory/ItemRepository.java index 9c975a08e2..da8a21ef18 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 @@ -64,6 +64,7 @@ public class ItemRepository { private final HoldingsRepository holdingsRepository; private final LoanTypeRepository loanTypeRepository; private final CollectionResourceClient circulationItemClient; + private final CollectionResourceClient circulationItemsByIdsClient; private final IdentityMap identityMap = new IdentityMap( item -> getProperty(item, "id")); @@ -72,7 +73,9 @@ public ItemRepository(Clients clients) { new ServicePointRepository(clients)), new MaterialTypeRepository(clients), new InstanceRepository(clients), new HoldingsRepository(clients.holdingsStorage()), - new LoanTypeRepository(clients.loanTypesStorage()), clients.circulationItemClient()); + new LoanTypeRepository(clients.loanTypesStorage()), + clients.circulationItemClient(), + clients.circulationItemsByIdsClient()); } public CompletableFuture> fetchFor(ItemRelatedRecord itemRelatedRecord) { @@ -254,6 +257,23 @@ private CompletableFuture>> fetchItems(Collection records.mapRecords(mapper::toDomain))) + .thenCompose(res -> res.value().getTotalRecords() == itemIds.size() + ? CompletableFuture.completedFuture(res) + : res.combineAfter(x -> lookupDcbItem(res, itemIds), MultipleRecords::combine)); + } + + private CompletableFuture>> + lookupDcbItem(Result> inventoryItems, Collection itemIds) { + log.debug("lookupDcbItem:: Looking up for DCB items"); + + var inventoryItemIds = inventoryItems.value().toKeys(Item::getItemId); + final var finder = new CqlIndexValuesFinder<>(createCirculationItemFinder()); + var dcbItemIds = itemIds.stream().filter(ids -> !inventoryItemIds.contains(ids)).toList(); + final var mapper = new ItemMapper(); + + return finder.findByIds(dcbItemIds) .thenApply(mapResult(identityMap::add)) .thenApply(mapResult(records -> records.mapRecords(mapper::toDomain))); } @@ -398,4 +418,8 @@ public CompletableFuture>> fetchItemsRelatedRecords private CqlQueryFinder createItemFinder() { return new CqlQueryFinder<>(itemsClient, "items", identity()); } + + private CqlQueryFinder createCirculationItemFinder() { + return new CqlQueryFinder<>(circulationItemsByIdsClient, "items", identity()); + } } diff --git a/src/main/java/org/folio/circulation/support/Clients.java b/src/main/java/org/folio/circulation/support/Clients.java index 3ffc41941a..787dd9bf4c 100644 --- a/src/main/java/org/folio/circulation/support/Clients.java +++ b/src/main/java/org/folio/circulation/support/Clients.java @@ -68,6 +68,7 @@ public class Clients { private final CollectionResourceClient departmentClient; private final CollectionResourceClient checkOutLockStorageClient; private final CollectionResourceClient circulationItemClient; + private final CollectionResourceClient circulationItemsByIdsClient; private final GetManyRecordsClient settingsStorageClient; public static Clients create(WebContext context, HttpClient httpClient) { @@ -136,6 +137,7 @@ private Clients(OkapiHttpClient client, WebContext context) { checkOutLockStorageClient = createCheckoutLockClient(client, context); settingsStorageClient = createSettingsStorageClient(client, context); circulationItemClient = createCirculationItemClient(client, context); + circulationItemsByIdsClient = createCirculationItemsByIdsClient(client, context); } catch(MalformedURLException e) { throw new InvalidOkapiLocationException(context.getOkapiLocation(), e); @@ -374,6 +376,10 @@ public CollectionResourceClient circulationItemClient() { return circulationItemClient; } + public CollectionResourceClient circulationItemsByIdsClient() { + return circulationItemsByIdsClient; + } + private static CollectionResourceClient getCollectionResourceClient( OkapiHttpClient client, WebContext context, String path) @@ -801,6 +807,12 @@ private CollectionResourceClient createCirculationItemClient( return getCollectionResourceClient(client, context, "/circulation-item"); } + private CollectionResourceClient createCirculationItemsByIdsClient( + OkapiHttpClient client, WebContext context) throws MalformedURLException { + + return getCollectionResourceClient(client, context, "/circulation-item/items"); + } + private GetManyRecordsClient createSettingsStorageClient( OkapiHttpClient client, WebContext context) throws MalformedURLException { diff --git a/src/test/java/api/loans/LoanAPITests.java b/src/test/java/api/loans/LoanAPITests.java index edb881d554..02cd4fe31a 100644 --- a/src/test/java/api/loans/LoanAPITests.java +++ b/src/test/java/api/loans/LoanAPITests.java @@ -1283,6 +1283,21 @@ void loanInCollectionDoesNotProvideItemInformationForUnknownItem() { loan.containsKey("item"), is(false)); } + @Test + void loanInCollectionDoesProvideItemInformationForCirculationItem() { + IndividualResource instance = instancesFixture.basedUponDunkirk(); + IndividualResource holdings = holdingsFixture.defaultWithHoldings(instance.getId()); + + IndividualResource locationsResource = locationsFixture.mainFloor(); + final IndividualResource circulationItem = circulationItemsFixture.createCirculationItem(UUID.randomUUID(), "100002222", holdings.getId(), locationsResource.getId()); + loansFixture.createLoan(circulationItem, usersFixture.jessica()); + + JsonObject loan = loansFixture.getLoans().getFirst(); + + assertThat("should be item information available", + loan.containsKey("item"), is(true)); + } + @Test void canPageLoans() { val user = usersFixture.steve(); diff --git a/src/test/java/api/support/APITests.java b/src/test/java/api/support/APITests.java index 47c85ff9a5..51b2076bbb 100644 --- a/src/test/java/api/support/APITests.java +++ b/src/test/java/api/support/APITests.java @@ -24,16 +24,6 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; -import org.junit.Assert; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.testcontainers.containers.KafkaContainer; -import org.testcontainers.utility.DockerImageName; - -import api.support.fakes.FakeModNotify; -import api.support.fakes.FakePubSub; -import api.support.fakes.FakeStorageModule; import api.support.fixtures.AddInfoFixture; import api.support.fixtures.AddressTypesFixture; import api.support.fixtures.AgeToLostFixture; @@ -43,6 +33,7 @@ import api.support.fixtures.CheckInFixture; import api.support.fixtures.CheckOutFixture; import api.support.fixtures.CheckOutLockFixture; +import api.support.fixtures.CirculationItemsFixture; import api.support.fixtures.CirculationRulesFixture; import api.support.fixtures.ClaimItemReturnedFixture; import api.support.fixtures.ConfigurationsFixture; @@ -79,6 +70,16 @@ import api.support.fixtures.TenantActivationFixture; import api.support.fixtures.UserManualBlocksFixture; import api.support.fixtures.UsersFixture; +import org.junit.Assert; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.testcontainers.containers.KafkaContainer; +import org.testcontainers.utility.DockerImageName; + +import api.support.fakes.FakeModNotify; +import api.support.fakes.FakePubSub; +import api.support.fakes.FakeStorageModule; import api.support.fixtures.policies.PoliciesActivationFixture; import api.support.http.IndividualResource; import api.support.http.ResourceClient; @@ -239,6 +240,9 @@ public abstract class APITests { protected final ProxyRelationshipsFixture proxyRelationshipsFixture = new ProxyRelationshipsFixture(proxyRelationshipsClient); + protected final CirculationItemsFixture circulationItemsFixture = new CirculationItemsFixture( + materialTypesFixture, loanTypesFixture); + protected final UsersFixture usersFixture = new UsersFixture(usersClient, patronGroupsFixture); diff --git a/src/test/java/api/support/builders/CirculationItemsBuilder.java b/src/test/java/api/support/builders/CirculationItemsBuilder.java new file mode 100644 index 0000000000..1a22550b58 --- /dev/null +++ b/src/test/java/api/support/builders/CirculationItemsBuilder.java @@ -0,0 +1,27 @@ +package api.support.builders; + +import io.vertx.core.json.JsonObject; + +import java.util.UUID; + +public class CirculationItemsBuilder implements Builder { + + private final JsonObject representation; + + public CirculationItemsBuilder(UUID itemId, String barcode, UUID holdingId, UUID locationId, UUID materialTypeId, UUID loanTypeId, boolean isDcb) { + + this.representation = new JsonObject() + .put("id", itemId) + .put("holdingsRecordId", holdingId) + .put("effectiveLocationId", locationId) + .put("barcode", barcode) + .put("materialTypeId", materialTypeId) + .put("temporaryLoanTypeId", loanTypeId) + .put("dcbItem", isDcb); + } + + @Override + public JsonObject create() { + return representation; + } +} diff --git a/src/test/java/api/support/fakes/FakeOkapi.java b/src/test/java/api/support/fakes/FakeOkapi.java index 1d5ebb6dfa..cec33182ac 100644 --- a/src/test/java/api/support/fakes/FakeOkapi.java +++ b/src/test/java/api/support/fakes/FakeOkapi.java @@ -409,6 +409,18 @@ public void start(Promise startFuture) throws IOException { .withRecordConstraint(this::userHasAlreadyAcquiredLock) .create().register(router); + new FakeStorageModuleBuilder() + .withRootPath("/circulation-item/items") + .withCollectionPropertyName("items") + .withChangeMetadata() + .create().register(router); + + new FakeStorageModuleBuilder() + .withRootPath("/circulation-item") + .withCollectionPropertyName("items") + .withChangeMetadata() + .create().register(router); + new FakeFeeFineOperationsModule().register(router); server.requestHandler(router) diff --git a/src/test/java/api/support/fixtures/CirculationItemsFixture.java b/src/test/java/api/support/fixtures/CirculationItemsFixture.java new file mode 100644 index 0000000000..2d1c8320ac --- /dev/null +++ b/src/test/java/api/support/fixtures/CirculationItemsFixture.java @@ -0,0 +1,30 @@ +package api.support.fixtures; + +import api.support.builders.CirculationItemsBuilder; +import api.support.http.IndividualResource; +import api.support.http.ResourceClient; + +import java.util.UUID; + +public class CirculationItemsFixture { + private final ResourceClient circulationItemsByIdsClient; + private final ResourceClient circulationItemClient; + private final MaterialTypesFixture materialTypesFixture; + private final LoanTypesFixture loanTypesFixture; + + public CirculationItemsFixture( + MaterialTypesFixture materialTypesFixture, + LoanTypesFixture loanTypesFixture) { + + circulationItemsByIdsClient = ResourceClient.forCirculationItemsByIds(); + circulationItemClient = ResourceClient.forCirculationItem(); + this.materialTypesFixture = materialTypesFixture; + this.loanTypesFixture = loanTypesFixture; + } + + public IndividualResource createCirculationItem(UUID itemId, String barcode, UUID holdingId, UUID locationId) { + CirculationItemsBuilder circulationItemsBuilder = new CirculationItemsBuilder(itemId, barcode, holdingId, locationId, materialTypesFixture.book().getId(), loanTypesFixture.canCirculate().getId(), true); + circulationItemClient.create(circulationItemsBuilder); + return circulationItemsByIdsClient.create(circulationItemsBuilder); + } +} diff --git a/src/test/java/api/support/http/InterfaceUrls.java b/src/test/java/api/support/http/InterfaceUrls.java index 2ee1a5dce0..e8b5677d90 100644 --- a/src/test/java/api/support/http/InterfaceUrls.java +++ b/src/test/java/api/support/http/InterfaceUrls.java @@ -44,6 +44,14 @@ public static URL itemsStorageUrl(String subPath) { return APITestContext.viaOkapiModuleUrl("/item-storage/items" + subPath); } + public static URL circulationItemsByIdsUrl(String subPath) { + return APITestContext.viaOkapiModuleUrl("/circulation-item/items" + subPath); + } + + public static URL circulationItemUrl(String subPath) { + return APITestContext.viaOkapiModuleUrl("/circulation-item" + subPath); + } + public static URL holdingsStorageUrl(String subPath) { return APITestContext.viaOkapiModuleUrl("/holdings-storage/holdings" + subPath); } diff --git a/src/test/java/api/support/http/ResourceClient.java b/src/test/java/api/support/http/ResourceClient.java index fc947fe16d..a07d01f1b6 100644 --- a/src/test/java/api/support/http/ResourceClient.java +++ b/src/test/java/api/support/http/ResourceClient.java @@ -29,6 +29,14 @@ public static ResourceClient forItems() { return new ResourceClient(InterfaceUrls::itemsStorageUrl, "items"); } + public static ResourceClient forCirculationItemsByIds() { + return new ResourceClient(InterfaceUrls::circulationItemsByIdsUrl, "items"); + } + + public static ResourceClient forCirculationItem() { + return new ResourceClient(InterfaceUrls::circulationItemUrl, "item"); + } + public static ResourceClient forHoldings() { return new ResourceClient(InterfaceUrls::holdingsStorageUrl, "holdingsRecords"); } 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 d389843e87..9c3a076ebc 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 @@ -37,7 +37,7 @@ class ItemRepositoryTests { @Test void canUpdateAnItemThatHasBeenFetched() { final var itemsClient = mock(CollectionResourceClient.class); - final var repository = createRepository(itemsClient, null); + final var repository = createRepository(itemsClient, null, null); final var itemId = UUID.randomUUID().toString(); @@ -63,7 +63,7 @@ void canUpdateAnItemThatHasBeenFetched() { @Test void cannotUpdateAnItemThatHasNotBeenFetched() { - final var repository = createRepository(null, null); + final var repository = createRepository(null, null, null); final var notFetchedItem = dummyItem(); @@ -75,7 +75,7 @@ void cannotUpdateAnItemThatHasNotBeenFetched() { @Test void nullItemIsNotUpdated() { - final var repository = createRepository(null, null); + final var repository = createRepository(null, null, null); final var updateResult = get(repository.updateItem(null)); @@ -87,8 +87,9 @@ void nullItemIsNotUpdated() { 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 circulationItemsByIdsClient = mock(CollectionResourceClient.class); + final var repository = createRepository(itemsClient, circulationItemsClient, circulationItemsByIdsClient); final var circulationItemJson = new JsonObject() .put("id", itemId) @@ -109,8 +110,9 @@ void returnCirculationItemWhenNotFound() { 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 circulationItemsByIdsClient = mock(CollectionResourceClient.class); + final var repository = createRepository(itemsClient, circulationItemsClient, circulationItemsByIdsClient); final var circulationItemJson = new JsonObject() .put("id", itemId) @@ -137,13 +139,14 @@ private void mockedClientGet(CollectionResourceClient client, String body) { () -> new Response(200, body, "application/json"))); } - private ItemRepository createRepository(CollectionResourceClient itemsClient, CollectionResourceClient circulationItemClient) { + private ItemRepository createRepository(CollectionResourceClient itemsClient, CollectionResourceClient circulationItemClient, CollectionResourceClient circulationItemsByIdsClient) { final var locationRepository = mock(LocationRepository.class); final var materialTypeRepository = mock(MaterialTypeRepository.class); final var instanceRepository = mock(InstanceRepository.class); final var holdingsRepository = mock(HoldingsRepository.class); final var loanTypeRepository = mock(LoanTypeRepository.class); + when(locationRepository.getEffectiveLocation(any())) .thenReturn(ofAsync(Location::unknown)); @@ -158,7 +161,7 @@ private ItemRepository createRepository(CollectionResourceClient itemsClient, Co return new ItemRepository(itemsClient, locationRepository, materialTypeRepository, instanceRepository, - holdingsRepository, loanTypeRepository, circulationItemClient); + holdingsRepository, loanTypeRepository, circulationItemClient, circulationItemsByIdsClient); } private Item dummyItem() {