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"