From c6e532ba65e85e18cdb489998a03b32ced6c9b5d Mon Sep 17 00:00:00 2001 From: nielserik Date: Mon, 26 May 2025 18:41:05 +0200 Subject: [PATCH] CIRC-2308 Add permissions, a validation, tests. --- descriptors/ModuleDescriptor-template.json | 39 ++++++++++++++++++- .../circulation/CirculationVerticle.java | 4 +- .../HoldByBarcodeResource.java | 21 ++++++++-- ...urce.java => PickupByBarcodeResource.java} | 32 +++++++++++---- .../api/loans/LoansForUseAtLocationTests.java | 38 ++++++++++++++++++ 5 files changed, 119 insertions(+), 15 deletions(-) rename src/main/java/org/folio/circulation/resources/foruseatlocation/{PickUpByBarcodeResource.java => PickupByBarcodeResource.java} (81%) diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index b035324c8f..2a5afd9457 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -277,14 +277,20 @@ "POST" ], "pathPattern": "/circulation/pickup-by-barcode-for-use-at-location", - "permissionsRequired": [] + "permissionsRequired": ["circulation.pickup-by-barcode-for-use-at-location.post"], + "modulePermissions": [ + "circulation.usage-at-location.all" + ] }, { "methods": [ "POST" ], "pathPattern": "/circulation/hold-by-barcode-for-use-at-location", - "permissionsRequired": [] + "permissionsRequired": ["circulation.hold-by-barcode-for-use-at-location.post"], + "modulePermissions": [ + "circulation.usage-at-location.all" + ] }, { "methods": [ @@ -1452,6 +1458,16 @@ "displayName": "circulation - renew loan using id", "description": "renew a loan using IDs for item and loanee" }, + { + "permissionName": "circulation.pickup-by-barcode-for-use-at-location.post", + "displayName": "circulation - pick up from hold shelf for use at location", + "description": "pick up item of an existing loan from hold shelf for use at location (i.e. in reading room)" + }, + { + "permissionName": "circulation.hold-by-barcode-for-use-at-location.post", + "displayName": "circulation - put item on hold shelf for another use at location", + "description": "put the item of an existing loan on the hold shelf for later use at location (i.e. in reading room)" + }, { "permissionName": "circulation.loans.collection.get", "displayName": "circulation - get loan collection", @@ -1736,6 +1752,8 @@ "circulation.check-in-by-barcode.post", "circulation.renew-by-barcode.post", "circulation.renew-by-id.post", + "circulation.hold-by-barcode-for-use-at-location.post", + "circulation.pickup-by-barcode-for-use-at-location.post", "circulation.loans.collection.get", "circulation.loans.item.get", "circulation.loans.item.post", @@ -2116,6 +2134,23 @@ "visible": false, "replaces": ["circulation.renew-loan"] }, + { + "permissionName": "circulation.usage-at-location.all", + "displayName" : "Put item on hold or pick it up for use at location", + "description" : "Permissions needed to take an item on or off hold shelf for use at location", + "subPermissions": [ + "circulation-storage.loans.item.put", + "circulation-storage.loans.item.get", + "circulation-storage.loans.collection.get", + "circulation-storage.loan-policies.item.get", + "circulation-storage.loan-policies.collection.get", + "circulation.internal.fetch-items.collection.get", + "users.item.get", + "users.collection.get", + "pubsub.publish.post" + ], + "visible": false + }, { "permissionName": "perms.circulation.loans.anonymize.all", "displayName" : "module permissions for one op", diff --git a/src/main/java/org/folio/circulation/CirculationVerticle.java b/src/main/java/org/folio/circulation/CirculationVerticle.java index 207f19d4a7..f0606dbb8c 100644 --- a/src/main/java/org/folio/circulation/CirculationVerticle.java +++ b/src/main/java/org/folio/circulation/CirculationVerticle.java @@ -53,7 +53,7 @@ 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.resources.foruseatlocation.PickupByBarcodeResource; import org.folio.circulation.support.logging.LogHelper; import org.folio.circulation.support.logging.Logging; @@ -100,7 +100,7 @@ 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 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/resources/foruseatlocation/HoldByBarcodeResource.java b/src/main/java/org/folio/circulation/resources/foruseatlocation/HoldByBarcodeResource.java index e124ed21ba..b02b096125 100644 --- a/src/main/java/org/folio/circulation/resources/foruseatlocation/HoldByBarcodeResource.java +++ b/src/main/java/org/folio/circulation/resources/foruseatlocation/HoldByBarcodeResource.java @@ -61,6 +61,7 @@ private void markHeld(RoutingContext routingContext) { requestResult .after(request -> findLoan(request, loanRepository, itemRepository, userRepository, errorHandler)) .thenApply(loan -> failWhenOpenLoanNotFoundForItem(loan, requestResult.value())) + .thenApply(loan -> failWhenOpenLoanIsNotForUseAtLocation(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( @@ -90,20 +91,34 @@ protected CompletableFuture> findLoan(HoldByBarcodeRequest request, ); } - private Result failWhenOpenLoanNotFoundForItem (Result loanResult, HoldByBarcodeRequest request) { - return loanResult.failWhen(this::loanIsNull, loan -> noOpenLoanFailure(request).get()); + private static Result failWhenOpenLoanNotFoundForItem (Result loanResult, HoldByBarcodeRequest request) { + return loanResult.failWhen(HoldByBarcodeResource::loanIsNull, loan -> noOpenLoanFailure(request).get()); } - private Result loanIsNull (Loan loan) { + private Result failWhenOpenLoanIsNotForUseAtLocation (Result loanResult, HoldByBarcodeRequest request) { + return loanResult.failWhen(HoldByBarcodeResource::loanIsNotForUseAtLocation, loan -> loanIsNotForUseAtLocationFailure(request).get()); + } + + private static Result loanIsNull (Loan loan) { return Result.succeeded(loan == null); } + private static Result loanIsNotForUseAtLocation(Loan loan) { + return Result.succeeded(!loan.isForUseAtLocation()); + } + private static Supplier noOpenLoanFailure(HoldByBarcodeRequest request) { return () -> new BadRequestFailure( format("No open loan found for the item barcode (%s)", request.getItemBarcode()) ); } + private static Supplier loanIsNotForUseAtLocationFailure(HoldByBarcodeRequest request) { + return () -> new BadRequestFailure( + format("The loan is open but is not for use at location, 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 similarity index 81% rename from src/main/java/org/folio/circulation/resources/foruseatlocation/PickUpByBarcodeResource.java rename to src/main/java/org/folio/circulation/resources/foruseatlocation/PickupByBarcodeResource.java index 4f521de143..9b7e680579 100644 --- a/src/main/java/org/folio/circulation/resources/foruseatlocation/PickUpByBarcodeResource.java +++ b/src/main/java/org/folio/circulation/resources/foruseatlocation/PickupByBarcodeResource.java @@ -34,11 +34,11 @@ import static org.folio.circulation.resources.handlers.error.CirculationErrorType.FAILED_TO_FIND_SINGLE_OPEN_LOAN; -public class PickUpByBarcodeResource extends Resource { +public class PickupByBarcodeResource extends Resource { private static final String rootPath = "/circulation/pickup-by-barcode-for-use-at-location"; - public PickUpByBarcodeResource(HttpClient client) { + public PickupByBarcodeResource(HttpClient client) { super(client); } @@ -63,6 +63,7 @@ private void markInUse(RoutingContext routingContext) { pickupByBarcodeRequest .after(request -> findLoan(request, loanRepository, itemRepository, userRepository, errorHandler)) .thenApply(loan -> failWhenOpenLoanForItemAndUserNotFound(loan, pickupByBarcodeRequest.value())) + .thenApply(loan -> failWhenOpenLoanIsNotForUseAtLocation(loan, pickupByBarcodeRequest.value())) .thenApply(loanResult -> loanResult.map(loan -> loan.changeStatusOfUsageAtLocation(USAGE_STATUS_IN_USE) .withAction(LoanAction.PICKED_UP_FOR_USE_AT_LOCATION))) @@ -89,19 +90,22 @@ protected CompletableFuture> findLoan(PickupByBarcodeRequest reques .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 static Result failWhenOpenLoanForItemAndUserNotFound (Result loanResult, PickupByBarcodeRequest request) { + return loanResult.failWhen(PickupByBarcodeResource::loanIsNull, loan -> noOpenLoanFailure(request).get()); } - private Result failWhenOpenLoanForItemAndUserNotFound (Result loanResult, PickupByBarcodeRequest request) { - return loanResult.failWhen(this::loanIsNull, loan -> noOpenLoanFailure(request).get()); + private static Result failWhenOpenLoanIsNotForUseAtLocation (Result loanResult, PickupByBarcodeRequest request) { + return loanResult.failWhen(PickupByBarcodeResource::loanIsNotForUseAtLocation, loan -> loanIsNotForUseAtLocationFailure(request).get()); } - private Result loanIsNull (Loan loan) { + private static Result loanIsNull (Loan loan) { return Result.succeeded(loan == null); } + private static Result loanIsNotForUseAtLocation(Loan loan) { + return Result.succeeded(!loan.isForUseAtLocation()); + } + private static Supplier noOpenLoanFailure(PickupByBarcodeRequest request) { return () -> new BadRequestFailure( format("No open loan found for item barcode (%s) and user (%s)", @@ -109,4 +113,16 @@ private static Supplier noOpenLoanFailure(PickupByBarcodeRequest re ); } + private static Supplier loanIsNotForUseAtLocationFailure(PickupByBarcodeRequest request) { + return () -> new BadRequestFailure( + format("The loan is open but is not for use at location, 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/test/java/api/loans/LoansForUseAtLocationTests.java b/src/test/java/api/loans/LoansForUseAtLocationTests.java index 8b495b2db1..688828615c 100644 --- a/src/test/java/api/loans/LoansForUseAtLocationTests.java +++ b/src/test/java/api/loans/LoansForUseAtLocationTests.java @@ -117,6 +117,25 @@ void holdWillFailWithDifferentItem() { new HoldByBarcodeRequestBuilder("different-item"), 400); } + @Test + void holdWillFailIfLoanIsNotForUseAtLocation() { + final LoanPolicyBuilder homeLoansPolicyBuilder = new LoanPolicyBuilder() + .withName("Home loans") + .withDescription("Policy for items that can be taken home") + .rolling(Period.days(30)); + + use(homeLoansPolicyBuilder); + + checkOutFixture.checkOutByBarcode( + new CheckOutByBarcodeRequestBuilder() + .forItem(item) + .to(borrower) + .at(servicePointsFixture.cd1())); + + holdForUseAtLocationFixture.holdForUseAtLocation( + new HoldByBarcodeRequestBuilder(item.getBarcode()), 400); + } + @Test void holdWillFailWithIncompleteRequest() { final LoanPolicyBuilder forUseAtLocationPolicyBuilder = new LoanPolicyBuilder() @@ -211,6 +230,25 @@ void pickupWillFailWithIncompleteRequestObject() { new PickupByBarcodeRequestBuilder(item.getBarcode(), null), 422); } + @Test + void pickupWillFailIfLoanIsNotForUseAtLocation() { + final LoanPolicyBuilder homeLoansPolicyBuilder = new LoanPolicyBuilder() + .withName("Home loans") + .withDescription("Policy for items that can be taken home") + .rolling(Period.days(30)); + + use(homeLoansPolicyBuilder); + + checkOutFixture.checkOutByBarcode( + new CheckOutByBarcodeRequestBuilder() + .forItem(item) + .to(borrower) + .at(servicePointsFixture.cd1())); + + pickupForUseAtLocationFixture.pickupForUseAtLocation( + new PickupByBarcodeRequestBuilder(item.getBarcode(), borrower.getBarcode()), 400); + } + @Test void willSetAtLocationUsageStatusToReturnedOnCheckIn() { final LoanPolicyBuilder forUseAtLocationPolicyBuilder = new LoanPolicyBuilder()