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);