From bf8f9643d3be95c31c1616bfbed0e7794b4bce42 Mon Sep 17 00:00:00 2001 From: nielserik Date: Wed, 28 Aug 2024 16:37:24 +0200 Subject: [PATCH] CIRC-2136 Add support for floating collections --- .../org/folio/circulation/domain/Item.java | 48 ++- .../folio/circulation/domain/Location.java | 10 + .../folio/circulation/domain/UpdateItem.java | 7 +- .../storage/inventory/ItemRepository.java | 15 +- .../resources/CheckInByBarcodeResource.java | 3 + .../resources/CheckInProcessAdapter.java | 19 + .../storage/mappers/ItemMapper.java | 2 + .../storage/mappers/LocationMapper.java | 3 + .../loans/scenarios/FloatingItemsTests.java | 405 ++++++++++++++++++ .../api/support/builders/LocationBuilder.java | 50 ++- .../support/examples/LocationExamples.java | 13 + .../support/fixtures/LocationsFixture.java | 8 + .../InstanceRequestItemsComparerTests.java | 2 +- .../domain/LoanCheckInServiceTest.java | 4 +- .../domain/OverdueFineServiceTest.java | 2 +- .../inventory/ItemRepositoryTests.java | 3 +- .../circulation/rules/Text2DroolsTest.java | 1 + .../services/FeeFineFacadeTest.java | 2 +- 18 files changed, 565 insertions(+), 32 deletions(-) create mode 100644 src/test/java/api/loans/scenarios/FloatingItemsTests.java diff --git a/src/main/java/org/folio/circulation/domain/Item.java b/src/main/java/org/folio/circulation/domain/Item.java index c27a296ef1..9fa7a7e042 100644 --- a/src/main/java/org/folio/circulation/domain/Item.java +++ b/src/main/java/org/folio/circulation/domain/Item.java @@ -41,6 +41,9 @@ public class Item { @Getter private final String shelvingOrder; @NonNull private final Location permanentLocation; + + @Getter + private final Location floatDestinationLocation; private final ServicePoint inTransitDestinationServicePoint; private boolean changed; @@ -57,7 +60,7 @@ public static Item from(JsonObject representation) { } public Item(String id, JsonObject itemRepresentation, Location effectiveLocation, LastCheckIn lastCheckIn, CallNumberComponents callNumberComponents, - String shelvingOrder, Location permanentLocation, + String shelvingOrder, Location permanentLocation, Location floatDestinationLocation, ServicePoint inTransitDestinationServicePoint, boolean changed, Holdings holdings, Instance instance, MaterialType materialType, LoanType loanType, ItemDescription description) { @@ -69,6 +72,7 @@ public Item(String id, JsonObject itemRepresentation, Location effectiveLocation this.callNumberComponents = callNumberComponents; this.shelvingOrder = shelvingOrder; this.permanentLocation = permanentLocation; + this.floatDestinationLocation = floatDestinationLocation; this.inTransitDestinationServicePoint = inTransitDestinationServicePoint; this.changed = changed; this.holdings = holdings; @@ -348,59 +352,85 @@ public String getPermanentLocationId() { return firstNonBlank(permanentLocation.getId(), holdings.getPermanentLocationId()); } + public String getFloatDestinationLocationId() { + return floatDestinationLocation != null ? floatDestinationLocation.getId() : null; + } + + public boolean canFloatThroughCheckInServicePoint() { + return getLocation() != null && + getLocation().isFloatingCollection() + && getFloatDestinationLocation().getId() != null; + } + public Item withLocation(Location newLocation) { return new Item(this.id, this.itemRepresentation, newLocation, this.lastCheckIn, this.callNumberComponents, this.shelvingOrder, - this.permanentLocation, this.inTransitDestinationServicePoint, this.changed, + this.permanentLocation, this.floatDestinationLocation, + this.inTransitDestinationServicePoint, this.changed, this.holdings, this.instance, this.materialType, this.loanType, description); } public Item withMaterialType(@NonNull MaterialType materialType) { return new Item(this.id, this.itemRepresentation, this.location, this.lastCheckIn, this.callNumberComponents, this.shelvingOrder, - this.permanentLocation, this.inTransitDestinationServicePoint, this.changed, + this.permanentLocation, this.floatDestinationLocation, + this.inTransitDestinationServicePoint, this.changed, this.holdings, this.instance, materialType, this.loanType, this.description); } public Item withHoldings(@NonNull Holdings holdings) { return new Item(this.id, this.itemRepresentation, this.location, this.lastCheckIn, this.callNumberComponents, this.shelvingOrder, - this.permanentLocation, this.inTransitDestinationServicePoint, this.changed, + this.permanentLocation, this.floatDestinationLocation, + this.inTransitDestinationServicePoint, this.changed, holdings, this.instance, this.materialType, this.loanType, this.description); } public Item withInstance(@NonNull Instance instance) { return new Item(this.id, this.itemRepresentation, this.location, this.lastCheckIn, this.callNumberComponents, this.shelvingOrder, - this.permanentLocation, this.inTransitDestinationServicePoint, this.changed, + this.permanentLocation, this.floatDestinationLocation, + this.inTransitDestinationServicePoint, this.changed, this.holdings, instance, this.materialType, this.loanType, this.description); } public Item withLoanType(@NonNull LoanType loanType) { return new Item(this.id, this.itemRepresentation, this.location, this.lastCheckIn, this.callNumberComponents, this.shelvingOrder, - this.permanentLocation, this.inTransitDestinationServicePoint, this.changed, + this.permanentLocation, this.floatDestinationLocation, + this.inTransitDestinationServicePoint, this.changed, this.holdings, this.instance, this.materialType, loanType, this.description); } public Item withLastCheckIn(@NonNull LastCheckIn lastCheckIn) { return new Item(this.id, this.itemRepresentation, this.location, lastCheckIn, this.callNumberComponents, this.shelvingOrder, - this.permanentLocation, this.inTransitDestinationServicePoint, this.changed, + this.permanentLocation, this.floatDestinationLocation, + this.inTransitDestinationServicePoint, this.changed, this.holdings, this.instance, this.materialType, this.loanType, this.description); } public Item withPermanentLocation(Location permanentLocation) { return new Item(this.id, this.itemRepresentation, this.location, this.lastCheckIn, this.callNumberComponents, this.shelvingOrder, - permanentLocation, this.inTransitDestinationServicePoint, this.changed, + permanentLocation, this.floatDestinationLocation, + this.inTransitDestinationServicePoint, this.changed, + this.holdings, this.instance, this.materialType, this.loanType, this.description); + } + + public Item withFloatDestinationLocation(Location floatDestinationLocation) { + return new Item(this.id, this.itemRepresentation, this.location, + this.lastCheckIn, this.callNumberComponents, this.shelvingOrder, + this.permanentLocation, floatDestinationLocation, + this.inTransitDestinationServicePoint, this.changed, this.holdings, this.instance, this.materialType, this.loanType, this.description); } public Item withInTransitDestinationServicePoint(ServicePoint servicePoint) { return new Item(this.id, this.itemRepresentation, this.location, this.lastCheckIn, this.callNumberComponents, this.shelvingOrder, - this.permanentLocation, servicePoint, this.changed, this.holdings, + this.permanentLocation, this.floatDestinationLocation, + servicePoint, this.changed, this.holdings, this.instance, this.materialType, this.loanType, this.description); } diff --git a/src/main/java/org/folio/circulation/domain/Location.java b/src/main/java/org/folio/circulation/domain/Location.java index a052fc9a17..5143ae4af9 100644 --- a/src/main/java/org/folio/circulation/domain/Location.java +++ b/src/main/java/org/folio/circulation/domain/Location.java @@ -16,6 +16,7 @@ public class Location { String discoveryDisplayName; @NonNull Collection servicePointIds; UUID primaryServicePointId; + Boolean isFloatingCollection; @NonNull Institution institution; @NonNull Campus campus; @NonNull Library library; @@ -27,6 +28,7 @@ public static Location unknown() { public static Location unknown(String id) { return new Location(id, null, null, null, List.of(), null, + false, Institution.unknown(null), Campus.unknown(null), Library.unknown(null), ServicePoint.unknown()); } @@ -85,23 +87,31 @@ public ServicePoint getPrimaryServicePoint() { return primaryServicePoint; } + public boolean isFloatingCollection() { + return isFloatingCollection; + } + public Location withInstitution(Institution institution) { return new Location(id, name, code, discoveryDisplayName, servicePointIds, primaryServicePointId, + isFloatingCollection, institution, campus, library, primaryServicePoint); } public Location withCampus(Campus campus) { return new Location(id, name, code, discoveryDisplayName, servicePointIds, primaryServicePointId, + isFloatingCollection, institution, campus, library, primaryServicePoint); } public Location withLibrary(Library library) { return new Location(id, name, code, discoveryDisplayName, servicePointIds, primaryServicePointId, + isFloatingCollection, institution, campus, library, primaryServicePoint); } public Location withPrimaryServicePoint(ServicePoint servicePoint) { return new Location(id, name, code, discoveryDisplayName, servicePointIds, primaryServicePointId, + isFloatingCollection, institution, campus, library, servicePoint); } diff --git a/src/main/java/org/folio/circulation/domain/UpdateItem.java b/src/main/java/org/folio/circulation/domain/UpdateItem.java index 9ba0caa300..cbf459554a 100644 --- a/src/main/java/org/folio/circulation/domain/UpdateItem.java +++ b/src/main/java/org/folio/circulation/domain/UpdateItem.java @@ -57,11 +57,12 @@ private Result changeItemOnCheckIn(Item item, Request request, UUID checkI return changeItemWithOutstandingRequest(item, request, checkInServicePointId); } else { if(Optional.ofNullable(item.getLocation()) - .map(location -> location.homeLocationIsServedBy(checkInServicePointId)) + .map(location -> + location.homeLocationIsServedBy(checkInServicePointId) + || (item.canFloatThroughCheckInServicePoint())) .orElse(false)) { return succeeded(item.available()); - } - else { + } else { return succeeded(item.inTransitToHome()); } } 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 3603768dda..9a5cb54d5d 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 @@ -4,6 +4,7 @@ import static java.util.concurrent.CompletableFuture.supplyAsync; import static java.util.function.Function.identity; import static org.folio.circulation.domain.ItemStatus.AVAILABLE; +import static org.folio.circulation.domain.ItemStatus.IN_TRANSIT; import static org.folio.circulation.domain.MultipleRecords.CombinationMatchers.matchRecordsById; import static org.folio.circulation.domain.representations.ItemProperties.LAST_CHECK_IN; import static org.folio.circulation.domain.representations.ItemProperties.STATUS_PROPERTY; @@ -91,6 +92,7 @@ public CompletableFuture> updateItem(Item item) { log.debug("updateItem:: parameters item: {}", item); final String IN_TRANSIT_DESTINATION_SERVICE_POINT_ID = "inTransitDestinationServicePointId"; + final String TEMPORARY_LOCATION_ID = "temporaryLocationId"; if (item == null) { log.info("updateItem:: item is null"); @@ -108,8 +110,14 @@ public CompletableFuture> updateItem(Item item) { new JsonObject().put("name", item.getStatus().getValue())); remove(updatedItemRepresentation, IN_TRANSIT_DESTINATION_SERVICE_POINT_ID); - write(updatedItemRepresentation, IN_TRANSIT_DESTINATION_SERVICE_POINT_ID, - item.getInTransitDestinationServicePointId()); + if (item.isInStatus(IN_TRANSIT)) { + write(updatedItemRepresentation, IN_TRANSIT_DESTINATION_SERVICE_POINT_ID, + item.getInTransitDestinationServicePointId()); + } else if (item.canFloatThroughCheckInServicePoint()) { + remove(updatedItemRepresentation, TEMPORARY_LOCATION_ID); + write(updatedItemRepresentation, TEMPORARY_LOCATION_ID, + item.getFloatDestinationLocationId()); + } final var lastCheckIn = item.getLastCheckIn(); @@ -176,7 +184,8 @@ private CompletableFuture>> fetchLocations( return result.combineAfter(this::fetchLocations, (items, locations) -> items .combineRecords(locations, Item::getPermanentLocationId, Item::withPermanentLocation, null) - .combineRecords(locations, Item::getEffectiveLocationId, Item::withLocation, null)); + .combineRecords(locations, Item::getEffectiveLocationId, Item::withLocation, null) + .combineRecords(locations, Item::getFloatDestinationLocationId, Item::withFloatDestinationLocation, null)); } private CompletableFuture>> fetchLocations( diff --git a/src/main/java/org/folio/circulation/resources/CheckInByBarcodeResource.java b/src/main/java/org/folio/circulation/resources/CheckInByBarcodeResource.java index a8e75f09ee..be2089da03 100644 --- a/src/main/java/org/folio/circulation/resources/CheckInByBarcodeResource.java +++ b/src/main/java/org/folio/circulation/resources/CheckInByBarcodeResource.java @@ -105,6 +105,9 @@ private void checkIn(RoutingContext routingContext) { .thenComposeAsync(checkInLoan -> checkInLoan.combineAfter( processAdapter::updateRequestQueue, CheckInContext::withRequestQueue)) .thenComposeAsync(r -> r.after(processAdapter::findFulfillableRequest)) + .thenComposeAsync(checkInContextResult -> + checkInContextResult.combineAfter(processAdapter::findFloatingDestination, + CheckInContext::withItemAndUpdatedLoan)) .thenComposeAsync(updateRequestQueueResult -> updateRequestQueueResult.combineAfter( processAdapter::updateItem, CheckInContext::withItemAndUpdatedLoan)) .thenApply(handleItemStatus -> handleItemStatus.next( diff --git a/src/main/java/org/folio/circulation/resources/CheckInProcessAdapter.java b/src/main/java/org/folio/circulation/resources/CheckInProcessAdapter.java index 9e8b396bb4..88bda6bfb9 100644 --- a/src/main/java/org/folio/circulation/resources/CheckInProcessAdapter.java +++ b/src/main/java/org/folio/circulation/resources/CheckInProcessAdapter.java @@ -12,6 +12,7 @@ import org.apache.logging.log4j.Logger; import org.folio.circulation.domain.CheckInContext; import org.folio.circulation.domain.Item; +import org.folio.circulation.domain.Location; import org.folio.circulation.domain.Loan; import org.folio.circulation.domain.LoanCheckInService; import org.folio.circulation.domain.OverdueFineService; @@ -27,6 +28,7 @@ import org.folio.circulation.infrastructure.storage.feesandfines.FeeFineOwnerRepository; import org.folio.circulation.infrastructure.storage.feesandfines.FeeFineRepository; import org.folio.circulation.infrastructure.storage.inventory.ItemRepository; +import org.folio.circulation.infrastructure.storage.inventory.LocationRepository; import org.folio.circulation.infrastructure.storage.loans.LoanPolicyRepository; import org.folio.circulation.infrastructure.storage.loans.LoanRepository; import org.folio.circulation.infrastructure.storage.loans.OverdueFinePolicyRepository; @@ -57,6 +59,7 @@ class CheckInProcessAdapter { private final UpdateRequestQueue requestQueueUpdate; private final LoanRepository loanRepository; private final ServicePointRepository servicePointRepository; + private final LocationRepository locationRepository; private final UserRepository userRepository; private final AddressTypeRepository addressTypeRepository; private final LogCheckInService logCheckInService; @@ -75,6 +78,7 @@ class CheckInProcessAdapter { RequestQueueRepository requestQueueRepository, UpdateItem updateItem, UpdateRequestQueue requestQueueUpdate, LoanRepository loanRepository, ServicePointRepository servicePointRepository, + LocationRepository locationRepository, UserRepository userRepository, AddressTypeRepository addressTypeRepository, LogCheckInService logCheckInService, @@ -93,6 +97,7 @@ class CheckInProcessAdapter { this.requestQueueUpdate = requestQueueUpdate; this.loanRepository = loanRepository; this.servicePointRepository = servicePointRepository; + this.locationRepository = locationRepository; this.userRepository = userRepository; this.addressTypeRepository = addressTypeRepository; this.logCheckInService = logCheckInService; @@ -134,6 +139,7 @@ public static CheckInProcessAdapter newInstance(Clients clients, requestQueueRepository), loanRepository, new ServicePointRepository(clients), + LocationRepository.using(clients), userRepository, new AddressTypeRepository(clients), new LogCheckInService(clients), @@ -296,4 +302,17 @@ CompletableFuture> findFulfillableRequest(CheckInContext return requestQueueService.findRequestFulfillableByItem(context.getItem(), context.getRequestQueue()) .thenApply(r -> r.map(context::withHighestPriorityFulfillableRequest)); } + + CompletableFuture> findFloatingDestination(CheckInContext context) { + Item item = context.getItem(); + if (item.getLocation().isFloatingCollection()) { + return locationRepository.fetchLocationsForServicePoint(context.getCheckInServicePointId().toString()) + .thenApply(rLocations -> rLocations.map(locations -> locations.stream() + .filter(Location::isFloatingCollection).findFirst() + .map(item::withFloatDestinationLocation).orElse(item))); + } else { + return Result.ofAsync(item); + } + } + } diff --git a/src/main/java/org/folio/circulation/storage/mappers/ItemMapper.java b/src/main/java/org/folio/circulation/storage/mappers/ItemMapper.java index aa56840d65..9d4174a41d 100644 --- a/src/main/java/org/folio/circulation/storage/mappers/ItemMapper.java +++ b/src/main/java/org/folio/circulation/storage/mappers/ItemMapper.java @@ -20,6 +20,7 @@ import io.vertx.core.json.JsonObject; public class ItemMapper { + public Item toDomain(JsonObject representation) { return new Item(getProperty(representation, "id"), representation, Location.unknown(getProperty(representation, "effectiveLocationId")), @@ -27,6 +28,7 @@ public Item toDomain(JsonObject representation) { CallNumberComponents.fromItemJson(representation), getProperty(representation, "effectiveShelvingOrder"), Location.unknown(getProperty(representation, "permanentLocationId")), + Location.unknown(), getInTransitServicePoint(representation), false, Holdings.unknown(getProperty(representation, "holdingsRecordId")), Instance.unknown(), diff --git a/src/main/java/org/folio/circulation/storage/mappers/LocationMapper.java b/src/main/java/org/folio/circulation/storage/mappers/LocationMapper.java index 105267b7bb..bc004c233e 100644 --- a/src/main/java/org/folio/circulation/storage/mappers/LocationMapper.java +++ b/src/main/java/org/folio/circulation/storage/mappers/LocationMapper.java @@ -2,6 +2,8 @@ import static org.folio.circulation.support.json.JsonPropertyFetcher.getArrayProperty; import static org.folio.circulation.support.json.JsonPropertyFetcher.getProperty; +import static org.folio.circulation.support.json.JsonPropertyFetcher.getBooleanProperty; + import java.util.Collection; import java.util.Optional; @@ -24,6 +26,7 @@ public Location toDomain(JsonObject representation) { getProperty(representation, "discoveryDisplayName"), getServicePointIds(representation), getPrimaryServicePointId(representation), + getBooleanProperty(representation, "isFloatingCollection"), Institution.unknown(getProperty(representation, "institutionId")), Campus.unknown(getProperty(representation, "campusId")), Library.unknown(getProperty(representation, "libraryId")), diff --git a/src/test/java/api/loans/scenarios/FloatingItemsTests.java b/src/test/java/api/loans/scenarios/FloatingItemsTests.java new file mode 100644 index 0000000000..669144a8ee --- /dev/null +++ b/src/test/java/api/loans/scenarios/FloatingItemsTests.java @@ -0,0 +1,405 @@ +package api.loans.scenarios; + +import api.support.APITests; +import api.support.CheckInByBarcodeResponse; +import api.support.builders.CheckInByBarcodeRequestBuilder; +import api.support.builders.LocationBuilder; +import api.support.fixtures.ItemExamples; +import api.support.http.IndividualResource; +import api.support.http.ItemResource; +import io.vertx.core.json.JsonObject; +import org.folio.circulation.support.utils.ClockUtil; +import org.hamcrest.CoreMatchers; +import org.hamcrest.core.IsNull; +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static api.support.matchers.UUIDMatcher.is; +import static org.hamcrest.MatcherAssert.assertThat; + +public class FloatingItemsTests extends APITests { + + @Test + void willSetFloatingItemsTemporaryLocationToFloatingCollectionAtServicePoint() { + + // Floating collection served by service point 'cd1'. + final IndividualResource floatingCollection = locationsFixture.floatingCollection(); + + // Another floating collection serviced by another service point + final IndividualResource servicePointTwo = servicePointsFixture.cd2(); + IndividualResource otherFloatingCollection = locationsFixture.createLocation( + new LocationBuilder() + .withName("Floating collection 2") + .forInstitution(UUID.randomUUID()) + .forCampus(UUID.randomUUID()) + .forLibrary(UUID.randomUUID()) + .withCode("FLOAT2") + .isFloatingCollection(true) + .withPrimaryServicePoint(servicePointTwo.getId())); + + final IndividualResource james = usersFixture.james(); + + final IndividualResource holdingsInFloatingLocation = + holdingsFixture.createHoldingsRecord(UUID.randomUUID(), floatingCollection.getId()); + + IndividualResource nod = itemsClient.create(ItemExamples.basedUponNod( + materialTypesFixture.book().getId(), + loanTypesFixture.canCirculate().getId()) + .withBarcode("565578437802") + .forHolding(holdingsInFloatingLocation.getId())); + + final IndividualResource loan = checkOutFixture.checkOutByBarcode(nod, james); + + final CheckInByBarcodeResponse checkInResponse = checkInFixture.checkInByBarcode( + new CheckInByBarcodeRequestBuilder().forItem(nod).at(servicePointTwo.getId())); + + JsonObject itemRepresentation = checkInResponse.getItem(); + + assertThat("item should be present in response", + itemRepresentation, IsNull.notNullValue()); + + assertThat("ID should be included for item", + itemRepresentation.getString("id"), is(nod.getId())); + + assertThat("barcode should be included for item", + itemRepresentation.getString("barcode"), CoreMatchers.is("565578437802")); + + assertThat("item status should be 'Available'", + itemRepresentation.getJsonObject("status").getString("name"), CoreMatchers.is("Available")); + + assertThat("available item should not have a destination", + itemRepresentation.containsKey("inTransitDestinationServicePointId"), + CoreMatchers.is(false)); + + JsonObject loanRepresentation = checkInResponse.getLoan(); + + assertThat("closed loan should be present in response", + loanRepresentation, IsNull.notNullValue()); + + assertThat("item (in loan) should not have a destination", + loanRepresentation.getJsonObject("item") + .containsKey("inTransitDestinationServicePointId"), CoreMatchers.is(false)); + + JsonObject updatedNod = itemsClient.getById(nod.getId()).getJson(); + + assertThat("stored item status should be 'Available'", + updatedNod.getJsonObject("status").getString("name"), CoreMatchers.is("Available")); + + assertThat("available item in storage should not have a destination", + updatedNod.containsKey("inTransitDestinationServicePointId"), CoreMatchers.is(false)); + + assertThat("available item's temporary location set to other floating collection", + updatedNod.getString("temporaryLocationId"), CoreMatchers.is(otherFloatingCollection.getId().toString())); + + final JsonObject storedLoan = loansStorageClient.getById(loan.getId()).getJson(); + + assertThat("stored loan status should be 'Closed'", + storedLoan.getJsonObject("status").getString("name"), CoreMatchers.is("Closed")); + + assertThat("item status snapshot in storage should be 'Available'", + storedLoan.getString("itemStatus"), CoreMatchers.is("Available")); + + assertThat("Checkin Service Point Id should be stored", + storedLoan.getString("checkinServicePointId"), is(servicePointTwo.getId())); + + } + + @Test + void willPutFloatingItemInTransitWhenCheckInServicePointServesNoFloatingCollection() { + final IndividualResource floatingCollection = locationsFixture.floatingCollection(); + + final IndividualResource servicePointTwo = servicePointsFixture.cd2(); + + // Location without floating collection, different service point + locationsFixture.createLocation( + new LocationBuilder() + .withName("Location 2") + .forInstitution(UUID.randomUUID()) + .forCampus(UUID.randomUUID()) + .forLibrary(UUID.randomUUID()) + .withCode("FLOAT2") + .isFloatingCollection(false) + .withPrimaryServicePoint(servicePointTwo.getId())); + + final IndividualResource james = usersFixture.james(); + + final IndividualResource holdingsInFloatingLocation = + holdingsFixture.createHoldingsRecord(UUID.randomUUID(), floatingCollection.getId()); + + IndividualResource nod = itemsClient.create(ItemExamples.basedUponNod( + materialTypesFixture.book().getId(), + loanTypesFixture.canCirculate().getId()) + .withBarcode("565578437802") + .forHolding(holdingsInFloatingLocation.getId())); + + final IndividualResource loan = checkOutFixture.checkOutByBarcode(nod, james); + + final CheckInByBarcodeResponse checkInResponse = checkInFixture.checkInByBarcode( + new CheckInByBarcodeRequestBuilder() + .forItem(nod) + .at(servicePointTwo.getId())); + + JsonObject itemRepresentation = checkInResponse.getItem(); + + System.out.println(itemsClient.get(nod).getJson().encodePrettily()); + + assertThat("item should be present in response", + itemRepresentation, IsNull.notNullValue()); + + assertThat("ID should be included for item", + itemRepresentation.getString("id"), is(nod.getId())); + + assertThat("barcode should be included for item", + itemRepresentation.getString("barcode"), CoreMatchers.is("565578437802")); + + assertThat("item status should be 'In transit'", + itemRepresentation.getJsonObject("status").getString("name"), CoreMatchers.is("In transit")); + + assertThat("item should have a destination", + itemRepresentation.containsKey("inTransitDestinationServicePointId"), + CoreMatchers.is(true)); + + JsonObject loanRepresentation = checkInResponse.getLoan(); + + assertThat("closed loan should be present in response", + loanRepresentation, IsNull.notNullValue()); + + assertThat("item (in loan) should have a destination", + loanRepresentation.getJsonObject("item") + .containsKey("inTransitDestinationServicePointId"), CoreMatchers.is(true)); + + JsonObject updatedNod = itemsClient.getById(nod.getId()).getJson(); + + assertThat("stored item status should be 'In transit'", + updatedNod.getJsonObject("status").getString("name"), CoreMatchers.is("In transit")); + + assertThat("item in storage should have a destination", + updatedNod.containsKey("inTransitDestinationServicePointId"), CoreMatchers.is(true)); + + final JsonObject storedLoan = loansStorageClient.getById(loan.getId()).getJson(); + + assertThat("stored loan status should be 'Closed'", + storedLoan.getJsonObject("status").getString("name"), CoreMatchers.is("Closed")); + + assertThat("item status snapshot in storage should be 'In transit'", + storedLoan.getString("itemStatus"), CoreMatchers.is("In transit")); + + assertThat("CheckIn Service Point id should be stored", + storedLoan.getString("checkinServicePointId"), is(servicePointTwo.getId())); + + } + + @Test + void willPutFloatingItemInTransitWhenHoldRequestWasIssuedAtDifferentServicePoint() { + final IndividualResource floatingCollection = locationsFixture.floatingCollection(); + + final IndividualResource servicePointTwo = servicePointsFixture.cd3(); + final IndividualResource pickUpServicePoint = servicePointsFixture.cd2(); + + + // Floating collection (location), different service point + locationsFixture.createLocation( + new LocationBuilder() + .withName("Floating collection 2") + .forInstitution(UUID.randomUUID()) + .forCampus(UUID.randomUUID()) + .forLibrary(UUID.randomUUID()) + .withCode("FLOAT2") + .isFloatingCollection(true) + .withPrimaryServicePoint(servicePointTwo.getId())); + + // Location without floating collection, with pickup service point + locationsFixture.createLocation( + new LocationBuilder() + .withName("Location 3") + .forInstitution(UUID.randomUUID()) + .forCampus(UUID.randomUUID()) + .forLibrary(UUID.randomUUID()) + .withCode("LOC3") + .isFloatingCollection(false) + .withPrimaryServicePoint(pickUpServicePoint.getId())); + + + final IndividualResource james = usersFixture.james(); + final IndividualResource jessica = usersFixture.jessica(); + + final IndividualResource instance = instancesFixture.basedUponDunkirk(); + final IndividualResource holdingsInFloatingLocation = + holdingsFixture.createHoldingsRecord(UUID.randomUUID(), floatingCollection.getId()); + + IndividualResource nod = itemsClient.create(ItemExamples.basedUponNod( + materialTypesFixture.book().getId(), + loanTypesFixture.canCirculate().getId()) + .withBarcode("565578437802") + .forHolding(holdingsInFloatingLocation.getId())); + + // Check out from floating collection + final IndividualResource loan = checkOutFixture.checkOutByBarcode(nod, james); + + requestsFixture.placeItemLevelHoldShelfRequest( + new ItemResource(nod, holdingsInFloatingLocation,instance), jessica, ClockUtil.getZonedDateTime(), pickUpServicePoint.getId()); + + // Check in at service point serving a floating collection. + final CheckInByBarcodeResponse checkInResponse = checkInFixture.checkInByBarcode( + new CheckInByBarcodeRequestBuilder() + .forItem(nod) + .at(servicePointTwo.getId())); + + JsonObject itemRepresentation = checkInResponse.getItem(); + + System.out.println(itemsClient.get(nod).getJson().encodePrettily()); + + assertThat("item should be present in response", + itemRepresentation, IsNull.notNullValue()); + + assertThat("ID should be included for item", + itemRepresentation.getString("id"), is(nod.getId())); + + assertThat("barcode should be included for item", + itemRepresentation.getString("barcode"), CoreMatchers.is("565578437802")); + + assertThat("item status should be 'In transit'", + itemRepresentation.getJsonObject("status").getString("name"), CoreMatchers.is("In transit")); + + assertThat("available item should have a destination", + itemRepresentation.containsKey("inTransitDestinationServicePointId"), + CoreMatchers.is(true)); + + JsonObject loanRepresentation = checkInResponse.getLoan(); + + assertThat("closed loan should be present in response", + loanRepresentation, IsNull.notNullValue()); + + assertThat("in transit item in storage should have a destination", + loanRepresentation.getJsonObject("item") + .getString("inTransitDestinationServicePointId"), + CoreMatchers.is(pickUpServicePoint.getId().toString())); + + assertThat("item (in loan) should have a destination", + loanRepresentation.getJsonObject("item") + .containsKey("inTransitDestinationServicePointId"), CoreMatchers.is(true)); + + JsonObject updatedNod = itemsClient.getById(nod.getId()).getJson(); + + assertThat("stored item status should be 'In transit'", + updatedNod.getJsonObject("status").getString("name"), CoreMatchers.is("In transit")); + + assertThat("in transit item in storage should have a destination", + updatedNod.getString("inTransitDestinationServicePointId"), + is(pickUpServicePoint.getId())); + + final JsonObject storedLoan = loansStorageClient.getById(loan.getId()).getJson(); + + assertThat("stored loan status should be 'Closed'", + storedLoan.getJsonObject("status").getString("name"), CoreMatchers.is("Closed")); + + assertThat("item status snapshot in storage should be 'In transit'", + storedLoan.getString("itemStatus"), CoreMatchers.is("In transit")); + + assertThat("Checkin Service Point Id should be stored", + storedLoan.getString("checkinServicePointId"), is(servicePointTwo.getId())); + + } + + + @Test + void willPutItemInTransitIfFloatingLocationWasOverriddenByNonFloatingLocation() { + // floating collection served by service point cd1. + final IndividualResource floatingCollection = locationsFixture.floatingCollection(); + + final IndividualResource servicePointTwo = servicePointsFixture.cd3(); + final IndividualResource pickUpServicePoint = servicePointsFixture.cd2(); + + // Floating collection (location), different service point + locationsFixture.createLocation( + new LocationBuilder() + .withName("Floating collection 2") + .forInstitution(UUID.randomUUID()) + .forCampus(UUID.randomUUID()) + .forLibrary(UUID.randomUUID()) + .withCode("FLOAT2") + .isFloatingCollection(true) + .withPrimaryServicePoint(servicePointTwo.getId())); + + // Location without floating collection, with pickup service point + IndividualResource location3 = locationsFixture.createLocation( + new LocationBuilder() + .withName("Location 3") + .forInstitution(UUID.randomUUID()) + .forCampus(UUID.randomUUID()) + .forLibrary(UUID.randomUUID()) + .withCode("LOC3") + .isFloatingCollection(false) + .withPrimaryServicePoint(pickUpServicePoint.getId())); + + final IndividualResource holdingsInFloatingLocation = + holdingsFixture.createHoldingsRecord(UUID.randomUUID(), floatingCollection.getId()); + + IndividualResource nod = itemsClient.create(ItemExamples.basedUponNod( + materialTypesFixture.book().getId(), + loanTypesFixture.canCirculate().getId()) + .withPermanentLocation(location3) + .withBarcode("565578437802") + .forHolding(holdingsInFloatingLocation.getId())); + + final IndividualResource james = usersFixture.james(); + + final IndividualResource loan = checkOutFixture.checkOutByBarcode(nod, james); + + final CheckInByBarcodeResponse checkInResponse = checkInFixture.checkInByBarcode( + new CheckInByBarcodeRequestBuilder() + .forItem(nod) + .at(servicePointTwo.getId())); + + JsonObject itemRepresentation = checkInResponse.getItem(); + + System.out.println(itemsClient.get(nod).getJson().encodePrettily()); + + assertThat("item should be present in response", + itemRepresentation, IsNull.notNullValue()); + + assertThat("ID should be included for item", + itemRepresentation.getString("id"), is(nod.getId())); + + assertThat("barcode should be included for item", + itemRepresentation.getString("barcode"), CoreMatchers.is("565578437802")); + + assertThat("item status should be 'In transit'", + itemRepresentation.getJsonObject("status").getString("name"), CoreMatchers.is("In transit")); + + assertThat("available item should have a destination", + itemRepresentation.containsKey("inTransitDestinationServicePointId"), + CoreMatchers.is(true)); + + JsonObject loanRepresentation = checkInResponse.getLoan(); + + assertThat("closed loan should be present in response", + loanRepresentation, IsNull.notNullValue()); + + assertThat("item (in loan) should have a destination", + loanRepresentation.getJsonObject("item") + .containsKey("inTransitDestinationServicePointId"), CoreMatchers.is(true)); + + JsonObject updatedNod = itemsClient.getById(nod.getId()).getJson(); + + assertThat("stored item status should be 'In transit'", + updatedNod.getJsonObject("status").getString("name"), CoreMatchers.is("In transit")); + + assertThat("in transit item in storage should have a destination", + updatedNod.getString("inTransitDestinationServicePointId"), + is(pickUpServicePoint.getId())); + + final JsonObject storedLoan = loansStorageClient.getById(loan.getId()).getJson(); + + assertThat("stored loan status should be 'Closed'", + storedLoan.getJsonObject("status").getString("name"), CoreMatchers.is("Closed")); + + assertThat("item status snapshot in storage should be 'In transit'", + storedLoan.getString("itemStatus"), CoreMatchers.is("In transit")); + + assertThat("Checkin Service Point Id should be stored", + storedLoan.getString("checkinServicePointId"), is(servicePointTwo.getId())); + + } +} diff --git a/src/test/java/api/support/builders/LocationBuilder.java b/src/test/java/api/support/builders/LocationBuilder.java index c12a2cc9a2..496d8c2f62 100644 --- a/src/test/java/api/support/builders/LocationBuilder.java +++ b/src/test/java/api/support/builders/LocationBuilder.java @@ -22,9 +22,10 @@ public class LocationBuilder extends JsonBuilder implements Builder { private final UUID primaryServicePointId; private final Set otherServicePointIds; private final String effectiveLocationPrimaryServicePointName; + private final Boolean isFloatingCollection; public LocationBuilder() { - this(null, null, null, null, null, null, null, new HashSet<>(), null); + this(null, null, null, null, null, null, null, new HashSet<>(), null, false); } private LocationBuilder( @@ -36,7 +37,8 @@ private LocationBuilder( UUID libraryId, UUID primaryServicePointId, Set otherServicePointIds, - String effectiveLocationPrimaryServicePointName) { + String effectiveLocationPrimaryServicePointName, + boolean isFloatingCollection) { if (otherServicePointIds == null) { otherServicePointIds = new HashSet<>(); @@ -51,6 +53,7 @@ private LocationBuilder( this.primaryServicePointId = primaryServicePointId; this.otherServicePointIds = otherServicePointIds; this.effectiveLocationPrimaryServicePointName = effectiveLocationPrimaryServicePointName; + this.isFloatingCollection = isFloatingCollection; } @Override @@ -75,6 +78,7 @@ public JsonObject create() { write(representation, "servicePointIds", mappedServicePointIds); write(representation, "effectiveLocationPrimaryServicePointName", effectiveLocationPrimaryServicePointName); } + write(representation, "isFloatingCollection", isFloatingCollection); return representation; } @@ -89,7 +93,8 @@ public LocationBuilder withName(String name) { this.libraryId, this.primaryServicePointId, this.otherServicePointIds, - this.effectiveLocationPrimaryServicePointName); + this.effectiveLocationPrimaryServicePointName, + this.isFloatingCollection); } public LocationBuilder withCode(String code) { @@ -102,7 +107,8 @@ public LocationBuilder withCode(String code) { this.libraryId, this.primaryServicePointId, this.otherServicePointIds, - this.effectiveLocationPrimaryServicePointName); + this.effectiveLocationPrimaryServicePointName, + this.isFloatingCollection); } public LocationBuilder forInstitution(UUID institutionId) { @@ -115,7 +121,8 @@ public LocationBuilder forInstitution(UUID institutionId) { this.libraryId, this.primaryServicePointId, this.otherServicePointIds, - this.effectiveLocationPrimaryServicePointName); + this.effectiveLocationPrimaryServicePointName, + this.isFloatingCollection); } public LocationBuilder forCampus(UUID campusId) { @@ -128,7 +135,8 @@ public LocationBuilder forCampus(UUID campusId) { this.libraryId, this.primaryServicePointId, this.otherServicePointIds, - this.effectiveLocationPrimaryServicePointName); + this.effectiveLocationPrimaryServicePointName, + this.isFloatingCollection); } public LocationBuilder forLibrary(UUID libraryId) { @@ -141,7 +149,8 @@ public LocationBuilder forLibrary(UUID libraryId) { libraryId, this.primaryServicePointId, this.otherServicePointIds, - this.effectiveLocationPrimaryServicePointName); + this.effectiveLocationPrimaryServicePointName, + this.isFloatingCollection); } public LocationBuilder withPrimaryServicePoint(UUID primaryServicePointId) { @@ -154,7 +163,8 @@ public LocationBuilder withPrimaryServicePoint(UUID primaryServicePointId) { this.libraryId, primaryServicePointId, this.otherServicePointIds, - this.effectiveLocationPrimaryServicePointName) + this.effectiveLocationPrimaryServicePointName, + this.isFloatingCollection) .servedBy(primaryServicePointId); } @@ -168,7 +178,8 @@ public LocationBuilder withDiscoveryDisplayName(String discoveryDisplayName) { this.libraryId, this.primaryServicePointId, this.otherServicePointIds, - this.effectiveLocationPrimaryServicePointName); + this.effectiveLocationPrimaryServicePointName, + this.isFloatingCollection); } public LocationBuilder withEffectiveLocationPrimaryServicePointName(String effectiveLocationPrimaryServicePointName) { @@ -181,10 +192,26 @@ public LocationBuilder withEffectiveLocationPrimaryServicePointName(String effec this.libraryId, primaryServicePointId, this.otherServicePointIds, - effectiveLocationPrimaryServicePointName) + effectiveLocationPrimaryServicePointName, + this.isFloatingCollection) .servedBy(primaryServicePointId); } + public LocationBuilder isFloatingCollection(boolean isFloatingCollection) { + return new LocationBuilder( + this.name, + this.code, + this.discoveryDisplayName, + this.institutionId, + this.campusId, + this.libraryId, + primaryServicePointId, + this.otherServicePointIds, + this.effectiveLocationPrimaryServicePointName, + isFloatingCollection + ); + } + public LocationBuilder servedBy(UUID servicePointId) { final HashSet servicePoints = new HashSet<>(otherServicePointIds); @@ -207,6 +234,7 @@ public LocationBuilder servedBy(Set servicePoints) { this.libraryId, this.primaryServicePointId, newServicePointIds, - this.effectiveLocationPrimaryServicePointName); + this.effectiveLocationPrimaryServicePointName, + this.isFloatingCollection); } } diff --git a/src/test/java/api/support/examples/LocationExamples.java b/src/test/java/api/support/examples/LocationExamples.java index 28811230d3..1b3b2f2c27 100644 --- a/src/test/java/api/support/examples/LocationExamples.java +++ b/src/test/java/api/support/examples/LocationExamples.java @@ -109,4 +109,17 @@ public LocationBuilder fourthFloorLocation() { .withPrimaryServicePoint(primaryServicePointId) .servedBy(otherServicePointIds); } + + public LocationBuilder floatingCollection() { + return new LocationBuilder() + .withName("Floating collection") + .withCode("NU/JC/DL/FC") + .withDiscoveryDisplayName("Floating collection") + .forInstitution(institutionId) + .forCampus(jubileeCampusId) + .forLibrary(djanoglyLibraryId) + .withPrimaryServicePoint(primaryServicePointId) + .isFloatingCollection(true); + } + } diff --git a/src/test/java/api/support/fixtures/LocationsFixture.java b/src/test/java/api/support/fixtures/LocationsFixture.java index 53b947260e..27a2fec2c3 100644 --- a/src/test/java/api/support/fixtures/LocationsFixture.java +++ b/src/test/java/api/support/fixtures/LocationsFixture.java @@ -68,6 +68,14 @@ public IndividualResource thirdFloor() { return locationRecordCreator.createIfAbsent( locationExamples.thirdFloor()); } + + public IndividualResource floatingCollection() { + final LocationExamples locationExamples = getLocationExamples(); + + return locationRecordCreator.createIfAbsent( + locationExamples.floatingCollection()); + } + public IndividualResource fourthServicePoint() { final LocationExamples locationExamples = getLocationExamplesForCd4(); diff --git a/src/test/java/org/folio/circulation/domain/InstanceRequestItemsComparerTests.java b/src/test/java/org/folio/circulation/domain/InstanceRequestItemsComparerTests.java index 81cf284ec4..fc93f354b9 100644 --- a/src/test/java/org/folio/circulation/domain/InstanceRequestItemsComparerTests.java +++ b/src/test/java/org/folio/circulation/domain/InstanceRequestItemsComparerTests.java @@ -251,7 +251,7 @@ private static Item createItem(UUID withServicePointId) { if (withServicePointId != null) { location = new Location(null, null, null, null, - List.of(withServicePointId), null, + List.of(withServicePointId), null, false, Institution.unknown(null), Campus.unknown(null), Library.unknown(null), ServicePoint.unknown(null)); } diff --git a/src/test/java/org/folio/circulation/domain/LoanCheckInServiceTest.java b/src/test/java/org/folio/circulation/domain/LoanCheckInServiceTest.java index 87024996d7..be02c84890 100644 --- a/src/test/java/org/folio/circulation/domain/LoanCheckInServiceTest.java +++ b/src/test/java/org/folio/circulation/domain/LoanCheckInServiceTest.java @@ -47,7 +47,7 @@ void isInHouseUseWhenNonPrimaryServicePointServesHomeLocation() { @NonNull UUID homeServicePointId = UUID.randomUUID(); Item item = Item.from(itemRepresentation) .withLocation(new Location(null, null, null, null, - List.of(checkInServicePoint), homeServicePointId, Institution.unknown(), + List.of(checkInServicePoint), homeServicePointId, false, Institution.unknown(), Campus.unknown(), Library.unknown(), ServicePoint.unknown(homeServicePointId.toString()))); @@ -106,7 +106,7 @@ void isNotInHouseUseWhenServicePointDoesNotServeHomeLocation() { private Location locationPrimarilyServing(@NonNull UUID homeServicePointId) { return new Location(null, null, null, null, - List.of(UUID.randomUUID()), homeServicePointId, Institution.unknown(), + List.of(UUID.randomUUID()), homeServicePointId, false, Institution.unknown(), Campus.unknown(), Library.unknown(), ServicePoint.unknown(homeServicePointId.toString())); } diff --git a/src/test/java/org/folio/circulation/domain/OverdueFineServiceTest.java b/src/test/java/org/folio/circulation/domain/OverdueFineServiceTest.java index 9856d01ea8..5c7a44d997 100644 --- a/src/test/java/org/folio/circulation/domain/OverdueFineServiceTest.java +++ b/src/test/java/org/folio/circulation/domain/OverdueFineServiceTest.java @@ -611,7 +611,7 @@ private Item createItem() { return Item.from(item) .withLocation(new Location(null, LOCATION_NAME, null, null, emptyList(), - SERVICE_POINT_ID, Institution.unknown(), Campus.unknown(), Library.unknown(), + SERVICE_POINT_ID, false, Institution.unknown(), Campus.unknown(), Library.unknown(), ServicePoint.unknown())) .withInstance(new Instance(UUID.randomUUID().toString(), TITLE, emptyList(), contributors, emptyList(), emptyList())) .withMaterialType(new MaterialType(ITEM_MATERIAL_TYPE_ID.toString(), ITEM_MATERIAL_TYPE_NAME, null)); 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 ce6afc117a..f32c4306b4 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 @@ -167,7 +167,8 @@ private ItemRepository createRepository(CollectionResourceClient itemsClient, Co } private Item dummyItem() { - return new Item(null, null, null, null, null, null, null, null, false, + return new Item(null, null, null, null, null, null, null, null, + null, false, Holdings.unknown(), Instance.unknown(), MaterialType.unknown(), LoanType.unknown(), ItemDescription.unknown()); } diff --git a/src/test/java/org/folio/circulation/rules/Text2DroolsTest.java b/src/test/java/org/folio/circulation/rules/Text2DroolsTest.java index 9e5512ab8e..f9ade91b9c 100644 --- a/src/test/java/org/folio/circulation/rules/Text2DroolsTest.java +++ b/src/test/java/org/folio/circulation/rules/Text2DroolsTest.java @@ -688,6 +688,7 @@ private MultiMap params(String itId, String ltId, String ptId, String lId) { private Location createLocation(String institutionId, String libraryId, String campusId) { return new Location(null, null, null, null, emptyList(), null, + false, Institution.unknown(institutionId), Campus.unknown(campusId), Library.unknown(libraryId), ServicePoint.unknown()); } diff --git a/src/test/java/org/folio/circulation/services/FeeFineFacadeTest.java b/src/test/java/org/folio/circulation/services/FeeFineFacadeTest.java index edb63950aa..d9c19a77af 100644 --- a/src/test/java/org/folio/circulation/services/FeeFineFacadeTest.java +++ b/src/test/java/org/folio/circulation/services/FeeFineFacadeTest.java @@ -124,7 +124,7 @@ void shouldForwardFailureIfAnAccountIsNotRefunded() throws Exception { private CreateAccountCommand.CreateAccountCommandBuilder createCommandBuilder() { final Item item = Item.from(new JsonObject()) .withLocation(new Location(null, "Main library", null, null, emptyList(), - null, Institution.unknown(), Campus.unknown(), + null, false, Institution.unknown(), Campus.unknown(), Library.unknown(), ServicePoint.unknown())); return CreateAccountCommand.builder()