Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 3 additions & 14 deletions src/main/java/org/folio/circulation/domain/Loan.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public Result<Loan> checkIn(Loan loan, ZonedDateTime systemDateTime,
}

if (loan.isForUseAtLocation()) {
loan.changeStatusOfUsageAtLocation("Returned");
loan.changeStatusOfUsageAtLocation("Returned", null);
}

return succeeded(loan.checkIn(request.getCheckInDate(), systemDateTime,
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/org/folio/circulation/domain/policy/LoanPolicy.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<HoldByBarcodeRequest> 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<Boolean> loanIsNull(HoldByBarcodeRequest request) {
return Result.succeeded(request.getLoan() == null);
}

static Result<Boolean> loanIsNotForUseAtLocation(HoldByBarcodeRequest request) {
return Result.succeeded(!request.getLoan().isForUseAtLocation());
}

static Supplier<HttpFailure> 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<HttpFailure> 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()));
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand All @@ -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<HoldByBarcodeRequest> 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<Result<Loan>> findLoan(HoldByBarcodeRequest request,
LoanRepository loanRepository, ItemRepository itemRepository, UserRepository userRepository,
CirculationErrorHandler errorHandler) {

protected CompletableFuture<Result<HoldByBarcodeRequest>> findLoan(HoldByBarcodeRequest request,
LoanRepository loanRepository, ItemRepository itemRepository, UserRepository userRepository,
CirculationErrorHandler errorHandler) {
final ItemByBarcodeInStorageFinder itemFinder =
new ItemByBarcodeInStorageFinder(itemRepository);

Expand All @@ -93,36 +104,62 @@ protected CompletableFuture<Result<Loan>> 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<Loan> failWhenOpenLoanNotFoundForItem (Result<Loan> loanResult, HoldByBarcodeRequest request) {
return loanResult.failWhen(HoldByBarcodeResource::loanIsNull, loan -> noOpenLoanFailure(request).get());
protected CompletableFuture<Result<HoldByBarcodeRequest>> findPolicy(HoldByBarcodeRequest request, LoanPolicyRepository loanPolicies) {
return loanPolicies.findPolicyForLoan(request.getLoan())
.thenApply(loanResult -> loanResult.map(request::withLoan));
}

protected CompletableFuture<Result<HoldByBarcodeRequest>> findServicePoint(HoldByBarcodeRequest request, ServicePointRepository servicePoints) {
return servicePoints.getServicePointById(request.getLoan().getCheckoutServicePointId())
.thenApply(servicePoint -> servicePoint.map(request::withServicePoint));
}

private Result<Loan> failWhenOpenLoanIsNotForUseAtLocation (Result<Loan> loanResult, HoldByBarcodeRequest request) {
return loanResult.failWhen(HoldByBarcodeResource::loanIsNotForUseAtLocation, loan -> loanIsNotForUseAtLocationFailure(request).get());
protected CompletableFuture<Result<HoldByBarcodeRequest>> findTenantTimeZone(HoldByBarcodeRequest request, SettingsRepository settings) {
return settings.lookupTimeZoneSettings()
.thenApply(zoneId -> zoneId.map(request::withTenantTimeZone));
}

private static Result<Boolean> loanIsNull (Loan loan) {
return Result.succeeded(loan == null);
protected CompletableFuture<Result<HoldByBarcodeRequest>> 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<Boolean> loanIsNotForUseAtLocation(Loan loan) {
return Result.succeeded(!loan.isForUseAtLocation());
private Result<HoldByBarcodeRequest> setStatusToHeldWithExpirationDate(Result<HoldByBarcodeRequest> request) {
return request.map (
req -> req.withLoan(req.getLoan().changeStatusOfUsageAtLocation(USAGE_STATUS_HELD, req.getHoldShelfExpirationDate())));
}


private Result<HoldByBarcodeRequest> setActionHeld(Result<HoldByBarcodeRequest> request) {
return request.map(req -> req.withLoan(req.getLoan().withAction(LoanAction.HELD_FOR_USE_AT_LOCATION)));
}

private static Supplier<HttpFailure> 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<HoldByBarcodeRequest> failWhenOpenLoanNotFoundForItem(Result<HoldByBarcodeRequest> request) {
return request.failWhen(HoldByBarcodeRequest::loanIsNull, req -> noOpenLoanFailure(req).get());
}

private static Supplier<HttpFailure> 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<HoldByBarcodeRequest> failWhenOpenLoanIsNotForUseAtLocation (Result<HoldByBarcodeRequest> request) {
return request.failWhen(HoldByBarcodeRequest::loanIsNotForUseAtLocation, req -> loanIsNotForUseAtLocationFailure(req).get());
}

private HttpResponse toResponse(JsonObject body) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())))
Expand Down
Loading