From 314502c499d17acfcdb19cc9f8b25270e71bcf60 Mon Sep 17 00:00:00 2001 From: nielserik Date: Thu, 14 Aug 2025 16:29:27 +0200 Subject: [PATCH] CIRC-2415 calculate exp date around closed days --- .../org/folio/circulation/domain/Loan.java | 17 +- .../domain/LoanCheckInService.java | 2 +- .../circulation/domain/policy/LoanPolicy.java | 12 ++ .../resources/CheckInProcessAdapter.java | 2 +- .../resources/CheckOutByBarcodeResource.java | 2 +- .../HoldByBarcodeRequest.java | 62 ++++++- .../HoldByBarcodeResource.java | 109 ++++++++----- .../PickupByBarcodeResource.java | 2 +- .../api/loans/LoansForUseAtLocationTests.java | 151 ++++++++++++++++-- .../builders/HoldByBarcodeRequestBuilder.java | 2 +- 10 files changed, 291 insertions(+), 70 deletions(-) diff --git a/src/main/java/org/folio/circulation/domain/Loan.java b/src/main/java/org/folio/circulation/domain/Loan.java index fc2c6f412a..c38ddc5b9b 100644 --- a/src/main/java/org/folio/circulation/domain/Loan.java +++ b/src/main/java/org/folio/circulation/domain/Loan.java @@ -47,7 +47,6 @@ import static org.folio.circulation.domain.representations.LoanProperties.STATUS; import static org.folio.circulation.domain.representations.LoanProperties.SYSTEM_RETURN_DATE; import static org.folio.circulation.domain.representations.LoanProperties.UPDATED_BY_USER_ID; -import static org.folio.circulation.domain.representations.LoanProperties.USAGE_STATUS_HELD; import static org.folio.circulation.domain.representations.LoanProperties.USER_ID; import static org.folio.circulation.support.ValidationErrorFailure.failedValidation; import static org.folio.circulation.support.json.JsonPropertyFetcher.getBooleanProperty; @@ -81,7 +80,6 @@ import org.apache.logging.log4j.Logger; import org.folio.circulation.domain.policy.LoanPolicy; import org.folio.circulation.domain.policy.OverdueFinePolicy; -import org.folio.circulation.domain.policy.Period; import org.folio.circulation.domain.policy.RemindersPolicy; import org.folio.circulation.domain.policy.lostitem.LostItemPolicy; import org.folio.circulation.domain.representations.LoanProperties; @@ -213,20 +211,11 @@ public String getAction() { return getProperty(representation, ACTION); } - public Loan changeStatusOfUsageAtLocation(String usageStatus) { - log.info("changeStatusOfUsageAtLocation:: parameters usageStatus: {}", usageStatus); + public Loan changeStatusOfUsageAtLocation(String usageStatus, ZonedDateTime holdShelfExpirationDate) { + log.info("changeStatusOfUsageAtLocation:: parameters usageStatus: {} expiration {}", usageStatus, holdShelfExpirationDate); writeByPath(representation, usageStatus, FOR_USE_AT_LOCATION, AT_LOCATION_USE_STATUS); writeByPath(representation, ClockUtil.getZonedDateTime().toString(), FOR_USE_AT_LOCATION, AT_LOCATION_USE_STATUS_DATE); - if (usageStatus.equals(USAGE_STATUS_HELD)) { - Period expiry = getLoanPolicy().getHoldShelfExpiryPeriodForUseAtLocation(); - if (expiry == null) { - log.warn("No hold shelf expiry period for use at location defined in loan policy {}", getLoanPolicy().getName()); - } else { - writeByPath(representation, expiry.plusDate(ClockUtil.getZonedDateTime()), FOR_USE_AT_LOCATION, AT_LOCATION_USE_EXPIRY_DATE); - return this; - } - } - remove(representation.getJsonObject(FOR_USE_AT_LOCATION), AT_LOCATION_USE_EXPIRY_DATE); + writeByPath(representation, holdShelfExpirationDate, FOR_USE_AT_LOCATION, AT_LOCATION_USE_EXPIRY_DATE); return this; } diff --git a/src/main/java/org/folio/circulation/domain/LoanCheckInService.java b/src/main/java/org/folio/circulation/domain/LoanCheckInService.java index 2456005560..e0cdf2beaa 100644 --- a/src/main/java/org/folio/circulation/domain/LoanCheckInService.java +++ b/src/main/java/org/folio/circulation/domain/LoanCheckInService.java @@ -40,7 +40,7 @@ public Result checkIn(Loan loan, ZonedDateTime systemDateTime, } if (loan.isForUseAtLocation()) { - loan.changeStatusOfUsageAtLocation("Returned"); + loan.changeStatusOfUsageAtLocation("Returned", null); } return succeeded(loan.checkIn(request.getCheckInDate(), systemDateTime, diff --git a/src/main/java/org/folio/circulation/domain/policy/LoanPolicy.java b/src/main/java/org/folio/circulation/domain/policy/LoanPolicy.java index 9b82a920c2..b0c7717e24 100644 --- a/src/main/java/org/folio/circulation/domain/policy/LoanPolicy.java +++ b/src/main/java/org/folio/circulation/domain/policy/LoanPolicy.java @@ -28,8 +28,10 @@ import org.folio.circulation.domain.RequestQueue; import org.folio.circulation.domain.RequestStatus; import org.folio.circulation.domain.RequestType; +import org.folio.circulation.domain.TimePeriod; import org.folio.circulation.resources.RenewalValidator; import org.folio.circulation.rules.AppliedRuleConditions; +import org.folio.circulation.storage.mappers.TimePeriodMapper; import org.folio.circulation.support.ValidationErrorFailure; import org.folio.circulation.support.http.server.ValidationError; import org.folio.circulation.support.results.Result; @@ -360,6 +362,16 @@ public Period getHoldShelfExpiryPeriodForUseAtLocation() { return null; } + public TimePeriod getHoldShelfExpiryTimePeriodForUseAtLocation() { + if (isForUseAtLocation()) { + JsonObject holdShelfExpiryPeriod = getObjectProperty(getLoansPolicy(), "holdShelfExpiryPeriodForUseAtLocation"); + if (holdShelfExpiryPeriod != null) { + return new TimePeriodMapper().toDomain(holdShelfExpiryPeriod); + } + } + return null; + } + public DueDateManagement getDueDateManagement() { JsonObject loansPolicyObj = getLoansPolicy(); if (Objects.isNull(loansPolicyObj)) { diff --git a/src/main/java/org/folio/circulation/resources/CheckInProcessAdapter.java b/src/main/java/org/folio/circulation/resources/CheckInProcessAdapter.java index 5bf52ce12b..1657d78667 100644 --- a/src/main/java/org/folio/circulation/resources/CheckInProcessAdapter.java +++ b/src/main/java/org/folio/circulation/resources/CheckInProcessAdapter.java @@ -278,7 +278,7 @@ CheckInContext setInHouseUse(CheckInContext checkInContext) { CheckInContext markReturnedIfForUseAtLocation(CheckInContext checkInContext) { Loan loan = checkInContext.getLoan(); if (loan != null && loan.isForUseAtLocation()) { - loan.changeStatusOfUsageAtLocation(USAGE_STATUS_RETURNED); + loan.changeStatusOfUsageAtLocation(USAGE_STATUS_RETURNED, null); } return checkInContext; } diff --git a/src/main/java/org/folio/circulation/resources/CheckOutByBarcodeResource.java b/src/main/java/org/folio/circulation/resources/CheckOutByBarcodeResource.java index f39a15ff0e..f6886c876f 100644 --- a/src/main/java/org/folio/circulation/resources/CheckOutByBarcodeResource.java +++ b/src/main/java/org/folio/circulation/resources/CheckOutByBarcodeResource.java @@ -315,7 +315,7 @@ private LoanAndRelatedRecords checkOutItem(LoanAndRelatedRecords loanAndRelatedR private LoanAndRelatedRecords markInUseIfForUseAtLocation(LoanAndRelatedRecords loanAndRelatedRecords) { Loan loan = loanAndRelatedRecords.getLoan(); if (loan.getLoanPolicy().isForUseAtLocation()) { - loan.changeStatusOfUsageAtLocation(USAGE_STATUS_IN_USE); + loan.changeStatusOfUsageAtLocation(USAGE_STATUS_IN_USE, null); } return loanAndRelatedRecords; } diff --git a/src/main/java/org/folio/circulation/resources/foruseatlocation/HoldByBarcodeRequest.java b/src/main/java/org/folio/circulation/resources/foruseatlocation/HoldByBarcodeRequest.java index a02087d575..abcd7f97e5 100644 --- a/src/main/java/org/folio/circulation/resources/foruseatlocation/HoldByBarcodeRequest.java +++ b/src/main/java/org/folio/circulation/resources/foruseatlocation/HoldByBarcodeRequest.java @@ -1,37 +1,91 @@ package org.folio.circulation.resources.foruseatlocation; import io.vertx.core.json.JsonObject; -import lombok.AllArgsConstructor; import lombok.Getter; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.folio.circulation.domain.Loan; +import org.folio.circulation.domain.ServicePoint; +import org.folio.circulation.support.BadRequestFailure; +import org.folio.circulation.support.HttpFailure; import org.folio.circulation.support.results.Result; import java.lang.invoke.MethodHandles; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.function.Supplier; +import static java.lang.String.format; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.folio.circulation.support.ValidationErrorFailure.failedValidation; import static org.folio.circulation.support.json.JsonPropertyFetcher.getProperty; import static org.folio.circulation.support.results.Result.succeeded; @Getter -@AllArgsConstructor public class HoldByBarcodeRequest { private static final String ITEM_BARCODE = "itemBarcode"; + private static final String SERVICE_POINT_ID = "servicePointId"; private final String itemBarcode; + private Loan loan; + private ServicePoint servicePoint; + private ZonedDateTime holdShelfExpirationDate; + private ZoneId tenantTimeZone; + + + public HoldByBarcodeRequest(String itemBarcode) { + this.itemBarcode = itemBarcode; + } private static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); static Result buildRequestFrom(JsonObject json) { final String itemBarcode = getProperty(json, ITEM_BARCODE); - if (isBlank(itemBarcode)) { String message = "Request to put item on hold shelf must have an item barcode"; log.warn("Missing information:: {}", message); return failedValidation(message, ITEM_BARCODE, null); } - return succeeded(new HoldByBarcodeRequest(itemBarcode)); + } + public HoldByBarcodeRequest withLoan(Loan loan) { + this.loan = loan; + return this; } + + public HoldByBarcodeRequest withServicePoint(ServicePoint servicePoint) { + this.servicePoint = servicePoint; + return this; + } + + public HoldByBarcodeRequest withTenantTimeZone(ZoneId tenantTimeZone) { + this.tenantTimeZone = tenantTimeZone; + return this; + } + + public HoldByBarcodeRequest withHoldShelfExpirationDate(ZonedDateTime date) { + this.holdShelfExpirationDate = date; + return this; + } + + static Result loanIsNull(HoldByBarcodeRequest request) { + return Result.succeeded(request.getLoan() == null); + } + + static Result loanIsNotForUseAtLocation(HoldByBarcodeRequest request) { + return Result.succeeded(!request.getLoan().isForUseAtLocation()); + } + + static Supplier noOpenLoanFailure(HoldByBarcodeRequest request) { + String message = "No open loan found for the item barcode."; + log.warn(message); + return () -> new BadRequestFailure(format(message + " (%s)", request.getItemBarcode())); + } + static Supplier loanIsNotForUseAtLocationFailure(HoldByBarcodeRequest request) { + String message = "The loan is open but is not for use at location."; + log.warn(message); + return () -> new BadRequestFailure(format(message + ", item barcode (%s)", request.getItemBarcode())); + } + + } diff --git a/src/main/java/org/folio/circulation/resources/foruseatlocation/HoldByBarcodeResource.java b/src/main/java/org/folio/circulation/resources/foruseatlocation/HoldByBarcodeResource.java index 8d8b5fe3e6..7286f919df 100644 --- a/src/main/java/org/folio/circulation/resources/foruseatlocation/HoldByBarcodeResource.java +++ b/src/main/java/org/folio/circulation/resources/foruseatlocation/HoldByBarcodeResource.java @@ -8,7 +8,14 @@ import org.apache.logging.log4j.Logger; import org.folio.circulation.domain.Loan; import org.folio.circulation.domain.LoanAction; +import org.folio.circulation.domain.TimePeriod; +import org.folio.circulation.domain.policy.ExpirationDateManagement; +import org.folio.circulation.domain.policy.Period; +import org.folio.circulation.domain.policy.library.ClosedLibraryStrategy; import org.folio.circulation.domain.representations.logs.LogEventType; +import org.folio.circulation.infrastructure.storage.CalendarRepository; +import org.folio.circulation.infrastructure.storage.ServicePointRepository; +import org.folio.circulation.infrastructure.storage.SettingsRepository; import org.folio.circulation.infrastructure.storage.inventory.ItemRepository; import org.folio.circulation.infrastructure.storage.loans.LoanPolicyRepository; import org.folio.circulation.infrastructure.storage.loans.LoanRepository; @@ -19,22 +26,24 @@ import org.folio.circulation.services.EventPublisher; import org.folio.circulation.storage.ItemByBarcodeInStorageFinder; import org.folio.circulation.storage.SingleOpenLoanForItemInStorageFinder; -import org.folio.circulation.support.BadRequestFailure; import org.folio.circulation.support.Clients; -import org.folio.circulation.support.HttpFailure; import org.folio.circulation.support.RouteRegistration; import org.folio.circulation.support.http.OkapiPermissions; import org.folio.circulation.support.http.server.HttpResponse; import org.folio.circulation.support.http.server.JsonHttpResponse; import org.folio.circulation.support.http.server.WebContext; import org.folio.circulation.support.results.Result; +import org.folio.circulation.support.utils.ClockUtil; import java.lang.invoke.MethodHandles; +import java.time.ZonedDateTime; import java.util.concurrent.CompletableFuture; -import java.util.function.Supplier; import static java.lang.String.format; -import static org.folio.circulation.domain.representations.LoanProperties.USAGE_STATUS_HELD; +import static org.folio.circulation.domain.policy.library.ClosedLibraryStrategyUtils.determineClosedLibraryStrategyForHoldShelfExpirationDate; +import static org.folio.circulation.domain.representations.LoanProperties.*; +import static org.folio.circulation.resources.foruseatlocation.HoldByBarcodeRequest.loanIsNotForUseAtLocationFailure; +import static org.folio.circulation.resources.foruseatlocation.HoldByBarcodeRequest.noOpenLoanFailure; import static org.folio.circulation.resources.handlers.error.CirculationErrorType.FAILED_TO_FIND_SINGLE_OPEN_LOAN; public class HoldByBarcodeResource extends Resource { @@ -60,31 +69,33 @@ private void markHeld(RoutingContext routingContext) { final var userRepository = new UserRepository(clients); final var loanRepository = new LoanRepository(clients, itemRepository, userRepository); final var loanPolicyRepository = new LoanPolicyRepository(clients); + final var servicePointRepository = new ServicePointRepository(clients); + final var settingsRepository = new SettingsRepository(clients); + final var calendarRepository = new CalendarRepository(clients); final EventPublisher eventPublisher = new EventPublisher(webContext,clients); JsonObject requestBodyAsJson = routingContext.body().asJsonObject(); - Result requestResult = HoldByBarcodeRequest.buildRequestFrom(requestBodyAsJson); - requestResult + HoldByBarcodeRequest.buildRequestFrom(requestBodyAsJson) .after(request -> findLoan(request, loanRepository, itemRepository, userRepository, errorHandler)) - .thenApply(loan -> failWhenOpenLoanNotFoundForItem(loan, requestResult.value())) - .thenApply(loan -> failWhenOpenLoanIsNotForUseAtLocation(loan, requestResult.value())) - .thenCompose(loanPolicyRepository::findPolicyForLoan) - .thenApply(loanResult -> loanResult.map(loan -> loan.changeStatusOfUsageAtLocation(USAGE_STATUS_HELD))) - .thenApply(loanResult -> loanResult.map(loan -> loan.withAction(LoanAction.HELD_FOR_USE_AT_LOCATION))) - .thenCompose(loanResult -> loanResult.after( - loan -> loanRepository.updateLoan(loanResult.value()))) + .thenApply(HoldByBarcodeResource::failWhenOpenLoanNotFoundForItem) + .thenApply(HoldByBarcodeResource::failWhenOpenLoanIsNotForUseAtLocation) + .thenCompose(request -> request.after(req -> findPolicy(req, loanPolicyRepository))) + .thenCompose(request -> request.after(req -> findServicePoint(req, servicePointRepository))) + .thenCompose(request -> request.after(req -> findTenantTimeZone(req, settingsRepository))) + .thenCompose(request -> request.after(req -> findHoldShelfExpirationDate(req, calendarRepository))) + .thenApply(this::setStatusToHeldWithExpirationDate) + .thenApply(this::setActionHeld) + .thenCompose(request -> request.after(req -> loanRepository.updateLoan(req.getLoan()))) .thenCompose(loanResult -> loanResult.after( loan -> eventPublisher.publishUsageAtLocationEvent(loan, LogEventType.LOAN))) - .thenApply(loanResult -> loanResult.map(Loan::asJson)) - .thenApply(loanAsJsonResult -> loanAsJsonResult.map(this::toResponse)) + .thenApply(loanResult -> loanResult.map(Loan::asJson).map(this::toResponse)) .thenAccept(webContext::writeResultToHttpResponse); } - protected CompletableFuture> findLoan(HoldByBarcodeRequest request, - LoanRepository loanRepository, ItemRepository itemRepository, UserRepository userRepository, - CirculationErrorHandler errorHandler) { - + protected CompletableFuture> findLoan(HoldByBarcodeRequest request, + LoanRepository loanRepository, ItemRepository itemRepository, UserRepository userRepository, + CirculationErrorHandler errorHandler) { final ItemByBarcodeInStorageFinder itemFinder = new ItemByBarcodeInStorageFinder(itemRepository); @@ -93,36 +104,62 @@ protected CompletableFuture> findLoan(HoldByBarcodeRequest request, return itemFinder.findItemByBarcode(request.getItemBarcode()) .thenCompose(itemResult -> itemResult.after(loanFinder::findSingleOpenLoan) - .thenApply(r -> errorHandler.handleValidationResult(r, FAILED_TO_FIND_SINGLE_OPEN_LOAN, (Loan) null)) + .thenApply(loanResult -> loanResult.map(request::withLoan)) + .thenApply(r -> errorHandler.handleValidationResult(r, FAILED_TO_FIND_SINGLE_OPEN_LOAN, request)) ); } - private static Result failWhenOpenLoanNotFoundForItem (Result loanResult, HoldByBarcodeRequest request) { - return loanResult.failWhen(HoldByBarcodeResource::loanIsNull, loan -> noOpenLoanFailure(request).get()); + protected CompletableFuture> findPolicy(HoldByBarcodeRequest request, LoanPolicyRepository loanPolicies) { + return loanPolicies.findPolicyForLoan(request.getLoan()) + .thenApply(loanResult -> loanResult.map(request::withLoan)); + } + + protected CompletableFuture> findServicePoint(HoldByBarcodeRequest request, ServicePointRepository servicePoints) { + return servicePoints.getServicePointById(request.getLoan().getCheckoutServicePointId()) + .thenApply(servicePoint -> servicePoint.map(request::withServicePoint)); } - private Result failWhenOpenLoanIsNotForUseAtLocation (Result loanResult, HoldByBarcodeRequest request) { - return loanResult.failWhen(HoldByBarcodeResource::loanIsNotForUseAtLocation, loan -> loanIsNotForUseAtLocationFailure(request).get()); + protected CompletableFuture> findTenantTimeZone(HoldByBarcodeRequest request, SettingsRepository settings) { + return settings.lookupTimeZoneSettings() + .thenApply(zoneId -> zoneId.map(request::withTenantTimeZone)); } - private static Result loanIsNull (Loan loan) { - return Result.succeeded(loan == null); + protected CompletableFuture> findHoldShelfExpirationDate(HoldByBarcodeRequest request, CalendarRepository calendars) { + Loan loan = request.getLoan(); + Period expiry = loan.getLoanPolicy().getHoldShelfExpiryPeriodForUseAtLocation(); + if (expiry == null) { + log.warn("No hold shelf expiry period for use at location defined in loan policy {}", loan.getLoanPolicy().getName()); + return Result.ofAsync(request); + } else { + final ZonedDateTime baseExpirationDate = expiry.plusDate(ClockUtil.getZonedDateTime()); + TimePeriod timePeriod = loan.getLoanPolicy().getHoldShelfExpiryTimePeriodForUseAtLocation(); + ExpirationDateManagement expirationDateManagement = request.getServicePoint().getHoldShelfClosedLibraryDateManagement(); + ClosedLibraryStrategy strategy = determineClosedLibraryStrategyForHoldShelfExpirationDate( + expirationDateManagement, baseExpirationDate, request.getTenantTimeZone(), timePeriod); + + return calendars.lookupOpeningDays(baseExpirationDate.withZoneSameInstant(request.getTenantTimeZone()).toLocalDate(), + request.getServicePoint().getId()) + .thenApply(adjacentOpeningDaysResult -> strategy.calculateDueDate(baseExpirationDate, adjacentOpeningDaysResult.value())) + .thenApply(dateTime -> dateTime.map(request::withHoldShelfExpirationDate)); + } } - private static Result loanIsNotForUseAtLocation(Loan loan) { - return Result.succeeded(!loan.isForUseAtLocation()); + private Result setStatusToHeldWithExpirationDate(Result request) { + return request.map ( + req -> req.withLoan(req.getLoan().changeStatusOfUsageAtLocation(USAGE_STATUS_HELD, req.getHoldShelfExpirationDate()))); + } + + + private Result setActionHeld(Result request) { + return request.map(req -> req.withLoan(req.getLoan().withAction(LoanAction.HELD_FOR_USE_AT_LOCATION))); } - private static Supplier noOpenLoanFailure(HoldByBarcodeRequest request) { - String message = "No open loan found for the item barcode."; - log.warn(message); - return () -> new BadRequestFailure(format(message + " (%s)", request.getItemBarcode())); + private static Result failWhenOpenLoanNotFoundForItem(Result request) { + return request.failWhen(HoldByBarcodeRequest::loanIsNull, req -> noOpenLoanFailure(req).get()); } - private static Supplier loanIsNotForUseAtLocationFailure(HoldByBarcodeRequest request) { - String message = "The loan is open but is not for use at location."; - log.warn(message); - return () -> new BadRequestFailure(format(message + ", item barcode (%s)", request.getItemBarcode())); + private static Result failWhenOpenLoanIsNotForUseAtLocation (Result request) { + return request.failWhen(HoldByBarcodeRequest::loanIsNotForUseAtLocation, req -> loanIsNotForUseAtLocationFailure(req).get()); } private HttpResponse toResponse(JsonObject body) { diff --git a/src/main/java/org/folio/circulation/resources/foruseatlocation/PickupByBarcodeResource.java b/src/main/java/org/folio/circulation/resources/foruseatlocation/PickupByBarcodeResource.java index b6d86943de..81a702dab3 100644 --- a/src/main/java/org/folio/circulation/resources/foruseatlocation/PickupByBarcodeResource.java +++ b/src/main/java/org/folio/circulation/resources/foruseatlocation/PickupByBarcodeResource.java @@ -70,7 +70,7 @@ private void markInUse(RoutingContext routingContext) { .thenApply(loan -> failWhenOpenLoanForItemAndUserNotFound(loan, pickupByBarcodeRequest.value())) .thenApply(loan -> failWhenOpenLoanIsNotForUseAtLocation(loan, pickupByBarcodeRequest.value())) .thenApply(loanResult -> loanResult.map(loan -> - loan.changeStatusOfUsageAtLocation(USAGE_STATUS_IN_USE) + loan.changeStatusOfUsageAtLocation(USAGE_STATUS_IN_USE, null) .withAction(LoanAction.PICKED_UP_FOR_USE_AT_LOCATION))) .thenCompose(loanResult -> loanResult.after( loan -> loanRepository.updateLoan(loanResult.value()))) diff --git a/src/test/java/api/loans/LoansForUseAtLocationTests.java b/src/test/java/api/loans/LoansForUseAtLocationTests.java index 8b32640d37..897abca6c9 100644 --- a/src/test/java/api/loans/LoansForUseAtLocationTests.java +++ b/src/test/java/api/loans/LoansForUseAtLocationTests.java @@ -6,16 +6,24 @@ import api.support.http.ItemResource; import api.support.http.UserResource; import io.vertx.core.json.JsonObject; +import org.folio.circulation.domain.policy.ExpirationDateManagement; import org.folio.circulation.domain.policy.Period; import org.folio.circulation.support.http.client.Response; import org.hamcrest.core.Is; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.time.ZonedDateTime; import java.util.Collections; import java.util.UUID; +import static api.support.fixtures.CalendarExamples.*; import static api.support.fixtures.ItemExamples.basedUponSmallAngryPlanet; +import static api.support.http.ResourceClient.forServicePoints; +import static java.lang.Boolean.TRUE; +import static java.time.ZoneOffset.UTC; +import static org.folio.circulation.support.utils.DateTimeUtil.atEndOfDay; +import static org.folio.circulation.support.utils.DateTimeUtil.atStartOfDay; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.CoreMatchers.nullValue; @@ -33,10 +41,8 @@ void beforeEach() { "Suffix", Collections.singletonList("CopyNumbers")); - final UUID servicePointId = servicePointsFixture.cd1().getId(); - final IndividualResource homeLocation = locationsFixture.basedUponExampleLocation( - item -> item.withPrimaryServicePoint(servicePointId)); + item -> item.withPrimaryServicePoint(UUID.fromString(CASE_FIRST_DAY_OPEN_SECOND_CLOSED_THIRD_OPEN))); ItemBuilder itemBuilder = basedUponSmallAngryPlanet( materialTypesFixture.book().getId(), @@ -74,20 +80,30 @@ void willSetAtLocationUsageStatusToInUseOnCheckout() { @Test void willMarkItemHeldByBarcode() { + // Check item out and put it on hold at 2020-10-27 (two days before mock calendar starts) + ZonedDateTime dateOfHold = atStartOfDay(FIRST_DAY_OPEN.minusDays(2), UTC).plusHours(10).plusSeconds(10); + mockClockManagerToReturnFixedDateTime(dateOfHold); + + Period holdShelfExpiryPeriod = Period.from(3, "Days"); + + forServicePoints().create(new ServicePointBuilder("Reading room", "RR", + "Circulation Desk -- Reading room").withPickupLocation(TRUE) + .withId(UUID.fromString(CASE_FIRST_DAY_OPEN_SECOND_CLOSED_THIRD_OPEN)) + .withholdShelfClosedLibraryDateManagement(ExpirationDateManagement.KEEP_THE_CURRENT_DUE_DATE.name())); + final LoanPolicyBuilder forUseAtLocationPolicyBuilder = new LoanPolicyBuilder() .withName("Reading room loans") .withDescription("Policy for items to be used at location") .rolling(Period.days(30)) .withForUseAtLocation(true) - .withHoldShelfExpiryPeriodForUseAtLocation(Period.from(5, "DAYS")); - + .withHoldShelfExpiryPeriodForUseAtLocation(holdShelfExpiryPeriod); use(forUseAtLocationPolicyBuilder); checkOutFixture.checkOutByBarcode( new CheckOutByBarcodeRequestBuilder() .forItem(item) .to(borrower) - .at(servicePointsFixture.cd1())); + .at(CASE_FIRST_DAY_OPEN_SECOND_CLOSED_THIRD_OPEN)); Response holdResponse = holdForUseAtLocationFixture.holdForUseAtLocation( new HoldByBarcodeRequestBuilder(item.getBarcode())); @@ -100,6 +116,119 @@ void willMarkItemHeldByBarcode() { forUseAtLocation.getString("status"), Is.is("Held")); assertThat("loan.forUseAtLocation.holdShelfExpirationDate", forUseAtLocation.getString("holdShelfExpirationDate"), notNullValue()); + assertThat("loan.forUseAtLocation.holdShelfExpirationDate", + forUseAtLocation.getString("holdShelfExpirationDate").replaceAll("\\.000",""), + Is.is(atEndOfDay(holdShelfExpiryPeriod.plusDate(dateOfHold),UTC).toString())); + } + + @Test + void willSetHoldShelfExpiryToEndOfDayBeforeClosedDayOnPutOnHold() { + // Check item out and put it on hold at 2020-10-27 (two days before mock calendar starts) + ZonedDateTime dateOfHold = atStartOfDay(FIRST_DAY_OPEN.minusDays(2), UTC).plusHours(10).plusSeconds(10); + mockClockManagerToReturnFixedDateTime(dateOfHold); + + Period holdShelfExpiryPeriod = Period.from(3, "Days"); + + forServicePoints().create(new ServicePointBuilder("Reading room", "RR", + "Circulation Desk -- Reading room").withPickupLocation(TRUE) + .withId(UUID.fromString(CASE_FIRST_DAY_OPEN_SECOND_CLOSED_THIRD_OPEN)) + .withholdShelfClosedLibraryDateManagement(ExpirationDateManagement.MOVE_TO_THE_END_OF_THE_PREVIOUS_OPEN_DAY.name())); + + final LoanPolicyBuilder forUseAtLocationPolicyBuilder = new LoanPolicyBuilder() + .withName("Reading room loans") + .withDescription("Policy for items to be used at location") + .rolling(Period.days(30)) + .withForUseAtLocation(true) + .withHoldShelfExpiryPeriodForUseAtLocation(holdShelfExpiryPeriod); + use(forUseAtLocationPolicyBuilder); + + checkOutFixture.checkOutByBarcode( + new CheckOutByBarcodeRequestBuilder() + .forItem(item) + .to(borrower) + .at(CASE_FIRST_DAY_OPEN_SECOND_CLOSED_THIRD_OPEN)); + + JsonObject forUseAtLocation = holdForUseAtLocationFixture.holdForUseAtLocation( + new HoldByBarcodeRequestBuilder(item.getBarcode())).getJson().getJsonObject("forUseAtLocation"); + + ZonedDateTime expectedExpiryDateTime = + atEndOfDay(holdShelfExpiryPeriod + .plusDate(dateOfHold) + .minusDays(1),UTC); // move back one day + + assertThat("loan.forUseAtLocation.holdShelfExpirationDate", + forUseAtLocation.getString("holdShelfExpirationDate").replaceAll("\\.000",""), + Is.is(expectedExpiryDateTime.toString())); + } + + @Test + void willSetHoldShelfExpiryToEndOfDayAfterClosedDayOnPutOnHold() { + // Check item out and put it on hold at 2020-10-27 (two days before mock calendar starts) + ZonedDateTime dateOfHold = atStartOfDay(FIRST_DAY_OPEN.minusDays(2), UTC).plusHours(10).plusSeconds(10); + mockClockManagerToReturnFixedDateTime(dateOfHold); + + Period holdShelfExpiryPeriod = Period.from(3, "Days"); + + forServicePoints().create(new ServicePointBuilder("Reading room", "RR", + "Circulation Desk -- Reading room").withPickupLocation(TRUE) + .withId(UUID.fromString(CASE_FIRST_DAY_OPEN_SECOND_CLOSED_THIRD_OPEN)) + .withholdShelfClosedLibraryDateManagement(ExpirationDateManagement.MOVE_TO_THE_END_OF_THE_NEXT_OPEN_DAY.name())); + + final LoanPolicyBuilder forUseAtLocationPolicyBuilder = new LoanPolicyBuilder() + .withName("Reading room loans") + .withDescription("Policy for items to be used at location") + .rolling(Period.days(30)) + .withForUseAtLocation(true) + .withHoldShelfExpiryPeriodForUseAtLocation(holdShelfExpiryPeriod); + use(forUseAtLocationPolicyBuilder); + + checkOutFixture.checkOutByBarcode( + new CheckOutByBarcodeRequestBuilder() + .forItem(item) + .to(borrower) + .at(CASE_FIRST_DAY_OPEN_SECOND_CLOSED_THIRD_OPEN)); + + JsonObject forUseAtLocation = holdForUseAtLocationFixture.holdForUseAtLocation( + new HoldByBarcodeRequestBuilder(item.getBarcode())).getJson().getJsonObject("forUseAtLocation"); + + ZonedDateTime expectedExpiryDateTime = + atEndOfDay(holdShelfExpiryPeriod + .plusDate(dateOfHold) + .plusDays(1),UTC); // move forward one day + + assertThat("loan.forUseAtLocation.holdShelfExpirationDate", + forUseAtLocation.getString("holdShelfExpirationDate").replaceAll("\\.000",""), + Is.is(expectedExpiryDateTime.toString())); + } + + @Test + void willSetNoHoldShelfExpirationIfPolicyNotDefined() { + ZonedDateTime dateOfHold = atStartOfDay(FIRST_DAY_OPEN.minusDays(2), UTC).plusHours(10).plusSeconds(10); + mockClockManagerToReturnFixedDateTime(dateOfHold); + + forServicePoints().create(new ServicePointBuilder("Reading room", "RR", + "Circulation Desk -- Reading room").withPickupLocation(TRUE) + .withId(UUID.fromString(CASE_FIRST_DAY_OPEN_SECOND_CLOSED_THIRD_OPEN)) + .withholdShelfClosedLibraryDateManagement(ExpirationDateManagement.MOVE_TO_THE_END_OF_THE_NEXT_OPEN_DAY.name())); + + final LoanPolicyBuilder forUseAtLocationPolicyBuilder = new LoanPolicyBuilder() + .withName("Reading room loans") + .withDescription("Policy for items to be used at location") + .rolling(Period.days(30)) + .withForUseAtLocation(true); + use(forUseAtLocationPolicyBuilder); + + checkOutFixture.checkOutByBarcode( + new CheckOutByBarcodeRequestBuilder() + .forItem(item) + .to(borrower) + .at(CASE_FIRST_DAY_OPEN_SECOND_CLOSED_THIRD_OPEN)); + + JsonObject forUseAtLocation = holdForUseAtLocationFixture.holdForUseAtLocation( + new HoldByBarcodeRequestBuilder(item.getBarcode())).getJson().getJsonObject("forUseAtLocation"); + + assertThat("loan.forUseAtLocation.holdShelfExpirationDate", + forUseAtLocation.getString("holdShelfExpirationDate"), nullValue()); } @Test @@ -109,7 +238,7 @@ void holdWillFailWithDifferentItem() { .withDescription("Policy for items to be used at location") .rolling(Period.days(30)) .withForUseAtLocation(true) - .withHoldShelfExpiryPeriodForUseAtLocation(Period.from(5, "DAYS")); + .withHoldShelfExpiryPeriodForUseAtLocation(Period.from(5, "Days")); use(forUseAtLocationPolicyBuilder); @@ -149,7 +278,7 @@ void holdWillFailWithIncompleteRequest() { .withDescription("Policy for items to be used at location") .rolling(Period.days(30)) .withForUseAtLocation(true) - .withHoldShelfExpiryPeriodForUseAtLocation(Period.from(5, "DAYS")); + .withHoldShelfExpiryPeriodForUseAtLocation(Period.from(5, "Days")); use(forUseAtLocationPolicyBuilder); @@ -171,7 +300,7 @@ void willMarkItemInUseByBarcode() { .withDescription("Policy for items to be used at location") .rolling(Period.days(30)) .withForUseAtLocation(true) - .withHoldShelfExpiryPeriodForUseAtLocation(Period.from(5, "DAYS")); + .withHoldShelfExpiryPeriodForUseAtLocation(Period.from(5, "Days")); use(forUseAtLocationPolicyBuilder); @@ -222,7 +351,7 @@ void pickupWillFailWithIncompleteRequestObject() { .withDescription("Policy for items to be used at location") .rolling(Period.days(30)) .withForUseAtLocation(true) - .withHoldShelfExpiryPeriodForUseAtLocation(Period.from(5, "DAYS")); + .withHoldShelfExpiryPeriodForUseAtLocation(Period.from(5, "Days")); use(forUseAtLocationPolicyBuilder); @@ -265,7 +394,7 @@ void willSetAtLocationUsageStatusToReturnedOnCheckIn() { .withDescription("Policy for items to be used at location") .rolling(Period.days(30)) .withForUseAtLocation(true) - .withHoldShelfExpiryPeriodForUseAtLocation(Period.from(5, "DAYS")); + .withHoldShelfExpiryPeriodForUseAtLocation(Period.from(5, "Days")); use(forUseAtLocationPolicyBuilder); diff --git a/src/test/java/api/support/builders/HoldByBarcodeRequestBuilder.java b/src/test/java/api/support/builders/HoldByBarcodeRequestBuilder.java index 3e72fdd98d..20e6365954 100644 --- a/src/test/java/api/support/builders/HoldByBarcodeRequestBuilder.java +++ b/src/test/java/api/support/builders/HoldByBarcodeRequestBuilder.java @@ -8,10 +8,10 @@ public class HoldByBarcodeRequestBuilder extends JsonBuilder implements Builder public HoldByBarcodeRequestBuilder(String itemBarcode) { this.itemBarcode = itemBarcode; } + @Override public JsonObject create() { final JsonObject request = new JsonObject(); - put(request, "itemBarcode", this.itemBarcode); return request; }