diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 37fcbce920..4223f70edf 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -737,6 +737,23 @@ } ] }, + { + "id": "print-events", + "version": "1.0", + "handlers": [ + { + "methods": ["POST"], + "pathPattern": "/circulation/print-events-entry", + "permissionsRequired": [ + "circulation.print-events-entry.item.post" + ], + "modulePermissions": [ + "print-events-storage.print-events-entry.item.post", + "circulation-storage.circulation-settings.collection.get" + ] + } + ] + }, { "id": "_timer", "version": "1.0", @@ -1318,6 +1335,10 @@ { "id": "circulation-settings-storage", "version": "1.0" + }, + { + "id": "print-events-storage", + "version": "1.0" } ], "optional": [ @@ -1327,6 +1348,11 @@ } ], "permissionSets": [ + { + "permissionName": "circulation.print-events-entry.item.post", + "displayName": "circulation - create print events", + "description": "create print event logs" + }, { "permissionName": "circulation.requests.queue.reorder.collection.post", "displayName": "circulation - reorder queue for an item", diff --git a/ramls/examples/print-events-request.json b/ramls/examples/print-events-request.json new file mode 100644 index 0000000000..8505827a64 --- /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/src/main/java/org/folio/circulation/CirculationVerticle.java b/src/main/java/org/folio/circulation/CirculationVerticle.java index f0d2371440..4956ddb53a 100644 --- a/src/main/java/org/folio/circulation/CirculationVerticle.java +++ b/src/main/java/org/folio/circulation/CirculationVerticle.java @@ -31,6 +31,7 @@ import org.folio.circulation.resources.OverdueFineCirculationRulesEngineResource; import org.folio.circulation.resources.OverdueFineScheduledNoticeProcessingResource; import org.folio.circulation.resources.PickSlipsResource; +import org.folio.circulation.resources.PrintEventsResource; import org.folio.circulation.resources.RequestByInstanceIdResource; import org.folio.circulation.resources.RequestCirculationRulesEngineResource; import org.folio.circulation.resources.RequestCollectionResource; @@ -152,6 +153,7 @@ public void start(Promise startFuture) { new LoanRelatedFeeFineClosedHandlerResource(client).register(router); new FeeFineBalanceChangedHandlerResource(client).register(router); new CirculationSettingsResource(client).register(router); + new PrintEventsResource(client).register(router); server.requestHandler(router) .listen(config().getInteger("port"), result -> { diff --git a/src/main/java/org/folio/circulation/domain/PrintEventRequest.java b/src/main/java/org/folio/circulation/domain/PrintEventRequest.java new file mode 100644 index 0000000000..0145f4256b --- /dev/null +++ b/src/main/java/org/folio/circulation/domain/PrintEventRequest.java @@ -0,0 +1,59 @@ +package org.folio.circulation.domain; + +import io.vertx.core.json.JsonObject; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.lang.invoke.MethodHandles; +import java.util.List; +import java.util.Set; + +import static lombok.AccessLevel.PRIVATE; +import static org.folio.circulation.support.json.JsonPropertyFetcher.getArrayProperty; +import static org.folio.circulation.support.json.JsonPropertyFetcher.getProperty; + +@AllArgsConstructor(access = PRIVATE) +@ToString(onlyExplicitlyIncluded = true) +public class PrintEventRequest { + private static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); + public static final String REQUEST_IDS_FIELD = "requestIds"; + public static final String REQUESTER_ID_FIELD = "requesterId"; + public static final String REQUESTER_NAME_FIELD = "requesterName"; + public static final String PRINT_DATE_FIELD = "printEventDate"; + + @ToString.Include + @Getter + private final JsonObject representation; + + @Getter + private final List requestIds; + @Getter + private final String requesterId; + @Getter + private final String requesterName; + @Getter + private final String printEventDate; + + public static PrintEventRequest from(JsonObject representation) { + final var requestIds = getArrayProperty(representation, REQUEST_IDS_FIELD).stream() + .map(String.class::cast) + .toList(); + final var requesterId = getProperty(representation, REQUESTER_ID_FIELD); + final var requesterName = getProperty(representation, REQUESTER_NAME_FIELD); + final var printEventDate = getProperty(representation, PRINT_DATE_FIELD); + + if (requestIds.isEmpty() || null == requesterId || null == requesterName || null == printEventDate || !containsOnlyKnownFields(representation)) { + log.info("from:: Print Event Request JSON is invalid: {},{},{},{},{}", representation, requestIds, requesterName, requesterId, printEventDate); + return null; + } + return new PrintEventRequest(representation, requestIds, requesterId, requesterName, printEventDate); + } + + private static boolean containsOnlyKnownFields(JsonObject representation) { + return Set.of(REQUEST_IDS_FIELD, REQUESTER_ID_FIELD, REQUESTER_NAME_FIELD, PRINT_DATE_FIELD) + .containsAll(representation.fieldNames()); + } +} diff --git a/src/main/java/org/folio/circulation/infrastructure/storage/PrintEventsRepository.java b/src/main/java/org/folio/circulation/infrastructure/storage/PrintEventsRepository.java new file mode 100644 index 0000000000..1a179857ff --- /dev/null +++ b/src/main/java/org/folio/circulation/infrastructure/storage/PrintEventsRepository.java @@ -0,0 +1,35 @@ +package org.folio.circulation.infrastructure.storage; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.folio.circulation.domain.PrintEventRequest; +import org.folio.circulation.support.Clients; +import org.folio.circulation.support.CollectionResourceClient; +import org.folio.circulation.support.http.client.ResponseInterpreter; +import org.folio.circulation.support.results.Result; + +import java.lang.invoke.MethodHandles; +import java.util.concurrent.CompletableFuture; + +import static org.folio.circulation.support.http.ResponseMapping.forwardOnFailure; +import static org.folio.circulation.support.results.Result.succeeded; + +public class PrintEventsRepository { + private static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); + + private final CollectionResourceClient printEventsStorageClient; + + public PrintEventsRepository(Clients clients) { + printEventsStorageClient = clients.printEventsStorageClient(); + } + + public CompletableFuture> create(PrintEventRequest printEventRequest) { + log.info("create:: parameters printEvent: {}", printEventRequest); + final var storagePrintEventRequest = printEventRequest.getRepresentation(); + final ResponseInterpreter interpreter = new ResponseInterpreter() + .on(201, succeeded(null)) + .otherwise(forwardOnFailure()); + return printEventsStorageClient.post(storagePrintEventRequest).thenApply(interpreter::flatMap); + } + +} diff --git a/src/main/java/org/folio/circulation/resources/PrintEventsResource.java b/src/main/java/org/folio/circulation/resources/PrintEventsResource.java new file mode 100644 index 0000000000..72ba78fcd1 --- /dev/null +++ b/src/main/java/org/folio/circulation/resources/PrintEventsResource.java @@ -0,0 +1,111 @@ +package org.folio.circulation.resources; + +import io.vertx.core.http.HttpClient; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; +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.domain.PrintEventRequest; +import org.folio.circulation.infrastructure.storage.CirculationSettingsRepository; +import org.folio.circulation.infrastructure.storage.PrintEventsRepository; +import org.folio.circulation.infrastructure.storage.requests.RequestRepository; +import org.folio.circulation.support.Clients; +import org.folio.circulation.support.RouteRegistration; +import org.folio.circulation.support.http.server.JsonHttpResponse; +import org.folio.circulation.support.http.server.WebContext; +import org.folio.circulation.support.results.Result; + +import java.lang.invoke.MethodHandles; +import java.util.Collection; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +import static org.folio.circulation.support.ValidationErrorFailure.singleValidationError; +import static org.folio.circulation.support.json.JsonPropertyFetcher.getProperty; +import static org.folio.circulation.support.results.Result.ofAsync; +import static org.folio.circulation.support.results.Result.succeeded; + +public class PrintEventsResource extends Resource { + private static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); + private static final String PRINT_EVENT_FLAG_QUERY = "query=name=printEventLogFeature"; + private static final String PRINT_EVENT_FEATURE_DISABLED_ERROR = "print event feature is disabled for this tenant"; + private static final String NO_CONFIG_FOUND_ERROR = "No configuration found for print event feature"; + private static final String MULTIPLE_CONFIGS_ERROR = "Multiple configurations found for print event feature"; + private static final String PRINT_EVENT_FLAG_PROPERTY_NAME = "enablePrintLog"; + + public PrintEventsResource(HttpClient client) { + super(client); + } + + @Override + public void register(Router router) { + new RouteRegistration("/circulation/print-events-entry", router) + .create(this::create); + } + + void create(RoutingContext routingContext) { + final var context = new WebContext(routingContext); + final var clients = Clients.create(context, client); + final var printEventsRepository = new PrintEventsRepository(clients); + final var circulationSettingsRepository = new CirculationSettingsRepository(clients); + final var requestRepository = new RequestRepository(clients); + final var incomingRepresentation = routingContext.body().asJsonObject(); + final var printEventRequest = PrintEventRequest.from(incomingRepresentation); + + log.info("create:: Creating print event: {}", printEventRequest); + + ofAsync(printEventRequest) + .thenApply(refuseWhenPrintEventRequestIsInvalid()) + .thenCompose(r -> r.after(validatePrintEventFeatureFlag(circulationSettingsRepository))) + .thenCompose(r -> r.after(validateRequests(requestRepository))) + .thenCompose(r -> r.after(printEventsRepository::create)) + .thenApply(r -> r.map(response -> JsonHttpResponse.created(null, null))) + .thenAccept(context::writeResultToHttpResponse); + } + + private static Function>> + validateRequests(RequestRepository requestRepository) { + return printRequest -> requestRepository.fetchRequests(printRequest.getRequestIds()) + .thenApply(printRequestList -> printRequestList.map(Collection::size)).thenApply(size -> { + if (size.value() != printRequest.getRequestIds().size()) { + return Result.failed(singleValidationError("invalid request found", "", "")); + } + return succeeded(printRequest); + }); + } + + private static Function, Result> + refuseWhenPrintEventRequestIsInvalid() { + return r -> r.failWhen(printEventRequest -> succeeded(printEventRequest == null), + circulationSetting -> singleValidationError("Print Event Request JSON is invalid", "", "")); + } + + private static Function>> validatePrintEventFeatureFlag( + CirculationSettingsRepository circulationSettingsRepository) { + return printEventRequest -> circulationSettingsRepository.findBy(PRINT_EVENT_FLAG_QUERY) + .thenApply(result -> + handleCirculationSettingResult(result.map(MultipleRecords::getRecords), printEventRequest) + ); + } + + private static Result handleCirculationSettingResult(Result> result, + PrintEventRequest printEventRequest) { + + int size = result.value().size(); + if (size == 0) { + return Result.failed(singleValidationError(NO_CONFIG_FOUND_ERROR, "", "")); + } else if (size > 1) { + return Result.failed(singleValidationError(MULTIPLE_CONFIGS_ERROR, "", "")); + } + boolean isEnabled = result.value().stream() + .map(x -> Boolean.valueOf(getProperty(x.getValue(), PRINT_EVENT_FLAG_PROPERTY_NAME))).findFirst().orElse(true); + + if (!isEnabled) { + return Result.failed(singleValidationError(PRINT_EVENT_FEATURE_DISABLED_ERROR, "", "")); + } + return succeeded(printEventRequest); + } + +} diff --git a/src/main/java/org/folio/circulation/support/Clients.java b/src/main/java/org/folio/circulation/support/Clients.java index 404b433461..99be88183c 100644 --- a/src/main/java/org/folio/circulation/support/Clients.java +++ b/src/main/java/org/folio/circulation/support/Clients.java @@ -70,6 +70,8 @@ public class Clients { private final CollectionResourceClient circulationItemClient; private final GetManyRecordsClient settingsStorageClient; private final CollectionResourceClient circulationSettingsStorageClient; + private final CollectionResourceClient printEventsStorageClient; + public static Clients create(WebContext context, HttpClient httpClient) { return new Clients(context.createHttpClient(httpClient), context); @@ -138,6 +140,8 @@ private Clients(OkapiHttpClient client, WebContext context) { settingsStorageClient = createSettingsStorageClient(client, context); circulationItemClient = createCirculationItemClient(client, context); circulationSettingsStorageClient = createCirculationSettingsStorageClient(client, context); + printEventsStorageClient = createPrintEventsStorageClient(client, context); + } catch(MalformedURLException e) { throw new InvalidOkapiLocationException(context.getOkapiLocation(), e); @@ -380,6 +384,10 @@ public CollectionResourceClient circulationSettingsStorageClient() { return circulationSettingsStorageClient; } + public CollectionResourceClient printEventsStorageClient() { + return printEventsStorageClient; + } + private static CollectionResourceClient getCollectionResourceClient( OkapiHttpClient client, WebContext context, String path) @@ -814,6 +822,13 @@ private CollectionResourceClient createCirculationSettingsStorageClient( "/circulation-settings-storage/circulation-settings"); } + private CollectionResourceClient createPrintEventsStorageClient( + OkapiHttpClient client, WebContext context) throws MalformedURLException { + + return getCollectionResourceClient(client, context, + "/print-events-storage/print-events-entry"); + } + private GetManyRecordsClient createSettingsStorageClient( OkapiHttpClient client, WebContext context) throws MalformedURLException { diff --git a/src/test/java/api/printEvents/PrintEventsTests.java b/src/test/java/api/printEvents/PrintEventsTests.java new file mode 100644 index 0000000000..0d8b03bce7 --- /dev/null +++ b/src/test/java/api/printEvents/PrintEventsTests.java @@ -0,0 +1,129 @@ +package api.printEvents; + +import api.support.APITests; +import api.support.builders.CirculationSettingBuilder; +import api.support.builders.RequestBuilder; +import io.vertx.core.json.JsonObject; +import org.folio.circulation.support.http.client.Response; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.stream.IntStream; + +import static api.support.http.InterfaceUrls.printEventsUrl; +import static api.support.matchers.ResponseStatusCodeMatcher.hasStatus; +import static org.folio.HttpStatus.HTTP_CREATED; +import static org.folio.HttpStatus.HTTP_UNPROCESSABLE_ENTITY; +import static org.hamcrest.MatcherAssert.assertThat; + +class PrintEventsTests extends APITests { + + + @Test + void postPrintEventsTest() { + circulationSettingsClient.create(new CirculationSettingBuilder() + .withName("printEventLogFeature") + .withValue(new JsonObject().put("enablePrintLog", true))); + JsonObject printRequest = getPrintEvent(); + printRequest.put("requestIds", createOneHundredRequests()); + System.out.println(printRequest.getString("requestIds")); + Response response = restAssuredClient.post(printRequest, printEventsUrl("/print-events-entry"), "post-print-event"); + assertThat(response, hasStatus(HTTP_CREATED)); + } + + @Test + void postPrintEventsWhenCirculationSettingIsNotPresentTest() { + JsonObject printRequest = getPrintEvent(); + printRequest.put("requestIds", List.of(UUID.randomUUID())); + Response response = restAssuredClient.post(printRequest, printEventsUrl("/print-events-entry"), "post-print-event"); + assertThat(response, hasStatus(HTTP_UNPROCESSABLE_ENTITY)); + } + + @Test + void postPrintEventsWhenDuplicateCirculationSettingFound() { + circulationSettingsClient.create(new CirculationSettingBuilder() + .withName("printEventLogFeature") + .withValue(new JsonObject().put("enablePrintLog", true))); + circulationSettingsClient.create(new CirculationSettingBuilder() + .withName("printEventLogFeature") + .withValue(new JsonObject().put("Enable-Print-Event", false))); + + JsonObject printRequest = getPrintEvent(); + printRequest.put("requestIds", List.of(UUID.randomUUID())); + Response response = restAssuredClient.post(printRequest, printEventsUrl("/print-events-entry"), "post-print-event"); + assertThat(response, hasStatus(HTTP_UNPROCESSABLE_ENTITY)); + } + + @Test + void postPrintEventsWhenPrintEventSettingIsDisable() { + circulationSettingsClient.create(new CirculationSettingBuilder() + .withName("printEventLogFeature") + .withValue(new JsonObject().put("enablePrintLog", false))); + + JsonObject printRequest = getPrintEvent(); + printRequest.put("requestIds", List.of(UUID.randomUUID())); + Response response = restAssuredClient.post(printRequest, printEventsUrl("/print-events-entry"), "post-print-event"); + assertThat(response, hasStatus(HTTP_UNPROCESSABLE_ENTITY)); + } + + @Test + void postPrintEventsWithInvalidRequestId() { + circulationSettingsClient.create(new CirculationSettingBuilder() + .withName("printEventLogFeature") + .withValue(new JsonObject().put("enablePrintLog", true))); + JsonObject printRequest = getPrintEvent(); + List requestIds = new ArrayList<>(createOneHundredRequests()); + requestIds.add(UUID.randomUUID()); + printRequest.put("requestIds", requestIds); + Response response = restAssuredClient.post(printRequest, printEventsUrl("/print-events-entry"), "post-print-event"); + assertThat(response, hasStatus(HTTP_UNPROCESSABLE_ENTITY)); + } + + + @Test + void postPrintEventsWithInvalidField() { + JsonObject printRequest = getPrintEvent(); + printRequest.put("invalidField", "invalid"); + Response response = restAssuredClient.post(printRequest, printEventsUrl("/print-events-entry"), "post-print-event"); + assertThat(response, hasStatus(HTTP_UNPROCESSABLE_ENTITY)); + } + + @Test + void postPrintEventsWithInvalidField_EmptyRequestIdsList() { + JsonObject printRequest = getPrintEvent(); + List requestIds = List.of(); + printRequest.put("requestIds", requestIds); + Response response = restAssuredClient.post(printRequest, printEventsUrl("/print-events-entry"), "post-print-event"); + assertThat(response, hasStatus(HTTP_UNPROCESSABLE_ENTITY)); + } + + @Test + void postPrintEventsWithInvalidField_NullField() { + JsonObject printRequest = getPrintEvent(); + printRequest.put("requesterId", null); + Response response = restAssuredClient.post(printRequest, printEventsUrl("/print-events-entry"), "post-print-event"); + assertThat(response, hasStatus(HTTP_UNPROCESSABLE_ENTITY)); + } + + private JsonObject getPrintEvent() { + return new JsonObject() + .put("requesterId", "5f5751b4-e352-4121-adca-204b0c2aec43") + .put("requesterName", "requester") + .put("printEventDate", "2024-06-25T14:30:00Z"); + } + + private List createOneHundredRequests() { + final UUID pickupServicePointId = servicePointsFixture.cd1().getId(); + + return IntStream.range(0, 100).mapToObj(notUsed -> requestsFixture.place( + new RequestBuilder() + .open() + .page() + .forItem(itemsFixture.basedUponSmallAngryPlanet()) + .by(usersFixture.charlotte()) + .fulfillToHoldShelf() + .withPickupServicePointId(pickupServicePointId)).getId()).toList(); + } +} diff --git a/src/test/java/api/support/APITests.java b/src/test/java/api/support/APITests.java index b872ce7165..6935d04e64 100644 --- a/src/test/java/api/support/APITests.java +++ b/src/test/java/api/support/APITests.java @@ -197,6 +197,9 @@ public abstract class APITests { protected final ResourceClient circulationSettingsClient = ResourceClient.forCirculationSettings(); + protected final ResourceClient printEventsClient = + ResourceClient.forPrintEvents(); + protected final ServicePointsFixture servicePointsFixture = new ServicePointsFixture(servicePointsClient); diff --git a/src/test/java/api/support/fakes/FakeOkapi.java b/src/test/java/api/support/fakes/FakeOkapi.java index 5cbecb8542..1ed5b8f5b0 100644 --- a/src/test/java/api/support/fakes/FakeOkapi.java +++ b/src/test/java/api/support/fakes/FakeOkapi.java @@ -422,6 +422,11 @@ public void start(Promise startFuture) throws IOException { .withChangeMetadata() .create().register(router); + new FakeStorageModuleBuilder() + .withRootPath("/print-events-storage/print-events-entry") + .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 59d35de534..23c5878ecc 100644 --- a/src/test/java/api/support/http/InterfaceUrls.java +++ b/src/test/java/api/support/http/InterfaceUrls.java @@ -337,4 +337,8 @@ public static URL settingsStorageUrl() { public static URL circulationSettingsUrl(String subPath) { return circulationModuleUrl("/circulation/settings" + subPath); } + + public static URL printEventsUrl(String subPath) { + return circulationModuleUrl("/circulation" + subPath); + } } diff --git a/src/test/java/api/support/http/ResourceClient.java b/src/test/java/api/support/http/ResourceClient.java index 8e9b7d63be..73893b3b33 100644 --- a/src/test/java/api/support/http/ResourceClient.java +++ b/src/test/java/api/support/http/ResourceClient.java @@ -276,6 +276,10 @@ public static ResourceClient forCirculationSettings() { return new ResourceClient(InterfaceUrls::circulationSettingsUrl, "circulationSettings"); } + public static ResourceClient forPrintEvents() { + return new ResourceClient(InterfaceUrls::printEventsUrl, " "); + } + private ResourceClient(UrlMaker urlMaker, String collectionArrayPropertyName) { this.urlMaker = urlMaker; this.collectionArrayPropertyName = collectionArrayPropertyName;