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;