From fb01b921423b31947047d429034081b44e718a34 Mon Sep 17 00:00:00 2001 From: SreejaMangarapu <164345887+SreejaMangarapu@users.noreply.github.com> Date: Thu, 6 Jun 2024 18:50:18 +0530 Subject: [PATCH 1/3] Circ 2084 (#1475) * CIRC-2084 changes required for dcb loan event * CIRC-2084 changes required for dcb loan event * CIRC-2084 added test cases for dcb loan * CIRC-2084 changes made in LoanRepository.java * CIRC-2084 Added isDcb property in loan.json and changed loan-storage version to 7.3 in ModuleDescriptor * CIRC-2084 renamed storage-loan-7-2.json to storage-loan-7-3.json and changes made in LoanRepository --- descriptors/ModuleDescriptor-template.json | 2 +- ramls/loan.json | 4 ++ .../storage/loans/LoanRepository.java | 14 ++++- src/test/java/api/loans/LoanAPITests.java | 53 +++++++++++++++++++ .../builders/CirculationItemsBuilder.java | 13 +++++ .../java/api/support/fakes/StorageSchema.java | 2 +- .../fixtures/CirculationItemsFixture.java | 14 +++++ .../api/support/fixtures/UserExamples.java | 7 +++ .../api/support/fixtures/UsersFixture.java | 6 +++ ...ge-loan-7-2.json => storage-loan-7-3.json} | 4 ++ 10 files changed, 116 insertions(+), 3 deletions(-) rename src/test/resources/{storage-loan-7-2.json => storage-loan-7-3.json} (97%) diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 7c3518710b..b41b0ffe80 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -1130,7 +1130,7 @@ "requires": [ { "id": "loan-storage", - "version": "7.1" + "version": "7.3" }, { "id": "circulation-rules-storage", diff --git a/ramls/loan.json b/ramls/loan.json index 125f2d1798..80125978c9 100644 --- a/ramls/loan.json +++ b/ramls/loan.json @@ -370,6 +370,10 @@ "type": "string", "format": "date-time" }, + "isDcb": { + "description": "Indicates whether or not this loan is associated for DCB use case", + "type": "boolean" + }, "metadata": { "description": "Metadata about creation and changes to loan, provided by the server (client should not provide)", "type": "object", diff --git a/src/main/java/org/folio/circulation/infrastructure/storage/loans/LoanRepository.java b/src/main/java/org/folio/circulation/infrastructure/storage/loans/LoanRepository.java index da995e2a69..d804634fd9 100644 --- a/src/main/java/org/folio/circulation/infrastructure/storage/loans/LoanRepository.java +++ b/src/main/java/org/folio/circulation/infrastructure/storage/loans/LoanRepository.java @@ -88,6 +88,9 @@ public class LoanRepository implements GetManyRecordsRepository { private static final String ID = "id"; private static final String USER_ID = "userId"; + private static final String IS_DCB = "isDcb"; + private static final String DCB_USER_LASTNAME = "DcbSystem"; + public LoanRepository(Clients clients, ItemRepository itemRepository, UserRepository userRepository) { @@ -294,6 +297,15 @@ private Result> mapResponseToLoans(Response response) { return MultipleRecords.from(response, Loan::from, RECORDS_PROPERTY_NAME); } + private static void addIsDcbProperty(Loan loan, Item item, JsonObject storageLoan) { + write(storageLoan, IS_DCB, isDcbLoan(loan, item)); + } + + private static boolean isDcbLoan(Loan loan, Item item) { + return item.isDcbItem() || (nonNull(loan.getUser()) && nonNull(loan.getUser().getLastName()) + && loan.getUser().getLastName().equalsIgnoreCase(DCB_USER_LASTNAME)); + } + private static JsonObject mapToStorageRepresentation(Loan loan, Item item) { log.debug("mapToStorageRepresentation:: parameters loan: {}, item: {}", loan, item); JsonObject storageLoan = loan.asJson(); @@ -307,7 +319,7 @@ private static JsonObject mapToStorageRepresentation(Loan loan, Item item) { removeProperty(storageLoan, FEESANDFINES); removeProperty(storageLoan, OVERDUE_FINE_POLICY); removeProperty(storageLoan, LOST_ITEM_POLICY); - + addIsDcbProperty(loan, item, storageLoan); updatePolicy(storageLoan, loan.getLoanPolicy(), "loanPolicyId"); updatePolicy(storageLoan, loan.getOverdueFinePolicy(), "overdueFinePolicyId"); updatePolicy(storageLoan, loan.getLostItemPolicy(), "lostItemPolicyId"); diff --git a/src/test/java/api/loans/LoanAPITests.java b/src/test/java/api/loans/LoanAPITests.java index faa2b5abdb..4d4f43131c 100644 --- a/src/test/java/api/loans/LoanAPITests.java +++ b/src/test/java/api/loans/LoanAPITests.java @@ -199,8 +199,61 @@ void canCreateALoan() { assertThat("has item volume", item.getString("volume"), is("testVolume")); + assertThat("isDcb should be false", + loan.getString("isDcb"), is("false")); + loanHasExpectedProperties(loan, user); } + @Test + void createLoanForDcbItem() { + IndividualResource instance = instancesFixture.basedUponDunkirk(); + IndividualResource holdings = holdingsFixture.defaultWithHoldings(instance.getId()); + var instanceTitle = "virtual Title"; + + IndividualResource locationsResource = locationsFixture.mainFloor(); + final IndividualResource circulationItem = circulationItemsFixture.createCirculationItem( + "100002222", holdings.getId(), locationsResource.getId(), instanceTitle); + + loansFixture.createLoan(circulationItem, usersFixture.jessica()); + JsonObject loan = loansFixture.getLoans().getFirst(); + + assertThat("isDcb should be true", + loan.getString("isDcb"), is("true")); + } + + @Test + void createLoanForDcbUser() { + IndividualResource instance = instancesFixture.basedUponDunkirk(); + IndividualResource holdings = holdingsFixture.defaultWithHoldings(instance.getId()); + var instanceTitle = "title"; + + IndividualResource locationsResource = locationsFixture.mainFloor(); + final IndividualResource circulationItem = circulationItemsFixture.createCirculationItemForDcb( + "100002222", holdings.getId(), locationsResource.getId(), instanceTitle, false); + + loansFixture.createLoan(circulationItem, usersFixture.groot()); + JsonObject loan = loansFixture.getLoans().getFirst(); + + assertThat("isDcb should be true", + loan.getString("isDcb"), is("true")); + } + + @Test + void createLoanForDcbUserAndDcbItem() { + IndividualResource instance = instancesFixture.basedUponDunkirk(); + IndividualResource holdings = holdingsFixture.defaultWithHoldings(instance.getId()); + var instanceTitle = "virtual Title"; + + IndividualResource locationsResource = locationsFixture.mainFloor(); + final IndividualResource circulationItem = circulationItemsFixture.createCirculationItem( + "100002222", holdings.getId(), locationsResource.getId(), instanceTitle); + + loansFixture.createLoan(circulationItem, usersFixture.groot()); + JsonObject loan = loansFixture.getLoans().getFirst(); + + assertThat("isDcb should be true", + loan.getString("isDcb"), is("true")); + } @Test void canGetLoanWithoutOpenFeesFines() { diff --git a/src/test/java/api/support/builders/CirculationItemsBuilder.java b/src/test/java/api/support/builders/CirculationItemsBuilder.java index 1ec177ec60..6d31c5ce00 100644 --- a/src/test/java/api/support/builders/CirculationItemsBuilder.java +++ b/src/test/java/api/support/builders/CirculationItemsBuilder.java @@ -170,4 +170,17 @@ public CirculationItemsBuilder withInstanceTitle(String instanceTitle) { instanceTitle); } + public CirculationItemsBuilder withDcb(boolean isDcb) { + return new CirculationItemsBuilder( + this.itemId, + this.barcode, + this.holdingId, + this.locationId, + this.materialTypeId, + this.loanTypeId, + isDcb, + this.lendingLibraryCode, + this.instanceTitle); + } + } diff --git a/src/test/java/api/support/fakes/StorageSchema.java b/src/test/java/api/support/fakes/StorageSchema.java index 9f00695474..87aa34eac9 100644 --- a/src/test/java/api/support/fakes/StorageSchema.java +++ b/src/test/java/api/support/fakes/StorageSchema.java @@ -12,7 +12,7 @@ public static JsonSchemaValidator validatorForStorageItemSchema() throws IOExcep } public static JsonSchemaValidator validatorForStorageLoanSchema() throws IOException { - return JsonSchemaValidator.fromResource("/storage-loan-7-2.json"); + return JsonSchemaValidator.fromResource("/storage-loan-7-3.json"); } public static JsonSchemaValidator validatorForLocationInstSchema() throws IOException { diff --git a/src/test/java/api/support/fixtures/CirculationItemsFixture.java b/src/test/java/api/support/fixtures/CirculationItemsFixture.java index 10e69d57f4..2444d9d89e 100644 --- a/src/test/java/api/support/fixtures/CirculationItemsFixture.java +++ b/src/test/java/api/support/fixtures/CirculationItemsFixture.java @@ -27,6 +27,20 @@ public IndividualResource createCirculationItem(String barcode, UUID holdingId, return circulationItemClient.create(circulationItemsBuilder); } + public IndividualResource createCirculationItemForDcb(String barcode, UUID holdingId, UUID locationId, + String instanceTitle, boolean isDcb) { + CirculationItemsBuilder circulationItemsBuilder = new CirculationItemsBuilder() + .withBarcode(barcode) + .withHoldingId(holdingId) + .withLoanType(loanTypesFixture.canCirculate().getId()) + .withMaterialType(materialTypesFixture.book().getId()) + .withLocationId(locationId) + .withInstanceTitle(instanceTitle) + .withDcb(isDcb); + + return circulationItemClient.create(circulationItemsBuilder); + } + public IndividualResource createCirculationItemWithLendingLibrary(String barcode, UUID holdingId, UUID locationId, String lendingLibrary) { CirculationItemsBuilder circulationItemsBuilder = new CirculationItemsBuilder().withBarcode(barcode).withHoldingId(holdingId) .withLoanType(loanTypesFixture.canCirculate().getId()).withMaterialType(materialTypesFixture.book().getId()) diff --git a/src/test/java/api/support/fixtures/UserExamples.java b/src/test/java/api/support/fixtures/UserExamples.java index 0ec213a826..b44bd89f96 100644 --- a/src/test/java/api/support/fixtures/UserExamples.java +++ b/src/test/java/api/support/fixtures/UserExamples.java @@ -32,6 +32,13 @@ static UserBuilder basedUponJamesRodwell() { } + static UserBuilder basedUponGroot() { + return new UserBuilder() + .withName("DcbSystem", "dcb") + .withBarcode("6430530304") + .withActive(true); + } + static UserBuilder basedUponCharlotteBroadwell() { return new UserBuilder() .withName("Broadwell", "Charlotte") diff --git a/src/test/java/api/support/fixtures/UsersFixture.java b/src/test/java/api/support/fixtures/UsersFixture.java index bc322a72d7..d442941d6d 100644 --- a/src/test/java/api/support/fixtures/UsersFixture.java +++ b/src/test/java/api/support/fixtures/UsersFixture.java @@ -2,6 +2,7 @@ import static api.support.fixtures.UserExamples.basedUponBobbyBibbin; import static api.support.fixtures.UserExamples.basedUponCharlotteBroadwell; +import static api.support.fixtures.UserExamples.basedUponGroot; import static api.support.fixtures.UserExamples.basedUponHenryHanks; import static api.support.fixtures.UserExamples.basedUponJamesRodwell; import static api.support.fixtures.UserExamples.basedUponJessicaPontefract; @@ -34,6 +35,11 @@ public UserResource jessica() { .inGroupFor(patronGroupsFixture.regular())); } + public UserResource groot() { + return createIfAbsent(basedUponGroot() + .inGroupFor(patronGroupsFixture.regular())); + } + public UserResource james() { return createIfAbsent(basedUponJamesRodwell() .inGroupFor(patronGroupsFixture.regular())); diff --git a/src/test/resources/storage-loan-7-2.json b/src/test/resources/storage-loan-7-3.json similarity index 97% rename from src/test/resources/storage-loan-7-2.json rename to src/test/resources/storage-loan-7-3.json index a721809f4a..136137f8af 100644 --- a/src/test/resources/storage-loan-7-2.json +++ b/src/test/resources/storage-loan-7-3.json @@ -93,6 +93,10 @@ "description": "Indicates whether or not this loan had its due date modified by a recall on the loaned item", "type": "boolean" }, + "isDcb": { + "description": "Indicates whether or not this loan is associated for DCB use case", + "type": "boolean" + }, "declaredLostDate" : { "description": "Date and time the item was declared lost during this loan", "type": "string", From f28ea79ebff11d314fae26ac6b79271b6b312887 Mon Sep 17 00:00:00 2001 From: Oleksandr Vidinieiev Date: Fri, 31 May 2024 12:04:29 +0300 Subject: [PATCH 2/3] CIRC-2095 Update interface holdings-storage to version 7.0 --- descriptors/ModuleDescriptor-template.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index b41b0ffe80..3ec694130d 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -1146,7 +1146,7 @@ }, { "id": "holdings-storage", - "version": "1.3 2.0 3.0 4.0 5.0 6.0" + "version": "1.3 2.0 3.0 4.0 5.0 6.0 7.0" }, { "id": "request-storage", From ef4e221e88dfa2a404a3872bdfd3fef371bd5ed2 Mon Sep 17 00:00:00 2001 From: Alexander Kurash Date: Fri, 21 Jun 2024 15:31:12 +0300 Subject: [PATCH 3/3] CIRC-2111 Create API wrapping settings CRUD (#1479) * CIRC-2111 Initial implementation * CIRC-2111 Add interface dependency * CIRC-2111 Fix delete method * CIRC-2111 Add more tests * CIRC-2111 Validate UUID - GET and DELETE * CIRC-2111 Remove query logging * CIRC-2111 Fix code smells * CIRC-2111 Remove redundant UUID parsing * CIRC-2111 Add validation tests * CIRC-2111 Apply suggestions from code review Co-authored-by: OleksandrVidinieiev <56632770+OleksandrVidinieiev@users.noreply.github.com> * CIRC-2111 Add param logging * CIRC-2111 Improve logging * CIRC-2111 Use peek --------- Co-authored-by: OleksandrVidinieiev <56632770+OleksandrVidinieiev@users.noreply.github.com> --- descriptors/ModuleDescriptor-template.json | 84 ++++++++++ ramls/circulation-setting.json | 33 ++++ ramls/circulation-settings.json | 23 +++ ramls/circulation-settings.raml | 105 ++++++++++++ ramls/examples/circulation-setting.json | 7 + ramls/examples/circulation-settings.json | 12 ++ .../circulation/CirculationVerticle.java | 2 + .../domain/CirculationSetting.java | 58 +++++++ .../CirculationSettingsRepository.java | 74 +++++++++ .../CirculationSettingsResource.java | 154 ++++++++++++++++++ .../folio/circulation/support/Clients.java | 13 ++ .../settings/CirculationSettingsTests.java | 106 ++++++++++++ src/test/java/api/support/APITests.java | 3 + .../builders/CirculationSettingBuilder.java | 34 ++++ .../java/api/support/fakes/FakeOkapi.java | 7 + .../java/api/support/http/InterfaceUrls.java | 3 + .../java/api/support/http/ResourceClient.java | 4 + 17 files changed, 722 insertions(+) create mode 100644 ramls/circulation-setting.json create mode 100644 ramls/circulation-settings.json create mode 100644 ramls/circulation-settings.raml create mode 100644 ramls/examples/circulation-setting.json create mode 100644 ramls/examples/circulation-settings.json create mode 100644 src/main/java/org/folio/circulation/domain/CirculationSetting.java create mode 100644 src/main/java/org/folio/circulation/infrastructure/storage/CirculationSettingsRepository.java create mode 100644 src/main/java/org/folio/circulation/resources/CirculationSettingsResource.java create mode 100644 src/test/java/api/settings/CirculationSettingsTests.java create mode 100644 src/test/java/api/support/builders/CirculationSettingBuilder.java diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 3ec694130d..c7eabed65f 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -682,6 +682,61 @@ } ] }, + { + "id": "circulation-settings", + "version": "1.0", + "handlers": [ + { + "methods": [ + "GET" + ], + "pathPattern": "/circulation/settings", + "permissionsRequired": [ + "circulation.settings.collection.get" + ], + "modulePermissions": [ + "circulation-storage.circulation-settings.collection.get" + ] + }, + { + "methods": ["GET"], + "pathPattern": "/circulation/settings/{id}", + "permissionsRequired": [ + "circulation.settings.item.get" + ], + "modulePermissions": [ + "circulation-storage.circulation-settings.item.get" + ] + }, { + "methods": ["PUT"], + "pathPattern": "/circulation/settings/{id}", + "permissionsRequired": [ + "circulation.settings.item.put" + ], + "modulePermissions": [ + "circulation-storage.circulation-settings.item.put" + ] + }, { + "methods": ["POST"], + "pathPattern": "/circulation/settings", + "permissionsRequired": [ + "circulation.settings.item.post" + ], + "modulePermissions": [ + "circulation-storage.circulation-settings.item.post" + ] + }, { + "methods": ["DELETE"], + "pathPattern": "/circulation/settings/{id}", + "permissionsRequired": [ + "circulation.settings.item.delete" + ], + "modulePermissions": [ + "circulation-storage.circulation-settings.item.delete" + ] + } + ] + }, { "id": "_timer", "version": "1.0", @@ -1259,6 +1314,10 @@ { "id": "settings", "version": "1.0" + }, + { + "id": "circulation-settings-storage", + "version": "1.0" } ], "optional": [ @@ -1518,6 +1577,31 @@ "displayName": "circulation settings - Read configuration", "description": "To read the configuration from mod settings." }, + { + "permissionName": "circulation.settings.collection.get", + "displayName": "circulation - get circulation settings", + "description": "get a collection of circulation settings" + }, + { + "permissionName": "circulation.settings.item.get", + "displayName": "circulation - get an individual circulation setting", + "description": "get an individual circulation setting by ID" + }, + { + "permissionName": "circulation.settings.item.put", + "displayName": "circulation - update circulation setting", + "description": "update circulation setting by ID" + }, + { + "permissionName": "circulation.settings.item.post", + "displayName": "circulation - create circulation setting", + "description": "create a new circulation setting" + }, + { + "permissionName": "circulation.settings.item.delete", + "displayName": "circulation - delete circulation setting", + "description": "delete circulation setting by ID" + }, { "permissionName": "circulation.all", "displayName": "circulation - all permissions", diff --git a/ramls/circulation-setting.json b/ramls/circulation-setting.json new file mode 100644 index 0000000000..0907442e8b --- /dev/null +++ b/ramls/circulation-setting.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Circulation Setting Schema", + "description": "Circulation setting", + "type": "object", + "properties": { + "id": { + "description": "ID of the circulation setting", + "type": "string", + "$ref": "raml-util/schemas/uuid.schema" + }, + "name": { + "description": "Circulation setting name", + "type": "string" + }, + "value": { + "description": "Circulation setting", + "type": "object", + "additionalProperties": true + }, + "metadata": { + "description": "Metadata about creation and changes, provided by the server (client should not provide)", + "type": "object", + "$ref": "raml-util/schemas/metadata.schema" + } + }, + "additionalProperties": false, + "required": [ + "id", + "name", + "value" + ] +} diff --git a/ramls/circulation-settings.json b/ramls/circulation-settings.json new file mode 100644 index 0000000000..99173df7ba --- /dev/null +++ b/ramls/circulation-settings.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Collection of Circulation settings", + "type": "object", + "properties": { + "circulationSettings": { + "description": "List of circulation settings", + "id": "circulationSettings", + "type": "array", + "items": { + "type": "object", + "$ref": "circulation-setting.json" + } + }, + "totalRecords": { + "type": "integer" + } + }, + "required": [ + "circulationSettings", + "totalRecords" + ] +} diff --git a/ramls/circulation-settings.raml b/ramls/circulation-settings.raml new file mode 100644 index 0000000000..731b9187c6 --- /dev/null +++ b/ramls/circulation-settings.raml @@ -0,0 +1,105 @@ +#%RAML 1.0 +title: Circulation Settings +version: v1.0 +protocols: [ HTTP, HTTPS ] +baseUri: http://localhost:9130 + +documentation: + - title: Circulation Settings API + content: API for circulation settings + +traits: + language: !include raml-util/traits/language.raml + pageable: !include raml-util/traits/pageable.raml + searchable: !include raml-util/traits/searchable.raml + validate: !include raml-util/traits/validation.raml + +types: + circulation-setting: !include circulation-setting.json + circulation-settings: !include circulation-settings.json + errors: !include raml-util/schemas/errors.schema + parameters: !include raml-util/schemas/parameters.schema + +resourceTypes: + collection: !include raml-util/rtypes/collection.raml + collection-item: !include raml-util/rtypes/item-collection.raml + +/circulation/settings: + type: + collection: + exampleCollection: !include examples/circulation-settings.json + exampleItem: !include examples/circulation-setting.json + schemaCollection: circulation-settings + schemaItem: circulation-setting + post: + is: [validate] + description: Create a new circulation setting + body: + application/json: + type: circulation-setting + responses: + 201: + description: "Circulation setting has been created" + body: + application/json: + type: circulation-setting + 500: + description: "Internal server error" + body: + text/plain: + example: "Internal server error" + get: + is: [validate, pageable, searchable: { description: "with valid searchable fields", example: "id=497f6eca-6276-4993-bfeb-98cbbbba8f79" }] + description: Get all circulation settings + responses: + 200: + description: "Circulation settings successfully retreived" + body: + application/json: + type: circulation-settings + 500: + description: "Internal server error" + body: + text/plain: + example: "Internal server error" + /{circulationSettingId}: + type: + collection-item: + exampleItem: !include examples/circulation-setting.json + schema: circulation-setting + get: + responses: + 200: + description: "Circulation setting successfully retreived" + body: + application/json: + type: circulation-setting + 500: + description: "Internal server error" + body: + text/plain: + example: "Internal server error" + put: + is: [ validate ] + body: + application/json: + type: circulation-setting + responses: + 204: + description: "Circulation settings have been saved" + 500: + description: "Internal server error" + body: + text/plain: + example: "Internal server error" + delete: + is: [validate] + responses: + 204: + description: "Circulation settings deleted" + 500: + description: "Internal server error" + body: + text/plain: + example: "Internal server error" + diff --git a/ramls/examples/circulation-setting.json b/ramls/examples/circulation-setting.json new file mode 100644 index 0000000000..35f0aba430 --- /dev/null +++ b/ramls/examples/circulation-setting.json @@ -0,0 +1,7 @@ +{ + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f09", + "name": "Sample settings", + "value": { + "org.folio.circulation.settings": "true" + } +} diff --git a/ramls/examples/circulation-settings.json b/ramls/examples/circulation-settings.json new file mode 100644 index 0000000000..b9b7a9c8b2 --- /dev/null +++ b/ramls/examples/circulation-settings.json @@ -0,0 +1,12 @@ +{ + "circulationSettings": [ + { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f09", + "name": "Sample settings", + "value": { + "org.folio.circulation.settings": "true" + } + } + ], + "totalRecords": 1 +} diff --git a/src/main/java/org/folio/circulation/CirculationVerticle.java b/src/main/java/org/folio/circulation/CirculationVerticle.java index cd6960808b..f0d2371440 100644 --- a/src/main/java/org/folio/circulation/CirculationVerticle.java +++ b/src/main/java/org/folio/circulation/CirculationVerticle.java @@ -10,6 +10,7 @@ import org.folio.circulation.resources.CheckInByBarcodeResource; import org.folio.circulation.resources.CheckOutByBarcodeResource; import org.folio.circulation.resources.CirculationRulesResource; +import org.folio.circulation.resources.CirculationSettingsResource; import org.folio.circulation.resources.ClaimItemReturnedResource; import org.folio.circulation.resources.DeclareClaimedReturnedItemAsMissingResource; import org.folio.circulation.resources.DeclareLostResource; @@ -150,6 +151,7 @@ public void start(Promise startFuture) { // Handlers new LoanRelatedFeeFineClosedHandlerResource(client).register(router); new FeeFineBalanceChangedHandlerResource(client).register(router); + new CirculationSettingsResource(client).register(router); server.requestHandler(router) .listen(config().getInteger("port"), result -> { diff --git a/src/main/java/org/folio/circulation/domain/CirculationSetting.java b/src/main/java/org/folio/circulation/domain/CirculationSetting.java new file mode 100644 index 0000000000..03f254f1ed --- /dev/null +++ b/src/main/java/org/folio/circulation/domain/CirculationSetting.java @@ -0,0 +1,58 @@ +package org.folio.circulation.domain; + +import static lombok.AccessLevel.PRIVATE; +import static org.folio.circulation.support.json.JsonPropertyFetcher.getObjectProperty; +import static org.folio.circulation.support.json.JsonPropertyFetcher.getProperty; + +import java.lang.invoke.MethodHandles; +import java.util.Set; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import io.vertx.core.json.JsonObject; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +@AllArgsConstructor(access = PRIVATE) +@ToString(onlyExplicitlyIncluded = true) +public class CirculationSetting { + private static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); + + public static final String ID_FIELD = "id"; + public static final String NAME_FIELD = "name"; + public static final String VALUE_FIELD = "value"; + public static final String METADATA_FIELD = "metadata"; + + @ToString.Include + @Getter + private final JsonObject representation; + + @Getter + private final String id; + + @Getter + private final String name; + + @Getter + private final JsonObject value; + + public static CirculationSetting from(JsonObject representation) { + final var id = getProperty(representation, ID_FIELD); + final var name = getProperty(representation, NAME_FIELD); + final var value = getObjectProperty(representation, VALUE_FIELD); + + if (id == null || name == null || value == null || !containsOnlyKnownFields(representation)) { + log.warn("from:: Circulation setting JSON is invalid: {}", representation); + return null; + } + + return new CirculationSetting(representation, id, name, value); + } + + private static boolean containsOnlyKnownFields(JsonObject representation) { + return Set.of(ID_FIELD, NAME_FIELD, VALUE_FIELD, METADATA_FIELD) + .containsAll(representation.fieldNames()); + } +} diff --git a/src/main/java/org/folio/circulation/infrastructure/storage/CirculationSettingsRepository.java b/src/main/java/org/folio/circulation/infrastructure/storage/CirculationSettingsRepository.java new file mode 100644 index 0000000000..8125f1761b --- /dev/null +++ b/src/main/java/org/folio/circulation/infrastructure/storage/CirculationSettingsRepository.java @@ -0,0 +1,74 @@ +package org.folio.circulation.infrastructure.storage; + +import static org.folio.circulation.support.http.ResponseMapping.forwardOnFailure; +import static org.folio.circulation.support.http.ResponseMapping.mapUsingJson; +import static org.folio.circulation.support.results.Result.failed; +import static org.folio.circulation.support.results.ResultBinding.flatMapResult; + +import java.lang.invoke.MethodHandles; +import java.util.concurrent.CompletableFuture; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.folio.circulation.domain.CirculationSetting; +import org.folio.circulation.domain.MultipleRecords; +import org.folio.circulation.support.Clients; +import org.folio.circulation.support.CollectionResourceClient; +import org.folio.circulation.support.FetchSingleRecord; +import org.folio.circulation.support.RecordNotFoundFailure; +import org.folio.circulation.support.http.client.ResponseInterpreter; +import org.folio.circulation.support.results.Result; + +public class CirculationSettingsRepository { + private static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); + public static final String RECORDS_PROPERTY_NAME = "circulationSettings"; + private final CollectionResourceClient circulationSettingsStorageClient; + + public CirculationSettingsRepository(Clients clients) { + circulationSettingsStorageClient = clients.circulationSettingsStorageClient(); + } + + public CompletableFuture> getById(String id) { + log.debug("getById:: parameters id: {}", id); + + return FetchSingleRecord.forRecord(RECORDS_PROPERTY_NAME) + .using(circulationSettingsStorageClient) + .mapTo(CirculationSetting::from) + .whenNotFound(failed(new RecordNotFoundFailure(RECORDS_PROPERTY_NAME, id))) + .fetch(id); + } + + public CompletableFuture>> findBy(String query) { + return circulationSettingsStorageClient.getManyWithRawQueryStringParameters(query) + .thenApply(flatMapResult(response -> + MultipleRecords.from(response, CirculationSetting::from, RECORDS_PROPERTY_NAME))); + } + + public CompletableFuture> create( + CirculationSetting circulationSetting) { + + log.debug("create:: parameters circulationSetting: {}", circulationSetting); + + final var storageCirculationSetting = circulationSetting.getRepresentation(); + + return circulationSettingsStorageClient.post(storageCirculationSetting) + .thenApply(interpreter()::flatMap); + } + + public CompletableFuture> update( + CirculationSetting circulationSetting) { + + log.debug("update:: parameters circulationSetting: {}", circulationSetting); + + final var storageCirculationSetting = circulationSetting.getRepresentation(); + + return circulationSettingsStorageClient.put(circulationSetting.getId(), storageCirculationSetting) + .thenApply(interpreter()::flatMap); + } + + private ResponseInterpreter interpreter() { + return new ResponseInterpreter() + .flatMapOn(201, mapUsingJson(CirculationSetting::from)) + .otherwise(forwardOnFailure()); + } +} diff --git a/src/main/java/org/folio/circulation/resources/CirculationSettingsResource.java b/src/main/java/org/folio/circulation/resources/CirculationSettingsResource.java new file mode 100644 index 0000000000..bfc6e989b6 --- /dev/null +++ b/src/main/java/org/folio/circulation/resources/CirculationSettingsResource.java @@ -0,0 +1,154 @@ +package org.folio.circulation.resources; + +import static org.folio.circulation.infrastructure.storage.CirculationSettingsRepository.RECORDS_PROPERTY_NAME; +import static org.folio.circulation.support.ValidationErrorFailure.singleValidationError; +import static org.folio.circulation.support.json.JsonPropertyFetcher.getProperty; +import static org.folio.circulation.support.results.MappingFunctions.toFixedValue; +import static org.folio.circulation.support.results.Result.ofAsync; +import static org.folio.circulation.support.results.Result.succeeded; + +import java.lang.invoke.MethodHandles; +import java.util.UUID; +import java.util.function.Function; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.folio.circulation.domain.CirculationSetting; +import org.folio.circulation.infrastructure.storage.CirculationSettingsRepository; +import org.folio.circulation.support.Clients; +import org.folio.circulation.support.http.server.JsonHttpResponse; +import org.folio.circulation.support.http.server.NoContentResponse; +import org.folio.circulation.support.http.server.WebContext; +import org.folio.circulation.support.results.Result; + +import io.vertx.core.http.HttpClient; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; + +public class CirculationSettingsResource extends CollectionResource { + private static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); + + public CirculationSettingsResource(HttpClient client) { + super(client, "/circulation/settings"); + } + + @Override + void create(RoutingContext routingContext) { + final var context = new WebContext(routingContext); + final var clients = Clients.create(context, client); + final var circulationSettingsRepository = new CirculationSettingsRepository(clients); + + final var incomingRepresentation = routingContext.body().asJsonObject(); + setRandomIdIfMissing(incomingRepresentation); + final var circulationSetting = CirculationSetting.from(incomingRepresentation); + log.debug("create:: Creating circulation setting: {}", () -> circulationSetting); + + ofAsync(circulationSetting) + .thenApply(refuseWhenCirculationSettingIsInvalid()) + .thenCompose(r -> r.after(circulationSettingsRepository::create)) + .thenApply(r -> r.map(CirculationSetting::getRepresentation)) + .thenApply(r -> r.map(JsonHttpResponse::created)) + .thenAccept(context::writeResultToHttpResponse); + } + + @Override + void replace(RoutingContext routingContext) { + final var context = new WebContext(routingContext); + final var clients = Clients.create(context, client); + final var circulationSettingsRepository = new CirculationSettingsRepository(clients); + + final var incomingRepresentation = routingContext.body().asJsonObject(); + final var circulationSetting = CirculationSetting.from(incomingRepresentation); + log.debug("replace:: Replacing circulation setting : {}", () -> circulationSetting); + + ofAsync(circulationSetting) + .thenApply(refuseWhenCirculationSettingIsInvalid()) + .thenCompose(r -> r.after(circulationSettingsRepository::update)) + .thenApply(r -> r.map(CirculationSetting::getRepresentation)) + .thenApply(r -> r.map(JsonHttpResponse::created)) + .thenAccept(context::writeResultToHttpResponse); + } + + @Override + void get(RoutingContext routingContext) { + final var context = new WebContext(routingContext); + final var clients = Clients.create(context, client); + final var circulationSettingsRepository = new CirculationSettingsRepository(clients); + + ofAsync(routingContext.request().getParam("id")) + .thenApply(refuseWhenIdIsInvalid()) + .thenApply(r -> r.peek(id -> log.debug("get:: parameters id: {}", id))) + .thenCompose(r -> r.after(circulationSettingsRepository::getById)) + .thenApply(r -> r.map(CirculationSetting::getRepresentation)) + .thenApply(r -> r.map(JsonHttpResponse::ok)) + .thenAccept(context::writeResultToHttpResponse); + } + + @Override + void delete(RoutingContext routingContext) { + final var context = new WebContext(routingContext); + final var clients = Clients.create(context, client); + + ofAsync(routingContext.request().getParam("id")) + .thenApply(refuseWhenIdIsInvalid()) + .thenApply(r -> r.peek(id -> log.debug("delete:: parameters id: {}", id))) + .thenCompose(r -> r.after(clients.circulationSettingsStorageClient()::delete)) + .thenApply(r -> r.map(toFixedValue(NoContentResponse::noContent))) + .thenAccept(context::writeResultToHttpResponse); + } + + @Override + void getMany(RoutingContext routingContext) { + final var context = new WebContext(routingContext); + final var clients = Clients.create(context, client); + final var circulationSettingsRepository = new CirculationSettingsRepository(clients); + + final var query = routingContext.request().query(); + log.debug("get:: parameters id: {}", () -> query); + + circulationSettingsRepository.findBy(query) + .thenApply(multipleLoanRecordsResult -> multipleLoanRecordsResult.map(multipleRecords -> + multipleRecords.asJson(CirculationSetting::getRepresentation, RECORDS_PROPERTY_NAME))) + .thenApply(r -> r.map(JsonHttpResponse::ok)) + .thenAccept(context::writeResultToHttpResponse); + } + + @Override + void empty(RoutingContext routingContext) { + WebContext context = new WebContext(routingContext); + Clients clients = Clients.create(context, client); + + clients.loansStorage().delete() + .thenApply(r -> r.map(toFixedValue(NoContentResponse::noContent))) + .thenAccept(context::writeResultToHttpResponse); + } + + private static void setRandomIdIfMissing(JsonObject representation) { + final var providedId = getProperty(representation, "id"); + if (providedId == null) { + representation.put("id", UUID.randomUUID().toString()); + } + } + + private static Function, Result> + refuseWhenCirculationSettingIsInvalid() { + + return r -> r.failWhen(circulationSetting -> succeeded(circulationSetting == null), + circulationSetting -> singleValidationError("Circulation setting JSON is invalid", "", "")); + } + + private static Function, Result> refuseWhenIdIsInvalid() { + return r -> r.failWhen(id -> succeeded(!uuidIsValid(id)), + circulationSetting -> singleValidationError("Circulation setting ID is not a valid UUID", + "", "")); + } + + private static boolean uuidIsValid(String providedId) { + try { + return providedId != null && providedId.equals(UUID.fromString(providedId).toString()); + } catch(IllegalArgumentException e) { + log.warn("uuidIsValid:: Invalid UUID"); + return false; + } + } +} diff --git a/src/main/java/org/folio/circulation/support/Clients.java b/src/main/java/org/folio/circulation/support/Clients.java index 3ffc41941a..404b433461 100644 --- a/src/main/java/org/folio/circulation/support/Clients.java +++ b/src/main/java/org/folio/circulation/support/Clients.java @@ -69,6 +69,7 @@ public class Clients { private final CollectionResourceClient checkOutLockStorageClient; private final CollectionResourceClient circulationItemClient; private final GetManyRecordsClient settingsStorageClient; + private final CollectionResourceClient circulationSettingsStorageClient; public static Clients create(WebContext context, HttpClient httpClient) { return new Clients(context.createHttpClient(httpClient), context); @@ -136,6 +137,7 @@ private Clients(OkapiHttpClient client, WebContext context) { checkOutLockStorageClient = createCheckoutLockClient(client, context); settingsStorageClient = createSettingsStorageClient(client, context); circulationItemClient = createCirculationItemClient(client, context); + circulationSettingsStorageClient = createCirculationSettingsStorageClient(client, context); } catch(MalformedURLException e) { throw new InvalidOkapiLocationException(context.getOkapiLocation(), e); @@ -374,6 +376,10 @@ public CollectionResourceClient circulationItemClient() { return circulationItemClient; } + public CollectionResourceClient circulationSettingsStorageClient() { + return circulationSettingsStorageClient; + } + private static CollectionResourceClient getCollectionResourceClient( OkapiHttpClient client, WebContext context, String path) @@ -801,6 +807,13 @@ private CollectionResourceClient createCirculationItemClient( return getCollectionResourceClient(client, context, "/circulation-item"); } + private CollectionResourceClient createCirculationSettingsStorageClient( + OkapiHttpClient client, WebContext context) throws MalformedURLException { + + return getCollectionResourceClient(client, context, + "/circulation-settings-storage/circulation-settings"); + } + private GetManyRecordsClient createSettingsStorageClient( OkapiHttpClient client, WebContext context) throws MalformedURLException { diff --git a/src/test/java/api/settings/CirculationSettingsTests.java b/src/test/java/api/settings/CirculationSettingsTests.java new file mode 100644 index 0000000000..da176eb5e9 --- /dev/null +++ b/src/test/java/api/settings/CirculationSettingsTests.java @@ -0,0 +1,106 @@ +package api.settings; + +import static api.support.http.InterfaceUrls.circulationSettingsUrl; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +import org.junit.jupiter.api.Test; + +import api.support.APITests; +import api.support.builders.CirculationSettingBuilder; +import api.support.http.CqlQuery; +import io.vertx.core.json.JsonObject; + +class CirculationSettingsTests extends APITests { + + public static final String NAME = "name"; + public static final String VALUE = "value"; + public static final String ERRORS = "errors"; + public static final String MESSAGE = "message"; + public static final String INVALID_JSON_MESSAGE = "Circulation setting JSON is invalid"; + + @Test + void crudOperationsTest() { + // Testing POST method + final var setting = circulationSettingsClient.create(new CirculationSettingBuilder() + .withName("initial-name") + .withValue(new JsonObject().put("initial-key", "initial-value"))); + final var settingId = setting.getId(); + + // Testing GET (individual setting) method + final var settingById = circulationSettingsClient.get(settingId); + assertThat(settingById.getJson().getString(NAME), is("initial-name")); + assertThat(settingById.getJson().getJsonObject(VALUE).getString("initial-key"), + is("initial-value")); + + // Testing GET (all) method + final var anotherSetting = circulationSettingsClient.create(new CirculationSettingBuilder() + .withName("another-name") + .withValue(new JsonObject().put("another-key", "another-value"))); + final var allSettings = circulationSettingsClient.getMany(CqlQuery.noQuery()); + assertThat(allSettings.size(), is(2)); + + // Testing DELETE method + circulationSettingsClient.delete(anotherSetting.getId()); + final var allSettingsAfterDeletion = circulationSettingsClient.getMany(CqlQuery.noQuery()); + assertThat(allSettingsAfterDeletion.size(), is(1)); + assertThat(allSettingsAfterDeletion.getFirst().getString(NAME), is("initial-name")); + assertThat(allSettingsAfterDeletion.getFirst().getJsonObject(VALUE).getString("initial-key"), + is("initial-value")); + + // Testing PUT method + circulationSettingsClient.replace(settingId, new CirculationSettingBuilder() + .withId(settingId) + .withName("new-name") + .withValue(new JsonObject().put("new-key", "new-value"))); + + final var updatedSetting = circulationSettingsClient.get(settingId); + + assertThat(updatedSetting.getJson().getString(NAME), is("new-name")); + assertThat(updatedSetting.getJson().getJsonObject(VALUE).getString("new-key"), + is("new-value")); + } + + @Test + void invalidRequestsTest() { + circulationSettingsClient.create(new CirculationSettingBuilder() + .withName("initial-name") + .withValue(new JsonObject().put("initial-key", "initial-value"))); + + // Testing GET with wrong UUID + restAssuredClient.get(circulationSettingsUrl("/" + randomId()), 404, + "get-circulation-setting"); + + // Testing GET with invalid ID (not a UUID) + var getErrors = restAssuredClient.get(circulationSettingsUrl("/not-a-uuid"), 422, + "get-circulation-setting"); + assertThat(getErrors.getJson().getJsonArray(ERRORS).getJsonObject(0).getString(MESSAGE), + is("Circulation setting ID is not a valid UUID")); + + // Testing DELETE with invalid ID + restAssuredClient.delete(circulationSettingsUrl("/" + randomId()), 204, + "delete-circulation-setting"); + + // Testing PUT with malformed JSON + var putErrors = restAssuredClient.put("{\"invalid-field\": \"invalid-value\"}", + circulationSettingsUrl("/" + randomId()), 422, "put-circulation-setting"); + assertThat(putErrors.getJson().getJsonArray(ERRORS).getJsonObject(0).getString(MESSAGE), + is(INVALID_JSON_MESSAGE)); + + var putErrorsNoValue = restAssuredClient.put("{\"name\": \"test-name\"}", + circulationSettingsUrl("/" + randomId()), 422, "put-circulation-setting"); + assertThat(putErrorsNoValue.getJson().getJsonArray(ERRORS).getJsonObject(0).getString(MESSAGE), + is(INVALID_JSON_MESSAGE)); + + // Testing POST with malformed JSON + var postErrors = restAssuredClient.post("{\"invalid-field\": \"invalid-value\"}", + circulationSettingsUrl(""), 422, "put-circulation-setting"); + assertThat(postErrors.getJson().getJsonArray(ERRORS).getJsonObject(0).getString(MESSAGE), + is(INVALID_JSON_MESSAGE)); + + var postErrorsNoValue = restAssuredClient.put("{\"name\": \"test-name\"}", + circulationSettingsUrl("/" + randomId()), 422, "put-circulation-setting"); + assertThat(postErrorsNoValue.getJson().getJsonArray(ERRORS).getJsonObject(0).getString(MESSAGE), + is(INVALID_JSON_MESSAGE)); + } +} diff --git a/src/test/java/api/support/APITests.java b/src/test/java/api/support/APITests.java index ddc2dce289..b872ce7165 100644 --- a/src/test/java/api/support/APITests.java +++ b/src/test/java/api/support/APITests.java @@ -194,6 +194,9 @@ public abstract class APITests { protected final ResourceClient actualCostRecordsClient = ResourceClient.forActualCostRecordsStorage(); + protected final ResourceClient circulationSettingsClient = + ResourceClient.forCirculationSettings(); + protected final ServicePointsFixture servicePointsFixture = new ServicePointsFixture(servicePointsClient); diff --git a/src/test/java/api/support/builders/CirculationSettingBuilder.java b/src/test/java/api/support/builders/CirculationSettingBuilder.java new file mode 100644 index 0000000000..a33fb99345 --- /dev/null +++ b/src/test/java/api/support/builders/CirculationSettingBuilder.java @@ -0,0 +1,34 @@ +package api.support.builders; + +import java.util.UUID; + +import io.vertx.core.json.JsonObject; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import lombok.With; + +@NoArgsConstructor +@AllArgsConstructor +@With +public class CirculationSettingBuilder extends JsonBuilder implements Builder { + private UUID id = null; + private String name = null; + private JsonObject value = null; + + @Override + public JsonObject create() { + JsonObject circulationSetting = new JsonObject(); + + if (id != null) { + put(circulationSetting, "id", id); + } + if (name != null) { + put(circulationSetting, "name", name); + } + if (value != null) { + put(circulationSetting, "value", value); + } + + return circulationSetting; + } +} diff --git a/src/test/java/api/support/fakes/FakeOkapi.java b/src/test/java/api/support/fakes/FakeOkapi.java index 9cdfb40600..5cbecb8542 100644 --- a/src/test/java/api/support/fakes/FakeOkapi.java +++ b/src/test/java/api/support/fakes/FakeOkapi.java @@ -415,6 +415,13 @@ public void start(Promise startFuture) throws IOException { .withChangeMetadata() .create().register(router); + new FakeStorageModuleBuilder() + .withRecordName("circulationSettings") + .withCollectionPropertyName("circulationSettings") + .withRootPath("/circulation-settings-storage/circulation-settings") + .withChangeMetadata() + .create().register(router); + new FakeFeeFineOperationsModule().register(router); server.requestHandler(router) diff --git a/src/test/java/api/support/http/InterfaceUrls.java b/src/test/java/api/support/http/InterfaceUrls.java index fedf39d696..59d35de534 100644 --- a/src/test/java/api/support/http/InterfaceUrls.java +++ b/src/test/java/api/support/http/InterfaceUrls.java @@ -334,4 +334,7 @@ public static URL settingsStorageUrl() { return APITestContext.viaOkapiModuleUrl("/settings/entries"); } + public static URL circulationSettingsUrl(String subPath) { + return circulationModuleUrl("/circulation/settings" + subPath); + } } diff --git a/src/test/java/api/support/http/ResourceClient.java b/src/test/java/api/support/http/ResourceClient.java index 844fff542d..8e9b7d63be 100644 --- a/src/test/java/api/support/http/ResourceClient.java +++ b/src/test/java/api/support/http/ResourceClient.java @@ -272,6 +272,10 @@ public static ResourceClient forActualCostRecordsStorage() { return new ResourceClient(InterfaceUrls::actualCostRecordsStorageUrl, "actualCostRecords"); } + public static ResourceClient forCirculationSettings() { + return new ResourceClient(InterfaceUrls::circulationSettingsUrl, "circulationSettings"); + } + private ResourceClient(UrlMaker urlMaker, String collectionArrayPropertyName) { this.urlMaker = urlMaker; this.collectionArrayPropertyName = collectionArrayPropertyName;