diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index f4f17887..f3cf1ef2 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -179,7 +179,7 @@ }, { "id": "request-storage", - "version": "6.0", + "version": "6.1", "handlers": [ { "methods": ["GET"], diff --git a/ramls/request.json b/ramls/request.json index aa582e03..f2fbfd1a 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/main/java/org/folio/persist/AbstractRepository.java b/src/main/java/org/folio/persist/AbstractRepository.java index 93bdbc7f..0d480a5f 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 07e05a95..68141494 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 a0c95007..b060c252 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 76f591e7..f9602bc8 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 4f896fac..fec71d3f 100644 --- a/src/test/java/org/folio/rest/api/CirculationSettingsAPITest.java +++ b/src/test/java/org/folio/rest/api/CirculationSettingsAPITest.java @@ -1,95 +1,147 @@ 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 static final String REQUEST_PRINT_SETTING = "Enable Request Print"; 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)); - } - - private JsonObject getCirculationSetting(String id) { - return new JsonObject() - .put("id", id) - .put("name", "sample") - .put("value", new JsonObject().put("sample", "OK")); + assertThat(deletedCirculationSettings.getStatusCode(), is(NOT_FOUND_STATUS)); } @Test - public void canCreateAndRetrieveEnableRequestPrintDetailsSetting() throws MalformedURLException, - ExecutionException, InterruptedException, TimeoutException { + @SneakyThrows + public void canCreateAndRetrieveEnableRequestPrintDetailsSetting() { String id = UUID.randomUUID().toString(); JsonObject enableRequestPrintDetailsSettingJson = new JsonObject(); - enableRequestPrintDetailsSettingJson.put("id", id); - enableRequestPrintDetailsSettingJson.put("name", "Enable Request Print"); - enableRequestPrintDetailsSettingJson.put("value", new JsonObject().put("Enable Request Print", true)); + enableRequestPrintDetailsSettingJson.put(ID_KEY, id); + enableRequestPrintDetailsSettingJson.put(NAME_KEY, REQUEST_PRINT_SETTING); + JsonObject enablePrintSettingJson = new JsonObject().put(REQUEST_PRINT_SETTING, true); + enableRequestPrintDetailsSettingJson.put(VALUE_KEY, enablePrintSettingJson); JsonObject circulationSettingsResponse = circulationSettingsClient.create(enableRequestPrintDetailsSettingJson).getJson(); JsonObject circulationSettingsById = circulationSettingsClient.getById(id).getJson(); - assertThat(circulationSettingsResponse.getString("id"), is(id)); - assertThat(circulationSettingsResponse.getString("name"), - is(enableRequestPrintDetailsSettingJson.getString("name"))); - assertThat(circulationSettingsById.getString("id"), is(id)); - assertThat(circulationSettingsById.getJsonObject("value"), - is(enableRequestPrintDetailsSettingJson.getJsonObject("value"))); + assertThat(circulationSettingsResponse.getString(ID_KEY), is(id)); + assertThat(circulationSettingsResponse.getString(NAME_KEY), + is(enableRequestPrintDetailsSettingJson.getString(NAME_KEY))); + assertThat(circulationSettingsById.getString(ID_KEY), is(id)); + assertThat(circulationSettingsById.getJsonObject(VALUE_KEY), + is(enableRequestPrintDetailsSettingJson.getJsonObject(VALUE_KEY))); + } + + private static String getValue(JsonObject circulationSettingsById) { + return circulationSettingsById.getJsonObject(VALUE_KEY).getString(SAMPLE_KEY); + } + + private JsonObject getCirculationSetting(String id) { + return new JsonObject() + .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; } } diff --git a/src/test/java/org/folio/rest/api/RequestsApiTest.java b/src/test/java/org/folio/rest/api/RequestsApiTest.java index 14521c85..cf243501 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 ccb8f60c..d849b35e 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"); + } + }