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());
}
}