diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json
index 751c082bf9..b035324c8f 100644
--- a/descriptors/ModuleDescriptor-template.json
+++ b/descriptors/ModuleDescriptor-template.json
@@ -272,6 +272,20 @@
"circulation.renew-loan.all"
]
},
+ {
+ "methods": [
+ "POST"
+ ],
+ "pathPattern": "/circulation/pickup-by-barcode-for-use-at-location",
+ "permissionsRequired": []
+ },
+ {
+ "methods": [
+ "POST"
+ ],
+ "pathPattern": "/circulation/hold-by-barcode-for-use-at-location",
+ "permissionsRequired": []
+ },
{
"methods": [
"GET"
diff --git a/ramls/examples/at-location-usage-status-change-error.json b/ramls/examples/at-location-usage-status-change-error.json
new file mode 100644
index 0000000000..97909e0fa9
--- /dev/null
+++ b/ramls/examples/at-location-usage-status-change-error.json
@@ -0,0 +1,13 @@
+{
+ "errors": [
+ {
+ "message": "TBD",
+ "parameters": [
+ {
+ "key": "itemId",
+ "value": "91719676-e7b5-4f83-bdab-cb70dd10c1e3"
+ }
+ ]
+ }
+ ]
+}
diff --git a/ramls/for-use-at-location-shelf.raml b/ramls/for-use-at-location-shelf.raml
new file mode 100644
index 0000000000..5e1e3f9e3d
--- /dev/null
+++ b/ramls/for-use-at-location-shelf.raml
@@ -0,0 +1,62 @@
+#%RAML 1.0
+title: Change usage status of items to be used at location (ie reading room)
+version: v0.1
+protocols: [ HTTP, HTTPS ]
+baseUri: http://localhost:9130
+
+documentation:
+ - title: API for changing usage status of items used at location
+ content: Change usage status API
+
+types:
+ errors: !include raml-util/schemas/errors.schema
+
+traits:
+ validate: !include raml-util/traits/validation.raml
+
+/circulation:
+ /hold-by-barcode-for-use-at-location:
+ post:
+ is: [validate]
+ body:
+ application/json:
+ type: !include hold-for-use-at-location-request.json
+ responses:
+ 200:
+ description: "The at-location usage status of the loaned item set to held"
+ 422:
+ description: "Unable to change the usage status for the loan"
+ body:
+ application/json:
+ type: errors
+ example: !include examples/at-location-usage-status-change-error.json
+ 404:
+ description: "The loan is not found"
+ 500:
+ description: "Internal server error, e.g. due to misconfiguration"
+ body:
+ text/plain:
+ example: "Internal server error, contact administrator"
+
+ /pickup-by-barcode-for-use-at-location:
+ post:
+ is: [validate]
+ body:
+ application/json:
+ type: !include pickup-for-use-at-location-request.json
+ responses:
+ 200:
+ description: "The at-location usage status of the loaned item set to in-use"
+ 422:
+ description: "Unable to change the usage status for the loan"
+ body:
+ application/json:
+ type: errors
+ example: !include examples/at-location-usage-status-change-error.json
+ 404:
+ description: "The loan is not found"
+ 500:
+ description: "Internal server error, e.g. due to misconfiguration"
+ body:
+ text/plain:
+ example: "Internal server error, contact administrator"
diff --git a/ramls/hold-for-use-at-location-request.json b/ramls/hold-for-use-at-location-request.json
new file mode 100644
index 0000000000..eef6c19853
--- /dev/null
+++ b/ramls/hold-for-use-at-location-request.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "type": "object",
+ "description": "Request to switch status of use at location to 'Held'",
+ "properties": {
+ "itemBarcode": {
+ "description": "Barcode of the item being in use at location",
+ "type": "string"
+ }
+ },
+ "required": [
+ "itemBarcode"
+ ]
+}
diff --git a/ramls/loan.json b/ramls/loan.json
index 05e92a46c8..96387df488 100644
--- a/ramls/loan.json
+++ b/ramls/loan.json
@@ -281,6 +281,26 @@
}
}
},
+ "forUseAtLocation": {
+ "description": "Status of loan/item that is to be used in the library",
+ "type": "object",
+ "properties": {
+ "status": {
+ "description": "Indicates if the item is currently used by or being held for the patron",
+ "type": "string",
+ "enum": [
+ "In use",
+ "Held",
+ "Returned"
+ ]
+ },
+ "statusDate": {
+ "description": "Date and time the status was registered",
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ },
"loanDate": {
"description": "Date and time when the loan began",
"type": "string",
diff --git a/ramls/pickup-for-use-at-location-request.json b/ramls/pickup-for-use-at-location-request.json
new file mode 100644
index 0000000000..e7dba09be7
--- /dev/null
+++ b/ramls/pickup-for-use-at-location-request.json
@@ -0,0 +1,19 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "type": "object",
+ "description": "Request to switch status of use at location to 'In use'",
+ "properties": {
+ "itemBarcode": {
+ "description": "Barcode of the item lent to the patron for use at location",
+ "type": "string"
+ },
+ "userBarcode": {
+ "description": "Barcode of the user (representing the patron)",
+ "type": "string"
+ }
+ },
+ "required": [
+ "itemBarcode",
+ "userBarcode"
+ ]
+}
diff --git a/ramls/put-on-hold-for-use-at-location.json b/ramls/put-on-hold-for-use-at-location.json
new file mode 100644
index 0000000000..6c46045d5b
--- /dev/null
+++ b/ramls/put-on-hold-for-use-at-location.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "type": "object",
+ "description": "Request to switch status of use at location to 'Held'",
+ "properties": {
+ "itemBarcode": {
+ "description": "Barcode of the item lent to the patron for use at location",
+ "type": "string"
+ }
+ },
+ "required": [
+ "itemBarcode"
+ ]
+}
diff --git a/src/main/java/org/folio/circulation/CirculationVerticle.java b/src/main/java/org/folio/circulation/CirculationVerticle.java
index 3cf5912167..207f19d4a7 100644
--- a/src/main/java/org/folio/circulation/CirculationVerticle.java
+++ b/src/main/java/org/folio/circulation/CirculationVerticle.java
@@ -52,6 +52,8 @@
import org.folio.circulation.resources.handlers.LoanRelatedFeeFineClosedHandlerResource;
import org.folio.circulation.resources.renewal.RenewByBarcodeResource;
import org.folio.circulation.resources.renewal.RenewByIdResource;
+import org.folio.circulation.resources.foruseatlocation.HoldByBarcodeResource;
+import org.folio.circulation.resources.foruseatlocation.PickUpByBarcodeResource;
import org.folio.circulation.support.logging.LogHelper;
import org.folio.circulation.support.logging.Logging;
@@ -97,7 +99,8 @@ public void start(Promise startFuture) {
new RenewByBarcodeResource(client).register(router);
new RenewByIdResource(client).register(router);
-
+ new HoldByBarcodeResource(client).register(router);
+ new PickUpByBarcodeResource(client).register(router);
new AllowedServicePointsResource(client).register(router);
new LoanCollectionResource(client).register(router);
new RequestCollectionResource(client).register(router);
diff --git a/src/main/java/org/folio/circulation/domain/EventType.java b/src/main/java/org/folio/circulation/domain/EventType.java
index 7c94f2d9c0..6ed9973906 100644
--- a/src/main/java/org/folio/circulation/domain/EventType.java
+++ b/src/main/java/org/folio/circulation/domain/EventType.java
@@ -10,5 +10,7 @@ public enum EventType {
LOAN_DUE_DATE_CHANGED,
LOAN_CLOSED,
LOG_RECORD,
- LOAN_RELATED_FEE_FINE_CLOSED
+ LOAN_RELATED_FEE_FINE_CLOSED,
+ ITEM_HELD_FOR_USE_AT_LOCATION,
+ ITEM_PICKED_UP_FOR_USE_AT_LOCATION
}
diff --git a/src/main/java/org/folio/circulation/domain/Loan.java b/src/main/java/org/folio/circulation/domain/Loan.java
index 0464fb2edd..d47908396e 100644
--- a/src/main/java/org/folio/circulation/domain/Loan.java
+++ b/src/main/java/org/folio/circulation/domain/Loan.java
@@ -21,6 +21,8 @@
import static org.folio.circulation.domain.representations.LoanProperties.ACTION_COMMENT;
import static org.folio.circulation.domain.representations.LoanProperties.AGED_TO_LOST_DATE;
import static org.folio.circulation.domain.representations.LoanProperties.AGED_TO_LOST_DELAYED_BILLING;
+import static org.folio.circulation.domain.representations.LoanProperties.AT_LOCATION_USE_STATUS;
+import static org.folio.circulation.domain.representations.LoanProperties.AT_LOCATION_USE_STATUS_DATE;
import static org.folio.circulation.domain.representations.LoanProperties.BILL_DATE;
import static org.folio.circulation.domain.representations.LoanProperties.BILL_NUMBER;
import static org.folio.circulation.domain.representations.LoanProperties.CHECKIN_SERVICE_POINT_ID;
@@ -30,6 +32,7 @@
import static org.folio.circulation.domain.representations.LoanProperties.DATE_LOST_ITEM_SHOULD_BE_BILLED;
import static org.folio.circulation.domain.representations.LoanProperties.DECLARED_LOST_DATE;
import static org.folio.circulation.domain.representations.LoanProperties.DUE_DATE;
+import static org.folio.circulation.domain.representations.LoanProperties.FOR_USE_AT_LOCATION;
import static org.folio.circulation.domain.representations.LoanProperties.ITEM_LOCATION_ID_AT_CHECKOUT;
import static org.folio.circulation.domain.representations.LoanProperties.ITEM_STATUS;
import static org.folio.circulation.domain.representations.LoanProperties.LAST_FEE_BILLED;
@@ -207,6 +210,16 @@ public String getAction() {
return getProperty(representation, ACTION);
}
+ public Loan changeStatusOfUsageAtLocation(String usageStatus) {
+ 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);
+ return this;
+ }
+
+ public boolean isForUseAtLocation() {
+ return representation.containsKey(FOR_USE_AT_LOCATION);
+ }
+
private void changeCheckInServicePointId(UUID servicePointId) {
log.debug("changeCheckInServicePointId:: parameters servicePointId: {}", servicePointId);
write(representation, "checkinServicePointId", servicePointId);
@@ -736,7 +749,6 @@ private void changeClaimedReturnedDate(ZonedDateTime claimedReturnedDate) {
public Loan closeLoan(LoanAction action) {
log.debug("closeLoan:: parameters action: {}", action);
changeStatus(LoanStatus.CLOSED);
-
changeAction(action);
removeActionComment();
diff --git a/src/main/java/org/folio/circulation/domain/LoanAction.java b/src/main/java/org/folio/circulation/domain/LoanAction.java
index 87fb1dbc7d..ed85cc6f8a 100644
--- a/src/main/java/org/folio/circulation/domain/LoanAction.java
+++ b/src/main/java/org/folio/circulation/domain/LoanAction.java
@@ -23,9 +23,9 @@ public enum LoanAction {
STAFF_INFO_ADDED("staffInfoAdded"),
RESOLVE_CLAIM_AS_RETURNED_BY_PATRON("checkedInReturnedByPatron"),
RESOLVE_CLAIM_AS_FOUND_BY_LIBRARY("checkedInFoundByLibrary"),
-
- REMINDER_FEE("reminderFee");
-
+ REMINDER_FEE("reminderFee"),
+ HELD_FOR_USE_AT_LOCATION("heldForUseAtLocation"),
+ PICKED_UP_FOR_USE_AT_LOCATION ("pickedUpForUseAtLocation");
private final String value;
LoanAction(String value) {
diff --git a/src/main/java/org/folio/circulation/domain/LoanCheckInService.java b/src/main/java/org/folio/circulation/domain/LoanCheckInService.java
index 78bf759466..2456005560 100644
--- a/src/main/java/org/folio/circulation/domain/LoanCheckInService.java
+++ b/src/main/java/org/folio/circulation/domain/LoanCheckInService.java
@@ -39,6 +39,10 @@ public Result checkIn(Loan loan, ZonedDateTime systemDateTime,
loan.removeAgedToLostBillingInfo();
}
+ if (loan.isForUseAtLocation()) {
+ loan.changeStatusOfUsageAtLocation("Returned");
+ }
+
return succeeded(loan.checkIn(request.getCheckInDate(), systemDateTime,
request.getServicePointId()));
}
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 26d9039b3a..26fd8f96cb 100644
--- a/src/main/java/org/folio/circulation/domain/policy/LoanPolicy.java
+++ b/src/main/java/org/folio/circulation/domain/policy/LoanPolicy.java
@@ -50,6 +50,7 @@ public class LoanPolicy extends Policy {
private static final String ALTERNATE_RENEWAL_LOAN_PERIOD_KEY = "alternateRenewalLoanPeriod";
private static final String ALLOW_RECALLS_TO_EXTEND_OVERDUE_LOANS = "allowRecallsToExtendOverdueLoans";
private static final String ALTERNATE_RECALL_RETURN_INTERVAL = "alternateRecallReturnInterval";
+ private static final String FOR_USE_AT_LOCATION = "forUseAtLocation";
private static final String INTERVAL_ID = "intervalId";
private static final String DURATION = "duration";
@@ -345,6 +346,10 @@ public boolean isNotLoanable() {
return !isLoanable();
}
+ public boolean isForUseAtLocation() {
+ return getBooleanProperty(getLoansPolicy(), FOR_USE_AT_LOCATION);
+ }
+
public DueDateManagement getDueDateManagement() {
JsonObject loansPolicyObj = getLoansPolicy();
if (Objects.isNull(loansPolicyObj)) {
diff --git a/src/main/java/org/folio/circulation/domain/representations/LoanProperties.java b/src/main/java/org/folio/circulation/domain/representations/LoanProperties.java
index f76015c503..784ed9ffcb 100644
--- a/src/main/java/org/folio/circulation/domain/representations/LoanProperties.java
+++ b/src/main/java/org/folio/circulation/domain/representations/LoanProperties.java
@@ -34,8 +34,13 @@ private LoanProperties() { }
public static final String DATE_LOST_ITEM_SHOULD_BE_BILLED = "dateLostItemShouldBeBilled";
public static final String METADATA = "metadata";
public static final String UPDATED_BY_USER_ID = "updatedByUserId";
+ public static final String FOR_USE_AT_LOCATION = "forUseAtLocation";
+ public static final String AT_LOCATION_USE_STATUS = "status";
+ public static final String AT_LOCATION_USE_STATUS_DATE = "statusDate";
+ public static final String USAGE_STATUS_IN_USE = "In use";
+ public static final String USAGE_STATUS_HELD = "Held";
+ public static final String USAGE_STATUS_RETURNED = "Returned";
public static final String CREATED_DATE = "createdDate";
-
public static final String REMINDERS = "reminders";
public static final String LAST_FEE_BILLED = "lastFeeBilled";
public static final String BILL_NUMBER = "number";
diff --git a/src/main/java/org/folio/circulation/domain/representations/logs/LogContextActionResolver.java b/src/main/java/org/folio/circulation/domain/representations/logs/LogContextActionResolver.java
index 78056f8de8..3465252bcd 100644
--- a/src/main/java/org/folio/circulation/domain/representations/logs/LogContextActionResolver.java
+++ b/src/main/java/org/folio/circulation/domain/representations/logs/LogContextActionResolver.java
@@ -7,8 +7,10 @@
import static org.folio.circulation.domain.LoanAction.CLOSED_LOAN;
import static org.folio.circulation.domain.LoanAction.DECLARED_LOST;
import static org.folio.circulation.domain.LoanAction.DUE_DATE_CHANGED;
+import static org.folio.circulation.domain.LoanAction.HELD_FOR_USE_AT_LOCATION;
import static org.folio.circulation.domain.LoanAction.ITEM_AGED_TO_LOST;
import static org.folio.circulation.domain.LoanAction.MISSING;
+import static org.folio.circulation.domain.LoanAction.PICKED_UP_FOR_USE_AT_LOCATION;
import static org.folio.circulation.domain.LoanAction.RECALLREQUESTED;
import static org.folio.circulation.domain.LoanAction.RENEWED;
import static org.folio.circulation.domain.LoanAction.RENEWED_THROUGH_OVERRIDE;
@@ -39,6 +41,8 @@ public class LogContextActionResolver {
loanLogActions.put(DUE_DATE_CHANGED.getValue(), "Changed due date");
loanLogActions.put(PATRON_INFO_ADDED.getValue(), "Patron info added");
loanLogActions.put(STAFF_INFO_ADDED.getValue(), "Staff info added");
+ loanLogActions.put(HELD_FOR_USE_AT_LOCATION.getValue(),"Held for use at location");
+ loanLogActions.put(PICKED_UP_FOR_USE_AT_LOCATION.getValue(), "Picked up for use at location");
}
public static String resolveAction(String action) {
diff --git a/src/main/java/org/folio/circulation/domain/representations/logs/LogEventType.java b/src/main/java/org/folio/circulation/domain/representations/logs/LogEventType.java
index c78301c543..1cdfada2c0 100644
--- a/src/main/java/org/folio/circulation/domain/representations/logs/LogEventType.java
+++ b/src/main/java/org/folio/circulation/domain/representations/logs/LogEventType.java
@@ -11,7 +11,9 @@ public enum LogEventType {
REQUEST_CREATED_THROUGH_OVERRIDE("REQUEST_CREATED_THROUGH_OVERRIDE_EVENT"),
REQUEST_UPDATED("REQUEST_UPDATED_EVENT"),
REQUEST_MOVED("REQUEST_MOVED_EVENT"),
- REQUEST_REORDERED("REQUEST_REORDERED_EVENT");
+ REQUEST_REORDERED("REQUEST_REORDERED_EVENT"),
+ HELD_FOR_USE_AT_LOCATION("HELD_FOR_USE_AT_LOCATION_EVENT"),
+ PICKED_UP_FOR_USE_AT_LOCATION("PICKED_UP_FOR_USE_AT_LOCATION_EVENT");
private final String value;
diff --git a/src/main/java/org/folio/circulation/resources/CheckInByBarcodeResource.java b/src/main/java/org/folio/circulation/resources/CheckInByBarcodeResource.java
index f55c6eabe1..8a25890f4b 100644
--- a/src/main/java/org/folio/circulation/resources/CheckInByBarcodeResource.java
+++ b/src/main/java/org/folio/circulation/resources/CheckInByBarcodeResource.java
@@ -105,6 +105,7 @@ private void checkIn(RoutingContext routingContext) {
processAdapter::findSingleOpenLoan, CheckInContext::withLoan))
.thenComposeAsync(findLoanResult -> findLoanResult.combineAfter(
processAdapter::checkInLoan, CheckInContext::withLoan))
+ .thenApply(r -> r.map(processAdapter::markReturnedIfForUseAtLocation))
.thenComposeAsync(checkInLoan -> checkInLoan.combineAfter(
processAdapter::updateRequestQueue, CheckInContext::withRequestQueue))
.thenComposeAsync(r -> r.after(processAdapter::findFulfillableRequest))
diff --git a/src/main/java/org/folio/circulation/resources/CheckInProcessAdapter.java b/src/main/java/org/folio/circulation/resources/CheckInProcessAdapter.java
index 0515c3c304..5c8caf4564 100644
--- a/src/main/java/org/folio/circulation/resources/CheckInProcessAdapter.java
+++ b/src/main/java/org/folio/circulation/resources/CheckInProcessAdapter.java
@@ -1,6 +1,7 @@
package org.folio.circulation.resources;
import static java.util.concurrent.CompletableFuture.completedFuture;
+import static org.folio.circulation.domain.representations.LoanProperties.USAGE_STATUS_RETURNED;
import static org.folio.circulation.support.results.Result.succeeded;
import java.lang.invoke.MethodHandles;
@@ -274,6 +275,14 @@ CheckInContext setInHouseUse(CheckInContext checkInContext) {
checkInContext.getCheckInRequest()));
}
+ CheckInContext markReturnedIfForUseAtLocation(CheckInContext checkInContext) {
+ Loan loan = checkInContext.getLoan();
+ if (loan != null && loan.isForUseAtLocation()) {
+ loan.changeStatusOfUsageAtLocation(USAGE_STATUS_RETURNED);
+ }
+ return checkInContext;
+ }
+
public CompletableFuture> logCheckInOperation(
CheckInContext checkInContext) {
diff --git a/src/main/java/org/folio/circulation/resources/CheckOutByBarcodeResource.java b/src/main/java/org/folio/circulation/resources/CheckOutByBarcodeResource.java
index 2467dc4a1d..81da450f09 100644
--- a/src/main/java/org/folio/circulation/resources/CheckOutByBarcodeResource.java
+++ b/src/main/java/org/folio/circulation/resources/CheckOutByBarcodeResource.java
@@ -3,6 +3,7 @@
import static java.util.concurrent.CompletableFuture.completedFuture;
import static org.folio.circulation.domain.ItemStatus.CHECKED_OUT;
import static org.folio.circulation.domain.LoanAction.CHECKED_OUT_THROUGH_OVERRIDE;
+import static org.folio.circulation.domain.representations.LoanProperties.USAGE_STATUS_IN_USE;
import static org.folio.circulation.resources.handlers.error.CirculationErrorType.FAILED_TO_FETCH_ITEM;
import static org.folio.circulation.resources.handlers.error.CirculationErrorType.FAILED_TO_FETCH_PROXY_USER;
import static org.folio.circulation.resources.handlers.error.CirculationErrorType.FAILED_TO_FETCH_USER;
@@ -178,6 +179,7 @@ CompletableFuture> checkOut(CheckOutByBarcodeReque
.thenApply(r -> r.next(this::setItemLocationIdAtCheckout))
.thenComposeAsync(r -> r.after(records -> checkOut(records, clients)))
.thenApply(r -> r.map(this::checkOutItem))
+ .thenApply(r -> r.map(this::markInUseIfForUseAtLocation))
.thenCompose(r -> r.after(l -> acquireLockIfNeededOrFail(settingsRepository,
checkOutLockRepository, l, checkOutLockId, validators, errorHandler)))
.thenComposeAsync(r -> r.after(requestQueueUpdate::onCheckOut))
@@ -305,6 +307,14 @@ private LoanAndRelatedRecords checkOutItem(LoanAndRelatedRecords loanAndRelatedR
return loanAndRelatedRecords.changeItemStatus(CHECKED_OUT);
}
+ private LoanAndRelatedRecords markInUseIfForUseAtLocation(LoanAndRelatedRecords loanAndRelatedRecords) {
+ Loan loan = loanAndRelatedRecords.getLoan();
+ if (loan.getLoanPolicy().isForUseAtLocation()) {
+ loan.changeStatusOfUsageAtLocation(USAGE_STATUS_IN_USE);
+ }
+ return loanAndRelatedRecords;
+ }
+
private Result createdLoanFrom(Result result,
CirculationErrorHandler errorHandler) {
diff --git a/src/main/java/org/folio/circulation/resources/foruseatlocation/HoldByBarcodeRequest.java b/src/main/java/org/folio/circulation/resources/foruseatlocation/HoldByBarcodeRequest.java
new file mode 100644
index 0000000000..1d39bab74e
--- /dev/null
+++ b/src/main/java/org/folio/circulation/resources/foruseatlocation/HoldByBarcodeRequest.java
@@ -0,0 +1,31 @@
+package org.folio.circulation.resources.foruseatlocation;
+
+import io.vertx.core.json.JsonObject;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.folio.circulation.support.results.Result;
+
+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 final String itemBarcode;
+
+ static Result buildRequestFrom(JsonObject json) {
+ final String itemBarcode = getProperty(json, ITEM_BARCODE);
+
+ if (isBlank(itemBarcode)) {
+ return failedValidation("Request to put item on hold shelf must have an item barcode",
+ ITEM_BARCODE, null);
+ }
+
+ return succeeded(new HoldByBarcodeRequest(itemBarcode));
+
+ }
+}
diff --git a/src/main/java/org/folio/circulation/resources/foruseatlocation/HoldByBarcodeResource.java b/src/main/java/org/folio/circulation/resources/foruseatlocation/HoldByBarcodeResource.java
new file mode 100644
index 0000000000..e124ed21ba
--- /dev/null
+++ b/src/main/java/org/folio/circulation/resources/foruseatlocation/HoldByBarcodeResource.java
@@ -0,0 +1,113 @@
+package org.folio.circulation.resources.foruseatlocation;
+
+import io.vertx.core.http.HttpClient;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.web.Router;
+import io.vertx.ext.web.RoutingContext;
+import org.folio.circulation.domain.Loan;
+import org.folio.circulation.domain.LoanAction;
+import org.folio.circulation.domain.representations.logs.LogEventType;
+import org.folio.circulation.infrastructure.storage.inventory.ItemRepository;
+import org.folio.circulation.infrastructure.storage.loans.LoanRepository;
+import org.folio.circulation.infrastructure.storage.users.UserRepository;
+import org.folio.circulation.resources.Resource;
+import org.folio.circulation.resources.handlers.error.CirculationErrorHandler;
+import org.folio.circulation.resources.handlers.error.OverridingErrorHandler;
+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 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.resources.handlers.error.CirculationErrorType.FAILED_TO_FIND_SINGLE_OPEN_LOAN;
+
+public class HoldByBarcodeResource extends Resource {
+ private static final String rootPath = "/circulation/hold-by-barcode-for-use-at-location";
+
+ public HoldByBarcodeResource(HttpClient client) {
+ super(client);
+ }
+
+ @Override
+ public void register(Router router) {
+ new RouteRegistration(rootPath, router).create(this::markHeld);
+ }
+
+ private void markHeld(RoutingContext routingContext) {
+ final WebContext webContext = new WebContext(routingContext);
+ final Clients clients = Clients.create(webContext, client);
+ final OkapiPermissions okapiPermissions = OkapiPermissions.from(webContext.getHeaders());
+ final CirculationErrorHandler errorHandler = new OverridingErrorHandler(okapiPermissions);
+ final var itemRepository = new ItemRepository(clients);
+ final var userRepository = new UserRepository(clients);
+ final var loanRepository = new LoanRepository(clients, itemRepository, userRepository);
+ final EventPublisher eventPublisher = new EventPublisher(routingContext);
+
+ JsonObject requestBodyAsJson = routingContext.body().asJsonObject();
+ Result requestResult = HoldByBarcodeRequest.buildRequestFrom(requestBodyAsJson);
+
+ requestResult
+ .after(request -> findLoan(request, loanRepository, itemRepository, userRepository, errorHandler))
+ .thenApply(loan -> failWhenOpenLoanNotFoundForItem(loan, requestResult.value()))
+ .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())))
+ .thenCompose(loanResult -> loanResult.after(
+ loan -> eventPublisher.publishUsageAtLocationEvent(loan, LogEventType.HELD_FOR_USE_AT_LOCATION)))
+ .thenApply(loanResult -> loanResult.map(Loan::asJson))
+ .thenApply(loanAsJsonResult -> loanAsJsonResult.map(this::toResponse))
+ .thenAccept(webContext::writeResultToHttpResponse);
+ }
+
+ protected CompletableFuture> findLoan(HoldByBarcodeRequest request,
+ LoanRepository loanRepository,
+ ItemRepository itemRepository,
+ UserRepository userRepository,
+ CirculationErrorHandler errorHandler) {
+
+ final ItemByBarcodeInStorageFinder itemFinder =
+ new ItemByBarcodeInStorageFinder(itemRepository);
+
+ final SingleOpenLoanForItemInStorageFinder loanFinder =
+ new SingleOpenLoanForItemInStorageFinder(loanRepository, userRepository, false);
+
+ return itemFinder.findItemByBarcode(request.getItemBarcode())
+ .thenCompose(itemResult -> itemResult.after(loanFinder::findSingleOpenLoan)
+ .thenApply(r -> errorHandler.handleValidationResult(r, FAILED_TO_FIND_SINGLE_OPEN_LOAN, (Loan) null))
+ );
+ }
+
+ private Result failWhenOpenLoanNotFoundForItem (Result loanResult, HoldByBarcodeRequest request) {
+ return loanResult.failWhen(this::loanIsNull, loan -> noOpenLoanFailure(request).get());
+ }
+
+ private Result loanIsNull (Loan loan) {
+ return Result.succeeded(loan == null);
+ }
+
+ private static Supplier noOpenLoanFailure(HoldByBarcodeRequest request) {
+ return () -> new BadRequestFailure(
+ format("No open loan found for the item barcode (%s)", request.getItemBarcode())
+ );
+ }
+
+ private HttpResponse toResponse(JsonObject body) {
+ return JsonHttpResponse.ok(body,
+ format("/circulation/loans/%s", body.getString("id")));
+ }
+
+
+}
diff --git a/src/main/java/org/folio/circulation/resources/foruseatlocation/PickUpByBarcodeResource.java b/src/main/java/org/folio/circulation/resources/foruseatlocation/PickUpByBarcodeResource.java
new file mode 100644
index 0000000000..4f521de143
--- /dev/null
+++ b/src/main/java/org/folio/circulation/resources/foruseatlocation/PickUpByBarcodeResource.java
@@ -0,0 +1,112 @@
+package org.folio.circulation.resources.foruseatlocation;
+
+import io.vertx.core.http.HttpClient;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.web.Router;
+import io.vertx.ext.web.RoutingContext;
+import org.folio.circulation.domain.Loan;
+import org.folio.circulation.domain.LoanAction;
+import org.folio.circulation.domain.representations.logs.LogEventType;
+import org.folio.circulation.infrastructure.storage.inventory.ItemRepository;
+import org.folio.circulation.infrastructure.storage.loans.LoanRepository;
+import org.folio.circulation.infrastructure.storage.users.UserRepository;
+import org.folio.circulation.resources.Resource;
+import org.folio.circulation.resources.handlers.error.CirculationErrorHandler;
+import org.folio.circulation.resources.handlers.error.OverridingErrorHandler;
+import org.folio.circulation.services.EventPublisher;
+import org.folio.circulation.storage.SingleOpenLoanByUserAndItemBarcodeFinder;
+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 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_IN_USE;
+import static org.folio.circulation.resources.foruseatlocation.PickupByBarcodeRequest.buildRequestFrom;
+
+import static org.folio.circulation.resources.handlers.error.CirculationErrorType.FAILED_TO_FIND_SINGLE_OPEN_LOAN;
+
+public class PickUpByBarcodeResource extends Resource {
+
+ private static final String rootPath = "/circulation/pickup-by-barcode-for-use-at-location";
+
+ public PickUpByBarcodeResource(HttpClient client) {
+ super(client);
+ }
+
+ @Override
+ public void register(Router router) {
+ new RouteRegistration(rootPath, router).create(this::markInUse);
+ }
+
+ private void markInUse(RoutingContext routingContext) {
+ final WebContext webContext = new WebContext(routingContext);
+ final Clients clients = Clients.create(webContext, client);
+ final OkapiPermissions okapiPermissions = OkapiPermissions.from(webContext.getHeaders());
+ final CirculationErrorHandler errorHandler = new OverridingErrorHandler(okapiPermissions);
+ final var itemRepository = new ItemRepository(clients);
+ final var userRepository = new UserRepository(clients);
+ final var loanRepository = new LoanRepository(clients, itemRepository, userRepository);
+ final EventPublisher eventPublisher = new EventPublisher(routingContext);
+
+ JsonObject requestBodyAsJson = routingContext.body().asJsonObject();
+ Result pickupByBarcodeRequest = buildRequestFrom(requestBodyAsJson);
+
+ pickupByBarcodeRequest
+ .after(request -> findLoan(request, loanRepository, itemRepository, userRepository, errorHandler))
+ .thenApply(loan -> failWhenOpenLoanForItemAndUserNotFound(loan, pickupByBarcodeRequest.value()))
+ .thenApply(loanResult -> loanResult.map(loan ->
+ loan.changeStatusOfUsageAtLocation(USAGE_STATUS_IN_USE)
+ .withAction(LoanAction.PICKED_UP_FOR_USE_AT_LOCATION)))
+ .thenCompose(loanResult -> loanResult.after(
+ loan -> loanRepository.updateLoan(loanResult.value())))
+ .thenCompose(loanResult -> loanResult.after(
+ loan -> eventPublisher.publishUsageAtLocationEvent(loan, LogEventType.PICKED_UP_FOR_USE_AT_LOCATION)))
+ .thenApply(loanResult -> loanResult.map(Loan::asJson))
+ .thenApply(jsonResult -> jsonResult.map(this::toResponse))
+ .thenAccept(webContext::writeResultToHttpResponse);
+ }
+
+ protected CompletableFuture> findLoan(PickupByBarcodeRequest request,
+ LoanRepository loanRepository,
+ ItemRepository itemRepository,
+ UserRepository userRepository,
+ CirculationErrorHandler errorHandler) {
+
+ final SingleOpenLoanByUserAndItemBarcodeFinder loanFinder
+ = new SingleOpenLoanByUserAndItemBarcodeFinder(loanRepository,
+ itemRepository, userRepository);
+
+ return loanFinder.findLoan(request.getItemBarcode(), request.getUserBarcode())
+ .thenApply(r -> errorHandler.handleValidationResult(r, FAILED_TO_FIND_SINGLE_OPEN_LOAN, r.value()));
+ }
+
+ private HttpResponse toResponse(JsonObject body) {
+ return JsonHttpResponse.ok(body,
+ format("/circulation/loans/%s", body.getString("id")));
+ }
+
+ private Result failWhenOpenLoanForItemAndUserNotFound (Result loanResult, PickupByBarcodeRequest request) {
+ return loanResult.failWhen(this::loanIsNull, loan -> noOpenLoanFailure(request).get());
+ }
+
+ private Result loanIsNull (Loan loan) {
+ return Result.succeeded(loan == null);
+ }
+
+ private static Supplier noOpenLoanFailure(PickupByBarcodeRequest request) {
+ return () -> new BadRequestFailure(
+ format("No open loan found for item barcode (%s) and user (%s)",
+ request.getItemBarcode(), request.getUserBarcode())
+ );
+ }
+
+}
diff --git a/src/main/java/org/folio/circulation/resources/foruseatlocation/PickupByBarcodeRequest.java b/src/main/java/org/folio/circulation/resources/foruseatlocation/PickupByBarcodeRequest.java
new file mode 100644
index 0000000000..0666a72cb4
--- /dev/null
+++ b/src/main/java/org/folio/circulation/resources/foruseatlocation/PickupByBarcodeRequest.java
@@ -0,0 +1,40 @@
+package org.folio.circulation.resources.foruseatlocation;
+
+import io.vertx.core.json.JsonObject;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.folio.circulation.support.results.Result;
+
+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 PickupByBarcodeRequest {
+ private static final String ITEM_BARCODE = "itemBarcode";
+ private static final String USER_BARCODE = "userBarcode";
+
+ private final String itemBarcode;
+ private final String userBarcode;
+
+ static Result buildRequestFrom(JsonObject json) {
+ final String itemBarcode = getProperty(json, ITEM_BARCODE);
+
+ if (isBlank(itemBarcode)) {
+ return failedValidation("Request to pick up from hold shelf must have an item barcode",
+ ITEM_BARCODE, null);
+ }
+
+ final String userBarcode = getProperty(json, USER_BARCODE);
+
+ if (isBlank(userBarcode)) {
+ return failedValidation("Request to pick up from hold shelf must have a user barcode",
+ USER_BARCODE, null);
+ }
+
+ return succeeded(new PickupByBarcodeRequest(itemBarcode, userBarcode));
+ }
+}
+
diff --git a/src/main/java/org/folio/circulation/services/EventPublisher.java b/src/main/java/org/folio/circulation/services/EventPublisher.java
index 3a34524b04..e1060e58a2 100644
--- a/src/main/java/org/folio/circulation/services/EventPublisher.java
+++ b/src/main/java/org/folio/circulation/services/EventPublisher.java
@@ -377,6 +377,11 @@ public CompletableFuture> publishNoticeLogEvent(NoticeLogContext no
return publishLogRecord(noticeLogContext.withDate(getZonedDateTime()).asJson(), eventType);
}
+ public CompletableFuture> publishUsageAtLocationEvent(Loan loan, LogEventType eventType) {
+ return publishLogRecord((LoanLogContext.from(loan)).asJson(), eventType)
+ .thenApply(r -> succeeded(loan));
+ }
+
public CompletableFuture> publishNoticeErrorLogEvent(
NoticeLogContext noticeLogContext, HttpFailure error) {
diff --git a/src/main/java/org/folio/circulation/storage/SingleOpenLoanByUserAndItemBarcodeFinder.java b/src/main/java/org/folio/circulation/storage/SingleOpenLoanByUserAndItemBarcodeFinder.java
index c2792a1779..8837693ff5 100644
--- a/src/main/java/org/folio/circulation/storage/SingleOpenLoanByUserAndItemBarcodeFinder.java
+++ b/src/main/java/org/folio/circulation/storage/SingleOpenLoanByUserAndItemBarcodeFinder.java
@@ -49,7 +49,6 @@ public CompletableFuture> findLoan(
private Function>> refuseWhenUserDoesNotMatch(
String userBarcode) {
-
return loan -> refuseWhenUserDoesNotMatch(loan, userBarcode);
}
diff --git a/src/test/java/api/loans/LoansForUseAtLocationTests.java b/src/test/java/api/loans/LoansForUseAtLocationTests.java
new file mode 100644
index 0000000000..8b495b2db1
--- /dev/null
+++ b/src/test/java/api/loans/LoansForUseAtLocationTests.java
@@ -0,0 +1,242 @@
+package api.loans;
+
+import api.support.APITests;
+import api.support.builders.*;
+import api.support.http.IndividualResource;
+import api.support.http.ItemResource;
+import api.support.http.UserResource;
+import io.vertx.core.json.JsonObject;
+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.util.Collections;
+import java.util.UUID;
+
+import static api.support.fixtures.ItemExamples.basedUponSmallAngryPlanet;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class LoansForUseAtLocationTests extends APITests {
+ private ItemResource item;
+ private UserResource borrower;
+
+ @BeforeEach
+ void beforeEach() {
+
+ HoldingBuilder holdingsBuilder = itemsFixture.applyCallNumberHoldings(
+ "CN",
+ "Prefix",
+ "Suffix",
+ Collections.singletonList("CopyNumbers"));
+
+ final UUID servicePointId = servicePointsFixture.cd1().getId();
+
+ final IndividualResource homeLocation = locationsFixture.basedUponExampleLocation(
+ item -> item.withPrimaryServicePoint(servicePointId));
+
+ ItemBuilder itemBuilder = basedUponSmallAngryPlanet(
+ materialTypesFixture.book().getId(),
+ loanTypesFixture.canCirculate().getId())
+ .withPermanentLocation(homeLocation);
+
+ item = itemsFixture.basedUponSmallAngryPlanet(itemBuilder, holdingsBuilder);
+
+ borrower = usersFixture.steve();
+ }
+
+ @Test
+ void willSetAtLocationUsageStatusToInUseOnCheckout() {
+ 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);
+
+ final IndividualResource response = checkOutFixture.checkOutByBarcode(
+ new CheckOutByBarcodeRequestBuilder()
+ .forItem(item)
+ .to(borrower)
+ .at(servicePointsFixture.cd1()));
+
+ JsonObject loan = loansFixture.getLoanById(response.getId()).getJson();
+ JsonObject forUseAtLocation = loan.getJsonObject("forUseAtLocation");
+ assertThat("loan.forUseAtLocation",
+ forUseAtLocation, notNullValue());
+ assertThat("loan.forUseAtLocation.status",
+ forUseAtLocation.getString("status"), Is.is("In use"));
+ }
+
+ @Test
+ void willMarkItemHeldByBarcode() {
+ 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(servicePointsFixture.cd1()));
+
+ Response holdResponse = holdForUseAtLocationFixture.holdForUseAtLocation(
+ new HoldByBarcodeRequestBuilder(item.getBarcode()));
+
+ JsonObject forUseAtLocation = holdResponse.getJson().getJsonObject("forUseAtLocation");
+ assertThat("loan.forUseAtLocation",
+ forUseAtLocation, notNullValue());
+ assertThat("loan.forUseAtLocation.status",
+ forUseAtLocation.getString("status"), Is.is("Held"));
+ }
+
+ @Test
+ void holdWillFailWithDifferentItem() {
+ 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(servicePointsFixture.cd1()));
+
+ holdForUseAtLocationFixture.holdForUseAtLocation(
+ new HoldByBarcodeRequestBuilder("different-item"), 400);
+ }
+
+ @Test
+ void holdWillFailWithIncompleteRequest() {
+ 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(servicePointsFixture.cd1()));
+
+ holdForUseAtLocationFixture.holdForUseAtLocation(
+ new HoldByBarcodeRequestBuilder(null), 422);
+ }
+
+
+ @Test
+ void willMarkItemInUseByBarcode() {
+ 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(servicePointsFixture.cd1()));
+
+ Response pickupResponse = pickupForUseAtLocationFixture.pickupForUseAtLocation(
+ new PickupByBarcodeRequestBuilder(item.getBarcode(), borrower.getBarcode()));
+
+ JsonObject forUseAtLocation = pickupResponse.getJson().getJsonObject("forUseAtLocation");
+ assertThat("loan.forUseAtLocation",
+ forUseAtLocation, notNullValue());
+ assertThat("loan.forUseAtLocation.status",
+ forUseAtLocation.getString("status"), Is.is("In use"));
+ }
+
+ @Test
+ void pickupWillFailWithDifferentItemOrDifferentUser() {
+ 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(servicePointsFixture.cd1()));
+
+ pickupForUseAtLocationFixture.pickupForUseAtLocation(
+ new PickupByBarcodeRequestBuilder("different-item", borrower.getBarcode()), 400);
+
+ pickupForUseAtLocationFixture.pickupForUseAtLocation(
+ new PickupByBarcodeRequestBuilder(item.getBarcode(), "different-user"), 400);
+
+ }
+
+ @Test
+ void pickupWillFailWithIncompleteRequestObject() {
+ 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(servicePointsFixture.cd1()));
+
+ pickupForUseAtLocationFixture.pickupForUseAtLocation(
+ new PickupByBarcodeRequestBuilder(null, borrower.getBarcode()), 422);
+
+ pickupForUseAtLocationFixture.pickupForUseAtLocation(
+ new PickupByBarcodeRequestBuilder(item.getBarcode(), null), 422);
+ }
+
+ @Test
+ void willSetAtLocationUsageStatusToReturnedOnCheckIn() {
+ 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(servicePointsFixture.cd1()));
+
+ final IndividualResource response = checkInFixture.checkInByBarcode(item);
+
+ JsonObject loan = loansFixture.getLoanById(
+ UUID.fromString(response.getJson().getJsonObject("loan").getString("id")))
+ .getJson();
+ JsonObject forUseAtLocation = loan.getJsonObject("forUseAtLocation");
+ assertThat("loan.forUseAtLocation",
+ forUseAtLocation, notNullValue());
+ assertThat("loan.forUseAtLocation.status",
+ forUseAtLocation.getString("status"), Is.is("Returned"));
+ }
+
+}
diff --git a/src/test/java/api/support/APITests.java b/src/test/java/api/support/APITests.java
index cb85e1196d..10883816fe 100644
--- a/src/test/java/api/support/APITests.java
+++ b/src/test/java/api/support/APITests.java
@@ -58,6 +58,8 @@
import api.support.fixtures.FeeFineAccountFixture;
import api.support.fixtures.FeeFineOwnerFixture;
import api.support.fixtures.FeeFineTypeFixture;
+import api.support.fixtures.ForUseAtLocationHoldFixture;
+import api.support.fixtures.ForUseAtLocationPickupFixture;
import api.support.fixtures.HoldingsFixture;
import api.support.fixtures.IdentifierTypesFixture;
import api.support.fixtures.InstancesFixture;
@@ -309,6 +311,9 @@ public abstract class APITests {
protected final ConfigurationsFixture configurationsFixture = new ConfigurationsFixture(configClient);
protected final SearchInstanceFixture searchFixture = new SearchInstanceFixture();
+ protected final ForUseAtLocationHoldFixture holdForUseAtLocationFixture = new ForUseAtLocationHoldFixture();
+ protected final ForUseAtLocationPickupFixture pickupForUseAtLocationFixture = new ForUseAtLocationPickupFixture();
+
protected APITests() {
this(true, false);
}
diff --git a/src/test/java/api/support/builders/HoldByBarcodeRequestBuilder.java b/src/test/java/api/support/builders/HoldByBarcodeRequestBuilder.java
new file mode 100644
index 0000000000..3e72fdd98d
--- /dev/null
+++ b/src/test/java/api/support/builders/HoldByBarcodeRequestBuilder.java
@@ -0,0 +1,19 @@
+package api.support.builders;
+
+import io.vertx.core.json.JsonObject;
+
+public class HoldByBarcodeRequestBuilder extends JsonBuilder implements Builder {
+ private final String itemBarcode;
+
+ public HoldByBarcodeRequestBuilder(String itemBarcode) {
+ this.itemBarcode = itemBarcode;
+ }
+ @Override
+ public JsonObject create() {
+ final JsonObject request = new JsonObject();
+
+ put(request, "itemBarcode", this.itemBarcode);
+ return request;
+ }
+}
+
diff --git a/src/test/java/api/support/builders/LoanPolicyBuilder.java b/src/test/java/api/support/builders/LoanPolicyBuilder.java
index 7fb031582a..0eb352ad87 100644
--- a/src/test/java/api/support/builders/LoanPolicyBuilder.java
+++ b/src/test/java/api/support/builders/LoanPolicyBuilder.java
@@ -40,6 +40,7 @@ public class LoanPolicyBuilder extends JsonBuilder implements Builder {
private final Period alternateCheckoutLoanPeriod;
private final Integer itemLimit;
private final Period gracePeriod;
+ private final boolean forUseAtLocation;
public LoanPolicyBuilder() {
this(UUID.randomUUID(),
@@ -65,7 +66,8 @@ public LoanPolicyBuilder() {
null,
null,
null,
- null
+ null,
+ false
);
}
@@ -88,6 +90,7 @@ public JsonObject create() {
put(loansPolicy, "profileId", loansProfile);
put(loansPolicy, "itemLimit", itemLimit);
putIfNotNull(loansPolicy, "gracePeriod", gracePeriod, Period::asJson);
+ put(loansPolicy, "forUseAtLocation", forUseAtLocation);
//TODO: Replace with sub-builders
if(Objects.equals(loansProfile, "Rolling")) {
@@ -289,4 +292,5 @@ public LoanPolicyBuilder withHolds(Period checkoutPeriod,
return withHolds(json);
}
+
}
diff --git a/src/test/java/api/support/builders/PickupByBarcodeRequestBuilder.java b/src/test/java/api/support/builders/PickupByBarcodeRequestBuilder.java
new file mode 100644
index 0000000000..bbaaa23c08
--- /dev/null
+++ b/src/test/java/api/support/builders/PickupByBarcodeRequestBuilder.java
@@ -0,0 +1,23 @@
+package api.support.builders;
+
+import io.vertx.core.json.JsonObject;
+
+public class PickupByBarcodeRequestBuilder extends JsonBuilder implements Builder {
+ private final String itemBarcode;
+ private final String userBarcode;
+
+ public PickupByBarcodeRequestBuilder(
+ String itemBarcode,
+ String userBarcode) {
+ this.itemBarcode = itemBarcode;
+ this.userBarcode = userBarcode;
+ }
+ @Override
+ public JsonObject create() {
+ final JsonObject request = new JsonObject();
+
+ put(request, "itemBarcode", this.itemBarcode);
+ put(request, "userBarcode", this.userBarcode);
+ return request;
+ }
+}
diff --git a/src/test/java/api/support/fixtures/ForUseAtLocationHoldFixture.java b/src/test/java/api/support/fixtures/ForUseAtLocationHoldFixture.java
new file mode 100644
index 0000000000..bc0d31112a
--- /dev/null
+++ b/src/test/java/api/support/fixtures/ForUseAtLocationHoldFixture.java
@@ -0,0 +1,27 @@
+package api.support.fixtures;
+
+import api.support.RestAssuredClient;
+import api.support.builders.HoldByBarcodeRequestBuilder;
+import org.folio.circulation.support.http.client.Response;
+
+import static api.support.APITestContext.getOkapiHeadersFromContext;
+import static api.support.http.InterfaceUrls.holdForUseAtLocationUrl;
+
+public class ForUseAtLocationHoldFixture {
+ private final RestAssuredClient restAssuredClient;
+
+ public ForUseAtLocationHoldFixture() {
+ restAssuredClient = new RestAssuredClient(getOkapiHeadersFromContext());
+ }
+
+ public Response holdForUseAtLocation(HoldByBarcodeRequestBuilder builder) {
+ return restAssuredClient.post(builder.create(), holdForUseAtLocationUrl(), 200,
+ "hold-for-use-at-location-by-barcode-request");
+ }
+
+ public void holdForUseAtLocation(HoldByBarcodeRequestBuilder builder, int expectedStatusCode) {
+ restAssuredClient.post(builder.create(), holdForUseAtLocationUrl(), expectedStatusCode,
+ "hold-for-use-at-location-by-barcode-request");
+ }
+
+}
diff --git a/src/test/java/api/support/fixtures/ForUseAtLocationPickupFixture.java b/src/test/java/api/support/fixtures/ForUseAtLocationPickupFixture.java
new file mode 100644
index 0000000000..2fc5a504b3
--- /dev/null
+++ b/src/test/java/api/support/fixtures/ForUseAtLocationPickupFixture.java
@@ -0,0 +1,27 @@
+package api.support.fixtures;
+
+import api.support.RestAssuredClient;
+import api.support.builders.PickupByBarcodeRequestBuilder;
+import org.folio.circulation.support.http.client.Response;
+
+import static api.support.APITestContext.getOkapiHeadersFromContext;
+import static api.support.http.InterfaceUrls.pickupForUseAtLocationUrl;
+
+public class ForUseAtLocationPickupFixture {
+ private final RestAssuredClient restAssuredClient;
+
+ public ForUseAtLocationPickupFixture() {
+ restAssuredClient = new RestAssuredClient(getOkapiHeadersFromContext());
+ }
+
+ public Response pickupForUseAtLocation(PickupByBarcodeRequestBuilder builder) {
+ return restAssuredClient.post(builder.create(), pickupForUseAtLocationUrl(), 200,
+ "pickup-for-use-at-location-by-barcode-request");
+ }
+
+ public void pickupForUseAtLocation(PickupByBarcodeRequestBuilder builder, int expectedStatusCode) {
+ restAssuredClient.post(builder.create(), pickupForUseAtLocationUrl(), expectedStatusCode,
+ "pickup-for-use-at-location-by-barcode-request");
+ }
+
+}
diff --git a/src/test/java/api/support/http/InterfaceUrls.java b/src/test/java/api/support/http/InterfaceUrls.java
index 2b4560793e..5abfcb039f 100644
--- a/src/test/java/api/support/http/InterfaceUrls.java
+++ b/src/test/java/api/support/http/InterfaceUrls.java
@@ -162,6 +162,14 @@ public static URL renewByIdUrl() {
return circulationModuleUrl("/circulation/renew-by-id");
}
+ public static URL holdForUseAtLocationUrl() {
+ return circulationModuleUrl("/circulation/hold-by-barcode-for-use-at-location");
+ }
+
+ public static URL pickupForUseAtLocationUrl() {
+ return circulationModuleUrl("/circulation/pickup-by-barcode-for-use-at-location");
+ }
+
public static URL loansUrl() {
return loansUrl("");
}
diff --git a/src/test/resources/storage-loan-7-3.json b/src/test/resources/storage-loan-7-3.json
index 136137f8af..5d419de379 100644
--- a/src/test/resources/storage-loan-7-3.json
+++ b/src/test/resources/storage-loan-7-3.json
@@ -37,6 +37,26 @@
},
"additionalProperties": false
},
+ "forUseAtLocation": {
+ "description": "Status of loan/item that is to be used in the library",
+ "type": "object",
+ "properties": {
+ "status": {
+ "description": "Indicates if the item is currently used by or being held for the patron",
+ "type": "string",
+ "enum": [
+ "In use",
+ "Held",
+ "Returned"
+ ]
+ },
+ "statusDate": {
+ "description": "Date and time the status was registered",
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ },
"loanDate": {
"description": "Date time when the loan began (typically represented according to rfc3339 section-5.6. Has not had the date-time format validation applied as was not supported at point of introduction and would now be a breaking change)",
"type": "string"