From 2d74a75f83e3cba04a6d27cc73b7be017f8d63c3 Mon Sep 17 00:00:00 2001 From: Roman Barannyk <53909129+roman-barannyk@users.noreply.github.com> Date: Mon, 22 Apr 2024 15:28:09 +0300 Subject: [PATCH 01/17] [CIRCSTORE-502] Add ecsRequestPhase field to request schema (#462) * CIRCSTORE-502 add ecsRequestPhase field to request schema * CIRCSTORE-502 add tests --- descriptors/ModuleDescriptor-template.json | 2 +- ramls/request.json | 5 +++ .../org/folio/rest/api/RequestsApiTest.java | 40 +++++++++++++++++++ .../builders/RequestRequestBuilder.java | 26 +++++++++--- 4 files changed, 67 insertions(+), 6 deletions(-) diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 85b318112..230491cc0 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -137,7 +137,7 @@ }, { "id": "request-storage", - "version": "6.0", + "version": "6.1", "handlers": [ { "methods": ["GET"], diff --git a/ramls/request.json b/ramls/request.json index aa582e033..f2fbfd1a4 100644 --- a/ramls/request.json +++ b/ramls/request.json @@ -18,6 +18,11 @@ "type": "string", "enum": ["Hold", "Recall", "Page"] }, + "ecsRequestPhase": { + "description": "Stage in ECS request process, absence of this field means this is a single-tenant request", + "type": "string", + "enum": ["Primary", "Secondary"] + }, "requestDate": { "description": "Date the request was made", "type": "string", diff --git a/src/test/java/org/folio/rest/api/RequestsApiTest.java b/src/test/java/org/folio/rest/api/RequestsApiTest.java index 14521c857..cf2435013 100644 --- a/src/test/java/org/folio/rest/api/RequestsApiTest.java +++ b/src/test/java/org/folio/rest/api/RequestsApiTest.java @@ -1958,6 +1958,46 @@ public void cannotCreateRequestWithoutStatus() ))); } + @Test + public void canCreateRequestWithEcsRequestPhase() throws MalformedURLException, + ExecutionException, InterruptedException, TimeoutException { + + JsonObject representation = createEntity( + new RequestRequestBuilder() + .page() + .primary() + .withId(UUID.randomUUID()) + .create(), + requestStorageUrl()).getJson(); + assertThat(representation.getString("ecsRequestPhase"), is("Primary")); + + representation = createEntity( + new RequestRequestBuilder() + .page() + .secondary() + .withId(UUID.randomUUID()) + .create(), + requestStorageUrl()).getJson(); + assertThat(representation.getString("ecsRequestPhase"), is("Secondary")); + } + + @Test + public void shouldReturn400IfInvalidEcsRequestPhase() throws MalformedURLException, + ExecutionException, InterruptedException, TimeoutException { + + var request = new RequestRequestBuilder() + .page() + .withEcsRequestPhase("Invalid") + .withId(UUID.randomUUID()) + .create(); + + CompletableFuture createCompleted = new CompletableFuture<>(); + client.post(requestStorageUrl(), request, TENANT_ID, ResponseHandler.json(createCompleted)); + + assertThat(createCompleted.get(5, TimeUnit.SECONDS).getStatusCode(), is(400)); + } + + private RequestDto.RequestDtoBuilder holdShelfOpenRequest() { return RequestDto.builder() .requesterId(UUID.randomUUID().toString()) diff --git a/src/test/java/org/folio/rest/support/builders/RequestRequestBuilder.java b/src/test/java/org/folio/rest/support/builders/RequestRequestBuilder.java index ccb8f60c6..d849b35e3 100644 --- a/src/test/java/org/folio/rest/support/builders/RequestRequestBuilder.java +++ b/src/test/java/org/folio/rest/support/builders/RequestRequestBuilder.java @@ -52,6 +52,7 @@ public class RequestRequestBuilder extends JsonBuilder { private final String patronComments; private final UUID holdingsRecordId; private final SearchIndex searchIndex; + private final String ecsRequestPhase; public RequestRequestBuilder() { this(UUID.randomUUID(), @@ -79,6 +80,7 @@ public RequestRequestBuilder() { null, null, UUID.randomUUID(), + null, null); } @@ -101,6 +103,7 @@ public JsonObject create() { put(request, "requestExpirationDate", this.requestExpirationDate); put(request, "holdShelfExpirationDate", this.holdShelfExpirationDate); put(request, "pickupServicePointId", this.pickupServicePointId); + put(request, "ecsRequestPhase", this.ecsRequestPhase); if (this.itemSummary != null) { final JsonObject item = new JsonObject(); @@ -204,7 +207,8 @@ public RequestRequestBuilder withNoId() { this.tags, this.patronComments, this.holdingsRecordId, - this.searchIndex); + this.searchIndex, + this.ecsRequestPhase); } public RequestRequestBuilder toHoldShelf() { @@ -247,7 +251,8 @@ public RequestRequestBuilder withItem(RequestItemSummary item) { this.tags, this.patronComments, this.holdingsRecordId, - this.searchIndex); + this.searchIndex, + this.ecsRequestPhase); } public RequestRequestBuilder withRequester( @@ -282,7 +287,8 @@ public RequestRequestBuilder withRequester( this.tags, this.patronComments, this.holdingsRecordId, - this.searchIndex); + this.searchIndex, + this.ecsRequestPhase); } public RequestRequestBuilder withRequester( @@ -316,7 +322,8 @@ public RequestRequestBuilder withRequester( this.tags, this.patronComments, this.holdingsRecordId, - this.searchIndex); + this.searchIndex, + this.ecsRequestPhase); } public RequestRequestBuilder withProxy( @@ -350,7 +357,8 @@ public RequestRequestBuilder withProxy( this.tags, this.patronComments, this.holdingsRecordId, - this.searchIndex); + this.searchIndex, + this.ecsRequestPhase); } public RequestRequestBuilder withNoPosition() { return withPosition(null); @@ -375,4 +383,12 @@ private class PatronSummary { } } + public RequestRequestBuilder primary() { + return withEcsRequestPhase("Primary"); + } + + public RequestRequestBuilder secondary() { + return withEcsRequestPhase("Secondary"); + } + } From 81c55e3d9ec864b82102384f3510837af20503a4 Mon Sep 17 00:00:00 2001 From: Magzhan Date: Tue, 11 Jun 2024 21:43:04 +0500 Subject: [PATCH 02/17] CIRCSTORE-509: Create CRUD API for storing circulation settings (#466) * CIRCSTORE-509 Create CRUD API for storing circulation settings * CIRCSTORE-509 Create CRUD API for storing circulation settings * CIRCSTORE-509 Create CRUD API for storing circulation settings * CIRCSTORE-509 Create CRUD API for storing circulation settings * CIRCSTORE-509 Create CRUD API for storing circulation settings * CIRCSTORE-509 Create CRUD API for storing circulation settings * CIRCSTORE-509 Create CRUD API for storing circulation settings * CIRCSTORE-509 Create CRUD API for storing circulation settings * CIRCSTORE-509 Create CRUD API for storing circulation settings * CIRCSTORE-509 Create CRUD API for storing circulation settings * CIRCSTORE-509 Create CRUD API for storing circulation settings * CIRCSTORE-509 Fix formatting Co-authored-by: OleksandrVidinieiev <56632770+OleksandrVidinieiev@users.noreply.github.com> --------- Co-authored-by: Alexander Kurash Co-authored-by: OleksandrVidinieiev <56632770+OleksandrVidinieiev@users.noreply.github.com> --- descriptors/ModuleDescriptor-template.json | 70 +++++++++++- ramls/circulation-setting.json | 33 ++++++ ramls/circulation-settings-storage.raml | 106 ++++++++++++++++++ ramls/circulation-settings.json | 23 ++++ ramls/examples/circulation-setting.json | 7 ++ ramls/examples/circulation-settings.json | 12 ++ .../CirculationSettingsRepository.java | 22 ++++ .../rest/impl/CirculationSettingsAPI.java | 67 +++++++++++ .../org/folio/rest/impl/TenantRefAPI.java | 4 +- .../service/CirculationSettingsService.java | 70 ++++++++++++ .../EntityChangedEventPublisherFactory.java | 14 +++ .../org/folio/support/ModuleConstants.java | 2 + .../topic/CirculationStorageKafkaTopic.java | 1 + .../templates/db_scripts/schema.json | 5 + .../rest/api/CirculationSettingsAPITest.java | 81 +++++++++++++ .../rest/support/http/InterfaceUrls.java | 4 + .../topic/KafkaAdminClientServiceTest.java | 3 +- 17 files changed, 521 insertions(+), 3 deletions(-) create mode 100644 ramls/circulation-setting.json create mode 100644 ramls/circulation-settings-storage.raml create mode 100644 ramls/circulation-settings.json create mode 100644 ramls/examples/circulation-setting.json create mode 100644 ramls/examples/circulation-settings.json create mode 100644 src/main/java/org/folio/persist/CirculationSettingsRepository.java create mode 100644 src/main/java/org/folio/rest/impl/CirculationSettingsAPI.java create mode 100644 src/main/java/org/folio/service/CirculationSettingsService.java create mode 100644 src/test/java/org/folio/rest/api/CirculationSettingsAPITest.java diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 230491cc0..0b1215845 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -104,6 +104,44 @@ } ] }, + { + "id": "circulation-settings-storage", + "version": "1.0", + "handlers": [ + { + "methods": ["GET"], + "pathPattern": "/circulation-settings-storage/circulation-settings", + "permissionsRequired": [ + "circulation-storage.circulation-settings.collection.get" + ] + }, + { + "methods": ["GET"], + "pathPattern": "/circulation-settings-storage/circulation-settings/{id}", + "permissionsRequired": [ + "circulation-storage.circulation-settings.item.get" + ] + }, { + "methods": ["PUT"], + "pathPattern": "/circulation-settings-storage/circulation-settings/{id}", + "permissionsRequired": [ + "circulation-storage.circulation-settings.item.put" + ] + }, { + "methods": ["POST"], + "pathPattern": "/circulation-settings-storage/circulation-settings", + "permissionsRequired": [ + "circulation-storage.circulation-settings.item.post" + ] + }, { + "methods": ["DELETE"], + "pathPattern": "/circulation-settings-storage/circulation-settings/{id}", + "permissionsRequired": [ + "circulation-storage.circulation-settings.item.delete" + ] + } + ] + }, { "id": "loan-policy-storage", "version": "2.3", @@ -970,6 +1008,31 @@ "displayName": "Circulation storage - get expired session patron ids collection", "description": "Get expired session patron ids collection from storage" }, + { + "permissionName": "circulation-storage.circulation-settings.collection.get", + "displayName": "Circulation storage - get circulation settings collection", + "description": "Get circulation settings collection from storage" + }, + { + "permissionName": "circulation-storage.circulation-settings.item.get", + "displayName": "Circulation storage - get circulation setting by id", + "description": "Get circulation setting by id from storage" + }, + { + "permissionName": "circulation-storage.circulation-settings.item.post", + "displayName": "Circulation storage - create circulation setting", + "description": "Create circulation setting in storage" + }, + { + "permissionName": "circulation-storage.circulation-settings.item.put", + "displayName": "Circulation storage - update circulation setting by id", + "description": "Update circulation setting by id" + }, + { + "permissionName": "circulation-storage.circulation-settings.item.delete", + "displayName": "Circulation storage - delete circulation setting by id", + "description": "Delete circulation setting by id" + }, { "permissionName": "circulation-storage.all", "displayName": "Circulation storage module - all permissions", @@ -1061,7 +1124,12 @@ "checkout-lock-storage.checkout-locks.item.post", "checkout-lock-storage.checkout-locks.item.delete", "checkout-lock-storage.checkout-locks.item.get", - "checkout-lock-storage.checkout-locks.collection.get" + "checkout-lock-storage.checkout-locks.collection.get", + "circulation-storage.circulation-settings.collection.get", + "circulation-storage.circulation-settings.item.get", + "circulation-storage.circulation-settings.item.post", + "circulation-storage.circulation-settings.item.put", + "circulation-storage.circulation-settings.item.delete" ] }, { diff --git a/ramls/circulation-setting.json b/ramls/circulation-setting.json new file mode 100644 index 000000000..0907442e8 --- /dev/null +++ b/ramls/circulation-setting.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Circulation Setting Schema", + "description": "Circulation setting", + "type": "object", + "properties": { + "id": { + "description": "ID of the circulation setting", + "type": "string", + "$ref": "raml-util/schemas/uuid.schema" + }, + "name": { + "description": "Circulation setting name", + "type": "string" + }, + "value": { + "description": "Circulation setting", + "type": "object", + "additionalProperties": true + }, + "metadata": { + "description": "Metadata about creation and changes, provided by the server (client should not provide)", + "type": "object", + "$ref": "raml-util/schemas/metadata.schema" + } + }, + "additionalProperties": false, + "required": [ + "id", + "name", + "value" + ] +} diff --git a/ramls/circulation-settings-storage.raml b/ramls/circulation-settings-storage.raml new file mode 100644 index 000000000..7f87a73e9 --- /dev/null +++ b/ramls/circulation-settings-storage.raml @@ -0,0 +1,106 @@ +#%RAML 1.0 +title: Circulation Settings Storage +version: v1.0 +protocols: [ HTTP, HTTPS ] +baseUri: http://localhost:9130 + +documentation: + - title: Circulation Settings Storage API + content: Storage for circulation settings + +traits: + language: !include raml-util/traits/language.raml + pageable: !include raml-util/traits/pageable.raml + searchable: !include raml-util/traits/searchable.raml + validate: !include raml-util/traits/validation.raml + +types: + circulation-setting: !include circulation-setting.json + circulation-settings: !include circulation-settings.json + errors: !include raml-util/schemas/errors.schema + parameters: !include raml-util/schemas/parameters.schema + +resourceTypes: + collection: !include raml-util/rtypes/collection.raml + collection-item: !include raml-util/rtypes/item-collection.raml + +/circulation-settings-storage: + /circulation-settings: + type: + collection: + exampleCollection: !include examples/circulation-settings.json + exampleItem: !include examples/circulation-setting.json + schemaCollection: circulation-settings + schemaItem: circulation-setting + post: + is: [validate] + description: Create a new circulation setting + body: + application/json: + type: circulation-setting + responses: + 201: + description: "Circulation setting has been created" + body: + application/json: + type: circulation-setting + 500: + description: "Internal server error" + body: + text/plain: + example: "Internal server error" + get: + is: [validate, pageable, searchable: { description: "with valid searchable fields", example: "id=497f6eca-6276-4993-bfeb-98cbbbba8f79" }] + description: Get all circulation settings + responses: + 200: + description: "Circulation settings successfully retreived" + body: + application/json: + type: circulation-settings + 500: + description: "Internal server error" + body: + text/plain: + example: "Internal server error" + /{circulationSettingsId}: + type: + collection-item: + exampleItem: !include examples/circulation-setting.json + schema: circulation-setting + get: + responses: + 200: + description: "Circulation setting successfully retreived" + body: + application/json: + type: circulation-setting + 500: + description: "Internal server error" + body: + text/plain: + example: "Internal server error" + put: + is: [ validate ] + body: + application/json: + type: circulation-setting + responses: + 204: + description: "Circulation settings have been saved." + 500: + description: "Internal server error" + body: + text/plain: + example: "Internal server error" + delete: + is: [ validate ] + responses: + 204: + description: "Circulation settings deleted" + 500: + description: "Internal server error" + body: + text/plain: + example: "Internal server error" + diff --git a/ramls/circulation-settings.json b/ramls/circulation-settings.json new file mode 100644 index 000000000..99173df7b --- /dev/null +++ b/ramls/circulation-settings.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Collection of Circulation settings", + "type": "object", + "properties": { + "circulationSettings": { + "description": "List of circulation settings", + "id": "circulationSettings", + "type": "array", + "items": { + "type": "object", + "$ref": "circulation-setting.json" + } + }, + "totalRecords": { + "type": "integer" + } + }, + "required": [ + "circulationSettings", + "totalRecords" + ] +} diff --git a/ramls/examples/circulation-setting.json b/ramls/examples/circulation-setting.json new file mode 100644 index 000000000..35f0aba43 --- /dev/null +++ b/ramls/examples/circulation-setting.json @@ -0,0 +1,7 @@ +{ + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f09", + "name": "Sample settings", + "value": { + "org.folio.circulation.settings": "true" + } +} diff --git a/ramls/examples/circulation-settings.json b/ramls/examples/circulation-settings.json new file mode 100644 index 000000000..b9b7a9c8b --- /dev/null +++ b/ramls/examples/circulation-settings.json @@ -0,0 +1,12 @@ +{ + "circulationSettings": [ + { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f09", + "name": "Sample settings", + "value": { + "org.folio.circulation.settings": "true" + } + } + ], + "totalRecords": 1 +} diff --git a/src/main/java/org/folio/persist/CirculationSettingsRepository.java b/src/main/java/org/folio/persist/CirculationSettingsRepository.java new file mode 100644 index 000000000..fd0f9757f --- /dev/null +++ b/src/main/java/org/folio/persist/CirculationSettingsRepository.java @@ -0,0 +1,22 @@ +package org.folio.persist; + +import static org.folio.rest.persist.PgUtil.postgresClient; +import static org.folio.support.ModuleConstants.CIRCULATION_SETTINGS_TABLE; + +import java.util.Map; + +import org.folio.rest.jaxrs.model.CirculationSetting; + +import io.vertx.core.Context; + +public class CirculationSettingsRepository + extends AbstractRepository { + + public CirculationSettingsRepository(Context context, Map okapiHeaders) { + + super(postgresClient(context, okapiHeaders), CIRCULATION_SETTINGS_TABLE, + CirculationSetting.class); + } + +} diff --git a/src/main/java/org/folio/rest/impl/CirculationSettingsAPI.java b/src/main/java/org/folio/rest/impl/CirculationSettingsAPI.java new file mode 100644 index 000000000..07e05a95f --- /dev/null +++ b/src/main/java/org/folio/rest/impl/CirculationSettingsAPI.java @@ -0,0 +1,67 @@ +package org.folio.rest.impl; + +import java.util.Map; + +import javax.ws.rs.core.Response; + +import org.folio.rest.jaxrs.model.CirculationSetting; +import org.folio.rest.jaxrs.resource.CirculationSettingsStorage; +import org.folio.service.CirculationSettingsService; + +import io.vertx.core.AsyncResult; +import io.vertx.core.Context; +import io.vertx.core.Handler; + +public class CirculationSettingsAPI implements CirculationSettingsStorage { + + @Override + public void postCirculationSettingsStorageCirculationSettings(String lang, + CirculationSetting circulationSettings, Map okapiHeaders, + Handler> asyncResultHandler, Context vertxContext) { + + new CirculationSettingsService(vertxContext, okapiHeaders) + .create(circulationSettings) + .onComplete(asyncResultHandler); + } + + @Override + public void getCirculationSettingsStorageCirculationSettings(int offset, + int limit, String query, String lang, Map okapiHeaders, + Handler> asyncResultHandler, Context vertxContext) { + + new CirculationSettingsService(vertxContext, okapiHeaders) + .getAll(offset, limit, query) + .onComplete(asyncResultHandler); + } + + @Override + public void getCirculationSettingsStorageCirculationSettingsByCirculationSettingsId( + String circulationSettingsId, String lang, Map okapiHeaders, + Handler> asyncResultHandler, Context vertxContext) { + + new CirculationSettingsService(vertxContext, okapiHeaders) + .findById(circulationSettingsId) + .onComplete(asyncResultHandler); + } + + @Override + public void putCirculationSettingsStorageCirculationSettingsByCirculationSettingsId( + String circulationSettingsId, String lang, CirculationSetting entity, + Map okapiHeaders, Handler> asyncResultHandler, + Context vertxContext) { + + new CirculationSettingsService(vertxContext, okapiHeaders) + .update(circulationSettingsId, entity) + .onComplete(asyncResultHandler); + } + + @Override + public void deleteCirculationSettingsStorageCirculationSettingsByCirculationSettingsId( + String circulationSettingsId, String lang, Map okapiHeaders, + Handler> asyncResultHandler, Context vertxContext) { + + new CirculationSettingsService(vertxContext, okapiHeaders) + .delete(circulationSettingsId) + .onComplete(asyncResultHandler); + } +} diff --git a/src/main/java/org/folio/rest/impl/TenantRefAPI.java b/src/main/java/org/folio/rest/impl/TenantRefAPI.java index af808b14d..bca5f474a 100644 --- a/src/main/java/org/folio/rest/impl/TenantRefAPI.java +++ b/src/main/java/org/folio/rest/impl/TenantRefAPI.java @@ -58,7 +58,9 @@ Future loadData(TenantAttributes attributes, String tenantId, .add("cancellation-reason-storage/cancellation-reasons") .withKey(SAMPLE_KEY).withLead(SAMPLE_LEAD) .add("loans", "loan-storage/loans") - .add("requests", "request-storage/requests"); + .add("requests", "request-storage/requests") + .add("circulation-settings-storage/circulation-settings"); + tl.perform(attributes, headers, vertx, res -> { if (res.failed()) { diff --git a/src/main/java/org/folio/service/CirculationSettingsService.java b/src/main/java/org/folio/service/CirculationSettingsService.java new file mode 100644 index 000000000..a0c950074 --- /dev/null +++ b/src/main/java/org/folio/service/CirculationSettingsService.java @@ -0,0 +1,70 @@ +package org.folio.service; + +import static org.folio.service.event.EntityChangedEventPublisherFactory.circulationSettingsEventPublisher; +import static org.folio.support.ModuleConstants.CIRCULATION_SETTINGS_TABLE; + +import java.util.Map; + +import javax.ws.rs.core.Response; + +import org.folio.persist.CirculationSettingsRepository; +import org.folio.rest.jaxrs.model.CirculationSetting; +import org.folio.rest.jaxrs.model.CirculationSettings; +import org.folio.rest.jaxrs.resource.CirculationSettingsStorage.DeleteCirculationSettingsStorageCirculationSettingsByCirculationSettingsIdResponse; +import org.folio.rest.jaxrs.resource.CirculationSettingsStorage.GetCirculationSettingsStorageCirculationSettingsByCirculationSettingsIdResponse; +import org.folio.rest.jaxrs.resource.CirculationSettingsStorage.GetCirculationSettingsStorageCirculationSettingsResponse; +import org.folio.rest.jaxrs.resource.CirculationSettingsStorage.PostCirculationSettingsStorageCirculationSettingsResponse; +import org.folio.rest.jaxrs.resource.CirculationSettingsStorage.PutCirculationSettingsStorageCirculationSettingsByCirculationSettingsIdResponse; +import org.folio.rest.persist.PgUtil; +import org.folio.service.event.EntityChangedEventPublisher; + +import io.vertx.core.Context; +import io.vertx.core.Future; + +public class CirculationSettingsService { + + private final Context vertxContext; + private final Map okapiHeaders; + private final CirculationSettingsRepository repository; + private final EntityChangedEventPublisher eventPublisher; + + public CirculationSettingsService(Context vertxContext, Map okapiHeaders) { + this.vertxContext = vertxContext; + this.okapiHeaders = okapiHeaders; + this.repository = new CirculationSettingsRepository(vertxContext, okapiHeaders); + this.eventPublisher = circulationSettingsEventPublisher(vertxContext, okapiHeaders); + } + + public Future getAll(int offset, int limit, String query) { + return PgUtil.get(CIRCULATION_SETTINGS_TABLE, CirculationSetting.class, + CirculationSettings.class, + query, offset, limit, okapiHeaders, vertxContext, + GetCirculationSettingsStorageCirculationSettingsResponse.class); + } + + public Future create(CirculationSetting circulationSetting) { + return PgUtil.post(CIRCULATION_SETTINGS_TABLE, circulationSetting, okapiHeaders, vertxContext, + PostCirculationSettingsStorageCirculationSettingsResponse.class) + .compose(eventPublisher.publishCreated()); + } + + public Future findById(String circulationSettingsId) { + return PgUtil.getById(CIRCULATION_SETTINGS_TABLE, CirculationSetting.class, + circulationSettingsId, okapiHeaders, vertxContext, + GetCirculationSettingsStorageCirculationSettingsByCirculationSettingsIdResponse.class); + } + + public Future update(String circulationSettingsId, CirculationSetting circulationSetting) { + return PgUtil.put(CIRCULATION_SETTINGS_TABLE, circulationSetting, circulationSettingsId, okapiHeaders, vertxContext, + PutCirculationSettingsStorageCirculationSettingsByCirculationSettingsIdResponse.class) + .compose(eventPublisher.publishUpdated(circulationSetting)); + } + + public Future delete(String circulationSettingsId) { + return repository.getById(circulationSettingsId).compose ( + circulationSetting -> PgUtil.deleteById(CIRCULATION_SETTINGS_TABLE, circulationSettingsId, okapiHeaders, vertxContext, + DeleteCirculationSettingsStorageCirculationSettingsByCirculationSettingsIdResponse.class) + .compose(eventPublisher.publishRemoved(circulationSetting)) + ); + } +} diff --git a/src/main/java/org/folio/service/event/EntityChangedEventPublisherFactory.java b/src/main/java/org/folio/service/event/EntityChangedEventPublisherFactory.java index c8aaecd78..54eccfe90 100644 --- a/src/main/java/org/folio/service/event/EntityChangedEventPublisherFactory.java +++ b/src/main/java/org/folio/service/event/EntityChangedEventPublisherFactory.java @@ -2,6 +2,7 @@ import static org.folio.rest.tools.utils.TenantTool.tenantId; import static org.folio.support.kafka.topic.CirculationStorageKafkaTopic.CHECK_IN; +import static org.folio.support.kafka.topic.CirculationStorageKafkaTopic.CIRCULATION_SETTINGS; import static org.folio.support.kafka.topic.CirculationStorageKafkaTopic.LOAN; import static org.folio.support.kafka.topic.CirculationStorageKafkaTopic.REQUEST; import static org.folio.support.kafka.topic.CirculationStorageKafkaTopic.RULES; @@ -10,10 +11,12 @@ import org.folio.persist.CheckInRepository; import org.folio.persist.CirculationRulesRepository; +import org.folio.persist.CirculationSettingsRepository; import org.folio.persist.LoanRepository; import org.folio.persist.RequestRepository; import org.folio.rest.jaxrs.model.CheckIn; import org.folio.rest.jaxrs.model.CirculationRules; +import org.folio.rest.jaxrs.model.CirculationSetting; import org.folio.rest.jaxrs.model.Loan; import org.folio.rest.jaxrs.model.Request; @@ -74,4 +77,15 @@ public static EntityChangedEventPublisher circulationR new CirculationRulesRepository(vertxContext, okapiHeaders)); } + public static EntityChangedEventPublisher circulationSettingsEventPublisher( + Context vertxContext, Map okapiHeaders) { + + return new EntityChangedEventPublisher<>(okapiHeaders, CirculationSetting::getId, NULL_ID, + new EntityChangedEventFactory<>(), + new DomainEventPublisher<>(vertxContext, + CIRCULATION_SETTINGS.fullTopicName(tenantId(okapiHeaders)), + FailureHandler.noOperation()), + new CirculationSettingsRepository(vertxContext, okapiHeaders)); + } + } diff --git a/src/main/java/org/folio/support/ModuleConstants.java b/src/main/java/org/folio/support/ModuleConstants.java index 2bb2ab639..ba92fd1ee 100644 --- a/src/main/java/org/folio/support/ModuleConstants.java +++ b/src/main/java/org/folio/support/ModuleConstants.java @@ -15,6 +15,8 @@ public class ModuleConstants { public static final String LOAN_TABLE = "loan"; public static final String OPEN_LOAN_STATUS = "Open"; public static final String REQUEST_TABLE = "request"; + public static final String CIRCULATION_SETTINGS_TABLE = + "circulation_settings"; public static final Class REQUEST_CLASS = Request.class; public static final String CHECKIN_TABLE = "check_in"; public static final Class CHECKIN_CLASS = CheckIn.class; diff --git a/src/main/java/org/folio/support/kafka/topic/CirculationStorageKafkaTopic.java b/src/main/java/org/folio/support/kafka/topic/CirculationStorageKafkaTopic.java index a8889f554..0f4ec85b3 100644 --- a/src/main/java/org/folio/support/kafka/topic/CirculationStorageKafkaTopic.java +++ b/src/main/java/org/folio/support/kafka/topic/CirculationStorageKafkaTopic.java @@ -4,6 +4,7 @@ public enum CirculationStorageKafkaTopic implements KafkaTopic { REQUEST("request", 10), + CIRCULATION_SETTINGS("circulation-settings", 10), LOAN("loan", 10), CHECK_IN("check-in", 10), RULES("rules", 10); diff --git a/src/main/resources/templates/db_scripts/schema.json b/src/main/resources/templates/db_scripts/schema.json index 180723cca..5ad434893 100644 --- a/src/main/resources/templates/db_scripts/schema.json +++ b/src/main/resources/templates/db_scripts/schema.json @@ -151,6 +151,11 @@ } ] }, + { + "tableName": "circulation_settings", + "withMetadata": true, + "withAuditing": false + }, { "tableName": "request", "withMetadata": true, diff --git a/src/test/java/org/folio/rest/api/CirculationSettingsAPITest.java b/src/test/java/org/folio/rest/api/CirculationSettingsAPITest.java new file mode 100644 index 000000000..0cfda7e2e --- /dev/null +++ b/src/test/java/org/folio/rest/api/CirculationSettingsAPITest.java @@ -0,0 +1,81 @@ +package org.folio.rest.api; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.junit.MatcherAssert.assertThat; + +import java.net.MalformedURLException; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import org.folio.rest.support.ApiTests; +import org.folio.rest.support.http.AssertingRecordClient; +import org.folio.rest.support.http.InterfaceUrls; +import org.folio.rest.support.spring.TestContextConfiguration; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.context.ContextConfiguration; + +import io.vertx.core.json.JsonObject; +import junitparams.JUnitParamsRunner; + +@RunWith(JUnitParamsRunner.class) +@ContextConfiguration(classes = TestContextConfiguration.class) +public class CirculationSettingsAPITest extends ApiTests { + + private final AssertingRecordClient circulationSettingsClient = + new AssertingRecordClient( + client, StorageTestSuite.TENANT_ID, InterfaceUrls::circulationSettingsUrl, + "circulation-settings"); + + @Test + public void canCreateAndRetrieveCirculationSettings() throws MalformedURLException, + ExecutionException, InterruptedException, TimeoutException { + + String id = UUID.randomUUID().toString(); + JsonObject circulationSettingsJson = getCirculationSetting(id); + JsonObject circulationSettingsResponse = + circulationSettingsClient.create(circulationSettingsJson).getJson(); + JsonObject circulationSettingsById = circulationSettingsClient.getById(id).getJson(); + + assertThat(circulationSettingsResponse.getString("id"), is(id)); + assertThat(circulationSettingsById.getString("id"), is(id)); + assertThat(circulationSettingsById.getJsonObject("value"), is( + circulationSettingsJson.getJsonObject("value"))); + } + + @Test + public void canUpdateCirculationSettings() throws MalformedURLException, + ExecutionException, InterruptedException, TimeoutException { + + String id = UUID.randomUUID().toString(); + JsonObject circulationSettingsJson = getCirculationSetting(id); + circulationSettingsClient.create(circulationSettingsJson).getJson(); + circulationSettingsClient.attemptPutById( + circulationSettingsJson.put("value", new JsonObject().put("sample", "DONE"))); + JsonObject updatedCirculationSettings = circulationSettingsClient.getById(id).getJson(); + + assertThat(updatedCirculationSettings.getString("id"), is(id)); + assertThat(updatedCirculationSettings.getJsonObject("value"), is( + circulationSettingsJson.getJsonObject("value"))); + } + + @Test + public void canDeleteCirculationSettings() throws MalformedURLException, + ExecutionException, InterruptedException, TimeoutException { + + UUID id = UUID.randomUUID(); + circulationSettingsClient.create(getCirculationSetting(id.toString())).getJson(); + circulationSettingsClient.deleteById(id); + var deletedCirculationSettings = circulationSettingsClient.attemptGetById(id); + assertThat(deletedCirculationSettings.getStatusCode(), is(404)); + } + + private JsonObject getCirculationSetting(String id) { + return new JsonObject() + .put("id", id) + .put("name", "sample") + .put("value", new JsonObject().put("sample", "OK")); + } + +} 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 0eacef610..a60caef57 100644 --- a/src/test/java/org/folio/rest/support/http/InterfaceUrls.java +++ b/src/test/java/org/folio/rest/support/http/InterfaceUrls.java @@ -48,4 +48,8 @@ public static URL checkOutStorageUrl(String subPath) throws MalformedURLExceptio return storageUrl("/check-out-lock-storage" + subPath); } + public static URL circulationSettingsUrl(String subPath) throws MalformedURLException { + return storageUrl("/circulation-settings-storage/circulation-settings" + subPath); + } + } diff --git a/src/test/java/org/folio/service/kafka/topic/KafkaAdminClientServiceTest.java b/src/test/java/org/folio/service/kafka/topic/KafkaAdminClientServiceTest.java index 66edb8ee0..e5b63c190 100644 --- a/src/test/java/org/folio/service/kafka/topic/KafkaAdminClientServiceTest.java +++ b/src/test/java/org/folio/service/kafka/topic/KafkaAdminClientServiceTest.java @@ -42,7 +42,8 @@ public class KafkaAdminClientServiceTest { "folio.foo-tenant.circulation.request", "folio.foo-tenant.circulation.loan", "folio.foo-tenant.circulation.check-in", - "folio.foo-tenant.circulation.rules" + "folio.foo-tenant.circulation.rules", + "folio.foo-tenant.circulation.circulation-settings" ); private KafkaAdminClient mockClient; From 81c99322b8807053c3ffe7da35f4afa7414d04bd Mon Sep 17 00:00:00 2001 From: Antony Hruschev Date: Fri, 16 Aug 2024 13:31:27 +0400 Subject: [PATCH 03/17] CIRCSTORE- 519 Update instead save with the same name circ settings. (#478) * CIRCSTORE-519 add index * CIRCSTORE-519 replace index * CIRCSTORE-519 add index * CIRCSTORE-519 add index * CIRCSTORE-519 refactored after review * CIRCSTORE-519 refactored after review using code_style schema * CIRCSTORE-519 refactored after review using code_style schema * CIRCSTORE-519 add unit test * CIRCSTORE-519 add unit test * CIRCSTORE-519 add unit test * CIRCSTORE-519 refactored after review * CIRCSTORE-519 refactored after review * CIRCSTORE-519 refactored logging * CIRCSTORE-519 refactored logging and code after review --- .../org/folio/persist/AbstractRepository.java | 4 + .../rest/impl/CirculationSettingsAPI.java | 28 +++-- .../service/CirculationSettingsService.java | 68 +++++++++-- .../templates/db_scripts/schema.json | 9 +- .../rest/api/CirculationSettingsAPITest.java | 111 +++++++++++++----- 5 files changed, 167 insertions(+), 53 deletions(-) diff --git a/src/main/java/org/folio/persist/AbstractRepository.java b/src/main/java/org/folio/persist/AbstractRepository.java index 93bdbc7fa..0d480a5f6 100644 --- a/src/main/java/org/folio/persist/AbstractRepository.java +++ b/src/main/java/org/folio/persist/AbstractRepository.java @@ -38,6 +38,10 @@ protected AbstractRepository(PostgresClient postgresClient, String tableName, this.recordType = recordType; } + public Future saveAndReturnUpdatedEntity(String id, T entity) { + return postgresClient.saveAndReturnUpdatedEntity(tableName, id, entity); + } + public Future save(String id, T entity) { return postgresClient.save(tableName, id, entity); } diff --git a/src/main/java/org/folio/rest/impl/CirculationSettingsAPI.java b/src/main/java/org/folio/rest/impl/CirculationSettingsAPI.java index 07e05a95f..68141494b 100644 --- a/src/main/java/org/folio/rest/impl/CirculationSettingsAPI.java +++ b/src/main/java/org/folio/rest/impl/CirculationSettingsAPI.java @@ -1,5 +1,10 @@ package org.folio.rest.impl; +import static io.vertx.core.Future.succeededFuture; +import static org.folio.rest.jaxrs.resource.CirculationSettingsStorage.PostCirculationSettingsStorageCirculationSettingsResponse.headersFor201; +import static org.folio.rest.jaxrs.resource.CirculationSettingsStorage.PostCirculationSettingsStorageCirculationSettingsResponse.respond201WithApplicationJson; +import static org.folio.rest.jaxrs.resource.CirculationSettingsStorage.PostCirculationSettingsStorageCirculationSettingsResponse.respond500WithTextPlain; + import java.util.Map; import javax.ws.rs.core.Response; @@ -21,7 +26,10 @@ public void postCirculationSettingsStorageCirculationSettings(String lang, new CirculationSettingsService(vertxContext, okapiHeaders) .create(circulationSettings) - .onComplete(asyncResultHandler); + .onSuccess(response -> asyncResultHandler.handle( + succeededFuture(respond201WithApplicationJson(circulationSettings, headersFor201())))) + .onFailure(throwable -> asyncResultHandler.handle( + succeededFuture(respond500WithTextPlain(throwable.getMessage())))); } @Override @@ -39,9 +47,9 @@ public void getCirculationSettingsStorageCirculationSettingsByCirculationSetting String circulationSettingsId, String lang, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { - new CirculationSettingsService(vertxContext, okapiHeaders) - .findById(circulationSettingsId) - .onComplete(asyncResultHandler); + new CirculationSettingsService(vertxContext, okapiHeaders) + .findById(circulationSettingsId) + .onComplete(asyncResultHandler); } @Override @@ -50,9 +58,9 @@ public void putCirculationSettingsStorageCirculationSettingsByCirculationSetting Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { - new CirculationSettingsService(vertxContext, okapiHeaders) - .update(circulationSettingsId, entity) - .onComplete(asyncResultHandler); + new CirculationSettingsService(vertxContext, okapiHeaders) + .update(circulationSettingsId, entity) + .onComplete(asyncResultHandler); } @Override @@ -60,8 +68,8 @@ public void deleteCirculationSettingsStorageCirculationSettingsByCirculationSett String circulationSettingsId, String lang, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { - new CirculationSettingsService(vertxContext, okapiHeaders) - .delete(circulationSettingsId) - .onComplete(asyncResultHandler); + new CirculationSettingsService(vertxContext, okapiHeaders) + .delete(circulationSettingsId) + .onComplete(asyncResultHandler); } } diff --git a/src/main/java/org/folio/service/CirculationSettingsService.java b/src/main/java/org/folio/service/CirculationSettingsService.java index a0c950074..b060c2524 100644 --- a/src/main/java/org/folio/service/CirculationSettingsService.java +++ b/src/main/java/org/folio/service/CirculationSettingsService.java @@ -1,26 +1,31 @@ package org.folio.service; +import static org.folio.rest.tools.utils.ValidationHelper.isDuplicate; import static org.folio.service.event.EntityChangedEventPublisherFactory.circulationSettingsEventPublisher; import static org.folio.support.ModuleConstants.CIRCULATION_SETTINGS_TABLE; +import java.util.List; import java.util.Map; import javax.ws.rs.core.Response; +import lombok.extern.log4j.Log4j2; import org.folio.persist.CirculationSettingsRepository; import org.folio.rest.jaxrs.model.CirculationSetting; import org.folio.rest.jaxrs.model.CirculationSettings; import org.folio.rest.jaxrs.resource.CirculationSettingsStorage.DeleteCirculationSettingsStorageCirculationSettingsByCirculationSettingsIdResponse; import org.folio.rest.jaxrs.resource.CirculationSettingsStorage.GetCirculationSettingsStorageCirculationSettingsByCirculationSettingsIdResponse; import org.folio.rest.jaxrs.resource.CirculationSettingsStorage.GetCirculationSettingsStorageCirculationSettingsResponse; -import org.folio.rest.jaxrs.resource.CirculationSettingsStorage.PostCirculationSettingsStorageCirculationSettingsResponse; import org.folio.rest.jaxrs.resource.CirculationSettingsStorage.PutCirculationSettingsStorageCirculationSettingsByCirculationSettingsIdResponse; +import org.folio.rest.persist.Criteria.Criteria; +import org.folio.rest.persist.Criteria.Criterion; import org.folio.rest.persist.PgUtil; import org.folio.service.event.EntityChangedEventPublisher; import io.vertx.core.Context; import io.vertx.core.Future; +@Log4j2 public class CirculationSettingsService { private final Context vertxContext; @@ -42,10 +47,11 @@ public Future getAll(int offset, int limit, String query) { GetCirculationSettingsStorageCirculationSettingsResponse.class); } - public Future create(CirculationSetting circulationSetting) { - return PgUtil.post(CIRCULATION_SETTINGS_TABLE, circulationSetting, okapiHeaders, vertxContext, - PostCirculationSettingsStorageCirculationSettingsResponse.class) - .compose(eventPublisher.publishCreated()); + public Future create(CirculationSetting circulationSetting) { + log.debug("create:: trying to save circulationSetting: {}", circulationSetting); + return repository.saveAndReturnUpdatedEntity(circulationSetting.getId(), + circulationSetting) + .recover(throwable -> updateSettingsValue(circulationSetting, throwable)); } public Future findById(String circulationSettingsId) { @@ -54,17 +60,57 @@ public Future findById(String circulationSettingsId) { GetCirculationSettingsStorageCirculationSettingsByCirculationSettingsIdResponse.class); } - public Future update(String circulationSettingsId, CirculationSetting circulationSetting) { - return PgUtil.put(CIRCULATION_SETTINGS_TABLE, circulationSetting, circulationSettingsId, okapiHeaders, vertxContext, + public Future update(String circulationSettingsId, + CirculationSetting circulationSetting) { + + return PgUtil.put(CIRCULATION_SETTINGS_TABLE, circulationSetting, circulationSettingsId, + okapiHeaders, vertxContext, PutCirculationSettingsStorageCirculationSettingsByCirculationSettingsIdResponse.class) .compose(eventPublisher.publishUpdated(circulationSetting)); } public Future delete(String circulationSettingsId) { - return repository.getById(circulationSettingsId).compose ( - circulationSetting -> PgUtil.deleteById(CIRCULATION_SETTINGS_TABLE, circulationSettingsId, okapiHeaders, vertxContext, - DeleteCirculationSettingsStorageCirculationSettingsByCirculationSettingsIdResponse.class) - .compose(eventPublisher.publishRemoved(circulationSetting)) + return repository.getById(circulationSettingsId) + .compose(circulationSetting -> PgUtil.deleteById(CIRCULATION_SETTINGS_TABLE, + circulationSettingsId, okapiHeaders, vertxContext, + DeleteCirculationSettingsStorageCirculationSettingsByCirculationSettingsIdResponse.class) + .compose(eventPublisher.publishRemoved(circulationSetting)) ); } + + private Future updateSettingsValue(CirculationSetting circulationSetting, + Throwable throwable) { + + if (!isDuplicate(throwable.getMessage())) { + log.debug("updateSettingsValue:: error during saving circulation setting: {}. message: {}.", + circulationSetting, throwable.getMessage()); + return Future.failedFuture(throwable); + } + + log.info("updateSettingsValue:: setting with name: {} already exists.", + circulationSetting.getName()); + + return getSettingsByName(circulationSetting.getName()) + .compose(settings -> updateSettings(settings, circulationSetting)); + } + + private Future updateSettings(List settings, + CirculationSetting circulationSetting) { + + settings.forEach(setting -> setting.setValue(circulationSetting.getValue())); + log.debug("updateSettings:: updating {} setting(s) with name '{}'", + settings::size, circulationSetting::getName); + return repository.update(settings) + .map(circulationSetting); + } + + private Future> getSettingsByName(String settingsName) { + log.debug("getSettingsByName:: trying to fetch setting by name: {}", settingsName); + Criterion filter = new Criterion(new Criteria() + .addField("'name'") + .setOperation("=") + .setVal(settingsName)); + + return repository.get(filter); + } } diff --git a/src/main/resources/templates/db_scripts/schema.json b/src/main/resources/templates/db_scripts/schema.json index 5ad434893..c2031099d 100644 --- a/src/main/resources/templates/db_scripts/schema.json +++ b/src/main/resources/templates/db_scripts/schema.json @@ -154,7 +154,14 @@ { "tableName": "circulation_settings", "withMetadata": true, - "withAuditing": false + "withAuditing": false, + "uniqueIndex": [ + { + "fieldName": "name", + "tOps": "ADD", + "caseSensitive": false + } + ] }, { "tableName": "request", diff --git a/src/test/java/org/folio/rest/api/CirculationSettingsAPITest.java b/src/test/java/org/folio/rest/api/CirculationSettingsAPITest.java index 5ec12cba3..9e9b7214b 100644 --- a/src/test/java/org/folio/rest/api/CirculationSettingsAPITest.java +++ b/src/test/java/org/folio/rest/api/CirculationSettingsAPITest.java @@ -1,75 +1,124 @@ package org.folio.rest.api; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.junit.MatcherAssert.assertThat; - -import java.net.MalformedURLException; -import java.util.UUID; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeoutException; - +import io.vertx.core.json.JsonObject; +import lombok.SneakyThrows; import org.folio.rest.support.ApiTests; import org.folio.rest.support.http.AssertingRecordClient; import org.folio.rest.support.http.InterfaceUrls; +import org.junit.Before; import org.junit.Test; -import io.vertx.core.json.JsonObject; +import java.util.UUID; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.junit.MatcherAssert.assertThat; public class CirculationSettingsAPITest extends ApiTests { + private static final String ID_KEY = "id"; + private static final String NAME_KEY = "name"; + private static final String VALUE_KEY = "value"; + private static final String SAMPLE_VALUE = "sample"; + private static final String SAMPLE_KEY = "sample"; + private static final String INITIAL_VALUE = "OK"; + private static final String UPDATED_VALUE = "OK1"; + 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 final AssertingRecordClient circulationSettingsClient = new AssertingRecordClient( - client, StorageTestSuite.TENANT_ID, InterfaceUrls::circulationSettingsUrl, - "circulation-settings"); + client, StorageTestSuite.TENANT_ID, InterfaceUrls::circulationSettingsUrl, + CIRCULATION_SETTINGS_PROPERTY); - @Test - public void canCreateAndRetrieveCirculationSettings() throws MalformedURLException, - ExecutionException, InterruptedException, TimeoutException { + @Before + public void beforeEach() { + StorageTestSuite.cleanUpTable(TABLE_NAME); + } + @Test + @SneakyThrows + public void updateInsteadCreateWithTheSameName() { String id = UUID.randomUUID().toString(); JsonObject circulationSettingsJson = getCirculationSetting(id); JsonObject circulationSettingsResponse = circulationSettingsClient.create(circulationSettingsJson).getJson(); + JsonObject circulationSettingsJsonUpdated = getUpdatedSettingsJson(); + circulationSettingsClient.create(circulationSettingsJsonUpdated); JsonObject circulationSettingsById = circulationSettingsClient.getById(id).getJson(); - assertThat(circulationSettingsResponse.getString("id"), is(id)); - assertThat(circulationSettingsById.getString("id"), is(id)); - assertThat(circulationSettingsById.getJsonObject("value"), is( - circulationSettingsJson.getJsonObject("value"))); + assertThatCorrectCreation(circulationSettingsResponse, circulationSettingsJson); + assertThat(circulationSettingsClient.getAll().getTotalRecords(), is(1)); + assertThat(getValue(circulationSettingsJsonUpdated), is(getValue(circulationSettingsById))); } @Test - public void canUpdateCirculationSettings() throws MalformedURLException, - ExecutionException, InterruptedException, TimeoutException { + @SneakyThrows + public void canCreateAndRetrieveCirculationSettings() { + String id = UUID.randomUUID().toString(); + JsonObject circulationSettingsJson = getCirculationSetting(id); + JsonObject circulationSettingsResponse = + circulationSettingsClient.create(circulationSettingsJson).getJson(); + JsonObject circulationSettingsById = circulationSettingsClient.getById(id).getJson(); + assertThat(circulationSettingsResponse.getString(ID_KEY), is(id)); + assertThat(circulationSettingsById.getString(ID_KEY), is(id)); + assertThat(circulationSettingsById.getJsonObject(VALUE_KEY), is( + circulationSettingsJson.getJsonObject(VALUE_KEY))); + } + + @Test + @SneakyThrows + public void canUpdateCirculationSettings() { String id = UUID.randomUUID().toString(); JsonObject circulationSettingsJson = getCirculationSetting(id); circulationSettingsClient.create(circulationSettingsJson).getJson(); circulationSettingsClient.attemptPutById( - circulationSettingsJson.put("value", new JsonObject().put("sample", "DONE"))); + circulationSettingsJson.put(VALUE_KEY, new JsonObject().put(SAMPLE_KEY, "DONE"))); JsonObject updatedCirculationSettings = circulationSettingsClient.getById(id).getJson(); - assertThat(updatedCirculationSettings.getString("id"), is(id)); - assertThat(updatedCirculationSettings.getJsonObject("value"), is( - circulationSettingsJson.getJsonObject("value"))); + assertThat(updatedCirculationSettings.getString(ID_KEY), is(id)); + assertThat(updatedCirculationSettings.getJsonObject(VALUE_KEY), is( + circulationSettingsJson.getJsonObject(VALUE_KEY))); } @Test - public void canDeleteCirculationSettings() throws MalformedURLException, - ExecutionException, InterruptedException, TimeoutException { - + @SneakyThrows + public void canDeleteCirculationSettings() { UUID id = UUID.randomUUID(); circulationSettingsClient.create(getCirculationSetting(id.toString())).getJson(); circulationSettingsClient.deleteById(id); var deletedCirculationSettings = circulationSettingsClient.attemptGetById(id); - assertThat(deletedCirculationSettings.getStatusCode(), is(404)); + assertThat(deletedCirculationSettings.getStatusCode(), is(NOT_FOUND_STATUS)); + } + + private static String getValue(JsonObject circulationSettingsById) { + return circulationSettingsById.getJsonObject(VALUE_KEY).getString(SAMPLE_KEY); } private JsonObject getCirculationSetting(String id) { return new JsonObject() - .put("id", id) - .put("name", "sample") - .put("value", new JsonObject().put("sample", "OK")); + .put(ID_KEY, id) + .put(NAME_KEY, SAMPLE_VALUE) + .put(VALUE_KEY, new JsonObject().put(SAMPLE_KEY, INITIAL_VALUE)); } + private static void assertThatCorrectCreation(JsonObject circulationSettingsResponse, + JsonObject circulationSettingsJson) { + + String actualCreatedId = circulationSettingsResponse.getString(ID_KEY); + String expectedCreatedId = circulationSettingsJson.getString(ID_KEY); + String actualCreatedName = circulationSettingsResponse.getString(NAME_KEY); + String expectedCreatedName = circulationSettingsJson.getString(NAME_KEY); + + assertThat(actualCreatedId, is(expectedCreatedId)); + assertThat(actualCreatedName, is(expectedCreatedName)); + } + + private JsonObject getUpdatedSettingsJson() { + String updatedId = UUID.randomUUID().toString(); + JsonObject circulationSettingsJsonUpdated = getCirculationSetting(updatedId); + JsonObject updatedValue = new JsonObject().put(SAMPLE_KEY, UPDATED_VALUE); + circulationSettingsJsonUpdated.put(VALUE_KEY, updatedValue); + return circulationSettingsJsonUpdated; + } } From 46fe4c6943c5f093a0a1851df80e3b8a5844210e Mon Sep 17 00:00:00 2001 From: Antony_Hruschev Date: Mon, 19 Aug 2024 21:06:56 +0400 Subject: [PATCH 04/17] CIRCSTORE-519 fixed unit test --- .../java/org/folio/rest/api/CirculationSettingsAPITest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/folio/rest/api/CirculationSettingsAPITest.java b/src/test/java/org/folio/rest/api/CirculationSettingsAPITest.java index 299661eee..88ea7f3d5 100644 --- a/src/test/java/org/folio/rest/api/CirculationSettingsAPITest.java +++ b/src/test/java/org/folio/rest/api/CirculationSettingsAPITest.java @@ -59,8 +59,8 @@ public void canCreateAndRetrieveCirculationSettings() { JsonObject circulationSettingsJson = getCirculationSetting(id); JsonObject circulationSettingsResponse = circulationSettingsClient.create(circulationSettingsJson).getJson(); - JsonObject circulationSettingsJsonUpdated = getUpdatedSettingsJson(); - circulationSettingsClient.create(circulationSettingsJsonUpdated); +// JsonObject circulationSettingsJsonUpdated = getUpdatedSettingsJson(); +// circulationSettingsClient.create(circulationSettingsJsonUpdated); JsonObject circulationSettingsById = circulationSettingsClient.getById(id).getJson(); assertThat(circulationSettingsResponse.getString(ID_KEY), is(id)); From 738f093f535da325bf2b39a8ee3fd06536d5089f Mon Sep 17 00:00:00 2001 From: Antony_Hruschev Date: Tue, 20 Aug 2024 15:51:01 +0400 Subject: [PATCH 05/17] CIRCSTORE-519 removed comments test --- .../java/org/folio/rest/api/CirculationSettingsAPITest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/test/java/org/folio/rest/api/CirculationSettingsAPITest.java b/src/test/java/org/folio/rest/api/CirculationSettingsAPITest.java index 88ea7f3d5..fec71d3f2 100644 --- a/src/test/java/org/folio/rest/api/CirculationSettingsAPITest.java +++ b/src/test/java/org/folio/rest/api/CirculationSettingsAPITest.java @@ -59,8 +59,6 @@ public void canCreateAndRetrieveCirculationSettings() { JsonObject circulationSettingsJson = getCirculationSetting(id); JsonObject circulationSettingsResponse = circulationSettingsClient.create(circulationSettingsJson).getJson(); -// JsonObject circulationSettingsJsonUpdated = getUpdatedSettingsJson(); -// circulationSettingsClient.create(circulationSettingsJsonUpdated); JsonObject circulationSettingsById = circulationSettingsClient.getById(id).getJson(); assertThat(circulationSettingsResponse.getString(ID_KEY), is(id)); From 58effff3d7465dfd49f2e33700de6b8eaace57e3 Mon Sep 17 00:00:00 2001 From: Antony_Hruschev Date: Tue, 27 Aug 2024 14:38:00 +0400 Subject: [PATCH 06/17] CIRCSTORE-525 implementation --- ..._staff_slips_transit_mediated_requests.sql | 8 +++ .../templates/db_scripts/schema.json | 5 ++ .../org/folio/rest/api/StaffSlipsApiTest.java | 14 +++--- .../StaffSlipsMigrationTestBase.java | 19 ++++--- ...itMediatedRequestsMigrationScriptTest.java | 49 +++++++++++++++++++ 5 files changed, 82 insertions(+), 13 deletions(-) create mode 100644 src/main/resources/templates/db_scripts/add_staff_slips_transit_mediated_requests.sql create mode 100644 src/test/java/org/folio/rest/api/migration/StaffSlipsTransitMediatedRequestsMigrationScriptTest.java diff --git a/src/main/resources/templates/db_scripts/add_staff_slips_transit_mediated_requests.sql b/src/main/resources/templates/db_scripts/add_staff_slips_transit_mediated_requests.sql new file mode 100644 index 000000000..2dfd420ae --- /dev/null +++ b/src/main/resources/templates/db_scripts/add_staff_slips_transit_mediated_requests.sql @@ -0,0 +1,8 @@ +INSERT INTO ${myuniversity}_${mymodule}.staff_slips(id, jsonb) +VALUES ('e6e29ec1-1a76-4913-bbd3-65f4ffd94e04', + jsonb_build_object( + 'id', 'e6e29ec1-1a76-4913-bbd3-65f4ffd94e04', + 'name', 'Transit (mediated requests)', + 'active', true, + 'template', '

')) +ON CONFLICT DO NOTHING; \ No newline at end of file diff --git a/src/main/resources/templates/db_scripts/schema.json b/src/main/resources/templates/db_scripts/schema.json index f9602bc8f..7503587fd 100644 --- a/src/main/resources/templates/db_scripts/schema.json +++ b/src/main/resources/templates/db_scripts/schema.json @@ -574,6 +574,11 @@ "snippetPath": "add_search_slips.sql", "fromModuleVersion": "17.2.0" }, + { + "run": "after", + "snippetPath": "add_staff_slips_transit_mediated_requests.sql", + "fromModuleVersion": "17.3.0" + }, { "run": "after", "snippetPath": "removePositionFromClosedRequests.sql", diff --git a/src/test/java/org/folio/rest/api/StaffSlipsApiTest.java b/src/test/java/org/folio/rest/api/StaffSlipsApiTest.java index d3218b8d6..835cf691f 100644 --- a/src/test/java/org/folio/rest/api/StaffSlipsApiTest.java +++ b/src/test/java/org/folio/rest/api/StaffSlipsApiTest.java @@ -38,8 +38,11 @@ public class StaffSlipsApiTest extends ApiTests { private static final String ID_KEY = "id"; private static final String ACTIVE_KEY = "active"; private static final String NAME_KEY = "name"; - private static final String DESCRIPTON_KEY = "description"; + private static final String DESCRIPTION_KEY = "description"; private static final String TEMPLATE_KEY = "template"; + private static final String [] STAFF_SLIP_NAMES = {"Hold", "Transit", "Request delivery", "Pick slip", + "Search slip (Hold requests)", "Transit (mediated requests)"}; + private static final String STAFF_SLIPS_KEY = "staffSlips"; private static AtomicBoolean isRefTestDone = new AtomicBoolean(false); @@ -66,10 +69,9 @@ private void canGetStaffSlipReferenceData() assertThat(getResponse.getStatusCode(), is(HttpURLConnection.HTTP_OK)); - JsonArray slipsJsonArray = getResponse.getJson().getJsonArray("staffSlips"); + JsonArray slipsJsonArray = getResponse.getJson().getJsonArray(STAFF_SLIPS_KEY); Object [] names = slipsJsonArray.stream().map(o -> ((JsonObject) o).getString(NAME_KEY)).toArray(); - assertThat(names, arrayContainingInAnyOrder("Hold", "Transit", "Request delivery", "Pick slip", - "Search slip (Hold requests)")); + assertThat(names, arrayContainingInAnyOrder(STAFF_SLIP_NAMES)); } /* Begin Tests */ @@ -86,7 +88,7 @@ public void canCreateAStaffSlip() assertThat(creationResponse.getJson().getString(ID_KEY), notNullValue()); assertThat(creationResponse.getJson().getBoolean(ACTIVE_KEY), is(true)); assertThat(creationResponse.getJson().getString(NAME_KEY), is(TEST_STAFF_SLIP_1_NAME)); - assertThat(creationResponse.getJson().getString(DESCRIPTON_KEY), is(TEST_STAFF_SLIP_1_DESCRIPTION)); + assertThat(creationResponse.getJson().getString(DESCRIPTION_KEY), is(TEST_STAFF_SLIP_1_DESCRIPTION)); assertThat(creationResponse.getJson().getString(TEMPLATE_KEY), is(TEST_STAFF_SLIP_1_Template)); } @@ -197,7 +199,7 @@ public void canUpdateStaffSlipById() JsonResponse getResponse = getCompleted.get(5, TimeUnit.SECONDS); assertThat(putReponse.getStatusCode(), is(HttpURLConnection.HTTP_NO_CONTENT)); - assertThat(getResponse.getJson().getString(DESCRIPTON_KEY), is(TEST_STAFF_SLIP_1_DESCRIPTION_ALTERNATE)); + assertThat(getResponse.getJson().getString(DESCRIPTION_KEY), is(TEST_STAFF_SLIP_1_DESCRIPTION_ALTERNATE)); } diff --git a/src/test/java/org/folio/rest/api/migration/StaffSlipsMigrationTestBase.java b/src/test/java/org/folio/rest/api/migration/StaffSlipsMigrationTestBase.java index 632848371..7a5be5962 100644 --- a/src/test/java/org/folio/rest/api/migration/StaffSlipsMigrationTestBase.java +++ b/src/test/java/org/folio/rest/api/migration/StaffSlipsMigrationTestBase.java @@ -13,25 +13,30 @@ import io.vertx.core.json.JsonObject; public class StaffSlipsMigrationTestBase extends MigrationTestBase { - public static final String TEMPLATE = "

"; + private static final String TEMPLATE = "

"; + private static final String ID_KEY = "id"; + private static final String NAME_KEY = "name"; + private static final String ACTIVE_KEY = "active"; + private static final String TEMPLATE_KEY = "template"; + private static final String STORAGE_URL = "/staff-slips-storage/staff-slips"; JsonObject getRecordById(JsonArray collection, String id) { return collection.stream() .map(index -> (JsonObject) index) - .filter(request -> StringUtils.equals(request.getString("id"), id)) + .filter(request -> StringUtils.equals(request.getString(ID_KEY), id)) .findFirst().orElse(null); } void assertStaffSlip(JsonObject staffSlip, String expectedId, String expectedName) { - assertThat(staffSlip.getString("id"), is(expectedId)); - assertThat(staffSlip.getString("name"), is(expectedName)); - assertThat(staffSlip.getBoolean("active"), is(true)); - assertThat(staffSlip.getString("template"), is(TEMPLATE)); + assertThat(staffSlip.getString(ID_KEY), is(expectedId)); + assertThat(staffSlip.getString(NAME_KEY), is(expectedName)); + assertThat(staffSlip.getBoolean(ACTIVE_KEY), is(true)); + assertThat(staffSlip.getString(TEMPLATE_KEY), is(TEMPLATE)); } URL staffSlipsStorageUrl(String subPath) throws MalformedURLException { - return StorageTestSuite.storageUrl("/staff-slips-storage/staff-slips" + subPath); + return StorageTestSuite.storageUrl(STORAGE_URL + subPath); } } diff --git a/src/test/java/org/folio/rest/api/migration/StaffSlipsTransitMediatedRequestsMigrationScriptTest.java b/src/test/java/org/folio/rest/api/migration/StaffSlipsTransitMediatedRequestsMigrationScriptTest.java new file mode 100644 index 000000000..aa510fbe3 --- /dev/null +++ b/src/test/java/org/folio/rest/api/migration/StaffSlipsTransitMediatedRequestsMigrationScriptTest.java @@ -0,0 +1,49 @@ +package org.folio.rest.api.migration; + +import static org.junit.Assert.assertThat; + +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.folio.rest.api.StorageTestSuite; +import org.folio.rest.support.JsonResponse; +import org.folio.rest.support.ResponseHandler; +import org.hamcrest.core.Is; +import org.junit.Before; +import org.junit.Test; + +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; + +public class StaffSlipsTransitMediatedRequestsMigrationScriptTest extends StaffSlipsMigrationTestBase{ + + private static final String STAFF_SLIP_ID = "e6e29ec1-1a76-4913-bbd3-65f4ffd94e04"; + private static final String SCRIPT_NAME = "add_staff_slips_transit_mediated_requests.sql"; + private static final String STAFF_SLIPS_KEY = "staffSlips"; + private static final String STAFF_SLIPS_SUB_PATH = ""; + private static final String STAFF_SLIP_NAME = "Transit (mediated requests)"; + private static final int TIMEOUT_VALUE = 5; + + @Before + public void beforeEach() throws MalformedURLException { + StorageTestSuite.deleteAll(staffSlipsStorageUrl(STAFF_SLIPS_SUB_PATH)); + } + + @Test + public void canMigrateStaffSlips() throws Exception { + executeMultipleSqlStatements(loadScript(SCRIPT_NAME)); + CompletableFuture getCompleted = new CompletableFuture<>(); + client.get(staffSlipsStorageUrl(STAFF_SLIPS_SUB_PATH), StorageTestSuite.TENANT_ID, + ResponseHandler.json(getCompleted)); + JsonResponse getResponse = getCompleted.get(TIMEOUT_VALUE, TimeUnit.SECONDS); + + assertThat(getResponse.getStatusCode(), Is.is(HttpURLConnection.HTTP_OK)); + + JsonArray slipsJsonArray = getResponse.getJson().getJsonArray(STAFF_SLIPS_KEY); + JsonObject staffSlips = getRecordById(slipsJsonArray, STAFF_SLIP_ID); + + assertStaffSlip(staffSlips, STAFF_SLIP_ID, STAFF_SLIP_NAME); + } +} From b35fa1ddbff603bd4c2bbb9299cf53ab2d387ff2 Mon Sep 17 00:00:00 2001 From: Roman Barannyk <53909129+roman-barannyk@users.noreply.github.com> Date: Thu, 29 Aug 2024 23:30:44 +0300 Subject: [PATCH 07/17] [CIRCSTORE-521] Publish request batch event when requests are reordered (#482) * CIRCSTORE-521 add request batch event publishing * CIRCSTORE-521 update logging * CIRCSTORE-521 fix broken test * CIRCSTORE-521 revert comments * CIRCSTORE-521 refactoring * CIRCSTORE-521 create test * CIRCSTORE-521 update test * CIRCSTORE-521 rename method * CIRCSTORE-521 use uuid reference for schema * CIRCSTORE-521 add events cleaning * CIRCSTORE-521 Change sorting in tests --------- Co-authored-by: alexanderkurash --- ramls/request-queue-reordering.json | 22 +++++ ramls/request-storage-batch.raml | 1 + .../org/folio/rest/impl/RequestsBatchAPI.java | 25 ++--- .../folio/service/BatchResourceService.java | 40 +++++--- .../EntityChangedEventPublisherFactory.java | 11 +++ .../request/RequestBatchResourceService.java | 38 ++++++-- .../topic/CirculationStorageKafkaTopic.java | 1 + .../folio/rest/api/RequestBatchAPITest.java | 95 +++++++++++++++---- .../rest/support/kafka/FakeKafkaConsumer.java | 21 +++- .../matchers/DomainEventAssertions.java | 15 +++ .../topic/KafkaAdminClientServiceTest.java | 3 +- 11 files changed, 212 insertions(+), 60 deletions(-) create mode 100644 ramls/request-queue-reordering.json diff --git a/ramls/request-queue-reordering.json b/ramls/request-queue-reordering.json new file mode 100644 index 000000000..f76bcdd88 --- /dev/null +++ b/ramls/request-queue-reordering.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Requests batch update", + "description": "List of ids reordered requests", + "type": "object", + "properties": { + "instanceId": { + "description": "Instance ID of reordered requests", + "type": "string", + "$ref": "raml-util/schemas/uuid.schema" + }, + "requestIds": { + "description": "Array of requests ids", + "type": "array", + "items": { + "type": "string", + "$ref": "raml-util/schemas/uuid.schema" + } + } + }, + "additionalProperties": false +} diff --git a/ramls/request-storage-batch.raml b/ramls/request-storage-batch.raml index 372865083..a6f0aaaae 100644 --- a/ramls/request-storage-batch.raml +++ b/ramls/request-storage-batch.raml @@ -11,6 +11,7 @@ documentation: types: requests-batch: !include requests-batch.json errors: !include raml-util/schemas/errors.schema + request-queue-reordering: !include request-queue-reordering.json traits: validate: !include raml-util/traits/validation.raml diff --git a/src/main/java/org/folio/rest/impl/RequestsBatchAPI.java b/src/main/java/org/folio/rest/impl/RequestsBatchAPI.java index ca7848ea5..d83da9321 100644 --- a/src/main/java/org/folio/rest/impl/RequestsBatchAPI.java +++ b/src/main/java/org/folio/rest/impl/RequestsBatchAPI.java @@ -6,7 +6,6 @@ import static org.folio.rest.jaxrs.resource.RequestStorageBatch.PostRequestStorageBatchRequestsResponse.respond201; import static org.folio.rest.jaxrs.resource.RequestStorageBatch.PostRequestStorageBatchRequestsResponse.respond422WithApplicationJson; import static org.folio.rest.jaxrs.resource.RequestStorageBatch.PostRequestStorageBatchRequestsResponse.respond500WithTextPlain; -import static org.folio.rest.tools.utils.TenantTool.tenantId; import java.util.Map; @@ -14,9 +13,7 @@ import org.folio.rest.annotations.Validate; import org.folio.rest.jaxrs.model.RequestsBatch; import org.folio.rest.jaxrs.resource.RequestStorageBatch; -import org.folio.rest.persist.PgUtil; import org.folio.rest.tools.utils.MetadataUtil; -import org.folio.service.BatchResourceService; import org.folio.service.request.RequestBatchResourceService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -26,7 +23,7 @@ import io.vertx.core.Handler; public class RequestsBatchAPI implements RequestStorageBatch { - private static final Logger LOG = LoggerFactory.getLogger(RequestsBatchAPI.class); + private static final Logger log = LoggerFactory.getLogger(RequestsBatchAPI.class); @Validate @Override @@ -38,36 +35,30 @@ public void postRequestStorageBatchRequests( MetadataUtil.populateMetadata(entity.getRequests(), okapiHeaders); } catch (Throwable e) { String msg = "Cannot populate metadata of request list elements: " + e.getMessage(); - LOG.error(msg, e); + log.error(msg, e); asyncResultHandler.handle(succeededFuture(respond500WithTextPlain(msg))); return; } - BatchResourceService batchUpdateService = new BatchResourceService( - PgUtil.postgresClient(context, okapiHeaders) - ); - - RequestBatchResourceService requestBatchUpdateService = - new RequestBatchResourceService(tenantId(okapiHeaders), batchUpdateService); - - requestBatchUpdateService.executeRequestBatchUpdate(entity.getRequests(), - updateResult -> { + log.info("postRequestStorageBatchRequests:: requests: {}", entity.getRequests()); + new RequestBatchResourceService(context, okapiHeaders) + .executeRequestBatchUpdate(entity.getRequests(), updateResult -> { // Successfully updated if (updateResult.succeeded()) { - LOG.debug("Batch update executed successfully"); + log.debug("Batch update executed successfully"); asyncResultHandler.handle(succeededFuture(respond201())); return; } // Update failed due to can not have more then one request in the same position if (hasSamePositionConstraintViolated(updateResult.cause())) { - LOG.warn("Same position constraint violated", updateResult.cause()); + log.warn("Same position constraint violated", updateResult.cause()); asyncResultHandler.handle(succeededFuture( respond422WithApplicationJson(samePositionInQueueError(null, null)) )); } else { // Other failure occurred - LOG.warn("Unhandled error occurred during update", updateResult.cause()); + log.warn("Unhandled error occurred during update", updateResult.cause()); asyncResultHandler.handle(succeededFuture( respond500WithTextPlain(updateResult.cause().getMessage()) )); diff --git a/src/main/java/org/folio/service/BatchResourceService.java b/src/main/java/org/folio/service/BatchResourceService.java index f1ef94069..53f6e90b6 100644 --- a/src/main/java/org/folio/service/BatchResourceService.java +++ b/src/main/java/org/folio/service/BatchResourceService.java @@ -23,7 +23,7 @@ import io.vertx.sqlclient.RowSet; public class BatchResourceService { - private static final Logger LOG = LoggerFactory.getLogger(BatchResourceService.class); + private static final Logger log = LoggerFactory.getLogger(BatchResourceService.class); private static final String WHERE_CLAUSE = "WHERE id = '%s'"; private final PostgresClient postgresClient; @@ -38,14 +38,17 @@ public BatchResourceService(PostgresClient postgresClient) { * @param batchFactories - Factory to create a batch update chunk. * @param onFinishHandler - Callback. */ - public void executeBatchUpdate( + public Future executeBatchUpdate( List>>> batchFactories, Handler> onFinishHandler) { + Promise promise = Promise.promise(); + postgresClient.startTx(connectionResult -> { if (connectionResult.failed()) { - LOG.warn("Can not start transaction", connectionResult.cause()); + log.warn("Cannot start transaction", connectionResult.cause()); onFinishHandler.handle(failedFuture(connectionResult.cause())); + promise.fail(connectionResult.cause()); return; } @@ -60,21 +63,32 @@ public void executeBatchUpdate( // Handle overall update result and decide on whether to commit or rollback transaction lastUpdate.onComplete(updateResult -> { if (updateResult.failed()) { - LOG.warn("Batch update rejected", updateResult.cause()); + log.warn("Batch update rejected", updateResult.cause()); // Rollback transaction and keep original cause. - postgresClient.rollbackTx(connectionResult, - rollback -> onFinishHandler.handle(failedFuture(updateResult.cause())) - ); + postgresClient.rollbackTx(connectionResult, rollback -> { + onFinishHandler.handle(failedFuture(updateResult.cause())); + promise.fail(updateResult.cause()); + }); } else { - LOG.debug("Update successful, committing transaction"); - - postgresClient.endTx(connectionResult, onFinishHandler); + log.debug("Update successful, committing transaction"); + + postgresClient.endTx(connectionResult, commitResult -> { + if (commitResult.succeeded()) { + onFinishHandler.handle(succeededFuture()); + promise.complete(); + } else { + log.warn("Failed to commit transaction", commitResult.cause()); + onFinishHandler.handle(failedFuture(commitResult.cause())); + promise.fail(commitResult.cause()); + } + }); } }); }); - } + return promise.future(); + } /** * Creates update single entity batch function. * @@ -92,7 +106,7 @@ public Function>> updateSingleEntityBatchF final Promise> promise = promise(); final Future connectionResult = succeededFuture(connection); - LOG.debug("Updating entity {} with id {}", entity, id); + log.debug("Updating entity {} with id {}", entity, id); postgresClient.update(connectionResult, tableName, entity, "jsonb", String.format(WHERE_CLAUSE, id), false, promise); @@ -113,7 +127,7 @@ public Function>> queryWithParamsBatchFactory( String query, Collection params) { return connection -> { - LOG.debug("Executing SQL [{}], got [{}] parameters", query, params.size()); + log.debug("Executing SQL [{}], got [{}] parameters", query, params.size()); final Promise> promise = promise(); final Future connectionResult = succeededFuture(connection); diff --git a/src/main/java/org/folio/service/event/EntityChangedEventPublisherFactory.java b/src/main/java/org/folio/service/event/EntityChangedEventPublisherFactory.java index 54eccfe90..7f37dc8b1 100644 --- a/src/main/java/org/folio/service/event/EntityChangedEventPublisherFactory.java +++ b/src/main/java/org/folio/service/event/EntityChangedEventPublisherFactory.java @@ -5,6 +5,7 @@ import static org.folio.support.kafka.topic.CirculationStorageKafkaTopic.CIRCULATION_SETTINGS; import static org.folio.support.kafka.topic.CirculationStorageKafkaTopic.LOAN; import static org.folio.support.kafka.topic.CirculationStorageKafkaTopic.REQUEST; +import static org.folio.support.kafka.topic.CirculationStorageKafkaTopic.REQUEST_QUEUE_REORDERING; import static org.folio.support.kafka.topic.CirculationStorageKafkaTopic.RULES; import java.util.Map; @@ -19,6 +20,7 @@ import org.folio.rest.jaxrs.model.CirculationSetting; import org.folio.rest.jaxrs.model.Loan; import org.folio.rest.jaxrs.model.Request; +import org.folio.rest.jaxrs.model.RequestQueueReordering; import io.vertx.core.Context; import lombok.extern.log4j.Log4j2; @@ -53,6 +55,15 @@ public static EntityChangedEventPublisher requestEventPublisher new RequestRepository(vertxContext, okapiHeaders)); } + public static EntityChangedEventPublisher + requestBatchEventPublisher(Context vertxContext, Map okapiHeaders) { + + return new EntityChangedEventPublisher<>(okapiHeaders, RequestQueueReordering::getInstanceId, + NULL_ID, new EntityChangedEventFactory<>(), new DomainEventPublisher<>(vertxContext, + REQUEST_QUEUE_REORDERING.fullTopicName(tenantId(okapiHeaders)), + FailureHandler.noOperation()), null); + } + public static EntityChangedEventPublisher checkInEventPublisher( Context vertxContext, Map okapiHeaders) { diff --git a/src/main/java/org/folio/service/request/RequestBatchResourceService.java b/src/main/java/org/folio/service/request/RequestBatchResourceService.java index 87a7ba44b..857899c44 100644 --- a/src/main/java/org/folio/service/request/RequestBatchResourceService.java +++ b/src/main/java/org/folio/service/request/RequestBatchResourceService.java @@ -2,21 +2,28 @@ import static java.util.stream.IntStream.rangeClosed; import static org.folio.rest.persist.PostgresClient.convertToPsqlStandard; +import static org.folio.rest.tools.utils.TenantTool.tenantId; +import static org.folio.service.event.EntityChangedEventPublisherFactory.requestBatchEventPublisher; import static org.folio.support.ModuleConstants.REQUEST_TABLE; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import org.folio.rest.jaxrs.model.Request; +import org.folio.rest.jaxrs.model.RequestQueueReordering; +import org.folio.rest.persist.PgUtil; import org.folio.rest.persist.SQLConnection; import org.folio.service.BatchResourceService; +import org.folio.service.event.EntityChangedEventPublisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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; @@ -29,12 +36,13 @@ public class RequestBatchResourceService { private final BatchResourceService batchResourceService; private final String tenantName; + private final EntityChangedEventPublisher eventPublisher; - public RequestBatchResourceService(String tenantName, - BatchResourceService batchResourceService) { - - this.batchResourceService = batchResourceService; - this.tenantName = tenantName; + public RequestBatchResourceService(Context context, Map okapiHeaders) { + this.batchResourceService = new BatchResourceService(PgUtil.postgresClient(context, + okapiHeaders)); + this.tenantName = tenantId(okapiHeaders); + this.eventPublisher = requestBatchEventPublisher(context, okapiHeaders); } /** @@ -59,8 +67,8 @@ public RequestBatchResourceService(String tenantName, * @param requests - List of requests to execute in batch. * @param onFinishHandler - Callback function. */ - public void executeRequestBatchUpdate( - List requests, Handler> onFinishHandler) { + public void executeRequestBatchUpdate(List requests, + Handler> onFinishHandler) { LOG.debug("Removing positions for all request to go through positions constraint"); List>>> allDatabaseOperations = @@ -80,7 +88,21 @@ public void executeRequestBatchUpdate( LOG.info("Executing batch update, total records to update [{}] (including remove positions)", allDatabaseOperations.size()); - batchResourceService.executeBatchUpdate(allDatabaseOperations, onFinishHandler); + RequestQueueReordering payload = mapRequestsToPayload(requests); + LOG.info("executeRequestBatchUpdate:: instanceId: {}, requests: {}", + payload.getInstanceId(), payload.getRequestIds()); + + batchResourceService.executeBatchUpdate(allDatabaseOperations, onFinishHandler) + .compose(v -> eventPublisher.publishCreated(payload.getInstanceId(), payload)); + } + + private RequestQueueReordering mapRequestsToPayload(List requests) { + + return new RequestQueueReordering() + .withRequestIds(requests.stream() + .map(Request::getId) + .toList()) + .withInstanceId(requests.get(0).getInstanceId()); } private Function>> removePositionsForRequestsBatch( diff --git a/src/main/java/org/folio/support/kafka/topic/CirculationStorageKafkaTopic.java b/src/main/java/org/folio/support/kafka/topic/CirculationStorageKafkaTopic.java index 0f4ec85b3..4dc536f1b 100644 --- a/src/main/java/org/folio/support/kafka/topic/CirculationStorageKafkaTopic.java +++ b/src/main/java/org/folio/support/kafka/topic/CirculationStorageKafkaTopic.java @@ -4,6 +4,7 @@ public enum CirculationStorageKafkaTopic implements KafkaTopic { REQUEST("request", 10), + REQUEST_QUEUE_REORDERING("request-queue-reordering", 10), CIRCULATION_SETTINGS("circulation-settings", 10), LOAN("loan", 10), CHECK_IN("check-in", 10), diff --git a/src/test/java/org/folio/rest/api/RequestBatchAPITest.java b/src/test/java/org/folio/rest/api/RequestBatchAPITest.java index 51ee63662..2ee133c0c 100644 --- a/src/test/java/org/folio/rest/api/RequestBatchAPITest.java +++ b/src/test/java/org/folio/rest/api/RequestBatchAPITest.java @@ -3,19 +3,23 @@ import static org.folio.rest.api.RequestsApiTest.requestStorageUrl; import static org.folio.rest.api.StorageTestSuite.TENANT_ID; import static org.folio.rest.api.StorageTestSuite.storageUrl; +import static org.folio.rest.support.kafka.FakeKafkaConsumer.removeAllEvents; +import static org.folio.rest.support.matchers.DomainEventAssertions.assertRequestQueueReorderingEvent; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.hasItems; -import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; import java.net.URL; import java.util.Arrays; import java.util.Comparator; +import java.util.List; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.function.Function; + import org.folio.rest.impl.RequestsBatchAPI; import org.folio.rest.jaxrs.model.Request; import org.folio.rest.support.ApiTests; @@ -41,6 +45,7 @@ public class RequestBatchAPITest extends ApiTests { @Before public void beforeEach() throws Exception { StorageTestSuite.deleteAll(requestStorageUrl()); + removeAllEvents(); } @After @@ -52,8 +57,8 @@ public void checkIdsAfterEach() { public void canUpdateRequestPositionsInBatch() throws Exception { UUID itemId = UUID.randomUUID(); - JsonObject firstRequest = createRequestForItemAtPosition(itemId, 1); - JsonObject secondRequest = createRequestForItemAtPosition(itemId, 2); + JsonObject firstRequest = createRequestAtPosition(itemId, null, 1); + JsonObject secondRequest = createRequestAtPosition(itemId, null, 2); ReorderRequest firstReorderRequest = new ReorderRequest(firstRequest, 2); ReorderRequest secondReorderRequest = new ReorderRequest(secondRequest, 1); @@ -81,8 +86,8 @@ public void canUpdateRequestPositionsInBatch() throws Exception { public void canCloseRequestsInBatch() throws Exception { UUID itemId = UUID.randomUUID(); - JsonObject firstRequest = createRequestForItemAtPosition(itemId, 1); - JsonObject secondRequest = createRequestForItemAtPosition(itemId, 2); + JsonObject firstRequest = createRequestAtPosition(itemId, null, 1); + JsonObject secondRequest = createRequestAtPosition(itemId, null, 2); firstRequest.put("status", Request.Status.CLOSED_FILLED.value()); firstRequest.remove("position"); @@ -111,8 +116,8 @@ public void canCloseRequestsInBatch() throws Exception { public void canUpdateRequestFulfillmentPreferenceInBatch() throws Exception { UUID itemId = UUID.randomUUID(); - JsonObject firstRequest = createRequestForItemAtPosition(itemId, 1); - JsonObject secondRequest = createRequestForItemAtPosition(itemId, 2); + JsonObject firstRequest = createRequestAtPosition(itemId, null, 1); + JsonObject secondRequest = createRequestAtPosition(itemId, null, 2); firstRequest.put("fulfillmentPreference", "Delivery"); secondRequest.put("fulfillmentPreference", "Delivery"); @@ -153,8 +158,8 @@ public void willAbortBatchUpdateOnPopulateMetadataException() throws Exception { public void willAbortBatchUpdateForRequestsAtTheSamePositionInAnItemsQueue() throws Exception { UUID itemId = UUID.randomUUID(); - JsonObject firstRequest = createRequestForItemAtPosition(itemId, 1); - JsonObject secondRequest = createRequestForItemAtPosition(itemId, 2); + JsonObject firstRequest = createRequestAtPosition(itemId, null, 1); + JsonObject secondRequest = createRequestAtPosition(itemId, null, 2); JsonResponse reorderResponse = attemptReorderRequests(ResponseHandler::json, new ReorderRequest(firstRequest, 1), @@ -170,8 +175,8 @@ public void willAbortBatchUpdateForRequestsAtTheSamePositionInAnItemsQueue() thr public void willAbortBatchUpdateWhenOnlyOnePositionIsModified() throws Exception { UUID itemId = UUID.randomUUID(); - JsonObject firstRequest = createRequestForItemAtPosition(itemId, 1); - JsonObject secondRequest = createRequestForItemAtPosition(itemId, 2); + JsonObject firstRequest = createRequestAtPosition(itemId, null, 1); + JsonObject secondRequest = createRequestAtPosition(itemId, null, 2); JsonResponse reorderResponse = attemptReorderRequests(ResponseHandler::json, new ReorderRequest(firstRequest, 2) @@ -186,8 +191,8 @@ public void willAbortBatchUpdateWhenOnlyOnePositionIsModified() throws Exception public void willAbortBatchUpdateOnNullPointerExceptionDueToNoIdInRequest() throws Exception { UUID itemId = UUID.randomUUID(); - JsonObject firstRequest = createRequestForItemAtPosition(itemId, 1); - JsonObject secondRequest = createRequestForItemAtPosition(itemId, 2); + JsonObject firstRequest = createRequestAtPosition(itemId, null, 1); + JsonObject secondRequest = createRequestAtPosition(itemId, null, 2); JsonObject firstRequestCopy = firstRequest.copy(); firstRequestCopy.remove("id"); @@ -205,7 +210,7 @@ public void willAbortBatchUpdateOnNullPointerExceptionDueToNoIdInRequest() throw public void cannotInjectSqlThroughRequestId() throws Exception { UUID itemId = UUID.randomUUID(); - JsonObject firstRequest = createRequestForItemAtPosition(itemId, 1); + JsonObject firstRequest = createRequestAtPosition(itemId, null, 1); JsonObject firstRequestCopy = firstRequest.copy(); firstRequestCopy.put("id", "1'; DELETE FROM request where id::text is not '1"); @@ -219,6 +224,36 @@ public void cannotInjectSqlThroughRequestId() throws Exception { assertRequestsNotUpdated(itemId, firstRequest); } + @Test + public void shouldPublishKafkaEventWhenUpdateRequestPositionsInBatchForTheInstance() + throws Exception { + + UUID instanceId = UUID.randomUUID(); + JsonObject firstRequest = createRequestAtPosition(null, instanceId, 1); + JsonObject secondRequest = createRequestAtPosition(null, instanceId, 2); + + ReorderRequest firstReorderRequest = new ReorderRequest(firstRequest, 2); + ReorderRequest secondReorderRequest = new ReorderRequest(secondRequest, 1); + + reorderRequests(firstReorderRequest, secondReorderRequest); + + JsonObject requestsForInstanceReply = getAllRequestsForInstance(instanceId); + assertThat(requestsForInstanceReply.getInteger("totalRecords"), is(2)); + JsonArray requestsFromDb = requestsForInstanceReply.getJsonArray("requests"); + assertThat(requestsFromDb.size(), is(2)); + List requestsSorted = requestsFromDb.stream() + .map(JsonObject.class::cast) + .sorted(Comparator.comparingInt(obj -> obj.getInteger("position"))) + .toList(); + assertThat(requestsSorted.get(0).getInteger("position"), is(1)); + assertThat(requestsSorted.get(1).getInteger("position"), is(2)); + assertThat(requestsSorted.get(0).getString("id"), is(secondRequest.getString("id"))); + assertThat(requestsSorted.get(1).getString("id"), is(firstRequest.getString("id"))); + + assertRequestQueueReorderingEvent(instanceId.toString(), List.of( + firstRequest.getString("id"), secondRequest.getString("id"))); + } + private JsonObject getAllRequestsForItem(UUID itemId) throws Exception { CompletableFuture getRequestsCompleted = new CompletableFuture<>(); @@ -228,10 +263,30 @@ private JsonObject getAllRequestsForItem(UUID itemId) throws Exception { return getRequestsCompleted.get(5, TimeUnit.SECONDS).getJson(); } - private JsonObject createRequestForItemAtPosition(UUID itemId, int position) throws Exception { - JsonObject request = createEntity( - new RequestRequestBuilder() + private JsonObject getAllRequestsForInstance(UUID instanceId) throws Exception { + CompletableFuture getRequestsCompleted = new CompletableFuture<>(); + + client.get(requestStorageUrl() + String.format("?query=instanceId==%s", instanceId), + TENANT_ID, ResponseHandler.json(getRequestsCompleted)); + + return getRequestsCompleted.get(5, TimeUnit.SECONDS).getJson(); + } + + private JsonObject createRequestAtPosition(UUID itemId, UUID instanceId, int position) + throws Exception { + + RequestRequestBuilder requestBuilder = null; + if (instanceId != null) { + requestBuilder = new RequestRequestBuilder() + .withInstanceId(instanceId) + .withRequestLevel("Title"); + } else { + requestBuilder = new RequestRequestBuilder() .withItemId(itemId) + .withRequestLevel("Item"); + } + JsonObject request = createEntity( + requestBuilder .withPosition(position) .recall() .toHoldShelf() @@ -240,7 +295,11 @@ private JsonObject createRequestForItemAtPosition(UUID itemId, int position) thr requestStorageUrl() ).getJson(); - assertThat(request.getString("itemId"), is(itemId.toString())); + if (itemId != null) { + assertThat(request.getString("itemId"), is(itemId.toString())); + } else { + assertThat(request.getString("instanceId"), is(instanceId.toString())); + } assertThat(request.getInteger("position"), is(position)); return request; diff --git a/src/test/java/org/folio/rest/support/kafka/FakeKafkaConsumer.java b/src/test/java/org/folio/rest/support/kafka/FakeKafkaConsumer.java index da59490dc..6f5dd0e5e 100644 --- a/src/test/java/org/folio/rest/support/kafka/FakeKafkaConsumer.java +++ b/src/test/java/org/folio/rest/support/kafka/FakeKafkaConsumer.java @@ -27,6 +27,7 @@ public final class FakeKafkaConsumer { private static final String REQUEST_TOPIC_NAME = "folio.test_tenant.circulation.request"; private static final String CHECKIN_TOPIC_NAME = "folio.test_tenant.circulation.check-in"; private static final String CIRCULATION_RULES_TOPIC_NAME = "folio.test_tenant.circulation.rules"; + private static final String REQUEST_QUEUE_REORDERING_TOPIC_NAME = "folio.test_tenant.circulation.request-queue-reordering"; private static final Map>> loanEvents = new ConcurrentHashMap<>(); @@ -36,19 +37,21 @@ public final class FakeKafkaConsumer { new ConcurrentHashMap<>(); private static final Map>> circulationRulesEvents = new ConcurrentHashMap<>(); - + private static final Map>> requestQueueReorderingEvents = + new ConcurrentHashMap<>(); private static final Map>>> topicToEvents = Map.of( LOAN_TOPIC_NAME, loanEvents, REQUEST_TOPIC_NAME, requestEvents, CHECKIN_TOPIC_NAME, checkInEvents, - CIRCULATION_RULES_TOPIC_NAME, circulationRulesEvents + CIRCULATION_RULES_TOPIC_NAME, circulationRulesEvents, + REQUEST_QUEUE_REORDERING_TOPIC_NAME, requestQueueReorderingEvents ); public FakeKafkaConsumer consume(Vertx vertx) { final KafkaConsumer consumer = create(vertx, consumerProperties()); consumer.subscribe(Set.of(LOAN_TOPIC_NAME, REQUEST_TOPIC_NAME, CHECKIN_TOPIC_NAME, - CIRCULATION_RULES_TOPIC_NAME)); + CIRCULATION_RULES_TOPIC_NAME, REQUEST_QUEUE_REORDERING_TOPIC_NAME)); consumer.handler(message -> { var recordEvents = topicToEvents.get(message.topic()); @@ -69,6 +72,7 @@ public static void removeAllEvents() { requestEvents.clear(); checkInEvents.clear(); circulationRulesEvents.clear(); + requestQueueReorderingEvents.clear(); } public static int getAllPublishedLoanCount() { @@ -94,6 +98,13 @@ public static Collection> getCirculation .orElseGet(Collections::emptyList); } + public static Collection> getRequestQueueReorderingEvents() { + return requestQueueReorderingEvents.values() + .stream() + .findFirst() + .orElseGet(Collections::emptyList); + } + public static KafkaConsumerRecord getFirstLoanEvent(String loanId) { return getFirstEvent(getLoanEvents(loanId)); } @@ -126,6 +137,10 @@ public static KafkaConsumerRecord getFirstCheckInEvent(Strin return getFirstEvent(getCheckInEvents(checkInId)); } + public static KafkaConsumerRecord getFirstRequestQueueReorderingEvent() { + return getFirstEvent(getRequestQueueReorderingEvents()); + } + private static KafkaConsumerRecord getFirstEvent( Collection> events) { diff --git a/src/test/java/org/folio/rest/support/matchers/DomainEventAssertions.java b/src/test/java/org/folio/rest/support/matchers/DomainEventAssertions.java index 5c3d9ba8d..9abcc1938 100644 --- a/src/test/java/org/folio/rest/support/matchers/DomainEventAssertions.java +++ b/src/test/java/org/folio/rest/support/matchers/DomainEventAssertions.java @@ -11,12 +11,14 @@ import static org.folio.rest.support.kafka.FakeKafkaConsumer.getCheckInEvents; import static org.folio.rest.support.kafka.FakeKafkaConsumer.getCirculationRulesEvents; import static org.folio.rest.support.kafka.FakeKafkaConsumer.getFirstLoanEvent; +import static org.folio.rest.support.kafka.FakeKafkaConsumer.getFirstRequestQueueReorderingEvent; import static org.folio.rest.support.kafka.FakeKafkaConsumer.getLastCheckInEvent; import static org.folio.rest.support.kafka.FakeKafkaConsumer.getLastCirculationRulesEvent; import static org.folio.rest.support.kafka.FakeKafkaConsumer.getLastLoanEvent; import static org.folio.rest.support.kafka.FakeKafkaConsumer.getLastRequestEvent; import static org.folio.rest.support.kafka.FakeKafkaConsumer.getLoanEvents; import static org.folio.rest.support.kafka.FakeKafkaConsumer.getRequestEvents; +import static org.folio.rest.support.kafka.FakeKafkaConsumer.getRequestQueueReorderingEvents; import static org.folio.rest.support.matchers.UUIDMatchers.hasUUIDFormat; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.nullValue; @@ -34,6 +36,7 @@ import org.folio.service.event.DomainEventType; import io.vertx.core.MultiMap; +import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; import io.vertx.kafka.client.consumer.KafkaConsumerRecord; import io.vertx.kafka.client.producer.KafkaHeader; @@ -108,6 +111,18 @@ public static void assertCreateEventForRequest(JsonObject request) { assertCreateEvent(getLastRequestEvent(requestId), request); } + public static void assertRequestQueueReorderingEvent(String instanceId, + List requestIds) { + + await().until(() -> getRequestQueueReorderingEvents().size(), greaterThan(0)); + + JsonObject payload = new JsonObject() + .put("instanceId", instanceId) + .put("requestIds", new JsonArray(requestIds)); + + assertCreateEvent(getFirstRequestQueueReorderingEvent(), payload); + } + public static void assertNoRequestEvent(String requestId) { await().during(1, SECONDS) .until(() -> getRequestEvents(requestId), is(empty())); diff --git a/src/test/java/org/folio/service/kafka/topic/KafkaAdminClientServiceTest.java b/src/test/java/org/folio/service/kafka/topic/KafkaAdminClientServiceTest.java index e5b63c190..574ecd6eb 100644 --- a/src/test/java/org/folio/service/kafka/topic/KafkaAdminClientServiceTest.java +++ b/src/test/java/org/folio/service/kafka/topic/KafkaAdminClientServiceTest.java @@ -43,7 +43,8 @@ public class KafkaAdminClientServiceTest { "folio.foo-tenant.circulation.loan", "folio.foo-tenant.circulation.check-in", "folio.foo-tenant.circulation.rules", - "folio.foo-tenant.circulation.circulation-settings" + "folio.foo-tenant.circulation.circulation-settings", + "folio.foo-tenant.circulation.request-queue-reordering" ); private KafkaAdminClient mockClient; From 078733e994904248e2e313ba72d5844f1d6a8e87 Mon Sep 17 00:00:00 2001 From: Roman Barannyk <53909129+roman-barannyk@users.noreply.github.com> Date: Mon, 16 Sep 2024 17:17:16 +0300 Subject: [PATCH 08/17] [CIRCSTORE-526] Add ILR support for publishing batch request update events (#489) * CIRCSTORE-521 add request batch event publishing * CIRCSTORE-521 update logging * CIRCSTORE-521 fix broken test * CIRCSTORE-521 revert comments * CIRCSTORE-521 refactoring * CIRCSTORE-521 create test * CIRCSTORE-521 update test * CIRCSTORE-521 rename method * CIRCSTORE-521 use uuid reference for schema * CIRCSTORE-521 add events cleaning * CIRCSTORE-521 extend request queue reordering schema * CIRCSTORE-521 conflict resolving * CIRCSTORE-526 test refactoring --- ramls/request-queue-reordering.json | 15 ++++++++++++++- .../request/RequestBatchResourceService.java | 17 +++++++++++------ .../org/folio/rest/api/RequestBatchAPITest.java | 12 ++++++++---- .../support/matchers/DomainEventAssertions.java | 7 +++++-- 4 files changed, 38 insertions(+), 13 deletions(-) diff --git a/ramls/request-queue-reordering.json b/ramls/request-queue-reordering.json index f76bcdd88..8098f1dda 100644 --- a/ramls/request-queue-reordering.json +++ b/ramls/request-queue-reordering.json @@ -9,6 +9,16 @@ "type": "string", "$ref": "raml-util/schemas/uuid.schema" }, + "itemId": { + "description": "Item ID of reordered requests", + "type": "string", + "$ref": "raml-util/schemas/uuid.schema" + }, + "requestLevel": { + "description": "Level of the request - Item or Title", + "type": "string", + "enum": ["Item", "Title"] + }, "requestIds": { "description": "Array of requests ids", "type": "array", @@ -18,5 +28,8 @@ } } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "requestLevel" + ] } diff --git a/src/main/java/org/folio/service/request/RequestBatchResourceService.java b/src/main/java/org/folio/service/request/RequestBatchResourceService.java index 857899c44..c863b1937 100644 --- a/src/main/java/org/folio/service/request/RequestBatchResourceService.java +++ b/src/main/java/org/folio/service/request/RequestBatchResourceService.java @@ -30,7 +30,7 @@ import io.vertx.sqlclient.RowSet; public class RequestBatchResourceService { - private static final Logger LOG = LoggerFactory.getLogger(RequestBatchResourceService.class); + private static final Logger log = LoggerFactory.getLogger(RequestBatchResourceService.class); private static final String REMOVE_POSITIONS_SQL = "UPDATE %s.%s SET jsonb = jsonb - 'position' WHERE id::text IN (%s)"; @@ -70,7 +70,7 @@ public RequestBatchResourceService(Context context, Map okapiHea public void executeRequestBatchUpdate(List requests, Handler> onFinishHandler) { - LOG.debug("Removing positions for all request to go through positions constraint"); + log.debug("Removing positions for all request to go through positions constraint"); List>>> allDatabaseOperations = new ArrayList<>(); @@ -85,24 +85,29 @@ public void executeRequestBatchUpdate(List requests, allDatabaseOperations.add(removePositionBatch); allDatabaseOperations.addAll(updateRequestsBatch); - LOG.info("Executing batch update, total records to update [{}] (including remove positions)", + log.info("Executing batch update, total records to update [{}] (including remove positions)", allDatabaseOperations.size()); RequestQueueReordering payload = mapRequestsToPayload(requests); - LOG.info("executeRequestBatchUpdate:: instanceId: {}, requests: {}", - payload.getInstanceId(), payload.getRequestIds()); + log.info("executeRequestBatchUpdate:: instanceId: {}, itemId: {}, requestLevel: {}, " + + "requests: {}", payload.getInstanceId(), payload.getItemId(), payload.getRequestLevel(), + payload.getRequestIds()); batchResourceService.executeBatchUpdate(allDatabaseOperations, onFinishHandler) .compose(v -> eventPublisher.publishCreated(payload.getInstanceId(), payload)); } private RequestQueueReordering mapRequestsToPayload(List requests) { + var firstRequest = requests.get(0); return new RequestQueueReordering() .withRequestIds(requests.stream() .map(Request::getId) .toList()) - .withInstanceId(requests.get(0).getInstanceId()); + .withInstanceId(firstRequest.getInstanceId()) + .withItemId(firstRequest.getItemId()) + .withRequestLevel(RequestQueueReordering.RequestLevel.valueOf( + firstRequest.getRequestLevel().name())); } private Function>> removePositionsForRequestsBatch( diff --git a/src/test/java/org/folio/rest/api/RequestBatchAPITest.java b/src/test/java/org/folio/rest/api/RequestBatchAPITest.java index 2ee133c0c..234733aab 100644 --- a/src/test/java/org/folio/rest/api/RequestBatchAPITest.java +++ b/src/test/java/org/folio/rest/api/RequestBatchAPITest.java @@ -3,6 +3,7 @@ import static org.folio.rest.api.RequestsApiTest.requestStorageUrl; import static org.folio.rest.api.StorageTestSuite.TENANT_ID; import static org.folio.rest.api.StorageTestSuite.storageUrl; +import static org.folio.rest.jaxrs.model.RequestQueueReordering.RequestLevel.TITLE; import static org.folio.rest.support.kafka.FakeKafkaConsumer.removeAllEvents; import static org.folio.rest.support.matchers.DomainEventAssertions.assertRequestQueueReorderingEvent; import static org.hamcrest.MatcherAssert.assertThat; @@ -22,6 +23,7 @@ import org.folio.rest.impl.RequestsBatchAPI; import org.folio.rest.jaxrs.model.Request; +import org.folio.rest.jaxrs.model.RequestQueueReordering; import org.folio.rest.support.ApiTests; import org.folio.rest.support.JsonResponse; import org.folio.rest.support.Response; @@ -245,13 +247,15 @@ public void shouldPublishKafkaEventWhenUpdateRequestPositionsInBatchForTheInstan .map(JsonObject.class::cast) .sorted(Comparator.comparingInt(obj -> obj.getInteger("position"))) .toList(); + String firstRequestId = firstRequest.getString("id"); + String secondRequestId = secondRequest.getString("id"); assertThat(requestsSorted.get(0).getInteger("position"), is(1)); assertThat(requestsSorted.get(1).getInteger("position"), is(2)); - assertThat(requestsSorted.get(0).getString("id"), is(secondRequest.getString("id"))); - assertThat(requestsSorted.get(1).getString("id"), is(firstRequest.getString("id"))); + assertThat(requestsSorted.get(0).getString("id"), is(secondRequestId)); + assertThat(requestsSorted.get(1).getString("id"), is(firstRequestId)); - assertRequestQueueReorderingEvent(instanceId.toString(), List.of( - firstRequest.getString("id"), secondRequest.getString("id"))); + assertRequestQueueReorderingEvent(instanceId.toString(), + requestsSorted.get(1).getString("itemId"), List.of(firstRequestId, secondRequestId), TITLE); } private JsonObject getAllRequestsForItem(UUID itemId) throws Exception { diff --git a/src/test/java/org/folio/rest/support/matchers/DomainEventAssertions.java b/src/test/java/org/folio/rest/support/matchers/DomainEventAssertions.java index 9abcc1938..f3fda249c 100644 --- a/src/test/java/org/folio/rest/support/matchers/DomainEventAssertions.java +++ b/src/test/java/org/folio/rest/support/matchers/DomainEventAssertions.java @@ -33,6 +33,7 @@ import org.awaitility.Awaitility; import org.awaitility.core.ConditionFactory; +import org.folio.rest.jaxrs.model.RequestQueueReordering; import org.folio.service.event.DomainEventType; import io.vertx.core.MultiMap; @@ -111,13 +112,15 @@ public static void assertCreateEventForRequest(JsonObject request) { assertCreateEvent(getLastRequestEvent(requestId), request); } - public static void assertRequestQueueReorderingEvent(String instanceId, - List requestIds) { + public static void assertRequestQueueReorderingEvent(String instanceId, String itemId, + List requestIds, RequestQueueReordering.RequestLevel requestLevel) { await().until(() -> getRequestQueueReorderingEvents().size(), greaterThan(0)); JsonObject payload = new JsonObject() .put("instanceId", instanceId) + .put("itemId", itemId) + .put("requestLevel", requestLevel.value()) .put("requestIds", new JsonArray(requestIds)); assertCreateEvent(getFirstRequestQueueReorderingEvent(), payload); From 5851ec83114064f7d4a9273efa0dc3a5a3429a34 Mon Sep 17 00:00:00 2001 From: Antony_Hruschev Date: Fri, 25 Oct 2024 21:20:06 +0400 Subject: [PATCH 09/17] Fixed StaffSlipsApiTest --- src/test/java/org/folio/rest/api/StaffSlipsApiTest.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/test/java/org/folio/rest/api/StaffSlipsApiTest.java b/src/test/java/org/folio/rest/api/StaffSlipsApiTest.java index 6f2af3450..71baffd25 100644 --- a/src/test/java/org/folio/rest/api/StaffSlipsApiTest.java +++ b/src/test/java/org/folio/rest/api/StaffSlipsApiTest.java @@ -41,7 +41,7 @@ public class StaffSlipsApiTest extends ApiTests { private static final String DESCRIPTION_KEY = "description"; private static final String TEMPLATE_KEY = "template"; private static final String [] STAFF_SLIP_NAMES = {"Hold", "Transit", "Request delivery", "Pick slip", - "Search slip (Hold requests)", "Transit (mediated requests)"}; + "Search slip (Hold requests)", "Transit (mediated requests)", "Due date receipt"}; private static final String STAFF_SLIPS_KEY = "staffSlips"; private static AtomicBoolean isRefTestDone = new AtomicBoolean(false); @@ -72,8 +72,6 @@ private void canGetStaffSlipReferenceData() JsonArray slipsJsonArray = getResponse.getJson().getJsonArray(STAFF_SLIPS_KEY); Object [] names = slipsJsonArray.stream().map(o -> ((JsonObject) o).getString(NAME_KEY)).toArray(); assertThat(names, arrayContainingInAnyOrder(STAFF_SLIP_NAMES)); - assertThat(names, arrayContainingInAnyOrder("Hold", "Transit", "Request delivery", "Pick slip", - "Search slip (Hold requests)","Due date receipt")); } /* Begin Tests */ From 0c13d703b8a2c7412400a4adbb60f5bc88a14b6d Mon Sep 17 00:00:00 2001 From: Irakli Merabishvili Date: Wed, 30 Oct 2024 19:24:53 +0400 Subject: [PATCH 10/17] Update NEWS --- NEWS.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index 52c1ae705..eae006683 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,16 @@ -## 17.3.0 -* [CIRCSTORE-533](https://folio-org.atlassian.net/browse/CIRCSTORE-533) Upgrade "holdings-storage" to 8.0 +## 17.3.0 2024-10-30 +* Upgrade RMB to 35.3.0, Vert.x to 4.5.9, Kafka wrapper to 3.1.1 (CIRCSTORE-520, CIRCSTORE-536, CIRCSTORE-499) +* Upgrade `holdings-storage` API (CIRCSTORE-533) +* Persisting due date slip template (CIRCSTORE-529) +* Update existing setting instead of inserting new record with the same name (CIRCSTORE-519) +* Create CRUD API for storing `circulation-settings` (CIRCSTORE-515) +* Implement POST API to fetch print Event Details (CIRCSTORE-514) +* Implementing post print Event API (CIRCSTORE-513) +* Added a new test case for persisting and retrieving EnableRequestPrintDetailsSetting (CIRCSTORE-512) +* Update interface `holdings-storage` to version 7.0 (CIRCSTORE-510) +* Add isDcb flag in loan.json (CIRCSTORE-506) +* Add required interface `configuration` (CIRCSTORE-505) +* Keep existing reference/sample data on upgrade (CIRCSTORE-496) ## 17.2.0 2024-03-20 * Upgrade RMB to 35.2.0, Vert.x to 4.5.5, Spring to 6.1.5 (CIRCSTORE-494) From 22aeb48df317fbcdb8045e1146153fe5e3727170 Mon Sep 17 00:00:00 2001 From: Irakli Merabishvili Date: Wed, 30 Oct 2024 19:25:21 +0400 Subject: [PATCH 11/17] [maven-release-plugin] prepare release v17.3.0 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index f88037b7b..57923a422 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 mod-circulation-storage org.folio - 17.3.0-SNAPSHOT + 17.3.0 Apache License 2.0 @@ -226,7 +226,7 @@ https://github.com/folio-org/mod-circulation-storage scm:git:git://github.com:folio-org/mod-circulation-storage.git scm:git:git@github.com:folio-org/mod-circulation-storage.git - HEAD + v17.3.0 From 6cb718dfa6f2cc2c5971d0cb5e0245e28b2fffd6 Mon Sep 17 00:00:00 2001 From: Irakli Merabishvili Date: Wed, 30 Oct 2024 19:25:22 +0400 Subject: [PATCH 12/17] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 57923a422..4e911d1ee 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 mod-circulation-storage org.folio - 17.3.0 + 17.4.0-SNAPSHOT Apache License 2.0 @@ -226,7 +226,7 @@ https://github.com/folio-org/mod-circulation-storage scm:git:git://github.com:folio-org/mod-circulation-storage.git scm:git:git@github.com:folio-org/mod-circulation-storage.git - v17.3.0 + HEAD From f7921897cbbb5c30bceb19efcd5d2ad04ed4f9be Mon Sep 17 00:00:00 2001 From: Azizbek Khushvakov <113523904+azizbekxm@users.noreply.github.com> Date: Fri, 22 Nov 2024 20:46:56 +0500 Subject: [PATCH 13/17] [CIRCSTORE-524] - Review and cleanup Module Descriptor for mod-circulation-storage (#495) * [CIRCSTORE-524] - Review and cleanup Module Descriptor for mod-circulation-storage * Update permission name --- descriptors/ModuleDescriptor-template.json | 9 +++++---- pom.xml | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 7dc9be027..c745cb6f9 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -78,7 +78,7 @@ }, { "methods": ["POST"], "pathPattern": "/loan-storage/loans/anonymize/{userId}", - "permissionsRequired": ["circulation-storage.loans.collection.anonymize.user.post"] + "permissionsRequired": ["circulation-storage.loans.anonymize.execute"] } ] }, @@ -757,9 +757,10 @@ "description": "Delete individual loan from storage" }, { - "permissionName": "circulation-storage.loans.collection.anonymize.user.post", + "permissionName": "circulation-storage.loans.anonymize.execute", "displayName": "Circulation storage - anonymize loans for a user", - "description": "Anonymize closed loans for a single user" + "description": "Anonymize closed loans for a single user", + "replaces": "circulation-storage.loans.collection.anonymize.user.post" }, { "permissionName": "anonymize-storage-loans.post", @@ -1082,7 +1083,7 @@ "circulation-storage.loans.item.put", "circulation-storage.loans.item.delete", "circulation-storage.loans.collection.delete", - "circulation-storage.loans.collection.anonymize.user.post", + "circulation-storage.loans.anonymize.execute", "circulation-storage.loans-history.collection.get", "circulation-storage.circulation-rules.get", "circulation-storage.circulation-rules.put", diff --git a/pom.xml b/pom.xml index 4e911d1ee..40555af2c 100644 --- a/pom.xml +++ b/pom.xml @@ -31,6 +31,9 @@ 4.5.9 35.3.0 6.1.5 + + + 1.0.0 @@ -513,6 +516,18 @@ + + org.folio + folio-module-descriptor-validator + ${folio-module-descriptor-validator.version} + + + + validate + + + + From 94078e9d2bc63310d8667c182614c79ad61f1216 Mon Sep 17 00:00:00 2001 From: Roman Barannyk <53909129+roman-barannyk@users.noreply.github.com> Date: Fri, 22 Nov 2024 18:49:53 +0200 Subject: [PATCH 14/17] CIRCSTORE-524 Remove replaces (#497) Merge pull request #140 from folio-org/RMB-724-RMB-718-totalRecords-none RMB-724, RMB-718: totalRecords=exact/estimated/none/auto --- descriptors/ModuleDescriptor-template.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index c745cb6f9..4e775f55c 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -760,7 +760,7 @@ "permissionName": "circulation-storage.loans.anonymize.execute", "displayName": "Circulation storage - anonymize loans for a user", "description": "Anonymize closed loans for a single user", - "replaces": "circulation-storage.loans.collection.anonymize.user.post" + "replaces": ["circulation-storage.loans.collection.anonymize.user.post"] }, { "permissionName": "anonymize-storage-loans.post", From bc434ed1975e613530b5293914745ebcef35ef0a Mon Sep 17 00:00:00 2001 From: Roman_Barannyk Date: Sat, 30 Nov 2024 17:20:42 +0200 Subject: [PATCH 15/17] Update NEWS --- NEWS.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/NEWS.md b/NEWS.md index eae006683..bb3d99373 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,12 @@ +## 17.3.1 2024-11-30 +* Add ILR support for publishing batch request update events (CIRCSTORE-526) +* Publish request batch event when requests are reordered (CIRCSTORE-521) +* Create new transit staff slip for mediated request activities (CIRCSTORE-525) +* Update instead save with the same name circ settings (CIRCSTORE-519) +* Create CRUD API for storing `circulation-settings` (CIRCSTORE-509) +* Add `ecsRequestPhase` field to request schema (CIRCSTORE-502) +* Review and cleanup Module Descriptor for mod-circulation-storage (CIRCSTORE-524) + ## 17.3.0 2024-10-30 * Upgrade RMB to 35.3.0, Vert.x to 4.5.9, Kafka wrapper to 3.1.1 (CIRCSTORE-520, CIRCSTORE-536, CIRCSTORE-499) * Upgrade `holdings-storage` API (CIRCSTORE-533) From 1f945ddd17e54b55c59a2fd0e6ef19ad5919e70a Mon Sep 17 00:00:00 2001 From: Roman_Barannyk Date: Sat, 30 Nov 2024 17:27:56 +0200 Subject: [PATCH 16/17] [maven-release-plugin] prepare release v17.3.1 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index abbe23a97..a38415672 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 mod-circulation-storage org.folio - 17.3.1-SNAPSHOT + 17.3.1 Apache License 2.0 @@ -229,7 +229,7 @@ https://github.com/folio-org/mod-circulation-storage scm:git:git://github.com:folio-org/mod-circulation-storage.git scm:git:git@github.com:folio-org/mod-circulation-storage.git - v17.3.0 + v17.3.1 From e13dbbf6bab5fdf2523bcc343b8cf9f93dd00580 Mon Sep 17 00:00:00 2001 From: Roman_Barannyk Date: Sat, 30 Nov 2024 17:28:01 +0200 Subject: [PATCH 17/17] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index a38415672..fedc1a9f6 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 mod-circulation-storage org.folio - 17.3.1 + 17.3.2-SNAPSHOT Apache License 2.0 @@ -229,7 +229,7 @@ https://github.com/folio-org/mod-circulation-storage scm:git:git://github.com:folio-org/mod-circulation-storage.git scm:git:git@github.com:folio-org/mod-circulation-storage.git - v17.3.1 + v17.3.0