diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 94d99bd635..bcf2b69853 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -2276,6 +2276,8 @@ "usergroups.collection.get", "usergroups.item.get", "pubsub.publish.post", + "configuration.entries.collection.get", + "configuration.entries.item.get", "mod-settings.entries.item.get", "mod-settings.entries.collection.get", "mod-settings.global.read.circulation" @@ -2375,6 +2377,8 @@ "pubsub.publish.post", "patron-notice.post", "circulation-storage.loans-history.collection.get", + "configuration.entries.collection.get", + "configuration.entries.item.get", "mod-settings.entries.item.get", "mod-settings.entries.collection.get", "mod-settings.global.read.circulation" diff --git a/src/main/java/org/folio/circulation/domain/configuration/TlrSettingsConfiguration.java b/src/main/java/org/folio/circulation/domain/configuration/TlrSettingsConfiguration.java index 75b12913ee..a16b61c95f 100644 --- a/src/main/java/org/folio/circulation/domain/configuration/TlrSettingsConfiguration.java +++ b/src/main/java/org/folio/circulation/domain/configuration/TlrSettingsConfiguration.java @@ -11,12 +11,14 @@ import io.vertx.core.json.JsonObject; import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; @AllArgsConstructor @Getter @ToString(onlyExplicitlyIncluded = true) +@EqualsAndHashCode public class TlrSettingsConfiguration { protected static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); diff --git a/src/main/java/org/folio/circulation/infrastructure/storage/ConfigurationRepository.java b/src/main/java/org/folio/circulation/infrastructure/storage/ConfigurationRepository.java index 339c280513..992628921e 100644 --- a/src/main/java/org/folio/circulation/infrastructure/storage/ConfigurationRepository.java +++ b/src/main/java/org/folio/circulation/infrastructure/storage/ConfigurationRepository.java @@ -12,6 +12,7 @@ import org.folio.circulation.domain.ConfigurationService; import org.folio.circulation.domain.MultipleRecords; import org.folio.circulation.domain.anonymization.config.LoanAnonymizationConfiguration; +import org.folio.circulation.domain.configuration.TlrSettingsConfiguration; import org.folio.circulation.support.Clients; import org.folio.circulation.support.GetManyRecordsClient; import org.folio.circulation.support.http.client.CqlQuery; @@ -19,7 +20,9 @@ import org.folio.circulation.support.results.Result; import io.vertx.core.json.JsonObject; +import lombok.extern.log4j.Log4j2; +@Log4j2 public class ConfigurationRepository { private static final String CONFIGS_KEY = "configs"; private static final String MODULE_NAME_KEY = "module"; @@ -48,6 +51,14 @@ public CompletableFuture> lookupSessionTimeout() { return lookupConfigurations(otherSettingsQuery, applySessionTimeout()); } + public CompletableFuture> lookupTlrSettings() { + log.info("lookupTlrSettings:: fetching TLR configuration"); + Result queryResult = defineModuleNameAndConfigNameFilter( + "SETTINGS", "TLR"); + + return findAndMapFirstConfiguration(queryResult, TlrSettingsConfiguration::from); + } + /** * Gets loan history tenant configuration - settings for loan anonymization * @@ -115,4 +126,26 @@ private Function, Integer> applySessionTimeout() .findSessionTimeout(configurations.getRecords()); } + /** + * Find first configuration and maps it to an object with a provided mapper + */ + private CompletableFuture> findAndMapFirstConfiguration( + Result cqlQueryResult, Function mapper) { + + return cqlQueryResult + .after(query -> configurationClient.getMany(query, DEFAULT_PAGE_LIMIT)) + .thenApply(result -> result.next(r -> from(r, Configuration::new, CONFIGS_KEY))) + .thenApply(result -> result.map(this::findFirstConfigurationAsJsonObject)) + .thenApply(result -> result.map(mapper)); + } + + private JsonObject findFirstConfigurationAsJsonObject( + MultipleRecords configurations) { + + return configurations.getRecords().stream() + .findFirst() + .map(Configuration::getValue) + .map(JsonObject::new) + .orElse(new JsonObject()); + } } diff --git a/src/main/java/org/folio/circulation/infrastructure/storage/SettingsRepository.java b/src/main/java/org/folio/circulation/infrastructure/storage/SettingsRepository.java index d5b2e32b84..cd738cd94b 100644 --- a/src/main/java/org/folio/circulation/infrastructure/storage/SettingsRepository.java +++ b/src/main/java/org/folio/circulation/infrastructure/storage/SettingsRepository.java @@ -21,14 +21,17 @@ import static java.util.function.Function.identity; import static org.folio.circulation.support.http.client.CqlQuery.exactMatch; import static org.folio.circulation.support.http.client.CqlQuery.exactMatchAny; +import static org.folio.circulation.support.results.Result.ofAsync; import static org.folio.circulation.support.results.Result.succeeded; public class SettingsRepository { private static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); private final GetManyRecordsClient settingsClient; + private final ConfigurationRepository configurationRepository; public SettingsRepository(Clients clients) { settingsClient = clients.settingsStorageClient(); + configurationRepository = new ConfigurationRepository(clients); } public CompletableFuture> lookUpCheckOutLockSettings() { @@ -52,9 +55,10 @@ public CompletableFuture> lookUpCheckOutLockSe } public CompletableFuture> lookupTlrSettings() { + log.info("lookupTlrSettings:: fetching TLR settings"); return fetchSettings("circulation", List.of("generalTlr", "regularTlr")) .thenApply(r -> r.map(SettingsRepository::extractAndMergeValues)) - .thenApply(r -> r.map(TlrSettingsConfiguration::from)); + .thenCompose(r -> r.after(this::buildTlrSettings)); } private CompletableFuture>> fetchSettings(String scope, String key) { @@ -76,4 +80,13 @@ private static JsonObject extractAndMergeValues(MultipleRecords entr .map(rec -> rec.getJsonObject("value")) .reduce(new JsonObject(), JsonObject::mergeIn); } + + private CompletableFuture> buildTlrSettings(JsonObject tlrSettings) { + if (tlrSettings.isEmpty()) { + log.info("getTlrSettings:: failed to find TLR settings, falling back to legacy configuration"); + return configurationRepository.lookupTlrSettings(); + } + + return ofAsync(TlrSettingsConfiguration.from(tlrSettings)); + } } diff --git a/src/test/java/org/folio/circulation/infrastructure/storage/SettingsRepositoryTest.java b/src/test/java/org/folio/circulation/infrastructure/storage/SettingsRepositoryTest.java index f3c7c60c92..ff17621af8 100644 --- a/src/test/java/org/folio/circulation/infrastructure/storage/SettingsRepositoryTest.java +++ b/src/test/java/org/folio/circulation/infrastructure/storage/SettingsRepositoryTest.java @@ -2,6 +2,9 @@ import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; +import lombok.SneakyThrows; + +import org.folio.circulation.domain.configuration.TlrSettingsConfiguration; import org.folio.circulation.support.Clients; import org.folio.circulation.support.CollectionResourceClient; import org.folio.circulation.support.ServerErrorFailure; @@ -13,11 +16,16 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import static org.folio.circulation.support.results.Result.ofAsync; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; class SettingsRepositoryTest { @@ -58,6 +66,93 @@ void testFetchSettingsWhenSettingsApiThrowError() throws ExecutionException, Int assertFalse(res.isCheckOutLockFeatureEnabled()); } + @Test + @SneakyThrows + void fetchTlrSettings() { + Clients clients = mock(Clients.class); + CollectionResourceClient settingsClient = mock(CollectionResourceClient.class); + CollectionResourceClient configurationClient = mock(CollectionResourceClient.class); + + JsonObject mockSettingsResponse = new JsonObject() + .put("items", new JsonArray() + .add(new JsonObject() + .put("id", UUID.randomUUID().toString()) + .put("scope", "circulation") + .put("key", "generalTlr") + .put("value", new JsonObject() + .put("titleLevelRequestsFeatureEnabled", true) + .put("createTitleLevelRequestsByDefault", true) + .put("tlrHoldShouldFollowCirculationRules", true)))) + .put("resultInfo", new JsonObject() + .put("totalRecords", 0) + .put("diagnostics", new JsonArray())); + + when(clients.settingsStorageClient()).thenReturn(settingsClient); + when(clients.configurationStorageClient()).thenReturn(configurationClient); + when(settingsClient.getMany(any(), any())) + .thenReturn(ofAsync(new Response(200, mockSettingsResponse.encode(), "application/json"))); + + TlrSettingsConfiguration actualResult = new SettingsRepository(clients) + .lookupTlrSettings() + .get(30, TimeUnit.SECONDS) + .value(); + + assertEquals(new TlrSettingsConfiguration(true, true, true, null, null, null), actualResult); + verify(settingsClient).getMany(any(), any()); + verifyNoInteractions(configurationClient); + } + + @Test + @SneakyThrows + void fallBackToLegacyConfigurationWhenTlrSettingsAreNotFound() { + Clients clients = mock(Clients.class); + CollectionResourceClient settingsClient = mock(CollectionResourceClient.class); + CollectionResourceClient configurationClient = mock(CollectionResourceClient.class); + + JsonObject mockEmptySettingsResponse = new JsonObject() + .put("items", new JsonArray()) + .put("resultInfo", new JsonObject() + .put("totalRecords", 0) + .put("diagnostics", new JsonArray())); + + JsonObject mockConfigurationResponse = new JsonObject() + .put("configs", new JsonArray().add( + new JsonObject() + .put("id", UUID.randomUUID().toString()) + .put("module", "SETTINGS") + .put("configName", "TLR") + .put("enabled", true) + .put("value", new JsonObject() + .put("titleLevelRequestsFeatureEnabled", true) + .put("createTitleLevelRequestsByDefault", true) + .put("tlrHoldShouldFollowCirculationRules", true) + .put("confirmationPatronNoticeTemplateId", null) + .put("cancellationPatronNoticeTemplateId", null) + .put("expirationPatronNoticeTemplateId", null) + .encode()))) + .put("totalRecords", 1) + .put("resultInfo", new JsonObject() + .put("totalRecords", 1) + .put("facets", new JsonArray()) + .put("diagnostics", new JsonArray())); + + when(clients.settingsStorageClient()).thenReturn(settingsClient); + when(clients.configurationStorageClient()).thenReturn(configurationClient); + when(settingsClient.getMany(any(), any())) + .thenReturn(ofAsync(new Response(200, mockEmptySettingsResponse.encode(), "application/json"))); + when(configurationClient.getMany(any(), any())) + .thenReturn(ofAsync(new Response(200, mockConfigurationResponse.encode(), "application/json"))); + + TlrSettingsConfiguration actualResult = new SettingsRepository(clients) + .lookupTlrSettings() + .get(30, TimeUnit.SECONDS) + .value(); + + assertEquals(new TlrSettingsConfiguration(true, true, true, null, null, null), actualResult); + verify(settingsClient).getMany(any(), any()); + verify(configurationClient).getMany(any(), any()); + } + private JsonObject createCheckoutLockJsonResponse(boolean checkoutFeatureFlag) { JsonObject checkoutLockResponseJson = new JsonObject(); checkoutLockResponseJson.put("id", UUID.randomUUID())