diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json
index ff14fe6c..f3cf1ef2 100644
--- a/descriptors/ModuleDescriptor-template.json
+++ b/descriptors/ModuleDescriptor-template.json
@@ -645,6 +645,30 @@
}
]
},
+ {
+ "id": "print-events-storage",
+ "version": "1.0",
+ "handlers": [
+ {
+ "methods": [
+ "POST"
+ ],
+ "pathPattern": "/print-events-storage/print-events-entry",
+ "permissionsRequired": [
+ "print-events-storage.print-events-entry.item.post"
+ ]
+ },
+ {
+ "methods": [
+ "POST"
+ ],
+ "pathPattern": "/print-events-storage/print-events-status",
+ "permissionsRequired": [
+ "print-events-storage.print-events-status.item.post"
+ ]
+ }
+ ]
+ },
{
"id": "_timer",
"version": "1.0",
@@ -677,6 +701,16 @@
}
],
"permissionSets": [
+ {
+ "permissionName": "print-events-storage.print-events-entry.item.post",
+ "displayName": "print events storage - save print event logs",
+ "description": "save print event log in storage"
+ },
+ {
+ "permissionName": "print-events-storage.print-events-status.item.post",
+ "displayName": "print-events-storage - Fetch print event status",
+ "description": "Fetch print event details for a batch of request Ids"
+ },
{
"permissionName": "check-in-storage.check-ins.collection.get",
"displayName": "Check-in storage - get check-ins collection",
@@ -1133,7 +1167,9 @@
"circulation-storage.circulation-settings.item.get",
"circulation-storage.circulation-settings.item.post",
"circulation-storage.circulation-settings.item.put",
- "circulation-storage.circulation-settings.item.delete"
+ "circulation-storage.circulation-settings.item.delete",
+ "print-events-storage.print-events-entry.item.post",
+ "print-events-storage.print-events-status.item.post"
]
},
{
diff --git a/ramls/examples/print-events-request.json b/ramls/examples/print-events-request.json
new file mode 100644
index 00000000..8505827a
--- /dev/null
+++ b/ramls/examples/print-events-request.json
@@ -0,0 +1,9 @@
+{
+ "requestIds": [
+ "059e54bb-53e5-4039-a2fb-b34358e88b0a",
+ "e70dcbae-30c6-47ac-94f8-4ffefd44a935"
+ ],
+ "requesterId": "d51470ea-5daa-480b-a4aa-09c8c6d9940e",
+ "requesterName": "requester",
+ "printEventDate": "2024-06-25T20:00:00+05:30"
+}
diff --git a/ramls/examples/print-events-status-request.json b/ramls/examples/print-events-status-request.json
new file mode 100644
index 00000000..5d017a3d
--- /dev/null
+++ b/ramls/examples/print-events-status-request.json
@@ -0,0 +1,6 @@
+{
+ "requestIds" : [
+ "fbbbe691-d6c6-4f40-b9dd-7364ccb1518a",
+ "fd831be3-f05f-4b6f-b68f-1a976ea1ab0f"
+ ]
+}
diff --git a/ramls/examples/print-events-status-response.json b/ramls/examples/print-events-status-response.json
new file mode 100644
index 00000000..06236ea3
--- /dev/null
+++ b/ramls/examples/print-events-status-response.json
@@ -0,0 +1,7 @@
+{
+ "requestId": "fbbbe691-d6c6-4f40-b9dd-7364ccb1518a",
+ "requesterId": "44642483-f4d4-4a29-a2ba-8eefcf53de16",
+ "requesterName": "vignesh",
+ "count": 2,
+ "printEventDate": "2024-07-04T07:07:00.000+00:00"
+}
diff --git a/ramls/examples/print-events-status-responses.json b/ramls/examples/print-events-status-responses.json
new file mode 100644
index 00000000..2c878bfe
--- /dev/null
+++ b/ramls/examples/print-events-status-responses.json
@@ -0,0 +1,19 @@
+{
+ "printEventsStatusResponses": [
+ {
+ "requestId": "fbbbe691-d6c6-4f40-b9dd-7364ccb1518a",
+ "requesterId": "44642483-f4d4-4a29-a2ba-8eefcf53de16",
+ "requesterName": "vignesh",
+ "count": 2,
+ "printEventDate": "2024-07-04T07:07:00.000+00:00"
+ },
+ {
+ "requestId": "fd831be3-f05f-4b6f-b68f-1a976ea1ab0f",
+ "requesterId": "44642483-f4d4-4a29-a2ba-8eefcf53de17",
+ "requesterName": "siddhu",
+ "count": 5,
+ "printEventDate": "2024-07-05T07:07:00.000+00:00"
+ }
+ ],
+ "totalRecords": 2
+}
diff --git a/ramls/print-events-request.json b/ramls/print-events-request.json
new file mode 100644
index 00000000..3f169b1c
--- /dev/null
+++ b/ramls/print-events-request.json
@@ -0,0 +1,38 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Print Events Request",
+ "type": "object",
+ "properties": {
+ "requestIds": {
+ "description": "List of request IDs",
+ "type": "array",
+ "minItems": 1,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[1-5][a-fA-F0-9]{3}-[89abAB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$"
+ }
+ },
+ "requesterId": {
+ "description": "ID of the requester",
+ "type": "string",
+ "$ref": "raml-util/schemas/uuid.schema",
+ "pattern": "^(?!\\s*$).+"
+ },
+ "requesterName": {
+ "description": "Name of the requester",
+ "type": "string",
+ "pattern": "^(?!\\s*$).+"
+ },
+ "printEventDate": {
+ "description": "Date and time when the print command is executed",
+ "type": "string",
+ "format": "date-time"
+ }
+ },
+ "required": [
+ "requestIds",
+ "requesterId",
+ "requesterName",
+ "printEventDate"
+ ]
+}
diff --git a/ramls/print-events-status-request.json b/ramls/print-events-status-request.json
new file mode 100644
index 00000000..010f0185
--- /dev/null
+++ b/ramls/print-events-status-request.json
@@ -0,0 +1,17 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Print Events Request",
+ "type": "object",
+ "properties": {
+ "requestIds": {
+ "description": "List of request IDs",
+ "type": "array",
+ "minItems": 1,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[1-5][a-fA-F0-9]{3}-[89abAB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$"
+ }
+ }
+ }
+}
+
diff --git a/ramls/print-events-status-response.json b/ramls/print-events-status-response.json
new file mode 100644
index 00000000..e22e9627
--- /dev/null
+++ b/ramls/print-events-status-response.json
@@ -0,0 +1,28 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Print events details",
+ "type": "object",
+ "properties": {
+ "requestId": {
+ "description": "ID of the request",
+ "type": "string"
+ },
+ "requesterId": {
+ "description": "ID of the requester",
+ "type": "string"
+ },
+ "requesterName": {
+ "description": "Name of the requester",
+ "type": "string"
+ },
+ "count": {
+ "description": "No of times the request is printed",
+ "type": "integer"
+ },
+ "printEventDate": {
+ "description": "Date and time when the print command is executed",
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+}
diff --git a/ramls/print-events-status-responses.json b/ramls/print-events-status-responses.json
new file mode 100644
index 00000000..8bddf672
--- /dev/null
+++ b/ramls/print-events-status-responses.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Collection of print events details",
+ "type": "object",
+ "properties": {
+ "printEventsStatusResponses": {
+ "description": "List of print events details",
+ "id": "printEvents",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "$ref": "print-events-status-response.json"
+ }
+ },
+ "totalRecords": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "printEventsStatusResponses",
+ "totalRecords"
+ ]
+}
diff --git a/ramls/print-events-storage.raml b/ramls/print-events-storage.raml
new file mode 100644
index 00000000..038f86a0
--- /dev/null
+++ b/ramls/print-events-storage.raml
@@ -0,0 +1,73 @@
+#%RAML 1.0
+title: Print Events Storage
+version: v1.0
+protocols: [ HTTP, HTTPS ]
+baseUri: http://localhost:9130
+
+documentation:
+ - title: Print Events Storage API
+ content: Storage for print events
+
+types:
+ print-events-request: !include print-events-request.json
+ print-events-status-request: !include print-events-status-request.json
+ print-events-status-responses: !include print-events-status-responses.json
+ errors: !include raml-util/schemas/errors.schema
+
+traits:
+ validate: !include raml-util/traits/validation.raml
+
+/print-events-storage:
+ /print-events-entry:
+ post:
+ is: [validate]
+ description: save a print event log
+ body:
+ application/json:
+ type: print-events-request
+ responses:
+ 201:
+ description: "All items have been successfully created or updated"
+ 409:
+ description: "Optimistic locking version conflict"
+ body:
+ text/plain:
+ example: "Version error"
+ 413:
+ description: "Payload too large"
+ body:
+ text/plain:
+ example: "Payload too large"
+ 422:
+ description: "Unprocessable entity"
+ body:
+ application/json:
+ type: errors
+ 500:
+ description: "Internal server error"
+ body:
+ text/plain:
+ example: "Internal server error"
+ /print-events-status:
+ post:
+ is: [validate]
+ description: Fetch batch of print event details
+ body:
+ application/json:
+ type: print-events-status-request
+ responses:
+ 200:
+ description: "Requests print event details are successfully retreived"
+ body:
+ application/json:
+ type: print-events-status-responses
+ 422:
+ description: "Unprocessable entity"
+ body:
+ application/json:
+ type: errors
+ 500:
+ description: "Internal server error"
+ body:
+ text/plain:
+ example: "Internal server error"
diff --git a/src/main/java/org/folio/persist/AbstractRepository.java b/src/main/java/org/folio/persist/AbstractRepository.java
index 0d480a5f..02a26236 100644
--- a/src/main/java/org/folio/persist/AbstractRepository.java
+++ b/src/main/java/org/folio/persist/AbstractRepository.java
@@ -123,4 +123,4 @@ public Future> deleteById(String id) {
return postgresClient.delete(tableName, id);
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/org/folio/rest/impl/PrintEventsApi.java b/src/main/java/org/folio/rest/impl/PrintEventsApi.java
new file mode 100644
index 00000000..023de7d0
--- /dev/null
+++ b/src/main/java/org/folio/rest/impl/PrintEventsApi.java
@@ -0,0 +1,37 @@
+package org.folio.rest.impl;
+
+import io.vertx.core.AsyncResult;
+import io.vertx.core.Context;
+import io.vertx.core.Handler;
+import org.folio.rest.jaxrs.model.PrintEventsRequest;
+import org.folio.rest.jaxrs.model.PrintEventsStatusRequest;
+import org.folio.rest.jaxrs.resource.PrintEventsStorage;
+import org.folio.service.PrintEventsService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.ws.rs.core.Response;
+import java.util.Map;
+
+import static io.vertx.core.Future.succeededFuture;
+
+public class PrintEventsApi implements PrintEventsStorage {
+ private static final Logger LOG = LoggerFactory.getLogger(PrintEventsApi.class);
+
+ @Override
+ public void postPrintEventsStoragePrintEventsEntry(PrintEventsRequest printEventsRequest, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) {
+ LOG.info("postPrintEventsStoragePrintEvents:: save print events {}", printEventsRequest);
+ new PrintEventsService(vertxContext, okapiHeaders)
+ .create(printEventsRequest)
+ .onSuccess(response -> asyncResultHandler.handle(succeededFuture(response)))
+ .onFailure(throwable -> asyncResultHandler.handle(succeededFuture(PostPrintEventsStoragePrintEventsEntryResponse.respond500WithTextPlain(throwable.getMessage()))));
+ }
+
+ @Override
+ public void postPrintEventsStoragePrintEventsStatus(PrintEventsStatusRequest entity, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) {
+ LOG.info("postPrintEventsStoragePrintEventsStatus:: Fetching print event details for requestIds {}",
+ entity.getRequestIds());
+ new PrintEventsService(vertxContext, okapiHeaders)
+ .getPrintEventRequestDetails(entity.getRequestIds(), asyncResultHandler);
+ }
+}
diff --git a/src/main/java/org/folio/rest/impl/TenantRefAPI.java b/src/main/java/org/folio/rest/impl/TenantRefAPI.java
index bca5f474..ff145ee2 100644
--- a/src/main/java/org/folio/rest/impl/TenantRefAPI.java
+++ b/src/main/java/org/folio/rest/impl/TenantRefAPI.java
@@ -7,6 +7,7 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.folio.support.kafka.topic.CirculationStorageKafkaTopic;
+import org.folio.dbschema.Versioned;
import org.folio.kafka.services.KafkaAdminClientService;
import org.folio.rest.annotations.Validate;
import org.folio.rest.jaxrs.model.TenantAttributes;
@@ -20,8 +21,6 @@
import io.vertx.core.Context;
import io.vertx.core.Future;
import io.vertx.core.Handler;
-import io.vertx.core.Promise;
-import io.vertx.core.Vertx;
public class TenantRefAPI extends TenantAPI {
@@ -41,37 +40,69 @@ Future loadData(TenantAttributes attributes, String tenantId,
.compose(r -> new KafkaAdminClientService(vertxContext.owner())
.createKafkaTopics(CirculationStorageKafkaTopic.values(), tenantId))
.compose(r -> super.loadData(attributes, tenantId, headers, vertxContext))
- .compose(superRecordsLoaded -> {
- log.info("Initializing of tenant's data");
- Vertx vertx = vertxContext.owner();
- Promise promise = Promise.promise();
- TenantLoading tl = new TenantLoading();
- tl.withKey(REFERENCE_KEY).withLead(REFERENCE_LEAD)
- .withIdContent()
- .add("loan-policy-storage/loan-policies")
- .add("request-policy-storage/request-policies")
- .add("patron-notice-policy-storage/patron-notice-policies")
- .add("staff-slips-storage/staff-slips")
- .withIdRaw()
- .add("circulation-rules-storage")
- .withIdContent()
- .add("cancellation-reason-storage/cancellation-reasons")
- .withKey(SAMPLE_KEY).withLead(SAMPLE_LEAD)
- .add("loans", "loan-storage/loans")
- .add("requests", "request-storage/requests")
- .add("circulation-settings-storage/circulation-settings");
-
-
- tl.perform(attributes, headers, vertx, res -> {
- if (res.failed()) {
- promise.fail(res.cause());
- } else {
- PubSubRegistrationService.registerModule(headers, vertx)
- .whenComplete((aBoolean, throwable) -> promise.complete());
- }
- });
- return promise.future();
- });
+ .compose(r -> loadData(attributes, headers, vertxContext))
+ .compose(r -> registerModuleInPubSub(headers, vertxContext))
+ .mapEmpty();
+ }
+
+ private Future loadData(TenantAttributes attributes, Map headers,
+ Context vertxContext) {
+
+ log.info("loadData:: Initializing tenant data");
+
+ TenantLoading tenantLoading = new TenantLoading();
+ tenantLoading.withKey(REFERENCE_KEY).withLead(REFERENCE_LEAD);
+ if (isNew(attributes, "14.0.0")) {
+ log.info("loadData:: Adding reference data: loan policies, request policies, " +
+ "patron notice policies, staff slips, circulation rules, cancellation reasons");
+
+ tenantLoading.withIdContent()
+ .add("loan-policy-storage/loan-policies")
+ .add("request-policy-storage/request-policies")
+ .add("patron-notice-policy-storage/patron-notice-policies")
+ .add("staff-slips-storage/staff-slips")
+ .withIdRaw()
+ .add("circulation-rules-storage")
+ .withIdContent()
+ .add("cancellation-reason-storage/cancellation-reasons");
+ }
+ tenantLoading.withKey(SAMPLE_KEY).withLead(SAMPLE_LEAD);
+ if (isNew(attributes, "7.0.0")) {
+ log.info("loadData:: Adding sample data: loans, circulation settings");
+
+ tenantLoading.add("loans", "loan-storage/loans")
+ .add("circulation-settings-storage/circulation-settings");
+ }
+ if (isNew(attributes, "16.1.0")) {
+ log.info("loadData:: Adding sample data: requests");
+
+ tenantLoading.add("requests", "request-storage/requests");
+ }
+ return tenantLoading.perform(attributes, headers, vertxContext, 0);
+ }
+
+ /**
+ * Returns attributes.getModuleFrom() < featureVersion or
+ * attributes.getModuleFrom() is null.
+ */
+ static boolean isNew(TenantAttributes attributes, String featureVersion) {
+ log.info("isNew:: params moduleFrom: {}, moduleTo: {}, purge: {}, featureVersion: {}",
+ attributes::getModuleFrom, attributes::getModuleTo, attributes::getPurge,
+ () -> featureVersion);
+ if (attributes.getModuleFrom() == null) {
+ log.info("isNew:: moduleFrom is null, quitting");
+ return true;
+ }
+ var since = new Versioned() { };
+ since.setFromModuleVersion(featureVersion);
+ var result = since.isNewForThisInstall(attributes.getModuleFrom());
+ log.info("isNew:: {}", result);
+ return result;
+ }
+
+ private Future registerModuleInPubSub(Map headers, Context vertxContext) {
+ var vertx = vertxContext.owner();
+ return Future.fromCompletionStage(PubSubRegistrationService.registerModule(headers, vertx));
}
@Validate
diff --git a/src/main/java/org/folio/rest/model/PrintEvent.java b/src/main/java/org/folio/rest/model/PrintEvent.java
new file mode 100644
index 00000000..82cc49b9
--- /dev/null
+++ b/src/main/java/org/folio/rest/model/PrintEvent.java
@@ -0,0 +1,15 @@
+package org.folio.rest.model;
+
+import lombok.Data;
+import org.folio.rest.jaxrs.model.Metadata;
+import java.util.Date;
+
+@Data
+public class PrintEvent {
+ private String id;
+ private String requestId;
+ private String requesterId;
+ private String requesterName;
+ private Date printEventDate;
+ private Metadata metadata;
+}
diff --git a/src/main/java/org/folio/service/CirculationSettingsService.java b/src/main/java/org/folio/service/CirculationSettingsService.java
index b060c252..e9ecafc8 100644
--- a/src/main/java/org/folio/service/CirculationSettingsService.java
+++ b/src/main/java/org/folio/service/CirculationSettingsService.java
@@ -82,8 +82,8 @@ private Future updateSettingsValue(CirculationSetting circul
Throwable throwable) {
if (!isDuplicate(throwable.getMessage())) {
- log.debug("updateSettingsValue:: error during saving circulation setting: {}. message: {}.",
- circulationSetting, throwable.getMessage());
+ log.warn("updateSettingsValue:: error during saving circulation setting: {}",
+ circulationSetting, throwable);
return Future.failedFuture(throwable);
}
@@ -113,4 +113,4 @@ private Future> getSettingsByName(String settingsName)
return repository.get(filter);
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/org/folio/service/PrintEventsService.java b/src/main/java/org/folio/service/PrintEventsService.java
new file mode 100644
index 00000000..be083823
--- /dev/null
+++ b/src/main/java/org/folio/service/PrintEventsService.java
@@ -0,0 +1,125 @@
+package org.folio.service;
+
+import io.vertx.core.AsyncResult;
+import io.vertx.core.Context;
+import io.vertx.core.Future;
+import io.vertx.core.Handler;
+import io.vertx.sqlclient.Row;
+import io.vertx.sqlclient.RowSet;
+import org.folio.rest.RestVerticle;
+import org.folio.rest.jaxrs.model.PrintEventsRequest;
+import org.folio.rest.jaxrs.model.PrintEventsStatusResponse;
+import org.folio.rest.jaxrs.model.PrintEventsStatusResponses;
+import org.folio.rest.jaxrs.resource.PrintEventsStorage;
+import org.folio.rest.model.PrintEvent;
+import org.folio.rest.persist.PgUtil;
+import org.folio.rest.persist.PostgresClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.ws.rs.core.Response;
+import java.time.ZoneOffset;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static io.vertx.core.Future.succeededFuture;
+import static org.folio.rest.persist.PgUtil.postgresClient;
+import static org.folio.rest.persist.PostgresClient.convertToPsqlStandard;
+import static org.folio.support.ModuleConstants.PRINT_EVENTS_TABLE;
+
+public class PrintEventsService {
+
+ private static final Logger LOG = LoggerFactory.getLogger(PrintEventsService.class);
+ private static final int MAX_ENTITIES = 10000;
+ private final Context vertxContext;
+ private final Map okapiHeaders;
+
+ private static final String PRINT_EVENT_FETCH_QUERY = """
+ WITH cte AS (
+ SELECT id, jsonb->>'requestId' AS request_id, jsonb->>'printEventDate' AS last_updated_date,
+ jsonb->>'requesterName' AS requester_name, jsonb->>'requesterId' AS requester_id,
+ COUNT(*) OVER (PARTITION BY jsonb->>'requestId') AS request_count,
+ ROW_NUMBER() OVER (PARTITION BY jsonb->>'requestId'
+ ORDER BY (jsonb->>'printEventDate')::timestamptz DESC) AS rank
+ FROM %s.%s
+ where jsonb->>'requestId' in (%s)
+ )
+ SELECT request_id, requester_name, requester_id, request_count, (last_updated_date)::timestamptz
+ FROM cte
+ WHERE
+ rank = 1;
+ """;
+
+
+
+ public PrintEventsService(Context vertxContext, Map okapiHeaders) {
+ this.vertxContext = vertxContext;
+ this.okapiHeaders = okapiHeaders;
+ }
+
+ public Future create(PrintEventsRequest printEventRequest) {
+ LOG.info("create:: save print events {}", printEventRequest);
+ List printEvents = printEventRequest.getRequestIds().stream().map(requestId -> {
+ PrintEvent event = new PrintEvent();
+ event.setRequestId(requestId);
+ event.setRequesterId(printEventRequest.getRequesterId());
+ event.setRequesterName(printEventRequest.getRequesterName());
+ event.setPrintEventDate(printEventRequest.getPrintEventDate());
+ return event;
+ }).toList();
+ return PgUtil.postSync(PRINT_EVENTS_TABLE, printEvents, MAX_ENTITIES, false, okapiHeaders, vertxContext,
+ PrintEventsStorage.PostPrintEventsStoragePrintEventsEntryResponse.class);
+ }
+
+ public void getPrintEventRequestDetails(List requestIds, Handler> asyncResultHandler) {
+ LOG.debug("getPrintEventRequestDetails:: Fetching print event details for requestIds {}", requestIds);
+ String tenantId = okapiHeaders.get(RestVerticle.OKAPI_HEADER_TENANT);
+ PostgresClient postgresClient = postgresClient(vertxContext, okapiHeaders);
+ postgresClient.execute(formatQuery(tenantId, requestIds), handler -> {
+ try {
+ if (handler.succeeded()) {
+ asyncResultHandler.handle(
+ succeededFuture(PrintEventsStorage.PostPrintEventsStoragePrintEventsStatusResponse
+ .respond200WithApplicationJson(mapRowSetToResponse(handler.result()))));
+ } else {
+ LOG.warn("getPrintEventRequestDetails:: Error while executing query", handler.cause());
+ asyncResultHandler.handle(succeededFuture(PrintEventsStorage.PostPrintEventsStoragePrintEventsStatusResponse
+ .respond500WithTextPlain(handler.cause())));
+ }
+ } catch (Exception ex) {
+ LOG.warn("getPrintEventRequestDetails:: Error while fetching print details", ex);
+ asyncResultHandler.handle(succeededFuture(PrintEventsStorage.PostPrintEventsStoragePrintEventsEntryResponse
+ .respond500WithTextPlain(ex.getMessage())));
+ }
+ });
+ }
+
+ private String formatQuery(String tenantId, List requestIds) {
+ String formattedRequestIds = requestIds
+ .stream()
+ .map(requestId -> "'" + requestId + "'")
+ .collect(Collectors.joining(", "));
+ return String.format(PRINT_EVENT_FETCH_QUERY, convertToPsqlStandard(tenantId), PRINT_EVENTS_TABLE, formattedRequestIds);
+ }
+
+ private PrintEventsStatusResponses mapRowSetToResponse(RowSet rowSet) {
+ PrintEventsStatusResponses printEventsStatusResponses = new PrintEventsStatusResponses();
+ List responseList = new ArrayList<>();
+ rowSet.forEach(row -> {
+ var response = new PrintEventsStatusResponse();
+ response.setRequestId(row.getString("request_id"));
+ response.setRequesterName(row.getString("requester_name"));
+ response.setRequesterId(row.getString("requester_id"));
+ response.setCount(row.getInteger("request_count"));
+ response.setPrintEventDate(Date.from(row.getLocalDateTime("last_updated_date")
+ .atZone(ZoneOffset.UTC).toInstant()));
+ responseList.add(response);
+ });
+ printEventsStatusResponses.setPrintEventsStatusResponses(responseList);
+ printEventsStatusResponses.setTotalRecords(rowSet.size());
+ return printEventsStatusResponses;
+ }
+}
diff --git a/src/main/java/org/folio/support/ModuleConstants.java b/src/main/java/org/folio/support/ModuleConstants.java
index ba92fd1e..f3ef2f52 100644
--- a/src/main/java/org/folio/support/ModuleConstants.java
+++ b/src/main/java/org/folio/support/ModuleConstants.java
@@ -29,6 +29,7 @@ public class ModuleConstants {
TlrFeatureToggleJob.class;
public static final String REQUEST_POLICY_TABLE = "request_policy";
public static final Class REQUEST_POLICY_CLASS = RequestPolicy.class;
+ public static final String PRINT_EVENTS_TABLE = "print_events";
private ModuleConstants(){
}
diff --git a/src/main/resources/templates/db_scripts/schema.json b/src/main/resources/templates/db_scripts/schema.json
index c2031099..f9602bc8 100644
--- a/src/main/resources/templates/db_scripts/schema.json
+++ b/src/main/resources/templates/db_scripts/schema.json
@@ -532,6 +532,21 @@
"removeAccents": true
}
]
+ },
+ {
+ "tableName": "print_events",
+ "withMetadata": true,
+ "withAuditing": false,
+ "index": [
+ {
+ "fieldName": "requestId",
+ "tOps": "ADD",
+ "caseSensitive": true,
+ "removeAccents": false,
+ "sqlExpression": "(jsonb->>'requestId')",
+ "sqlExpressionQuery": "$"
+ }
+ ]
}
],
"scripts": [
diff --git a/src/test/java/org/folio/rest/api/CirculationSettingsAPITest.java b/src/test/java/org/folio/rest/api/CirculationSettingsAPITest.java
index 9e9b7214..08c92861 100644
--- a/src/test/java/org/folio/rest/api/CirculationSettingsAPITest.java
+++ b/src/test/java/org/folio/rest/api/CirculationSettingsAPITest.java
@@ -24,6 +24,7 @@ public class CirculationSettingsAPITest extends ApiTests {
private static final String TABLE_NAME = "circulation_settings";
private static final String CIRCULATION_SETTINGS_PROPERTY = "circulation-settings";
private static final int NOT_FOUND_STATUS = 404;
+ private static final String REQUEST_PRINT_SETTING = "Enable Request Print";
private final AssertingRecordClient circulationSettingsClient =
new AssertingRecordClient(
@@ -91,6 +92,28 @@ public void canDeleteCirculationSettings() {
assertThat(deletedCirculationSettings.getStatusCode(), is(NOT_FOUND_STATUS));
}
+ @Test
+ @SneakyThrows
+ public void canCreateAndRetrieveEnableRequestPrintDetailsSetting() {
+ String id = UUID.randomUUID().toString();
+ JsonObject enableRequestPrintDetailsSettingJson = new JsonObject();
+ enableRequestPrintDetailsSettingJson.put(ID_KEY, id);
+ enableRequestPrintDetailsSettingJson.put(NAME_KEY, REQUEST_PRINT_SETTING);
+ JsonObject enablePrintSettingJson = new JsonObject().put(REQUEST_PRINT_SETTING, true);
+ enableRequestPrintDetailsSettingJson.put(VALUE_KEY, enablePrintSettingJson);
+
+ JsonObject circulationSettingsResponse =
+ circulationSettingsClient.create(enableRequestPrintDetailsSettingJson).getJson();
+ JsonObject circulationSettingsById = circulationSettingsClient.getById(id).getJson();
+
+ assertThat(circulationSettingsResponse.getString(ID_KEY), is(id));
+ assertThat(circulationSettingsResponse.getString(NAME_KEY),
+ is(enableRequestPrintDetailsSettingJson.getString(NAME_KEY)));
+ assertThat(circulationSettingsById.getString(ID_KEY), is(id));
+ assertThat(circulationSettingsById.getJsonObject(VALUE_KEY),
+ is(enableRequestPrintDetailsSettingJson.getJsonObject(VALUE_KEY)));
+ }
+
private static String getValue(JsonObject circulationSettingsById) {
return circulationSettingsById.getJsonObject(VALUE_KEY).getString(SAMPLE_KEY);
}
@@ -121,4 +144,4 @@ private JsonObject getUpdatedSettingsJson() {
circulationSettingsJsonUpdated.put(VALUE_KEY, updatedValue);
return circulationSettingsJsonUpdated;
}
-}
+}
\ No newline at end of file
diff --git a/src/test/java/org/folio/rest/api/PrintEventsAPITest.java b/src/test/java/org/folio/rest/api/PrintEventsAPITest.java
new file mode 100644
index 00000000..c7f93248
--- /dev/null
+++ b/src/test/java/org/folio/rest/api/PrintEventsAPITest.java
@@ -0,0 +1,196 @@
+package org.folio.rest.api;
+
+import io.vertx.core.json.JsonObject;
+import org.folio.rest.support.ApiTests;
+import org.folio.rest.support.JsonResponse;
+import org.folio.rest.support.ResponseHandler;
+import org.junit.Test;
+
+import java.net.MalformedURLException;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.IntStream;
+
+import static org.folio.rest.support.http.InterfaceUrls.printEventsUrl;
+import static org.folio.rest.support.matchers.HttpResponseStatusCodeMatchers.isCreated;
+import static org.folio.rest.support.matchers.HttpResponseStatusCodeMatchers.isOk;
+import static org.folio.rest.support.matchers.HttpResponseStatusCodeMatchers.isUnprocessableEntity;
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.junit.MatcherAssert.assertThat;
+
+public class PrintEventsAPITest extends ApiTests {
+
+ @Test
+ public void canCreatePrintEventLog() throws MalformedURLException, ExecutionException, InterruptedException {
+ JsonObject printEventsJson = getPrintEvent();
+ final CompletableFuture postCompleted = new CompletableFuture<>();
+ client.post(printEventsUrl("/print-events-entry"), printEventsJson, StorageTestSuite.TENANT_ID,
+ ResponseHandler.json(postCompleted));
+ final JsonResponse postResponse = postCompleted.get();
+ assertThat(postResponse, isCreated());
+ }
+
+ @Test
+ public void createPrintEventLogWithMissingFields() throws MalformedURLException, ExecutionException, InterruptedException {
+ List requestIds = List.of("5f5751b4-e352-4121-adca-204b0c2aec43", "5f5751b4-e352-4121-adca-204b0c2aec44");
+ JsonObject printEventsJson = new JsonObject()
+ .put("requestIds", requestIds)
+ .put("requesterName", "Sample Requester")
+ .put("printEventDate", "2024-06-25T14:30:00Z");
+ final CompletableFuture postCompleted = new CompletableFuture<>();
+ client.post(printEventsUrl("/print-events-entry"), printEventsJson, StorageTestSuite.TENANT_ID,
+ ResponseHandler.json(postCompleted));
+ final JsonResponse postResponse = postCompleted.get();
+ assertThat(postResponse, isUnprocessableEntity());
+ }
+
+ @Test
+ public void createPrintEventLogWithBlankFields() throws MalformedURLException, ExecutionException, InterruptedException {
+ JsonObject printEventsJson = getPrintEvent();
+ printEventsJson.put("requesterId", " ");
+ final CompletableFuture postCompleted = new CompletableFuture<>();
+ client.post(printEventsUrl("/print-events-entry"), printEventsJson, StorageTestSuite.TENANT_ID,
+ ResponseHandler.json(postCompleted));
+ final JsonResponse postResponse = postCompleted.get();
+ assertThat(postResponse, isUnprocessableEntity());
+ }
+
+ @Test
+ public void createPrintEventLogWhenRequestListIsEmpty() throws MalformedURLException, ExecutionException, InterruptedException {
+ List requestIds = List.of();
+ JsonObject printEventsJson = getPrintEvent();
+ printEventsJson.put("requestIds", requestIds);
+ final CompletableFuture postCompleted = new CompletableFuture<>();
+ client.post(printEventsUrl("/print-events-entry"), printEventsJson, StorageTestSuite.TENANT_ID,
+ ResponseHandler.json(postCompleted));
+ final JsonResponse postResponse = postCompleted.get();
+ assertThat(postResponse, isUnprocessableEntity());
+ }
+
+ @Test
+ public void createAndGetPrintEventDetails() throws MalformedURLException, ExecutionException, InterruptedException {
+ List requestIds = IntStream.range(0, 10)
+ .mapToObj(notUsed -> UUID.randomUUID())
+ .toList();
+
+ // Creating print event entry for batch of requestIds
+ JsonObject printEventsJson = getPrintEvent();
+ printEventsJson.put("requestIds", requestIds);
+ printEventsJson.put("requesterName", "requester1");
+ CompletableFuture postCompleted = new CompletableFuture<>();
+ client.post(printEventsUrl("/print-events-entry"), printEventsJson, StorageTestSuite.TENANT_ID,
+ ResponseHandler.json(postCompleted));
+ JsonResponse postResponse = postCompleted.get();
+ assertThat(postResponse, isCreated());
+
+ // Fetching the print event status details for the batch of requestIds
+ CompletableFuture printEventStatusResponse = new CompletableFuture<>();
+ client.post(printEventsUrl("/print-events-status"), createPrintRequestIds(requestIds), StorageTestSuite.TENANT_ID,
+ ResponseHandler.json(printEventStatusResponse));
+ JsonResponse response = printEventStatusResponse.get();
+ assertThat(response, isOk());
+ var jsonObject = response.getJson();
+ assertThat(jsonObject.getInteger("totalRecords"), is(10));
+ var printEventsArray = jsonObject.getJsonArray("printEventsStatusResponses");
+ IntStream.range(0, printEventsArray.size())
+ .mapToObj(printEventsArray::getJsonObject)
+ .forEach(printEvent -> {
+ assertThat(printEvent.getInteger("count"), is(1));
+ assertThat(printEvent.getString("requesterName"), is("requester1"));
+ assertThat(printEvent.getString("requesterId"), is("5f5751b4-e352-4121-adca-204b0c2aec43"));
+ assertThat(printEvent.getString("printEventDate"), is("2024-07-15T14:30:00.000+00:00"));
+ });
+
+ // creating another print event entry for first 5 requestIds in batch
+ var requestId2 = UUID.randomUUID();
+ printEventsJson = getPrintEvent();
+ printEventsJson.put("requestIds", requestIds.subList(0, 5));
+ printEventsJson.put("requesterId", requestId2);
+ printEventsJson.put("requesterName", "requester2");
+ printEventsJson.put("printEventDate", "2024-07-15T14:32:00Z");
+ postCompleted = new CompletableFuture<>();
+ client.post(printEventsUrl("/print-events-entry"), printEventsJson, StorageTestSuite.TENANT_ID,
+ ResponseHandler.json(postCompleted));
+ postResponse = postCompleted.get();
+ assertThat(postResponse, isCreated());
+
+ // Fetching the print event status details for the first 5 request Ids in batch.
+ // As the first 5 request ids are printed twice
+ // count will be 2 and the latest requester id, name and printDate will be returned
+ printEventStatusResponse = new CompletableFuture<>();
+ client.post(printEventsUrl("/print-events-status"),
+ createPrintRequestIds(requestIds.subList(0, 5)), StorageTestSuite.TENANT_ID,
+ ResponseHandler.json(printEventStatusResponse));
+ response = printEventStatusResponse.get();
+ assertThat(response, isOk());
+ jsonObject = response.getJson();
+ assertThat(jsonObject.getInteger("totalRecords"), is(5));
+ printEventsArray = jsonObject.getJsonArray("printEventsStatusResponses");
+ IntStream.range(0, printEventsArray.size())
+ .mapToObj(printEventsArray::getJsonObject)
+ .forEach(printEvent -> {
+ assertThat(printEvent.getInteger("count"), is(2));
+ assertThat(printEvent.getString("requesterName"), is("requester2"));
+ assertThat(printEvent.getString("requesterId"), is(requestId2.toString()));
+ assertThat(printEvent.getString("printEventDate"), is("2024-07-15T14:32:00.000+00:00"));
+ });
+
+ // Fetching the print event status details for the last 5 request Ids from batch
+ printEventStatusResponse = new CompletableFuture<>();
+ client.post(printEventsUrl("/print-events-status"),
+ createPrintRequestIds(requestIds.subList(5, requestIds.size())), StorageTestSuite.TENANT_ID,
+ ResponseHandler.json(printEventStatusResponse));
+ response = printEventStatusResponse.get();
+ assertThat(response, isOk());
+ jsonObject = response.getJson();
+ assertThat(jsonObject.getInteger("totalRecords"), is(5));
+ printEventsArray = jsonObject.getJsonArray("printEventsStatusResponses");
+ IntStream.range(0, printEventsArray.size())
+ .mapToObj(printEventsArray::getJsonObject)
+ .forEach(printEvent -> {
+ assertThat(printEvent.getInteger("count"), is(1));
+ assertThat(printEvent.getString("requesterName"), is("requester1"));
+ assertThat(printEvent.getString("requesterId"), is("5f5751b4-e352-4121-adca-204b0c2aec43"));
+ assertThat(printEvent.getString("printEventDate"), is("2024-07-15T14:30:00.000+00:00"));
+ });
+ }
+
+ @Test
+ public void getPrintEventStatusWithEmptyRequestIds() throws MalformedURLException, ExecutionException, InterruptedException {
+ JsonObject printEventsStatusRequestJson = createPrintRequestIds(List.of());
+ CompletableFuture getCompleted = new CompletableFuture<>();
+ client.post(printEventsUrl("/print-events-status"), printEventsStatusRequestJson, StorageTestSuite.TENANT_ID,
+ ResponseHandler.json(getCompleted));
+ JsonResponse postResponse = getCompleted.get();
+ assertThat(postResponse, isUnprocessableEntity());
+ }
+
+ @Test
+ public void getPrintEventStatusWithInvalidRequestIds() throws MalformedURLException, ExecutionException, InterruptedException {
+ CompletableFuture printEventStatusResponse = new CompletableFuture<>();
+ JsonObject printEventsStatusRequestJson = createPrintRequestIds(List.of(UUID.randomUUID(), UUID.randomUUID()));
+ client.post(printEventsUrl("/print-events-status"), printEventsStatusRequestJson, StorageTestSuite.TENANT_ID,
+ ResponseHandler.json(printEventStatusResponse));
+ JsonResponse response = printEventStatusResponse.get();
+ assertThat(response, isOk());
+ var jsonObject = response.getJson();
+ assertThat(jsonObject.getInteger("totalRecords"), is(0));
+ assertThat(jsonObject.getJsonArray("printEventsStatusResponses").size(), is(0));
+ }
+
+ private JsonObject getPrintEvent() {
+ List requestIds = List.of("5f5751b4-e352-4121-adca-204b0c2aec43", "5f5751b4-e352-4121-adca-204b0c2aec44");
+ return new JsonObject()
+ .put("requestIds", requestIds)
+ .put("requesterId", "5f5751b4-e352-4121-adca-204b0c2aec43")
+ .put("requesterName", "requester")
+ .put("printEventDate", "2024-07-15T14:30:00Z");
+ }
+
+ private JsonObject createPrintRequestIds(List requestIds) {
+ return new JsonObject()
+ .put("requestIds", requestIds);
+ }
+}
diff --git a/src/test/java/org/folio/rest/api/StorageTestSuite.java b/src/test/java/org/folio/rest/api/StorageTestSuite.java
index bf9dbb70..bc1d76cd 100644
--- a/src/test/java/org/folio/rest/api/StorageTestSuite.java
+++ b/src/test/java/org/folio/rest/api/StorageTestSuite.java
@@ -82,7 +82,8 @@
ActualCostRecordAPITest.class,
EventConsumerVerticleTest.class,
CheckOutLockAPITest.class,
- CirculationSettingsAPITest.class
+ CirculationSettingsAPITest.class,
+ PrintEventsAPITest.class
})
public class StorageTestSuite {
diff --git a/src/test/java/org/folio/rest/api/TenantRefApiTests.java b/src/test/java/org/folio/rest/api/TenantRefApiTests.java
index e3a40745..2c39c7f8 100644
--- a/src/test/java/org/folio/rest/api/TenantRefApiTests.java
+++ b/src/test/java/org/folio/rest/api/TenantRefApiTests.java
@@ -19,6 +19,8 @@
import static org.folio.rest.jaxrs.model.Request.Status.CLOSED_FILLED;
import static org.folio.rest.jaxrs.model.Request.Status.CLOSED_PICKUP_EXPIRED;
import static org.folio.rest.jaxrs.model.Request.Status.CLOSED_UNFILLED;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
@@ -43,6 +45,7 @@
import org.folio.rest.jaxrs.model.TenantAttributes;
import org.folio.rest.jaxrs.model.TenantJob;
import org.folio.rest.persist.PostgresClient;
+import org.folio.rest.tools.utils.ModuleName;
import org.folio.rest.tools.utils.NetworkUtils;
import org.junit.AfterClass;
import org.junit.Before;
@@ -66,6 +69,7 @@
import io.vertx.ext.web.client.WebClient;
import io.vertx.sqlclient.Row;
import io.vertx.sqlclient.RowSet;
+import io.vertx.sqlclient.Tuple;
@RunWith(VertxUnitRunner.class)
public class TenantRefApiTests {
@@ -108,6 +112,7 @@ public class TenantRefApiTests {
private static final String REQUEST_ID_MISSING_SUFFIX = "ecd86aab-a0ac-4d3c-bb90-0df390b1c6c4";
private static final String REQUEST_ID_MISSING_PICKUP_SERVICE_POINT_NAME =
"87a7dfd9-8fdb-4b0d-9529-14912b484860";
+ private static final String OTHER_CANCELLATION_REASON_ID = "b548b182-55c2-4741-b169-616d9cd995a8";
private static StubMapping itemStorageStub;
private static StubMapping holdingsStorageStub;
@@ -456,6 +461,17 @@ public void migrationShouldNotCorrectFulfillmentPreferenceSpellingFromMigratedVe
});
}
+ @Test
+ public void keepReferenceData(TestContext context) {
+ setOtherCancellationReasonName("foo")
+ .compose(x -> assertOtherCancellationReasonName(context, "foo"))
+ .compose(x -> postTenant(context, "16.1.0", ModuleName.getModuleVersion()))
+ .compose(x -> assertOtherCancellationReasonName(context, "foo"))
+ .compose(x -> postTenant(context, "0.0.0", ModuleName.getModuleVersion()))
+ .compose(x -> assertOtherCancellationReasonName(context, "Other"))
+ .onComplete(context.asyncAssertSuccess());
+ }
+
private void jobFailsWhenRequestValidationFails(TestContext context, Async async,
JsonObject request, String expectedErrorMessage) {
@@ -595,6 +611,22 @@ private void validateRequestSearchMigrationResult(TestContext context, JsonObjec
}
}
+ private static Future> setOtherCancellationReasonName(String name) {
+ var json = new JsonObject()
+ .put("id", OTHER_CANCELLATION_REASON_ID)
+ .put("name", name)
+ .put("description", "Other")
+ .put("requiresAdditionalInformation", true);
+ return postgresClient.execute("UPDATE cancellation_reason SET jsonb=$1 WHERE id=$2",
+ Tuple.of(json, OTHER_CANCELLATION_REASON_ID));
+ }
+
+ private static Future assertOtherCancellationReasonName(TestContext context, String expected) {
+ return postgresClient.selectSingle("SELECT jsonb->>'name' FROM cancellation_reason WHERE id=$1",
+ Tuple.of(OTHER_CANCELLATION_REASON_ID))
+ .onComplete(context.asyncAssertSuccess(row -> assertThat(row.getString(0), is(expected))));
+ }
+
static void deleteTenant(TenantClient tenantClient) {
CompletableFuture future = new CompletableFuture<>();
tenantClient.deleteTenantByOperationId(jobId, deleted -> {
diff --git a/src/test/java/org/folio/rest/support/http/InterfaceUrls.java b/src/test/java/org/folio/rest/support/http/InterfaceUrls.java
index a60caef5..5d05430f 100644
--- a/src/test/java/org/folio/rest/support/http/InterfaceUrls.java
+++ b/src/test/java/org/folio/rest/support/http/InterfaceUrls.java
@@ -47,6 +47,9 @@ public static URL requestExpirationUrl() throws MalformedURLException {
public static URL checkOutStorageUrl(String subPath) throws MalformedURLException {
return storageUrl("/check-out-lock-storage" + subPath);
}
+ public static URL printEventsUrl(String subPath) throws MalformedURLException {
+ return storageUrl("/print-events-storage" + subPath);
+ }
public static URL circulationSettingsUrl(String subPath) throws MalformedURLException {
return storageUrl("/circulation-settings-storage/circulation-settings" + subPath);