From 5936db126c5c26b0cafbb496d19b2d863e77ee65 Mon Sep 17 00:00:00 2001 From: Saba-Zedginidze-EPAM <148070844+Saba-Zedginidze-EPAM@users.noreply.github.com> Date: Thu, 31 Oct 2024 17:56:52 +0400 Subject: [PATCH] [MODORDERS-1203] Total expended amount is not displayed when order has no fund distributions (#1044) * [MODORDERS-1203] Extend FiscalYearService to fetch current FY * [MODORDERS-1203] Minor refactor for ConfigurationEntriesCache * [MODORDERS-1203] Fetch and cache system timezone * [MODORDERS-1203] Create caches with CacheUtils * [MODORDERS-1203] Get current fiscal year correctly * [MODORDERS-1203] Update unit tests * [MODORDERS-1203] Fetch current FY when calculating totals * [MODORDERS-1203] Fix dependency version * [MODORDERS-1203] Remove unused import * [MODORDERS-1203] Update permissions and add unit test * [MODORDERS-1203] Avoid NPEs * [MODORDERS-1203] Improve logging * [MODORDERS-1203] Avoid NPEs --- descriptors/ModuleDescriptor-template.json | 3 + pom.xml | 2 +- .../org/folio/config/ApplicationConfig.java | 9 +- .../org/folio/orders/utils/CacheUtils.java | 30 ++++ .../org/folio/orders/utils/HelperUtils.java | 4 + .../orders/utils/ResourcePathResolver.java | 8 +- .../caches/ConfigurationEntriesCache.java | 91 +++++------ .../folio/service/caches/InventoryCache.java | 18 +-- .../caches/JobExecutionTotalRecordsCache.java | 9 +- .../caches/JobProfileSnapshotCache.java | 9 +- .../caches/MappingParametersCache.java | 8 +- .../ConfigurationEntriesService.java | 44 +++--- .../ConsortiumConfigurationService.java | 10 +- .../ConsortiumUserTenantsRetriever.java | 14 +- .../service/finance/FiscalYearService.java | 94 +++++++++++- ...positeOrderTotalFieldsPopulateService.java | 32 ++-- .../organization/OrganizationService.java | 9 +- .../service/settings/SettingsRetriever.java | 9 +- .../caches/ConfigurationEntriesCacheTest.java | 99 ++++++++++++ .../ConfigurationEntriesServiceTest.java | 145 ++++++++++++++++++ .../finance/FiscalYearServiceTest.java | 108 +++++++++++++ ...teOrderTotalFieldsPopulateServiceTest.java | 23 ++- 22 files changed, 614 insertions(+), 164 deletions(-) create mode 100644 src/main/java/org/folio/orders/utils/CacheUtils.java create mode 100644 src/test/java/org/folio/service/caches/ConfigurationEntriesCacheTest.java create mode 100644 src/test/java/org/folio/service/configuration/ConfigurationEntriesServiceTest.java diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 70447a33c..afdcf0ca4 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -66,6 +66,8 @@ "invoice.invoice-lines.collection.get", "finance.transactions.collection.get", "finance.exchange-rate.item.get", + "finance.fiscal-years.item.get", + "finance.fiscal-years.collection.get", "finance.funds.item.get", "finance.funds.collection.get", "finance.ledgers.current-fiscal-year.item.get", @@ -848,6 +850,7 @@ "inventory-storage.instance-statuses.collection.get", "inventory-storage.contributor-name-types.collection.get", "user-tenants.collection.get", + "configuration.entries.collection.get", "consortia.sharing-instances.item.post", "acquisitions-units-storage.units.collection.get", "acquisitions-units-storage.memberships.collection.get" diff --git a/pom.xml b/pom.xml index 3dd68eea4..6db67cc44 100755 --- a/pom.xml +++ b/pom.xml @@ -335,7 +335,7 @@ org.folio mod-di-converter-storage-client - 2.3.0-SNAPSHOT + 2.3.0 org.apache.httpcomponents diff --git a/src/main/java/org/folio/config/ApplicationConfig.java b/src/main/java/org/folio/config/ApplicationConfig.java index aba73568b..65a7ad62a 100644 --- a/src/main/java/org/folio/config/ApplicationConfig.java +++ b/src/main/java/org/folio/config/ApplicationConfig.java @@ -271,8 +271,8 @@ OrganizationService organizationService(RestClient restClient) { } @Bean - FiscalYearService fiscalYearService(RestClient restClient, FundService fundService) { - return new FiscalYearService(restClient, fundService); + FiscalYearService fiscalYearService(RestClient restClient, FundService fundService, ConfigurationEntriesCache configurationEntriesCache) { + return new FiscalYearService(restClient, fundService, configurationEntriesCache); } @Bean @@ -424,8 +424,9 @@ HoldingsSummaryService holdingsSummaryService(PurchaseOrderStorageService purcha } @Bean - CompositeOrderDynamicDataPopulateService totalExpendedPopulateService(TransactionService transactionService, InvoiceService invoiceService, InvoiceLineService invoiceLineService) { - return new CompositeOrderTotalFieldsPopulateService(transactionService, invoiceService, invoiceLineService); + CompositeOrderDynamicDataPopulateService totalExpendedPopulateService(TransactionService transactionService, InvoiceService invoiceService, + InvoiceLineService invoiceLineService, FiscalYearService fiscalYearService) { + return new CompositeOrderTotalFieldsPopulateService(transactionService, invoiceService, invoiceLineService, fiscalYearService); } @Bean("orderLinesSummaryPopulateService") diff --git a/src/main/java/org/folio/orders/utils/CacheUtils.java b/src/main/java/org/folio/orders/utils/CacheUtils.java new file mode 100644 index 000000000..994a0e007 --- /dev/null +++ b/src/main/java/org/folio/orders/utils/CacheUtils.java @@ -0,0 +1,30 @@ +package org.folio.orders.utils; + +import com.github.benmanes.caffeine.cache.AsyncCache; +import com.github.benmanes.caffeine.cache.Caffeine; +import io.vertx.core.Context; +import io.vertx.core.Vertx; + +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + +public class CacheUtils { + + public static AsyncCache buildAsyncCache(Vertx vertx, long cacheExpirationTime) { + return buildAsyncCache(task -> vertx.runOnContext(v -> task.run()), cacheExpirationTime); + } + + public static AsyncCache buildAsyncCache(Context context, long cacheExpirationTime) { + return buildAsyncCache(task -> context.runOnContext(v -> task.run()), cacheExpirationTime); + } + + private static AsyncCache buildAsyncCache(Executor executor, long cacheExpirationTime) { + return Caffeine.newBuilder() + .expireAfterWrite(cacheExpirationTime, TimeUnit.SECONDS) + .executor(executor) + .buildAsync(); + } + + private CacheUtils() {} + +} diff --git a/src/main/java/org/folio/orders/utils/HelperUtils.java b/src/main/java/org/folio/orders/utils/HelperUtils.java index ccb886a0c..aea701b2a 100644 --- a/src/main/java/org/folio/orders/utils/HelperUtils.java +++ b/src/main/java/org/folio/orders/utils/HelperUtils.java @@ -440,4 +440,8 @@ public interface FunctionReturningFuture { Future apply(I item); } + public interface BiFunctionReturningFuture { + Future apply(I1 item1, I2 item2); + } + } diff --git a/src/main/java/org/folio/orders/utils/ResourcePathResolver.java b/src/main/java/org/folio/orders/utils/ResourcePathResolver.java index 376049038..cb03de7b2 100644 --- a/src/main/java/org/folio/orders/utils/ResourcePathResolver.java +++ b/src/main/java/org/folio/orders/utils/ResourcePathResolver.java @@ -5,8 +5,6 @@ import java.util.Map; import java.util.stream.Collectors; -import static org.folio.rest.RestConstants.PATH_PARAM_PLACE_HOLDER; - public class ResourcePathResolver { private ResourcePathResolver() { @@ -49,6 +47,8 @@ private ResourcePathResolver() { public static final String CONFIGURATION_ENTRIES = "configurations.entries"; public static final String LEDGER_FY_ROLLOVERS = "finance.ledger-rollovers"; public static final String LEDGER_FY_ROLLOVER_ERRORS = "finance.ledger-rollovers-errors"; + public static final String LEDGER_CURRENT_FISCAL_YEAR = "finance.ledger.current-fiscal-year"; + public static final String FISCAL_YEARS = "finance.fiscal-years"; public static final String ORDER_INVOICE_RELATIONSHIP = "order-invoice-relationship"; public static final String EXPORT_HISTORY = "export-history"; public static final String TAGS = "tags"; @@ -96,11 +96,13 @@ private ResourcePathResolver() { apis.put(CONFIGURATION_ENTRIES, "/configurations/entries"); apis.put(LEDGER_FY_ROLLOVERS, "/finance/ledger-rollovers"); apis.put(LEDGER_FY_ROLLOVER_ERRORS, "/finance/ledger-rollovers-errors"); + apis.put(LEDGER_CURRENT_FISCAL_YEAR, "/finance/ledgers/{id}/current-fiscal-year"); + apis.put(FISCAL_YEARS, "/finance/fiscal-years"); apis.put(ORDER_INVOICE_RELATIONSHIP, "/orders-storage/order-invoice-relns"); apis.put(EXPORT_HISTORY, "/orders-storage/export-history"); apis.put(TAGS, "/tags"); apis.put(USERS, "/users"); - apis.put(CONSORTIA_USER_TENANTS, "/consortia/" + PATH_PARAM_PLACE_HOLDER + "/user-tenants"); + apis.put(CONSORTIA_USER_TENANTS, "/consortia/{id}/user-tenants"); apis.put(ORDER_SETTINGS, "/orders-storage/settings"); apis.put(ROUTING_LISTS, "/orders-storage/routing-lists"); diff --git a/src/main/java/org/folio/service/caches/ConfigurationEntriesCache.java b/src/main/java/org/folio/service/caches/ConfigurationEntriesCache.java index 256529050..2221548d2 100644 --- a/src/main/java/org/folio/service/caches/ConfigurationEntriesCache.java +++ b/src/main/java/org/folio/service/caches/ConfigurationEntriesCache.java @@ -1,14 +1,13 @@ package org.folio.service.caches; +import static org.folio.orders.utils.CacheUtils.buildAsyncCache; import static org.folio.orders.utils.HelperUtils.SYSTEM_CONFIG_MODULE_NAME; import static org.folio.service.configuration.ConfigurationEntriesService.CONFIG_QUERY; import static org.folio.service.configuration.ConfigurationEntriesService.TENANT_CONFIGURATION_ENTRIES; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import io.vertx.core.Vertx; +import lombok.extern.log4j.Log4j2; +import org.folio.orders.utils.HelperUtils.BiFunctionReturningFuture; import org.folio.processing.mapping.defaultmapper.processor.parameters.MappingParameters; import org.folio.rest.core.models.RequestContext; import org.folio.rest.core.models.RequestEntry; @@ -16,37 +15,35 @@ import org.folio.service.UserService; import org.folio.service.configuration.ConfigurationEntriesService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import com.github.benmanes.caffeine.cache.AsyncCache; -import com.github.benmanes.caffeine.cache.Caffeine; import io.vertx.core.Future; -import io.vertx.core.Vertx; import io.vertx.core.json.JsonObject; +@Log4j2 @Component public class ConfigurationEntriesCache { - private static final Logger log = LogManager.getLogger(); + + private static final String UNIQUE_CACHE_KEY_PATTERN = "%s_%s_%s"; + @Value("${orders.cache.configuration-entries.expiration.time.seconds:30}") + private long cacheExpirationTime; private final AsyncCache configsCache; private final AsyncCache systemCurrencyCache; - private static final String UNIQUE_CACHE_KEY_PATTERN = "%s_%s_%s"; + private final AsyncCache systemTimezoneCache; private final ConfigurationEntriesService configurationEntriesService; + + @Autowired public ConfigurationEntriesCache(ConfigurationEntriesService configurationEntriesService) { this.configurationEntriesService = configurationEntriesService; - configsCache = Caffeine.newBuilder() - .expireAfterWrite(30, TimeUnit.SECONDS) - .executor(task -> Vertx.currentContext() - .runOnContext(v -> task.run())) - .buildAsync(); - - systemCurrencyCache = Caffeine.newBuilder() - .expireAfterWrite(30, TimeUnit.SECONDS) - .executor(task -> Vertx.currentContext() - .runOnContext(v -> task.run())) - .buildAsync(); + var context = Vertx.currentContext(); + configsCache = buildAsyncCache(context, cacheExpirationTime); + systemCurrencyCache = buildAsyncCache(context, cacheExpirationTime); + systemTimezoneCache = buildAsyncCache(context, cacheExpirationTime); } /** @@ -55,25 +52,28 @@ public ConfigurationEntriesCache(ConfigurationEntriesService configurationEntrie * @return CompletableFuture with {@link String} ISBN UUID value */ public Future loadConfiguration(String module, RequestContext requestContext) { - try { - RequestEntry requestEntry = new RequestEntry(TENANT_CONFIGURATION_ENTRIES) - .withQuery(String.format(CONFIG_QUERY, module)) - .withOffset(0) - .withLimit(Integer.MAX_VALUE); + return loadConfigurationData(module, requestContext, configsCache, configurationEntriesService::loadConfiguration); + } - var cacheKey = buildUniqueKey(requestEntry, requestContext); + public Future getSystemCurrency(RequestContext requestContext) { + return loadConfigurationData(SYSTEM_CONFIG_MODULE_NAME, requestContext, systemCurrencyCache, configurationEntriesService::getSystemCurrency); + } - return Future.fromCompletionStage(configsCache.get(cacheKey, (key, executor) -> loadConfiguration(requestEntry, requestContext))); - } catch (Exception e) { - log.error("loadConfiguration:: Error loading tenant configuration from cache, tenantId: '{}'", TenantTool.tenantId(requestContext.getHeaders()), e); - return Future.failedFuture(e); - } + public Future getSystemTimeZone(RequestContext requestContext) { + return loadConfigurationData(SYSTEM_CONFIG_MODULE_NAME, requestContext, systemTimezoneCache, configurationEntriesService::getSystemTimeZone); } - private CompletableFuture loadConfiguration(RequestEntry requestEntry, RequestContext requestContext) { - return configurationEntriesService.loadConfiguration(requestEntry, requestContext) - .toCompletionStage() - .toCompletableFuture(); + private Future loadConfigurationData(String module, RequestContext requestContext, AsyncCache cache, + BiFunctionReturningFuture configExtractor) { + var requestEntry = new RequestEntry(TENANT_CONFIGURATION_ENTRIES) + .withQuery(String.format(CONFIG_QUERY, module)) + .withOffset(0) + .withLimit(Integer.MAX_VALUE); + var cacheKey = buildUniqueKey(requestEntry, requestContext); + return Future.fromCompletionStage(cache.get(cacheKey, (key, executor) -> + configExtractor.apply(requestEntry, requestContext) + .onFailure(t -> log.error("Error loading tenant configuration, tenantId: '{}'", TenantTool.tenantId(requestContext.getHeaders()), t)) + .toCompletionStage().toCompletableFuture())); } private String buildUniqueKey(RequestEntry requestEntry, RequestContext requestContext) { @@ -83,25 +83,4 @@ private String buildUniqueKey(RequestEntry requestEntry, RequestContext requestC return String.format(UNIQUE_CACHE_KEY_PATTERN, tenantId, userId, endpoint); } - public Future getSystemCurrency(RequestContext requestContext) { - - try { - RequestEntry requestEntry = new RequestEntry(TENANT_CONFIGURATION_ENTRIES) - .withQuery(String.format(CONFIG_QUERY, SYSTEM_CONFIG_MODULE_NAME)) - .withOffset(0) - .withLimit(Integer.MAX_VALUE); - - var cacheKey = buildUniqueKey(requestEntry, requestContext); - - return Future.fromCompletionStage(systemCurrencyCache.get(cacheKey, (key, executor) -> getSystemCurrency(requestEntry, requestContext))); - } catch (Exception e) { - log.warn("get:: Error loading system currency from cache, tenantId: '{}'", TenantTool.tenantId(requestContext.getHeaders()), e); - return Future.failedFuture(e); - } - } - private CompletableFuture getSystemCurrency(RequestEntry requestEntry, RequestContext requestContext) { - return configurationEntriesService.getSystemCurrency(requestEntry, requestContext) - .toCompletionStage() - .toCompletableFuture(); - } } diff --git a/src/main/java/org/folio/service/caches/InventoryCache.java b/src/main/java/org/folio/service/caches/InventoryCache.java index c2b01f7fc..1a2a8ec4d 100644 --- a/src/main/java/org/folio/service/caches/InventoryCache.java +++ b/src/main/java/org/folio/service/caches/InventoryCache.java @@ -1,7 +1,6 @@ package org.folio.service.caches; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -14,12 +13,13 @@ import org.springframework.stereotype.Component; import com.github.benmanes.caffeine.cache.AsyncCache; -import com.github.benmanes.caffeine.cache.Caffeine; import io.vertx.core.Future; import io.vertx.core.Vertx; import io.vertx.core.json.JsonObject; +import static org.folio.orders.utils.CacheUtils.buildAsyncCache; + @Component public class InventoryCache { private static final Logger log = LogManager.getLogger(); @@ -37,17 +37,9 @@ public class InventoryCache { public InventoryCache(InventoryService inventoryService) { this.inventoryService = inventoryService; - asyncCache = Caffeine.newBuilder() - .expireAfterWrite(30, TimeUnit.SECONDS) - .executor(task -> Vertx.currentContext() - .runOnContext(v -> task.run())) - .buildAsync(); - - asyncJsonCache = Caffeine.newBuilder() - .expireAfterWrite(30, TimeUnit.SECONDS) - .executor(task -> Vertx.currentContext() - .runOnContext(v -> task.run())) - .buildAsync(); + var context = Vertx.currentContext(); + asyncCache = buildAsyncCache(context, 30); + asyncJsonCache = buildAsyncCache(context, 30); } public Future getISBNProductTypeId(RequestContext requestContext) { diff --git a/src/main/java/org/folio/service/caches/JobExecutionTotalRecordsCache.java b/src/main/java/org/folio/service/caches/JobExecutionTotalRecordsCache.java index b7df91470..1f9938ded 100644 --- a/src/main/java/org/folio/service/caches/JobExecutionTotalRecordsCache.java +++ b/src/main/java/org/folio/service/caches/JobExecutionTotalRecordsCache.java @@ -1,7 +1,6 @@ package org.folio.service.caches; import com.github.benmanes.caffeine.cache.AsyncCache; -import com.github.benmanes.caffeine.cache.Caffeine; import io.vertx.core.Future; import io.vertx.core.Vertx; import io.vertx.core.http.HttpMethod; @@ -15,7 +14,8 @@ import org.springframework.stereotype.Component; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; + +import static org.folio.orders.utils.CacheUtils.buildAsyncCache; /** * An in-memory cache that stores total amount of imported records @@ -38,10 +38,7 @@ public class JobExecutionTotalRecordsCache { @Autowired public JobExecutionTotalRecordsCache(Vertx vertx) { - cache = Caffeine.newBuilder() - .expireAfterAccess(cacheExpirationTime, TimeUnit.SECONDS) - .executor(task -> vertx.runOnContext(v -> task.run())) - .buildAsync(); + cache = buildAsyncCache(vertx, cacheExpirationTime); } /** diff --git a/src/main/java/org/folio/service/caches/JobProfileSnapshotCache.java b/src/main/java/org/folio/service/caches/JobProfileSnapshotCache.java index 8cf52a931..65f58ab35 100644 --- a/src/main/java/org/folio/service/caches/JobProfileSnapshotCache.java +++ b/src/main/java/org/folio/service/caches/JobProfileSnapshotCache.java @@ -2,7 +2,6 @@ import java.util.Optional; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; import org.apache.http.HttpStatus; import org.apache.logging.log4j.LogManager; @@ -15,13 +14,14 @@ import org.springframework.stereotype.Component; import com.github.benmanes.caffeine.cache.AsyncCache; -import com.github.benmanes.caffeine.cache.Caffeine; import io.vertx.core.Future; import io.vertx.core.Vertx; import io.vertx.core.http.HttpMethod; import io.vertx.core.json.Json; +import static org.folio.orders.utils.CacheUtils.buildAsyncCache; + @Component public class JobProfileSnapshotCache { @@ -34,10 +34,7 @@ public class JobProfileSnapshotCache { @Autowired public JobProfileSnapshotCache(Vertx vertx) { - cache = Caffeine.newBuilder() - .expireAfterAccess(cacheExpirationTime, TimeUnit.SECONDS) - .executor(task -> vertx.runOnContext(v -> task.run())) - .buildAsync(); + cache = buildAsyncCache(vertx, cacheExpirationTime); } public Future> get(String profileSnapshotId, OkapiConnectionParams params) { diff --git a/src/main/java/org/folio/service/caches/MappingParametersCache.java b/src/main/java/org/folio/service/caches/MappingParametersCache.java index 9393566db..a28d1c811 100644 --- a/src/main/java/org/folio/service/caches/MappingParametersCache.java +++ b/src/main/java/org/folio/service/caches/MappingParametersCache.java @@ -1,7 +1,6 @@ package org.folio.service.caches; import com.github.benmanes.caffeine.cache.AsyncCache; -import com.github.benmanes.caffeine.cache.Caffeine; import io.vertx.core.Future; import io.vertx.core.Vertx; import io.vertx.core.http.HttpMethod; @@ -48,10 +47,10 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.concurrent.TimeUnit; import java.util.function.Function; import static java.lang.String.format; +import static org.folio.orders.utils.CacheUtils.buildAsyncCache; import static org.folio.orders.utils.QueryUtils.encodeQuery; import static org.folio.rest.RestConstants.ID; import static org.folio.orders.utils.HelperUtils.collectResultsOnSuccess; @@ -93,10 +92,7 @@ public MappingParametersCache(Vertx vertx, RestClient restClient, AcquisitionsUnitsService acquisitionsUnitsService, AcquisitionMethodsService acquisitionMethodsService) { LOGGER.info("MappingParametersCache:: settings limit: '{}'", settingsLimit); - cache = Caffeine.newBuilder() - .expireAfterAccess(cacheExpirationTime, TimeUnit.SECONDS) - .executor(task -> vertx.runOnContext(v -> task.run())) - .buildAsync(); + cache = buildAsyncCache(vertx, cacheExpirationTime); this.restClient = restClient; this.acquisitionsUnitsService = acquisitionsUnitsService; this.acquisitionMethodsService = acquisitionMethodsService; diff --git a/src/main/java/org/folio/service/configuration/ConfigurationEntriesService.java b/src/main/java/org/folio/service/configuration/ConfigurationEntriesService.java index ff64b45ef..9a1abddd2 100644 --- a/src/main/java/org/folio/service/configuration/ConfigurationEntriesService.java +++ b/src/main/java/org/folio/service/configuration/ConfigurationEntriesService.java @@ -1,8 +1,7 @@ package org.folio.service.configuration; +import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.folio.rest.core.RestClient; import org.folio.rest.core.models.RequestContext; import org.folio.rest.core.models.RequestEntry; @@ -11,13 +10,19 @@ import io.vertx.core.Future; import io.vertx.core.json.JsonObject; +@Log4j2 public class ConfigurationEntriesService { - private static final Logger logger = LogManager.getLogger(); public static final String TENANT_CONFIGURATION_ENTRIES = "/configurations/entries"; public static final String CONFIG_QUERY = "module==%s"; public static final String LOCALE_SETTINGS = "localeSettings"; - public static final String CURRENCY_USD = "USD"; + + public static final String CURRENCY_CONFIG = "currency"; + public static final String DEFAULT_CURRENCY = "USD"; + + public static final String TZ_CONFIG = "timezone"; + public static final String TZ_UTC = "UTC"; + private final RestClient restClient; public ConfigurationEntriesService(RestClient restClient) { @@ -27,32 +32,31 @@ public ConfigurationEntriesService(RestClient restClient) { public Future loadConfiguration(RequestEntry requestEntry, RequestContext requestContext) { return restClient.get(requestEntry, Configs.class, requestContext) .map(configs -> { - if (logger.isDebugEnabled()) { - logger.debug("The response from mod-configuration: {}", JsonObject.mapFrom(configs).encodePrettily()); + if (log.isDebugEnabled()) { + log.debug("The response from mod-configuration: {}", JsonObject.mapFrom(configs).encodePrettily()); } - JsonObject config = new JsonObject(); - + var config = new JsonObject(); configs.getConfigs() .forEach(entry -> config.put(entry.getConfigName(), entry.getValue())); return config; }); } - public Future getSystemCurrency(RequestEntry requestEntry, RequestContext requestContext) { + return loadConfiguration(requestEntry, requestContext) + .map(jsonConfig -> extractLocalSettingConfigValueByName(jsonConfig, CURRENCY_CONFIG, DEFAULT_CURRENCY)); + } - + public Future getSystemTimeZone(RequestEntry requestEntry, RequestContext requestContext) { return loadConfiguration(requestEntry, requestContext) - .compose(config -> { - String localeSettings = config.getString(LOCALE_SETTINGS); - String systemCurrency; - if (StringUtils.isEmpty(localeSettings)) { - systemCurrency = CURRENCY_USD; - } else { - systemCurrency = new JsonObject(config.getString(LOCALE_SETTINGS)).getString("currency", "USD"); - } - return Future.succeededFuture(systemCurrency); - }); + .map(jsonConfig -> extractLocalSettingConfigValueByName(jsonConfig, TZ_CONFIG, TZ_UTC)); + } + + private String extractLocalSettingConfigValueByName(JsonObject config, String name, String defaultValue) { + String localeSettings = config.getString(LOCALE_SETTINGS); + return StringUtils.isEmpty(localeSettings) + ? defaultValue + : new JsonObject(localeSettings).getString(name, defaultValue); } } diff --git a/src/main/java/org/folio/service/consortium/ConsortiumConfigurationService.java b/src/main/java/org/folio/service/consortium/ConsortiumConfigurationService.java index 39d5a016a..fe7067218 100644 --- a/src/main/java/org/folio/service/consortium/ConsortiumConfigurationService.java +++ b/src/main/java/org/folio/service/consortium/ConsortiumConfigurationService.java @@ -1,7 +1,6 @@ package org.folio.service.consortium; import com.github.benmanes.caffeine.cache.AsyncCache; -import com.github.benmanes.caffeine.cache.Caffeine; import io.vertx.core.Future; import io.vertx.core.Vertx; import org.apache.commons.lang3.StringUtils; @@ -18,7 +17,8 @@ import java.util.Optional; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; + +import static org.folio.orders.utils.CacheUtils.buildAsyncCache; public class ConsortiumConfigurationService { private static final Logger logger = LogManager.getLogger(ConsortiumConfigurationService.class); @@ -36,11 +36,7 @@ public class ConsortiumConfigurationService { public ConsortiumConfigurationService(RestClient restClient) { this.restClient = restClient; - - asyncCache = Caffeine.newBuilder() - .expireAfterWrite(cacheExpirationTime, TimeUnit.SECONDS) - .executor(task -> Vertx.currentContext().runOnContext(v -> task.run())) - .buildAsync(); + asyncCache = buildAsyncCache(Vertx.currentContext(), cacheExpirationTime); } public Future> getConsortiumConfiguration(RequestContext requestContext) { diff --git a/src/main/java/org/folio/service/consortium/ConsortiumUserTenantsRetriever.java b/src/main/java/org/folio/service/consortium/ConsortiumUserTenantsRetriever.java index 6b73607fa..0880e71c9 100644 --- a/src/main/java/org/folio/service/consortium/ConsortiumUserTenantsRetriever.java +++ b/src/main/java/org/folio/service/consortium/ConsortiumUserTenantsRetriever.java @@ -1,7 +1,6 @@ package org.folio.service.consortium; import com.github.benmanes.caffeine.cache.AsyncCache; -import com.github.benmanes.caffeine.cache.Caffeine; import io.vertx.core.Future; import io.vertx.core.Vertx; import io.vertx.core.json.JsonObject; @@ -14,14 +13,13 @@ import java.util.List; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.stream.IntStream; +import static org.folio.orders.utils.CacheUtils.buildAsyncCache; import static org.folio.orders.utils.RequestContextUtil.getUserIdFromContext; import static org.folio.orders.utils.ResourcePathResolver.CONSORTIA_USER_TENANTS; import static org.folio.orders.utils.ResourcePathResolver.resourcesPath; -import static org.folio.rest.RestConstants.PATH_PARAM_PLACE_HOLDER; import static org.folio.service.pieces.util.UserTenantFields.COLLECTION_USER_TENANTS; import static org.folio.service.pieces.util.UserTenantFields.TENANT_ID; import static org.folio.service.pieces.util.UserTenantFields.USER_ID; @@ -41,10 +39,8 @@ public class ConsortiumUserTenantsRetriever { public ConsortiumUserTenantsRetriever(RestClient restClient) { this.restClient = restClient; - asyncCache = Caffeine.newBuilder() - .expireAfterWrite(cacheExpirationTime, TimeUnit.SECONDS) - .executor(task -> Vertx.currentContext().runOnContext(v -> task.run())) - .buildAsync(); + var context = Vertx.currentContext(); + asyncCache = buildAsyncCache(context, cacheExpirationTime); } public Future> getUserTenants(String consortiumId, RequestContext requestContext) { @@ -59,8 +55,8 @@ public Future> getUserTenants(String consortiumId, RequestContext r } private CompletableFuture> getUserTenantsFromRemote(String userId, String consortiumId, RequestContext requestContext) { - var url = CONSORTIA_USER_TENANTS_ENDPOINT.replace(PATH_PARAM_PLACE_HOLDER, consortiumId); - var requestEntry = new RequestEntry(url) + var requestEntry = new RequestEntry(CONSORTIA_USER_TENANTS_ENDPOINT) + .withId(consortiumId) .withOffset(0) .withLimit(Integer.MAX_VALUE) .withQueryParameter(USER_ID.getValue(), userId); diff --git a/src/main/java/org/folio/service/finance/FiscalYearService.java b/src/main/java/org/folio/service/finance/FiscalYearService.java index 107b49f34..5adb528da 100644 --- a/src/main/java/org/folio/service/finance/FiscalYearService.java +++ b/src/main/java/org/folio/service/finance/FiscalYearService.java @@ -1,13 +1,25 @@ package org.folio.service.finance; +import static org.folio.orders.utils.CacheUtils.buildAsyncCache; +import static org.folio.orders.utils.ResourcePathResolver.FISCAL_YEARS; +import static org.folio.orders.utils.ResourcePathResolver.LEDGER_CURRENT_FISCAL_YEAR; +import static org.folio.orders.utils.ResourcePathResolver.resourceByIdPath; +import static org.folio.orders.utils.ResourcePathResolver.resourcesPath; import static org.folio.rest.core.exceptions.ErrorCodes.CURRENT_FISCAL_YEAR_NOT_FOUND; +import java.time.Instant; +import java.time.ZoneId; import java.util.Collections; +import java.util.Date; import java.util.List; import java.util.Objects; import java.util.concurrent.CompletionException; +import com.github.benmanes.caffeine.cache.AsyncCache; +import io.vertx.core.Vertx; +import lombok.extern.log4j.Log4j2; import org.folio.rest.acq.model.finance.FiscalYear; +import org.folio.rest.acq.model.finance.FiscalYearCollection; import org.folio.rest.core.RestClient; import org.folio.rest.core.exceptions.HttpException; import org.folio.rest.core.models.RequestContext; @@ -15,22 +27,40 @@ import org.folio.rest.jaxrs.model.Parameter; import io.vertx.core.Future; +import org.folio.rest.tools.utils.TenantTool; +import org.folio.service.caches.ConfigurationEntriesCache; +import org.springframework.beans.factory.annotation.Value; +@Log4j2 public class FiscalYearService { - private static final String CURRENT_FISCAL_YEAR = "/finance/ledgers/{id}/current-fiscal-year"; + private static final String LEDGER_CURRENT_FISCAL_YEAR_ENDPOINT = resourcesPath(LEDGER_CURRENT_FISCAL_YEAR); + private static final String FISCAL_YEARS_ENDPOINT = resourcesPath(FISCAL_YEARS); + private static final String FISCAL_YEAR_BY_ID_ENDPOINT = resourceByIdPath(FISCAL_YEARS, "{id}"); + private static final String FISCAL_YEAR_BY_SERIES_QUERY = "series==\"%s\" AND periodEnd>=%s sortBy periodStart"; + private static final String CACHE_KEY_TEMPLATE = "%s.%s"; + + @Value("${orders.cache.fiscal-years.expiration.time.seconds:300}") + private long cacheExpirationTime; + private final AsyncCache currentFiscalYearCacheBySeries; + private final AsyncCache seriesCacheByFiscalYearId; private final RestClient restClient; private final FundService fundService; + private final ConfigurationEntriesCache configurationEntriesCache; - public FiscalYearService(RestClient restClient, FundService fundService) { + public FiscalYearService(RestClient restClient, FundService fundService, ConfigurationEntriesCache configurationEntriesCache) { this.restClient = restClient; this.fundService = fundService; + this.configurationEntriesCache = configurationEntriesCache; + var context = Vertx.currentContext(); + currentFiscalYearCacheBySeries = buildAsyncCache(context, cacheExpirationTime); + seriesCacheByFiscalYearId = buildAsyncCache(context, cacheExpirationTime); } public Future getCurrentFiscalYear(String ledgerId, RequestContext requestContext) { - RequestEntry requestEntry = new RequestEntry(CURRENT_FISCAL_YEAR).withId(ledgerId); + var requestEntry = new RequestEntry(LEDGER_CURRENT_FISCAL_YEAR_ENDPOINT).withId(ledgerId); return restClient.get(requestEntry, FiscalYear.class, requestContext) .recover(t -> { Throwable cause = Objects.nonNull(t.getCause()) ? t.getCause() : t; @@ -49,7 +79,65 @@ public Future getCurrentFiscalYearByFundId(String fundId, RequestCon .compose(fund -> getCurrentFiscalYear(fund.getLedgerId(), requestContext)); } + /** + * Retrieves the current fiscal year ID for the series related to the fiscal year corresponding to the given ID. + * + * @param fiscalYearId fiscal year ID + * @param requestContext {@link RequestContext} to be used for the request + * @return future with the current fiscal year ID + */ + public Future getCurrentFYForSeriesByFYId(String fiscalYearId, RequestContext requestContext) { + return getSeriesByFiscalYearId(fiscalYearId, requestContext) + .compose(series -> getCurrentFiscalYearForSeries(series, requestContext)); + } + + private Future getSeriesByFiscalYearId(String fiscalYearId, RequestContext requestContext) { + var cacheKey = CACHE_KEY_TEMPLATE.formatted(fiscalYearId, TenantTool.tenantId(requestContext.getHeaders())); + return Future.fromCompletionStage(seriesCacheByFiscalYearId.get(cacheKey, (key, executor) -> + getFiscalYearById(fiscalYearId, requestContext) + .map(FiscalYear::getSeries) + .toCompletionStage().toCompletableFuture())); + } + + private Future getFiscalYearById(String fiscalYearId, RequestContext requestContext) { + var requestEntry = new RequestEntry(FISCAL_YEAR_BY_ID_ENDPOINT).withId(fiscalYearId); + return restClient.get(requestEntry, FiscalYear.class, requestContext) + .onFailure(t -> log.error("Unable to fetch fiscal year by id: {}", fiscalYearId, t)); + } + + private Future getCurrentFiscalYearForSeries(String series, RequestContext requestContext) { + var cacheKey = CACHE_KEY_TEMPLATE.formatted(series, TenantTool.tenantId(requestContext.getHeaders())); + return Future.fromCompletionStage(currentFiscalYearCacheBySeries.get(cacheKey, (key, executor) -> + configurationEntriesCache.getSystemTimeZone(requestContext) + .map(timezone -> Instant.now().atZone(ZoneId.of(timezone)).toLocalDate()) + .map(now -> FISCAL_YEAR_BY_SERIES_QUERY.formatted(series, now)) + .map(query -> new RequestEntry(FISCAL_YEARS_ENDPOINT).withQuery(query).withLimit(3).withOffset(0)) + .compose(requestEntry -> restClient.get(requestEntry, FiscalYearCollection.class, requestContext)) + .map(fiscalYearCollection -> extractCurrentFiscalYearId(fiscalYearCollection.getFiscalYears())) + .onFailure(t -> log.error("Unable to fetch current fiscal year for series: {}", series, t)) + .toCompletionStage().toCompletableFuture())); + } + + private String extractCurrentFiscalYearId(List fiscalYears) { + if (fiscalYears.isEmpty()) { + return null; + } + if (fiscalYears.size() == 1) { + return fiscalYears.get(0).getId(); + } + var now = new Date(); + var firstYear = fiscalYears.get(0); + var secondYear = fiscalYears.get(1); + if (firstYear.getPeriodStart().before(now) && firstYear.getPeriodEnd().after(now) + && secondYear.getPeriodStart().before(now) && secondYear.getPeriodEnd().after(now) + && firstYear.getPeriodEnd().after(secondYear.getPeriodStart())) { + return secondYear.getId(); + } + return firstYear.getId(); + } + private boolean isFiscalYearNotFound(Throwable t) { return t instanceof HttpException && ((HttpException) t).getCode() == 404; } + } diff --git a/src/main/java/org/folio/service/orders/CompositeOrderTotalFieldsPopulateService.java b/src/main/java/org/folio/service/orders/CompositeOrderTotalFieldsPopulateService.java index 2c0a3e845..69184b39e 100644 --- a/src/main/java/org/folio/service/orders/CompositeOrderTotalFieldsPopulateService.java +++ b/src/main/java/org/folio/service/orders/CompositeOrderTotalFieldsPopulateService.java @@ -2,18 +2,20 @@ import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import lombok.extern.log4j.Log4j2; import one.util.streamex.StreamEx; -import org.apache.commons.lang3.StringUtils; import org.folio.models.CompositeOrderRetrieveHolder; import org.folio.rest.acq.model.finance.FiscalYear; import org.folio.rest.acq.model.finance.Transaction; import org.folio.rest.acq.model.invoice.Invoice; import org.folio.rest.acq.model.invoice.InvoiceLine; import org.folio.rest.core.models.RequestContext; +import org.folio.service.finance.FiscalYearService; import org.folio.service.finance.transaction.TransactionService; import org.folio.service.invoice.InvoiceLineService; import org.folio.service.invoice.InvoiceService; @@ -30,20 +32,18 @@ public class CompositeOrderTotalFieldsPopulateService implements CompositeOrderD private final TransactionService transactionService; private final InvoiceService invoiceService; private final InvoiceLineService invoiceLineService; + private final FiscalYearService fiscalYearService; - public CompositeOrderTotalFieldsPopulateService(TransactionService transactionService, - InvoiceService invoiceService, - InvoiceLineService invoiceLineService) { + public CompositeOrderTotalFieldsPopulateService(TransactionService transactionService, InvoiceService invoiceService, + InvoiceLineService invoiceLineService, FiscalYearService fiscalYearService) { this.transactionService = transactionService; this.invoiceService = invoiceService; this.invoiceLineService = invoiceLineService; + this.fiscalYearService = fiscalYearService; } @Override public Future populate(CompositeOrderRetrieveHolder holder, RequestContext requestContext) { - if (holder.getFiscalYear() == null) { - return Future.succeededFuture(holder.withTotalExpended(0d).withTotalCredited(0d).withTotalEncumbered(0d)); - } // We need to fetch invoices, invoice lines and transactions in order to calculate total fields // totalEncumbered = sum of transactions amounts // totalExpended = sum of invoice lines positive total values @@ -51,7 +51,8 @@ public Future populate(CompositeOrderRetrieveHolde var query = "transactionType==Encumbrance AND encumbrance.sourcePurchaseOrderId==%s AND fiscalYearId==%s" .formatted(holder.getOrderId(), holder.getFiscalYearId()); return invoiceService.getInvoicesByOrderId(holder.getOrderId(), requestContext) - .compose(invoices -> getInvoiceLinesByInvoiceIds(invoices, holder.getFiscalYear(), requestContext) + .compose(invoices -> getCurrentFiscalYearIds(invoices, holder.getFiscalYear(), requestContext) + .compose(fiscalYears -> getInvoiceLinesByInvoiceIds(invoices, fiscalYears, requestContext)) .map(invoiceLines -> groupInvoiceLinesByInvoices(invoices, invoiceLines)) .compose(invoiceToInvoiceLinesMap -> transactionService.getTransactions(query, requestContext) .map(transactions -> populateTotalFields(holder, invoiceToInvoiceLinesMap, transactions)))) @@ -59,9 +60,20 @@ public Future populate(CompositeOrderRetrieveHolde holder.getOrderId(), holder.getFiscalYearId(), t)); } - private Future> getInvoiceLinesByInvoiceIds(List invoices, FiscalYear fiscalYear, RequestContext requestContext) { + private Future> getCurrentFiscalYearIds(List invoices, FiscalYear fiscalYear, RequestContext requestContext) { + if (fiscalYear != null && fiscalYear.getId() != null) { + return Future.succeededFuture(Set.of(fiscalYear.getId())); + } + return collectResultsOnSuccess(invoices.stream() + .map(Invoice::getFiscalYearId) + .filter(Objects::nonNull) + .map(fiscalYearId -> fiscalYearService.getCurrentFYForSeriesByFYId(fiscalYearId, requestContext)).toList()) + .map(fiscalYearsIds -> fiscalYearsIds.stream().filter(Objects::nonNull).collect(Collectors.toSet())); + } + + private Future> getInvoiceLinesByInvoiceIds(List invoices, Set fiscalYearIds, RequestContext requestContext) { return collectResultsOnSuccess(invoices.stream() - .filter(invoice -> StringUtils.equals(invoice.getFiscalYearId(), fiscalYear.getId())) + .filter(invoice -> invoice.getFiscalYearId() != null && fiscalYearIds.contains(invoice.getFiscalYearId())) .map(invoice -> invoiceLineService.getInvoiceLinesByInvoiceId(invoice.getId(), requestContext)).toList()) .map(invoiceLinesLists -> invoiceLinesLists.stream().flatMap(List::stream).toList()); } diff --git a/src/main/java/org/folio/service/organization/OrganizationService.java b/src/main/java/org/folio/service/organization/OrganizationService.java index bb796e299..a82c0bf9f 100644 --- a/src/main/java/org/folio/service/organization/OrganizationService.java +++ b/src/main/java/org/folio/service/organization/OrganizationService.java @@ -1,6 +1,7 @@ package org.folio.service.organization; import static java.util.stream.Collectors.toList; +import static org.folio.orders.utils.CacheUtils.buildAsyncCache; import static org.folio.orders.utils.QueryUtils.convertIdsToCqlQuery; import static org.folio.orders.utils.QueryUtils.encodeQuery; import static org.folio.rest.RestConstants.ERROR_CAUSE; @@ -15,7 +16,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; @@ -37,7 +37,6 @@ import org.springframework.stereotype.Component; import com.github.benmanes.caffeine.cache.AsyncCache; -import com.github.benmanes.caffeine.cache.Caffeine; import io.vertx.core.Future; import io.vertx.core.Promise; @@ -56,11 +55,7 @@ public class OrganizationService { private final Errors processingErrors = new Errors(); public OrganizationService(RestClient restClient) { - asyncCache = Caffeine.newBuilder() - .expireAfterWrite(30, TimeUnit.SECONDS) - .executor(task -> Vertx.currentContext() - .runOnContext(v -> task.run())) - .buildAsync(); + asyncCache = buildAsyncCache(Vertx.currentContext(), 30); this.restClient = restClient; } diff --git a/src/main/java/org/folio/service/settings/SettingsRetriever.java b/src/main/java/org/folio/service/settings/SettingsRetriever.java index bb60383dc..26b9d3567 100644 --- a/src/main/java/org/folio/service/settings/SettingsRetriever.java +++ b/src/main/java/org/folio/service/settings/SettingsRetriever.java @@ -1,7 +1,6 @@ package org.folio.service.settings; import com.github.benmanes.caffeine.cache.AsyncCache; -import com.github.benmanes.caffeine.cache.Caffeine; import io.vertx.core.Future; import io.vertx.core.Vertx; import lombok.extern.log4j.Log4j2; @@ -17,8 +16,8 @@ import java.util.Optional; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; +import static org.folio.orders.utils.CacheUtils.buildAsyncCache; import static org.folio.orders.utils.ResourcePathResolver.ORDER_SETTINGS; import static org.folio.orders.utils.ResourcePathResolver.resourcesPath; @@ -37,11 +36,7 @@ public class SettingsRetriever { public SettingsRetriever(RestClient restClient) { this.restClient = restClient; - - asyncCache = Caffeine.newBuilder() - .expireAfterWrite(cacheExpirationTime, TimeUnit.SECONDS) - .executor(task -> Vertx.currentContext().runOnContext(v -> task.run())) - .buildAsync(); + asyncCache = buildAsyncCache(Vertx.currentContext(), cacheExpirationTime); } public Future> getSettingByKey(SettingKey settingKey, RequestContext requestContext) { diff --git a/src/test/java/org/folio/service/caches/ConfigurationEntriesCacheTest.java b/src/test/java/org/folio/service/caches/ConfigurationEntriesCacheTest.java new file mode 100644 index 000000000..96879fb25 --- /dev/null +++ b/src/test/java/org/folio/service/caches/ConfigurationEntriesCacheTest.java @@ -0,0 +1,99 @@ +package org.folio.service.caches; + +import io.vertx.core.Future; +import io.vertx.core.json.JsonObject; +import org.folio.CopilotGenerated; +import org.folio.rest.core.models.RequestContext; +import org.folio.rest.core.models.RequestEntry; +import org.folio.service.configuration.ConfigurationEntriesService; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; + +@CopilotGenerated +@ExtendWith(MockitoExtension.class) +public class ConfigurationEntriesCacheTest { + + @Mock + private RequestContext requestContextMock; + @Mock + private ConfigurationEntriesService configurationEntriesServiceMock; + @InjectMocks + private ConfigurationEntriesCache configurationEntriesCache; + + @Test + void shouldLoadConfigurationFromCache() { + String module = "testModule"; + JsonObject config = new JsonObject().put("key", "value"); + doReturn(Future.succeededFuture(config)) + .when(configurationEntriesServiceMock).loadConfiguration(any(RequestEntry.class), any(RequestContext.class)); + + Future result = configurationEntriesCache.loadConfiguration(module, requestContextMock); + + assertEquals(config, result.result()); + } + + @Test + void shouldReturnSystemCurrencyFromCache() { + String currency = "USD"; + doReturn(Future.succeededFuture(currency)) + .when(configurationEntriesServiceMock).getSystemCurrency(any(RequestEntry.class), any(RequestContext.class)); + + Future result = configurationEntriesCache.getSystemCurrency(requestContextMock); + + assertEquals(currency, result.result()); + } + + @Test + void shouldReturnSystemTimeZoneFromCache() { + String timeZone = "UTC"; + doReturn(Future.succeededFuture(timeZone)) + .when(configurationEntriesServiceMock).getSystemTimeZone(any(RequestEntry.class), any(RequestContext.class)); + + Future result = configurationEntriesCache.getSystemTimeZone(requestContextMock); + + assertEquals(timeZone, result.result()); + } + + @Test + void shouldFailToLoadConfigurationWhenServiceFails() { + String module = "testModule"; + doReturn(Future.failedFuture(new RuntimeException("Service failure"))) + .when(configurationEntriesServiceMock).loadConfiguration(any(RequestEntry.class), any(RequestContext.class)); + + Future result = configurationEntriesCache.loadConfiguration(module, requestContextMock); + + assertTrue(result.failed()); + assertEquals(RuntimeException.class, result.cause().getClass()); + } + + @Test + void shouldFailToReturnSystemCurrencyWhenServiceFails() { + doReturn(Future.failedFuture(new RuntimeException("Service failure"))) + .when(configurationEntriesServiceMock).getSystemCurrency(any(RequestEntry.class), any(RequestContext.class)); + + Future result = configurationEntriesCache.getSystemCurrency(requestContextMock); + + assertTrue(result.failed()); + assertEquals(RuntimeException.class, result.cause().getClass()); + } + + @Test + void shouldFailToReturnSystemTimeZoneWhenServiceFails() { + doReturn(Future.failedFuture(new RuntimeException("Service failure"))) + .when(configurationEntriesServiceMock).getSystemTimeZone(any(RequestEntry.class), any(RequestContext.class)); + + Future result = configurationEntriesCache.getSystemTimeZone(requestContextMock); + + assertTrue(result.failed()); + assertEquals(RuntimeException.class, result.cause().getClass()); + } + +} diff --git a/src/test/java/org/folio/service/configuration/ConfigurationEntriesServiceTest.java b/src/test/java/org/folio/service/configuration/ConfigurationEntriesServiceTest.java new file mode 100644 index 000000000..d80923cd2 --- /dev/null +++ b/src/test/java/org/folio/service/configuration/ConfigurationEntriesServiceTest.java @@ -0,0 +1,145 @@ +package org.folio.service.configuration; + +import io.vertx.core.Future; +import io.vertx.core.json.JsonObject; +import org.folio.CopilotGenerated; +import org.folio.rest.core.RestClient; +import org.folio.rest.core.models.RequestContext; +import org.folio.rest.core.models.RequestEntry; +import org.folio.rest.jaxrs.model.Config; +import org.folio.rest.jaxrs.model.Configs; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; + +@CopilotGenerated +@ExtendWith(MockitoExtension.class) +public class ConfigurationEntriesServiceTest { + + @Mock + private RequestContext requestContextMock; + @Mock + private RestClient restClientMock; + @InjectMocks + private ConfigurationEntriesService configurationEntriesService; + + @Test + void shouldLoadConfigurationSuccessfully() { + RequestEntry requestEntry = new RequestEntry("/configurations/entries"); + JsonObject expectedConfig = new JsonObject().put("key", "value"); + Configs configs = new Configs().withConfigs(List.of(new Config().withConfigName("key").withValue("value"))); + + doReturn(Future.succeededFuture(configs)) + .when(restClientMock).get(eq(requestEntry), eq(Configs.class), any(RequestContext.class)); + + Future result = configurationEntriesService.loadConfiguration(requestEntry, requestContextMock); + + assertEquals(expectedConfig, result.result()); + } + + @Test + void shouldReturnDefaultCurrencyWhenLocaleSettingsNotPresent() { + RequestEntry requestEntry = new RequestEntry("/configurations/entries"); + JsonObject config = new JsonObject(); + Configs configs = new Configs().withConfigs(List.of()); + + doReturn(Future.succeededFuture(configs)) + .when(restClientMock).get(eq(requestEntry), eq(Configs.class), any(RequestContext.class)); + + Future result = configurationEntriesService.getSystemCurrency(requestEntry, requestContextMock); + + assertEquals("USD", result.result()); + } + + @Test + void shouldReturnConfiguredCurrency() { + RequestEntry requestEntry = new RequestEntry("/configurations/entries"); + JsonObject config = new JsonObject().put("localeSettings", new JsonObject().put("currency", "EUR").encode()); + Configs configs = new Configs().withConfigs(List.of(new Config().withConfigName("localeSettings").withValue(config.getString("localeSettings")))); + + doReturn(Future.succeededFuture(configs)) + .when(restClientMock).get(eq(requestEntry), eq(Configs.class), any(RequestContext.class)); + + Future result = configurationEntriesService.getSystemCurrency(requestEntry, requestContextMock); + + assertEquals("EUR", result.result()); + } + + @Test + void shouldReturnDefaultTimeZoneWhenLocaleSettingsNotPresent() { + RequestEntry requestEntry = new RequestEntry("/configurations/entries"); + JsonObject config = new JsonObject(); + Configs configs = new Configs().withConfigs(List.of()); + + doReturn(Future.succeededFuture(configs)) + .when(restClientMock).get(eq(requestEntry), eq(Configs.class), any(RequestContext.class)); + + Future result = configurationEntriesService.getSystemTimeZone(requestEntry, requestContextMock); + + assertEquals("UTC", result.result()); + } + + @Test + void shouldReturnConfiguredTimeZone() { + RequestEntry requestEntry = new RequestEntry("/configurations/entries"); + JsonObject config = new JsonObject().put("localeSettings", new JsonObject().put("timezone", "PST").encode()); + Configs configs = new Configs().withConfigs(List.of(new Config().withConfigName("localeSettings").withValue(config.getString("localeSettings")))); + + doReturn(Future.succeededFuture(configs)) + .when(restClientMock).get(eq(requestEntry), eq(Configs.class), any(RequestContext.class)); + + Future result = configurationEntriesService.getSystemTimeZone(requestEntry, requestContextMock); + + assertEquals("PST", result.result()); + } + + @Test + void shouldFailToLoadConfigurationWhenRestClientFails() { + RequestEntry requestEntry = new RequestEntry("/configurations/entries"); + + doReturn(Future.failedFuture(new RuntimeException("Service failure"))) + .when(restClientMock).get(eq(requestEntry), eq(Configs.class), any(RequestContext.class)); + + Future result = configurationEntriesService.loadConfiguration(requestEntry, requestContextMock); + + assertTrue(result.failed()); + assertEquals(RuntimeException.class, result.cause().getClass()); + } + + @Test + void shouldFailToReturnSystemCurrencyWhenRestClientFails() { + RequestEntry requestEntry = new RequestEntry("/configurations/entries"); + + doReturn(Future.failedFuture(new RuntimeException("Service failure"))) + .when(restClientMock).get(eq(requestEntry), eq(Configs.class), any(RequestContext.class)); + + Future result = configurationEntriesService.getSystemCurrency(requestEntry, requestContextMock); + + assertTrue(result.failed()); + assertEquals(RuntimeException.class, result.cause().getClass()); + } + + @Test + void shouldFailToReturnSystemTimeZoneWhenRestClientFails() { + RequestEntry requestEntry = new RequestEntry("/configurations/entries"); + + doReturn(Future.failedFuture(new RuntimeException("Service failure"))) + .when(restClientMock).get(eq(requestEntry), eq(Configs.class), any(RequestContext.class)); + + Future result = configurationEntriesService.getSystemTimeZone(requestEntry, requestContextMock); + + assertTrue(result.failed()); + assertEquals(RuntimeException.class, result.cause().getClass()); + } + +} diff --git a/src/test/java/org/folio/service/finance/FiscalYearServiceTest.java b/src/test/java/org/folio/service/finance/FiscalYearServiceTest.java index f18b0ec21..5cc6c6a42 100644 --- a/src/test/java/org/folio/service/finance/FiscalYearServiceTest.java +++ b/src/test/java/org/folio/service/finance/FiscalYearServiceTest.java @@ -5,14 +5,22 @@ import static org.folio.TestConstants.ID_DOES_NOT_EXIST; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.verify; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.List; import java.util.UUID; import io.vertx.junit5.VertxTestContext; +import org.folio.CopilotGenerated; +import org.folio.rest.acq.model.finance.FiscalYearCollection; import org.folio.rest.core.models.RequestEntry; import org.folio.rest.core.RestClient; import io.vertx.junit5.VertxExtension; @@ -20,6 +28,7 @@ import org.folio.rest.core.exceptions.HttpException; import org.folio.rest.core.models.RequestContext; +import org.folio.service.caches.ConfigurationEntriesCache; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; @@ -30,6 +39,7 @@ import io.vertx.core.Future; import org.mockito.Spy; +@CopilotGenerated(partiallyGenerated = true) @ExtendWith(VertxExtension.class) public class FiscalYearServiceTest { @@ -39,10 +49,14 @@ public class FiscalYearServiceTest { private RestClient restClientMock; @Mock private RequestContext requestContextMock; + @Mock + private ConfigurationEntriesCache configurationEntriesCacheMock; @BeforeEach public void initMocks() { MockitoAnnotations.openMocks(this); + doReturn(Future.succeededFuture("UTC")) + .when(configurationEntriesCacheMock).getSystemTimeZone(any(RequestContext.class)); } @Test @@ -75,4 +89,98 @@ void testShouldThrowHttpException(VertxTestContext vertxTestContext) { vertxTestContext.completeNow(); }); } + + @Test + void shouldReturnCurrentFiscalYearIdForSeries() { + String fiscalYearId = UUID.randomUUID().toString(); + String series = "FY2023"; + String currentFiscalYearId = UUID.randomUUID().toString(); + FiscalYear fiscalYear = new FiscalYear().withId(fiscalYearId).withSeries(series) + .withPeriodStart(Date.from(Instant.now().minus(265, ChronoUnit.DAYS))) + .withPeriodEnd(Date.from(Instant.now().plus(100, ChronoUnit.DAYS))); + FiscalYear currentFiscalYear = new FiscalYear().withId(currentFiscalYearId).withSeries(series) + .withPeriodStart(Date.from(Instant.now().minus(165, ChronoUnit.DAYS))) + .withPeriodEnd(Date.from(Instant.now().plus(200, ChronoUnit.DAYS))); + FiscalYearCollection fiscalYearCollection = new FiscalYearCollection().withFiscalYears(List.of(fiscalYear, currentFiscalYear)); + + doReturn(Future.succeededFuture(fiscalYear)) + .when(restClientMock).get(any(RequestEntry.class), eq(FiscalYear.class), any(RequestContext.class)); + doReturn(Future.succeededFuture(fiscalYearCollection)) + .when(restClientMock).get(any(RequestEntry.class), eq(FiscalYearCollection.class), any(RequestContext.class)); + + Future result = fiscalYearService.getCurrentFYForSeriesByFYId(fiscalYearId, requestContextMock); + + assertEquals(currentFiscalYearId, result.result()); + } + + @Test + void shouldReturnCurrentFiscalYearIdForSeriesIfOnlyOneFiscalYearIsFound() { + String fiscalYearId = UUID.randomUUID().toString(); + String series = "FY2023"; + FiscalYear fiscalYear = new FiscalYear().withId(fiscalYearId).withSeries(series) + .withPeriodStart(Date.from(Instant.now().minus(265, ChronoUnit.DAYS))) + .withPeriodEnd(Date.from(Instant.now().plus(100, ChronoUnit.DAYS))); + FiscalYearCollection fiscalYearCollection = new FiscalYearCollection().withFiscalYears(List.of(fiscalYear)); + + doReturn(Future.succeededFuture(fiscalYear)) + .when(restClientMock).get(any(RequestEntry.class), eq(FiscalYear.class), any(RequestContext.class)); + doReturn(Future.succeededFuture(fiscalYearCollection)) + .when(restClientMock).get(any(RequestEntry.class), eq(FiscalYearCollection.class), any(RequestContext.class)); + + Future result = fiscalYearService.getCurrentFYForSeriesByFYId(fiscalYearId, requestContextMock); + + assertEquals(fiscalYearId, result.result()); + } + + @Test + void shouldReturnNullFiscalYearIdForSeriesIfNoFiscalYearIsFound() { + String fiscalYearId = UUID.randomUUID().toString(); + String series = "FY2023"; + FiscalYear fiscalYear = new FiscalYear().withId(fiscalYearId).withSeries(series) + .withPeriodStart(Date.from(Instant.now().minus(265, ChronoUnit.DAYS))) + .withPeriodEnd(Date.from(Instant.now().plus(100, ChronoUnit.DAYS))); + FiscalYearCollection fiscalYearCollection = new FiscalYearCollection(); + + doReturn(Future.succeededFuture(fiscalYear)) + .when(restClientMock).get(any(RequestEntry.class), eq(FiscalYear.class), any(RequestContext.class)); + doReturn(Future.succeededFuture(fiscalYearCollection)) + .when(restClientMock).get(any(RequestEntry.class), eq(FiscalYearCollection.class), any(RequestContext.class)); + + Future result = fiscalYearService.getCurrentFYForSeriesByFYId(fiscalYearId, requestContextMock); + + assertNull(result.result()); + } + + @Test + void shouldThrowExceptionWhenFiscalYearNotFound() { + String fiscalYearId = UUID.randomUUID().toString(); + + doReturn(Future.failedFuture(new HttpException(404, "Fiscal year not found"))) + .when(restClientMock).get(any(RequestEntry.class), eq(FiscalYear.class), any(RequestContext.class)); + + Future result = fiscalYearService.getCurrentFYForSeriesByFYId(fiscalYearId, requestContextMock); + + assertTrue(result.failed()); + assertEquals(HttpException.class, result.cause().getClass()); + assertEquals(404, ((HttpException) result.cause()).getCode()); + } + + @Test + void shouldThrowExceptionWhenCurrentFiscalYearNotFoundForSeries() { + String fiscalYearId = UUID.randomUUID().toString(); + String series = "FY2023"; + FiscalYear fiscalYear = new FiscalYear().withId(fiscalYearId).withSeries(series); + + doReturn(Future.succeededFuture(fiscalYear)) + .when(restClientMock).get(any(RequestEntry.class), eq(FiscalYear.class), any(RequestContext.class)); + doReturn(Future.failedFuture(new HttpException(404, "Fiscal year not found"))) + .when(restClientMock).get(any(RequestEntry.class), eq(FiscalYearCollection.class), any(RequestContext.class)); + + Future result = fiscalYearService.getCurrentFYForSeriesByFYId(fiscalYearId, requestContextMock); + + assertTrue(result.failed()); + assertEquals(HttpException.class, result.cause().getClass()); + assertEquals(404, ((HttpException) result.cause()).getCode()); + } + } diff --git a/src/test/java/org/folio/service/orders/CompositeOrderTotalFieldsPopulateServiceTest.java b/src/test/java/org/folio/service/orders/CompositeOrderTotalFieldsPopulateServiceTest.java index 53da604d0..27815e90d 100644 --- a/src/test/java/org/folio/service/orders/CompositeOrderTotalFieldsPopulateServiceTest.java +++ b/src/test/java/org/folio/service/orders/CompositeOrderTotalFieldsPopulateServiceTest.java @@ -18,6 +18,7 @@ import org.folio.rest.acq.model.invoice.InvoiceLine; import org.folio.rest.core.models.RequestContext; import org.folio.rest.jaxrs.model.CompositePurchaseOrder; +import org.folio.service.finance.FiscalYearService; import org.folio.service.finance.transaction.TransactionService; import org.folio.service.invoice.InvoiceLineService; import org.folio.service.invoice.InvoiceService; @@ -37,12 +38,12 @@ public class CompositeOrderTotalFieldsPopulateServiceTest { @Mock private TransactionService transactionService; - @Mock private InvoiceService invoiceService; - @Mock private InvoiceLineService invoiceLineService; + @Mock + private FiscalYearService fiscalYearService; @Mock private RequestContext requestContext; @@ -117,16 +118,26 @@ void shouldReturnZeroWhenNoInvoiceLinesAndTransactionsExist() { } @Test - void shouldHandleNullFiscalYearAndTransactions() { + void shouldHandleNullFiscalYearWithInvoicesInvoiceLinesAndNoTransactions() { + String fiscalYearId = UUID.randomUUID().toString(); + Invoice invoice1 = new Invoice().withId(UUID.randomUUID().toString()).withCurrency("USD").withFiscalYearId(fiscalYearId); + Invoice invoice2 = new Invoice().withId(UUID.randomUUID().toString()).withCurrency("USD").withFiscalYearId(fiscalYearId); + InvoiceLine invoiceLine1 = new InvoiceLine().withInvoiceId(invoice1.getId()).withTotal(100.0); + InvoiceLine invoiceLine2 = new InvoiceLine().withInvoiceId(invoice2.getId()).withTotal(-500.0); + List invoices = List.of(invoice1, invoice2); CompositePurchaseOrder order = new CompositePurchaseOrder().withId(UUID.randomUUID().toString()); CompositeOrderRetrieveHolder holder = new CompositeOrderRetrieveHolder(order); - when(transactionService.getTransactions(anyString(), any())).thenReturn(Future.succeededFuture(Collections.emptyList())); + when(invoiceService.getInvoicesByOrderId(anyString(), any())).thenReturn(Future.succeededFuture(invoices)); + when(invoiceLineService.getInvoiceLinesByInvoiceId(eq(invoice1.getId()), any())).thenReturn(Future.succeededFuture(List.of(invoiceLine1))); + when(invoiceLineService.getInvoiceLinesByInvoiceId(eq(invoice2.getId()), any())).thenReturn(Future.succeededFuture(List.of(invoiceLine2))); + when(transactionService.getTransactions(anyString(), any())).thenReturn(Future.succeededFuture(List.of())); + when(fiscalYearService.getCurrentFYForSeriesByFYId(anyString(), any())).thenReturn(Future.succeededFuture(fiscalYearId)); CompositeOrderRetrieveHolder resultHolder = populateService.populate(holder, requestContext).result(); assertEquals(0.0, resultHolder.getOrder().getTotalEncumbered()); - assertEquals(0.0, resultHolder.getOrder().getTotalExpended()); - assertEquals(0.0, resultHolder.getOrder().getTotalCredited()); + assertEquals(100.0, resultHolder.getOrder().getTotalExpended()); + assertEquals(500.0, resultHolder.getOrder().getTotalCredited()); } }