diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json
index c079f031..d52c1e5d 100644
--- a/descriptors/ModuleDescriptor-template.json
+++ b/descriptors/ModuleDescriptor-template.json
@@ -10,6 +10,10 @@
"id": "authtoken",
"version": "2.0"
},
+ {
+ "id": "configuration",
+ "version": "2.0"
+ },
{
"id": "permissions",
"version": "5.3"
@@ -274,7 +278,8 @@
"perms.users.get",
"perms.users.assign.immutable",
"data-export.job.collection.get",
- "data-export.config.collection.get"
+ "data-export.config.collection.get",
+ "configuration.entries.collection.get"
]
},
{
diff --git a/src/main/java/org/folio/client/ConfigurationClient.java b/src/main/java/org/folio/client/ConfigurationClient.java
new file mode 100644
index 00000000..c3e9331e
--- /dev/null
+++ b/src/main/java/org/folio/client/ConfigurationClient.java
@@ -0,0 +1,15 @@
+package org.folio.client;
+
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.service.annotation.GetExchange;
+import org.springframework.web.service.annotation.HttpExchange;
+
+import tools.jackson.databind.JsonNode;
+
+@HttpExchange("configurations")
+public interface ConfigurationClient {
+
+ @GetExchange("/entries")
+ JsonNode getConfigurations(@RequestParam("query") String query, @RequestParam("limit") int limit);
+
+}
diff --git a/src/main/java/org/folio/des/config/HttpClientConfiguration.java b/src/main/java/org/folio/des/config/HttpClientConfiguration.java
index e8e8f41c..37e71f24 100644
--- a/src/main/java/org/folio/des/config/HttpClientConfiguration.java
+++ b/src/main/java/org/folio/des/config/HttpClientConfiguration.java
@@ -2,6 +2,7 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
+import org.folio.client.ConfigurationClient;
import org.folio.des.client.DataExportSpringClient;
import org.folio.des.client.ExportWorkerClient;
import org.folio.des.exceptions.RestClientErrorHandler;
@@ -20,6 +21,11 @@ public class HttpClientConfiguration {
private final RestClientErrorHandler errorHandler;
+ @Bean
+ public ConfigurationClient configurationClient(HttpServiceProxyFactory factory) {
+ return factory.createClient(ConfigurationClient.class);
+ }
+
@Bean
public DataExportSpringClient dataExportSpringClient(HttpServiceProxyFactory factory) {
return factory.createClient(DataExportSpringClient.class);
diff --git a/src/main/java/org/folio/des/scheduling/util/ScheduleUtil.java b/src/main/java/org/folio/des/scheduling/util/ScheduleUtil.java
index 1dd34c04..604f2a4c 100644
--- a/src/main/java/org/folio/des/scheduling/util/ScheduleUtil.java
+++ b/src/main/java/org/folio/des/scheduling/util/ScheduleUtil.java
@@ -68,7 +68,7 @@ private static boolean isMigrationNeededForVersions(String moduleFrom, String mo
(moduleFromSemVer == null || moduleFromSemVer.compareTo(minQuartzSupportVersion) < 0);
}
- private static SemVer moduleVersionToSemVer(String version) {
+ public static SemVer moduleVersionToSemVer(String version) {
try {
return new SemVer(version);
} catch (IllegalArgumentException ex) {
diff --git a/src/main/java/org/folio/des/service/FolioTenantService.java b/src/main/java/org/folio/des/service/FolioTenantService.java
index e991b91b..8cd73ab8 100644
--- a/src/main/java/org/folio/des/service/FolioTenantService.java
+++ b/src/main/java/org/folio/des/service/FolioTenantService.java
@@ -9,9 +9,9 @@
import org.folio.des.scheduling.quartz.ScheduledJobsRemover;
import org.folio.des.service.bursarlegacy.BursarExportLegacyJobService;
import org.folio.des.service.bursarlegacy.BursarMigrationService;
+import org.folio.des.service.config.ConfigurationMigrationService;
import org.folio.des.service.config.impl.BursarFeesFinesExportConfigService;
import org.folio.spring.FolioExecutionContext;
-import org.folio.spring.exception.TenantUpgradeException;
import org.folio.spring.liquibase.FolioSpringLiquibase;
import org.folio.spring.service.PrepareSystemUserService;
import org.folio.spring.service.TenantService;
@@ -20,8 +20,6 @@
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
-import liquibase.exception.LiquibaseException;
-import liquibase.exception.UnexpectedLiquibaseException;
import lombok.extern.log4j.Log4j2;
@Log4j2
@@ -42,6 +40,7 @@ public class FolioTenantService extends TenantService {
private final BursarMigrationService bursarMigrationService;
private final BursarFeesFinesExportConfigService bursarFeesFinesExportConfigService;
private final FolioExecutionContext folioExecutionContext;
+ private final ConfigurationMigrationService configurationMigrationService;
public FolioTenantService(JdbcTemplate jdbcTemplate, FolioExecutionContext context, FolioSpringLiquibase folioSpringLiquibase,
PrepareSystemUserService prepareSystemUserService, KafkaService kafka,
@@ -49,7 +48,7 @@ public FolioTenantService(JdbcTemplate jdbcTemplate, FolioExecutionContext conte
BursarScheduledJobInitializer bursarScheduledJobInitializer, OldJobDeleteScheduler oldJobDeleteScheduler,
BursarExportLegacyJobService bursarExportLegacyJobService, JobService jobService,
BursarMigrationService bursarMigrationService, BursarFeesFinesExportConfigService bursarFeesFinesExportConfigService,
- FolioExecutionContext folioExecutionContext) {
+ FolioExecutionContext folioExecutionContext, ConfigurationMigrationService configurationMigrationService) {
super(jdbcTemplate, context, folioSpringLiquibase);
this.prepareSystemUserService = prepareSystemUserService;
this.kafka = kafka;
@@ -62,6 +61,7 @@ public FolioTenantService(JdbcTemplate jdbcTemplate, FolioExecutionContext conte
this.bursarMigrationService = bursarMigrationService;
this.bursarFeesFinesExportConfigService = bursarFeesFinesExportConfigService;
this.folioExecutionContext = folioExecutionContext;
+ this.configurationMigrationService = configurationMigrationService;
}
@Override
@@ -75,6 +75,7 @@ protected void beforeLiquibaseUpdate(TenantAttributes tenantAttributes) {
protected void afterTenantUpdate(TenantAttributes tenantAttributes) {
try {
prepareSystemUserService.setupSystemUser();
+ configurationMigrationService.migrateConfigurationData(tenantAttributes, context.getTenantId());
bursarMigrationService.updateLegacyBursarIfNeeded(tenantAttributes, bursarFeesFinesExportConfigService,
bursarExportLegacyJobService, jobService);
bursarScheduledJobInitializer.initAllScheduledJob(tenantAttributes);
diff --git a/src/main/java/org/folio/des/service/config/ConfigurationMigrationService.java b/src/main/java/org/folio/des/service/config/ConfigurationMigrationService.java
new file mode 100644
index 00000000..da938e73
--- /dev/null
+++ b/src/main/java/org/folio/des/service/config/ConfigurationMigrationService.java
@@ -0,0 +1,130 @@
+package org.folio.des.service.config;
+
+import static org.folio.des.scheduling.util.ScheduleUtil.moduleVersionToSemVer;
+
+import org.apache.commons.lang3.StringUtils;
+import org.folio.client.ConfigurationClient;
+import org.folio.des.domain.dto.ExportTypeSpecificParameters;
+import org.folio.spring.FolioModuleMetadata;
+import org.folio.tenant.domain.dto.TenantAttributes;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Service;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import tools.jackson.databind.JsonNode;
+import tools.jackson.databind.ObjectMapper;
+
+@Service
+@RequiredArgsConstructor
+@Log4j2
+public class ConfigurationMigrationService {
+
+ private static final String MIGRATION_TARGET_VERSION = "3.6.0";
+ private static final String CONFIGURATION_QUERY = "module==mod-data-export-spring";
+ private static final String EXPORT_CONFIG_TABLE = "export_config";
+ private static final String EXPORT_CONFIG_SQL = """
+ INSERT INTO %s.%s (
+ id, config_name, type, tenant, export_type_specific_parameters,
+ schedule_frequency, schedule_period, schedule_time, week_days,
+ created_date, created_by, updated_date, updated_by
+ ) VALUES (
+ ?::uuid, ?, ?, ?, ?::jsonb,
+ ?, ?, ?, ?::jsonb,
+ ?::timestamp, ?::uuid, ?::timestamp, ?::uuid
+ ) ON CONFLICT (id) DO NOTHING
+ """;
+
+ private final JdbcTemplate jdbcTemplate;
+ private final ObjectMapper objectMapper;
+ private final ConfigurationClient configurationClient;
+ private final FolioModuleMetadata folioModuleMetadata;
+
+ public void migrateConfigurationData(TenantAttributes attributes, String tenantId) {
+ log.info("migrateConfigurationData:: Attempting to migrate configuration data from mod-configuration for tenant: {}", tenantId);
+ if (!isMigrationNeeded(attributes)) {
+ log.info("migrateConfigurationData:: Migration not needed for tenant: {}. Skipping configuration data migration.", tenantId);
+ return;
+ }
+
+ try {
+ var configsResponse = fetchConfigurationEntries();
+ if (configsResponse == null || !configsResponse.has("configs") || configsResponse.get("configs").isEmpty()) {
+ log.info("migrateConfigurationData:: No configuration entries found to migrate");
+ return;
+ }
+ configsResponse.get("configs").forEach(config -> migrateConfigEntry(tenantId, config));
+ } catch (Exception e) {
+ log.warn("migrateConfigurationData:: Failed to migrate configuration data from mod-configuration. ", e);
+ }
+ }
+
+ private static boolean isMigrationNeeded(TenantAttributes tenantAttributes) {
+ var moduleFrom = tenantAttributes.getModuleFrom();
+ if (StringUtils.isBlank(moduleFrom)) {
+ return true;
+ }
+ return moduleVersionToSemVer(MIGRATION_TARGET_VERSION).compareTo(moduleVersionToSemVer(moduleFrom)) > 0;
+ }
+
+ private JsonNode fetchConfigurationEntries() {
+ return configurationClient.getConfigurations(CONFIGURATION_QUERY, Integer.MAX_VALUE);
+ }
+
+ private void migrateConfigEntry(String tenantId, JsonNode config) {
+ var id = config.path("id").asString();
+
+ try {
+ JsonNode valueObj = objectMapper.readTree(config.path("value").asString("{}"));
+
+ var type = valueObj.path("type").asString(null);
+ var exportTypeSpecificParameters = valueObj.path("exportTypeSpecificParameters");
+
+ if (StringUtils.isBlank(type) || type.equals("null") || !validateExportTypeSpecificParams(exportTypeSpecificParameters)) {
+ log.warn("migrateConfigEntry:: Skipping config {} due to missing type or exportTypeSpecificParameters", id);
+ return;
+ }
+
+ var metadata = config.path("metadata");
+ var weekDays = valueObj.path("weekDays");
+
+ jdbcTemplate.update(EXPORT_CONFIG_SQL.formatted(folioModuleMetadata.getDBSchemaName(tenantId), EXPORT_CONFIG_TABLE),
+ id,
+ config.path("configName").asString(),
+ type,
+ valueObj.path("tenant").asString(null),
+ exportTypeSpecificParameters.toString(),
+ valueObj.path("scheduleFrequency").asIntOpt().stream().boxed().findFirst().orElse(null),
+ valueObj.path("schedulePeriod").asString(null),
+ valueObj.path("scheduleTime").asString(null),
+ weekDays.isMissingNode() ? null : weekDays.toString(),
+ metadata.path("createdDate").asString(null),
+ metadata.path("createdByUserId").asString(null),
+ metadata.path("updatedDate").asString(null),
+ metadata.path("updatedByUserId").asString(null)
+ );
+ log.info("migrateConfigEntry:: Successfully migrated export config with id: {}", id);
+ } catch (Exception e) {
+ log.error("migrateConfigEntry:: Failed to insert export config with id: {}", id, e);
+ }
+ }
+
+
+ /**
+ * Validates the exportTypeSpecificParameters by attempting to convert it to the ExportTypeSpecificParameters class.
+
+ * @param exportTypeSpecificParameters the JsonNode containing the export type specific parameters to validate
+ */
+ private boolean validateExportTypeSpecificParams(JsonNode exportTypeSpecificParameters) {
+ if (exportTypeSpecificParameters.isMissingNode()) {
+ return false;
+ }
+ try {
+ objectMapper.treeToValue(exportTypeSpecificParameters, ExportTypeSpecificParameters.class);
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+}
diff --git a/src/main/resources/db/changelog/changelog-master.xml b/src/main/resources/db/changelog/changelog-master.xml
index b63cb3bd..4dd243b8 100644
--- a/src/main/resources/db/changelog/changelog-master.xml
+++ b/src/main/resources/db/changelog/changelog-master.xml
@@ -16,6 +16,5 @@
-
diff --git a/src/main/resources/db/changelog/changes/07_11_2025_migrate_configuration_data.xml b/src/main/resources/db/changelog/changes/07_11_2025_migrate_configuration_data.xml
deleted file mode 100644
index 6329e239..00000000
--- a/src/main/resources/db/changelog/changes/07_11_2025_migrate_configuration_data.xml
+++ /dev/null
@@ -1,52 +0,0 @@
-
-
-
-
-
-
-
-
- DO '
- DECLARE
- BEGIN
- IF EXISTS (SELECT 1 FROM information_schema.tables
- WHERE table_schema = ''${tenantname}_mod_configuration'' AND table_name = ''config_data'')
- THEN
- INSERT INTO ${tenantname}_mod_data_export_spring.export_config (
- id, config_name,
- type, tenant, export_type_specific_parameters, schedule_frequency, schedule_period, schedule_time, week_days,
- created_date, created_by, updated_date, updated_by
- )
- SELECT
- C.id::uuid,
- C.jsonb->>''configName'',
- (C.jsonb->>''value'')::jsonb->>''type'',
- (C.jsonb->>''value'')::jsonb->>''tenant'',
- (C.jsonb->>''value'')::jsonb->''exportTypeSpecificParameters'',
- ((C.jsonb->>''value'')::jsonb->>''scheduleFrequency'')::integer,
- (C.jsonb->>''value'')::jsonb->>''schedulePeriod'',
- (C.jsonb->>''value'')::jsonb->>''scheduleTime'',
- (C.jsonb->>''value'')::jsonb->''weekDays'',
- (C.jsonb->''metadata''->>''createdDate'')::timestamp,
- (C.jsonb->''metadata''->>''createdByUserId'')::uuid,
- (C.jsonb->''metadata''->>''updatedDate'')::timestamp,
- (C.jsonb->''metadata''->>''updatedByUserId'')::uuid
- FROM ${tenantname}_mod_configuration.config_data C
- WHERE C.jsonb->>''module'' = ''mod-data-export-spring''
- AND (C.jsonb->>''value'')::jsonb->>''type'' IS NOT NULL
- AND (C.jsonb->>''value'')::jsonb->''exportTypeSpecificParameters'' IS NOT NULL
- AND NOT EXISTS (
- SELECT 1 FROM ${tenantname}_mod_data_export_spring.export_config EC
- WHERE EC.config_name = C.jsonb->>''configName''
- )
- ON CONFLICT (id) DO NOTHING;
- END IF;
- END;
- ' LANGUAGE plpgsql;
-
-
-
-
diff --git a/src/test/java/org/folio/des/builder/job/JobCommandBuilderResolverTest.java b/src/test/java/org/folio/des/builder/job/JobCommandBuilderResolverTest.java
index 3c9c86d8..66486085 100644
--- a/src/test/java/org/folio/des/builder/job/JobCommandBuilderResolverTest.java
+++ b/src/test/java/org/folio/des/builder/job/JobCommandBuilderResolverTest.java
@@ -10,10 +10,10 @@
import java.util.Optional;
import java.util.UUID;
+import org.folio.client.ConfigurationClient;
import org.folio.de.entity.Job;
import org.folio.des.client.DataExportSpringClient;
import org.folio.des.client.ExportWorkerClient;
-import org.folio.des.config.HttpClientConfiguration;
import org.folio.des.config.JacksonConfiguration;
import org.folio.des.config.ServiceConfiguration;
import org.folio.des.config.scheduling.QuartzSchemaInitializer;
@@ -29,6 +29,7 @@
import org.folio.des.domain.dto.ExportType;
import org.folio.des.domain.dto.ExportTypeSpecificParameters;
import org.folio.des.domain.dto.VendorEdiOrdersExportConfig;
+import org.folio.des.service.config.ConfigurationMigrationService;
import org.folio.spring.client.AuthnClient;
import org.folio.spring.client.PermissionsClient;
import org.folio.spring.client.UsersClient;
@@ -66,6 +67,10 @@ class JobCommandBuilderResolverTest {
private UsersClient usersClient;
@MockitoBean
private PermissionsClient permissionsClient;
+ @MockitoBean
+ private ConfigurationClient configurationClient;
+ @MockitoBean
+ private ConfigurationMigrationService configurationMigrationService;
@ParameterizedTest
@DisplayName("Should retrieve builder for specific export type if builder is registered in the resolver")
diff --git a/src/test/java/org/folio/des/service/FolioTenantServiceTest.java b/src/test/java/org/folio/des/service/FolioTenantServiceTest.java
index 26a1861d..465e69c7 100644
--- a/src/test/java/org/folio/des/service/FolioTenantServiceTest.java
+++ b/src/test/java/org/folio/des/service/FolioTenantServiceTest.java
@@ -13,6 +13,7 @@
import org.folio.des.scheduling.quartz.OldJobDeleteScheduler;
import org.folio.des.scheduling.quartz.ScheduledJobsRemover;
import org.folio.des.service.bursarlegacy.BursarMigrationService;
+import org.folio.des.service.config.ConfigurationMigrationService;
import org.folio.spring.FolioExecutionContext;
import org.folio.spring.service.PrepareSystemUserService;
import org.folio.tenant.domain.dto.TenantAttributes;
@@ -52,6 +53,9 @@ class FolioTenantServiceTest {
@Mock
BursarMigrationService bursarMigrationService;
+ @Mock
+ ConfigurationMigrationService configurationMigrationService;
+
@Test
void shouldDoProcessAfterTenantUpdating() {
TenantAttributes tenantAttributes = createTenantAttributes();
@@ -70,6 +74,8 @@ void shouldDoProcessAfterTenantUpdating() {
.scheduleOldJobDeletion(any());
doNothing().when(bursarMigrationService)
.updateLegacyBursarIfNeeded(eq(tenantAttributes), any(), any(), any());
+ doNothing().when(configurationMigrationService)
+ .migrateConfigurationData(any(), any());
folioTenantService.afterTenantUpdate(tenantAttributes);
@@ -78,6 +84,7 @@ void shouldDoProcessAfterTenantUpdating() {
verify(bursarScheduledJobInitializer, times(1)).initAllScheduledJob(tenantAttributes);
verify(oldJobDeleteScheduler, times(1)).scheduleOldJobDeletion(any());
verify(bursarMigrationService, times(1)).updateLegacyBursarIfNeeded(eq(tenantAttributes), any(), any(), any());
+ verify(configurationMigrationService, times(1)).migrateConfigurationData(any(), any());
verify(kafka, times(1)).createKafkaTopics();
verify(kafka, times(1)).restartEventListeners();
}
diff --git a/src/test/java/org/folio/des/service/config/ConfigurationMigrationServiceTest.java b/src/test/java/org/folio/des/service/config/ConfigurationMigrationServiceTest.java
new file mode 100644
index 00000000..7049ab74
--- /dev/null
+++ b/src/test/java/org/folio/des/service/config/ConfigurationMigrationServiceTest.java
@@ -0,0 +1,188 @@
+package org.folio.des.service.config;
+
+import static org.folio.des.support.TestUtils.loadData;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.UUID;
+
+import org.folio.client.ConfigurationClient;
+import org.folio.des.CopilotGenerated;
+import org.folio.des.repository.ExportConfigRepository;
+import org.folio.des.support.BaseTest;
+import org.folio.tenant.domain.dto.TenantAttributes;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+import org.springframework.test.context.jdbc.Sql;
+import org.springframework.transaction.annotation.Transactional;
+
+@Transactional
+@TestPropertySource(properties = "spring.jpa.properties.hibernate.default_schema=diku_mod_data_export_spring")
+@CopilotGenerated(partiallyGenerated = true, model = "Claude Opus 4.6")
+class ConfigurationMigrationServiceTest extends BaseTest {
+
+ @Autowired
+ private ConfigurationMigrationService migrationService;
+
+ @Autowired
+ private ExportConfigRepository exportConfigRepository;
+
+ @MockitoBean
+ private ConfigurationClient configurationClient;
+
+ @ParameterizedTest
+ @DisplayName("Should migrate when upgrading from old version or fresh install (null)")
+ @CsvSource(value = {"mod-data-export-spring-3.6.0-SNAPSHOT", "mod-data-export-spring-3.6.0-SNAPSHOT.123", "3.0.0", "''", "null"}, nullValues = "null")
+ void shouldMigrateWhenVersionRequiresMigration(String moduleFrom) {
+ var attributes = tenantAttributes(moduleFrom);
+ when(configurationClient.getConfigurations(anyString(), anyInt()))
+ .thenReturn(loadData("data/configuration-entries/empty-response.json"));
+
+ assertDoesNotThrow(() -> migrationService.migrateConfigurationData(attributes, TENANT));
+ verify(configurationClient).getConfigurations(anyString(), anyInt());
+ }
+
+ @ParameterizedTest
+ @DisplayName("Should skip migration when version is at or above target")
+ @CsvSource({"mod-data-export-spring-3.6.1-SNAPSHOT", "mod-data-export-spring-3.6.0", "mod-data-export-spring-4.0.0"})
+ void shouldSkipMigrationWhenVersionDoesNotRequireMigration(String moduleFrom) {
+ var attributes = tenantAttributes(moduleFrom);
+
+ assertDoesNotThrow(() -> migrationService.migrateConfigurationData(attributes, TENANT));
+ verify(configurationClient, never()).getConfigurations(anyString(), anyInt());
+ }
+
+ @Test
+ @DisplayName("Should handle configuration client exception gracefully")
+ void shouldHandleConfigurationClientException() {
+ var attributes = tenantAttributes(null);
+ when(configurationClient.getConfigurations(anyString(), anyInt()))
+ .thenThrow(new RuntimeException("Connection refused"));
+
+ assertDoesNotThrow(() -> migrationService.migrateConfigurationData(attributes, TENANT));
+ }
+
+ @Test
+ @DisplayName("Should handle null response from configuration client")
+ void shouldHandleNullConfigurationResponse() {
+ var attributes = tenantAttributes(null);
+ when(configurationClient.getConfigurations(anyString(), anyInt()))
+ .thenReturn(null);
+
+ assertDoesNotThrow(() -> migrationService.migrateConfigurationData(attributes, TENANT));
+ }
+
+ @Test
+ @DisplayName("Should handle empty configs array from configuration client")
+ void shouldHandleEmptyConfigsArray() {
+ var attributes = tenantAttributes(null);
+ when(configurationClient.getConfigurations(anyString(), anyInt()))
+ .thenReturn(loadData("data/configuration-entries/empty-response.json"));
+
+ assertDoesNotThrow(() -> migrationService.migrateConfigurationData(attributes, TENANT));
+ assertEquals(0, exportConfigRepository.count());
+ }
+
+ @Test
+ @DisplayName("Should insert valid configuration entry")
+ void shouldInsertValidConfigurationEntry() {
+ var attributes = tenantAttributes(null);
+ when(configurationClient.getConfigurations(anyString(), anyInt()))
+ .thenReturn(loadData("data/configuration-entries/valid-entry-response.json"));
+
+ migrationService.migrateConfigurationData(attributes, TENANT);
+
+ assertEquals(1, exportConfigRepository.count());
+ }
+
+ @Test
+ @DisplayName("Should handle duplicate records in response by inserting only once")
+ void shouldHandleDuplicateRecordsInResponse() {
+ var attributes = tenantAttributes(null);
+ when(configurationClient.getConfigurations(anyString(), anyInt()))
+ .thenReturn(loadData("data/configuration-entries/duplicate-entries-response.json"));
+
+ assertDoesNotThrow(() -> migrationService.migrateConfigurationData(attributes, TENANT));
+ assertEquals(1, exportConfigRepository.count());
+ }
+
+ @Test
+ @Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD,
+ scripts = "classpath:data/configuration-entries/existing-export-config.sql")
+ @DisplayName("Should not overwrite existing DB record when config with same id is received")
+ void shouldNotOverwriteExistingDbRecord() {
+ var attributes = tenantAttributes(null);
+ when(configurationClient.getConfigurations(anyString(), anyInt()))
+ .thenReturn(loadData("data/configuration-entries/conflicting-entry-response.json"));
+
+ assertDoesNotThrow(() -> migrationService.migrateConfigurationData(attributes, TENANT));
+
+ // Verify the original record is unchanged
+ assertEquals(1, exportConfigRepository.count());
+ var entity = exportConfigRepository.findById(UUID.fromString("a1111111-1111-1111-1111-111111111111"));
+ assertTrue(entity.isPresent());
+ assertEquals("BURSAR_FEES_FINES", entity.get().getType());
+ }
+
+ @Test
+ @DisplayName("Should skip config entries with missing type")
+ void shouldSkipEntriesWithMissingType() {
+ var attributes = tenantAttributes(null);
+ when(configurationClient.getConfigurations(anyString(), anyInt()))
+ .thenReturn(loadData("data/configuration-entries/missing-type-response.json"));
+
+ assertDoesNotThrow(() -> migrationService.migrateConfigurationData(attributes, TENANT));
+ assertEquals(0, exportConfigRepository.count());
+ }
+
+ @ParameterizedTest
+ @DisplayName("Should skip config entries with invalid exportTypeSpecificParameters")
+ @ValueSource(strings = {
+ "data/configuration-entries/missing-params-response.json",
+ "data/configuration-entries/invalid-params-response.json"
+ })
+ void shouldSkipEntriesWithInvalidExportTypeSpecificParameters(String responsePath) {
+ var attributes = tenantAttributes(null);
+ when(configurationClient.getConfigurations(anyString(), anyInt())).thenReturn(loadData(responsePath));
+
+ assertDoesNotThrow(() -> migrationService.migrateConfigurationData(attributes, TENANT));
+ assertEquals(0, exportConfigRepository.count());
+ }
+
+ @Test
+ @DisplayName("Should be idempotent when migration is run twice with the same data")
+ void shouldBeIdempotentWhenMigrationRunTwice() {
+ var attributes = tenantAttributes(null);
+ var response = loadData("data/configuration-entries/valid-entry-response.json");
+ when(configurationClient.getConfigurations(anyString(), anyInt()))
+ .thenReturn(response);
+
+ migrationService.migrateConfigurationData(attributes, TENANT);
+ assertEquals(1, exportConfigRepository.count());
+
+ assertDoesNotThrow(() -> migrationService.migrateConfigurationData(attributes, TENANT));
+ assertEquals(1, exportConfigRepository.count());
+ }
+
+ private TenantAttributes tenantAttributes(String moduleFrom) {
+ var attrs = new TenantAttributes();
+ attrs.setModuleFrom(moduleFrom);
+ attrs.setModuleTo("mod-data-export-spring-3.0.0");
+ return attrs;
+ }
+}
+
+
+
diff --git a/src/test/java/org/folio/des/support/TestUtils.java b/src/test/java/org/folio/des/support/TestUtils.java
index 865376b2..024601ec 100644
--- a/src/test/java/org/folio/des/support/TestUtils.java
+++ b/src/test/java/org/folio/des/support/TestUtils.java
@@ -2,10 +2,13 @@
import static org.folio.des.service.config.ExportConfigConstants.DEFAULT_CONFIG_NAME;
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
+import java.util.stream.Collectors;
import org.folio.des.domain.dto.BursarExportFilterAge;
import org.folio.des.domain.dto.BursarExportFilterCondition;
@@ -13,12 +16,21 @@
import org.folio.des.domain.dto.BursarExportJob;
import org.folio.des.domain.dto.ExportConfig;
import org.folio.des.domain.dto.ExportTypeSpecificParameters;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
+import lombok.val;
+import tools.jackson.databind.JsonNode;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.json.JsonMapper;
@UtilityClass
public class TestUtils {
+ private static final String CLASSPATH_PREFIX = "classpath:";
+ private static final ObjectMapper MAPPER = JsonMapper.builder().build();
+
public static ExportConfig getBursarExportConfig() {
return new ExportConfig()
.id(UUID.randomUUID().toString())
@@ -55,4 +67,22 @@ private static Field getDeclaredFieldRecursive(String field, Class> cls) {
}
}
+ @SneakyThrows
+ public static T loadData(String path, Class cls) {
+ return MAPPER.readValue(readContent(path), cls);
+ }
+
+ @SneakyThrows
+ public static JsonNode loadData(String path) {
+ return MAPPER.readTree(readContent(path));
+ }
+
+ @SneakyThrows
+ private static String readContent(String path) {
+ val fullPath = path.startsWith(CLASSPATH_PREFIX) ? path : CLASSPATH_PREFIX + path;
+ val resource = new PathMatchingResourcePatternResolver().getResource(fullPath);
+ return new BufferedReader(new InputStreamReader(resource.getInputStream()))
+ .lines().collect(Collectors.joining(System.lineSeparator()));
+ }
+
}
diff --git a/src/test/resources/config/application.yml b/src/test/resources/config/application.yml
deleted file mode 100644
index c4c4208d..00000000
--- a/src/test/resources/config/application.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-folio:
- system:
- password: testpassword
- exchange:
- enabled: true
\ No newline at end of file
diff --git a/src/test/resources/data/configuration-entries/conflicting-entry-response.json b/src/test/resources/data/configuration-entries/conflicting-entry-response.json
new file mode 100644
index 00000000..6639aad9
--- /dev/null
+++ b/src/test/resources/data/configuration-entries/conflicting-entry-response.json
@@ -0,0 +1,17 @@
+{
+ "configs": [
+ {
+ "id": "a1111111-1111-1111-1111-111111111111",
+ "configName": "different_name",
+ "module": "mod-data-export-spring",
+ "value": "{\"type\":\"EDIFACT_ORDERS_EXPORT\",\"tenant\":\"other_tenant\",\"exportTypeSpecificParameters\":{\"vendorEdiOrdersExportConfig\":{}},\"schedulePeriod\":\"DAY\",\"scheduleFrequency\":1,\"scheduleTime\":\"12:00:00.000Z\"}",
+ "metadata": {
+ "createdDate": "2025-01-15T10:00:00.000+00:00",
+ "createdByUserId": "625dd2b6-b6f2-4f77-90fe-68954b26ee3c",
+ "updatedDate": "2025-01-15T10:00:00.000+00:00",
+ "updatedByUserId": "625dd2b6-b6f2-4f77-90fe-68954b26ee3c"
+ }
+ }
+ ]
+}
+
diff --git a/src/test/resources/data/configuration-entries/duplicate-entries-response.json b/src/test/resources/data/configuration-entries/duplicate-entries-response.json
new file mode 100644
index 00000000..23d7e8a1
--- /dev/null
+++ b/src/test/resources/data/configuration-entries/duplicate-entries-response.json
@@ -0,0 +1,29 @@
+{
+ "configs": [
+ {
+ "id": "c3333333-3333-3333-3333-333333333333",
+ "configName": "dup_config",
+ "module": "mod-data-export-spring",
+ "value": "{\"type\":\"BURSAR_FEES_FINES\",\"tenant\":\"diku\",\"exportTypeSpecificParameters\":{\"bursarFeeFines\":{\"daysOutstanding\":10}},\"schedulePeriod\":\"DAY\",\"scheduleFrequency\":1,\"scheduleTime\":\"12:00:00.000Z\"}",
+ "metadata": {
+ "createdDate": "2025-01-15T10:00:00.000+00:00",
+ "createdByUserId": "625dd2b6-b6f2-4f77-90fe-68954b26ee3c",
+ "updatedDate": "2025-01-15T10:00:00.000+00:00",
+ "updatedByUserId": "625dd2b6-b6f2-4f77-90fe-68954b26ee3c"
+ }
+ },
+ {
+ "id": "c3333333-3333-3333-3333-333333333333",
+ "configName": "dup_config",
+ "module": "mod-data-export-spring",
+ "value": "{\"type\":\"BURSAR_FEES_FINES\",\"tenant\":\"diku\",\"exportTypeSpecificParameters\":{\"bursarFeeFines\":{\"daysOutstanding\":10}},\"schedulePeriod\":\"DAY\",\"scheduleFrequency\":1,\"scheduleTime\":\"12:00:00.000Z\"}",
+ "metadata": {
+ "createdDate": "2025-01-15T10:00:00.000+00:00",
+ "createdByUserId": "625dd2b6-b6f2-4f77-90fe-68954b26ee3c",
+ "updatedDate": "2025-01-15T10:00:00.000+00:00",
+ "updatedByUserId": "625dd2b6-b6f2-4f77-90fe-68954b26ee3c"
+ }
+ }
+ ]
+}
+
diff --git a/src/test/resources/data/configuration-entries/empty-response.json b/src/test/resources/data/configuration-entries/empty-response.json
new file mode 100644
index 00000000..0adafada
--- /dev/null
+++ b/src/test/resources/data/configuration-entries/empty-response.json
@@ -0,0 +1,4 @@
+{
+ "configs": []
+}
+
diff --git a/src/test/resources/data/configuration-entries/existing-export-config.sql b/src/test/resources/data/configuration-entries/existing-export-config.sql
new file mode 100644
index 00000000..91052e80
--- /dev/null
+++ b/src/test/resources/data/configuration-entries/existing-export-config.sql
@@ -0,0 +1,11 @@
+INSERT INTO diku_mod_data_export_spring.export_config
+ (id, config_name, type, tenant, export_type_specific_parameters,
+ schedule_frequency, schedule_period, schedule_time, week_days,
+ created_date, created_by, updated_date, updated_by)
+VALUES
+ ('a1111111-1111-1111-1111-111111111111', 'export_config_parameters', 'BURSAR_FEES_FINES', 'diku',
+ '{"bursarFeeFines": {"daysOutstanding": 10}}',
+ 1, 'DAY', '12:00:00.000Z', null,
+ '2025-01-01 00:00:00', '625dd2b6-b6f2-4f77-90fe-68954b26ee3c',
+ '2025-01-01 00:00:00', '625dd2b6-b6f2-4f77-90fe-68954b26ee3c');
+
diff --git a/src/test/resources/data/configuration-entries/invalid-params-response.json b/src/test/resources/data/configuration-entries/invalid-params-response.json
new file mode 100644
index 00000000..06b7da67
--- /dev/null
+++ b/src/test/resources/data/configuration-entries/invalid-params-response.json
@@ -0,0 +1,17 @@
+{
+ "configs": [
+ {
+ "id": "e5555555-5555-5555-5555-555555555555",
+ "configName": "some_config",
+ "module": "mod-data-export-spring",
+ "value": "{\"id\":\"550e8400-e29b-41d4-a716-446655440000\",\"tenant\":\"testTenant\",\"schedulePeriod\":\"NONE\",\"type\":\"CLAIMS\",\"exportTypeSpecificParameters\":{\"bursarFeeFines\":null,\"vendorEdiOrdersExportConfig\":{\"exportConfigId\":\"550e8400-e29b-41d4-a716-446655440000\",\"vendorId\":\"11111111-1111-1111-1111-111111111111\",\"integrationType\":\"Claiming\",\"transmissionMethod\":\"File Download\",\"fileFormat\":\"EDI\",\"configName\":\"testConfig\",\"configDescription\":\"Test integration configuration for claims export\",\"isDefaultConfig\":false,\"ediConfig\":{\"vendorEdiType\":\"31B/US-SAN\",\"vendorEdiCode\":\"VENDOR-EDI-CODE\",\"libEdiCode\":\"LIB-EDI-CODE\",\"libEdiType\":\"31B/US-SAN\",\"ediNamingConvention\":\"{organizationCode}-{integrationName}-{exportJobEndDate}\",\"sendAccountNumber\":false,\"supportOrder\":true,\"supportInvoice\":false,\"accountNoList\":[\"1234567890\"],\"defaultAcquisitionMethods\":[\"df26d81b-9d63-4ff8-bf41-49bf75cfa70e\"],\"notes\":\"EDI configuration notes\"},\"ediFtp\":{\"ftpConnMode\":\"Active\",\"ftpFormat\":\"FTP\",\"ftpMode\":\"ASCII\",\"ftpPort\":22,\"orderDirectory\":\"/files\",\"invoiceDirectory\":\"/files\",\"password\":\"Ffx29%pu\",\"serverAddress\":\"ftp://ftp.ci.folio.org\",\"username\":\"folio\",\"isPrimaryTransmissionMethod\":null,\"notes\":\"FTP configuration notes\"},\"ediSchedule\":{\"enableScheduledExport\":false,\"scheduleParameters\":{\"id\":\"550e8400-e29b-41d4-a716-446655440000\",\"scheduleFrequency\":1,\"schedulePeriod\":\"DAY\",\"schedulingDate\":null,\"scheduleTime\":\"12:00:00\",\"weekDays\":null,\"timeZone\":\"UTC\"},\"schedulingNotes\":null}},\"query\":null,\"eHoldingsExportConfig\":null,\"authorityControlExportConfig\":null},\"scheduleFrequency\":null,\"scheduleTime\":null,\"weekDays\":null}",
+ "metadata": {
+ "createdDate": "2025-01-15T10:00:00.000+00:00",
+ "createdByUserId": "625dd2b6-b6f2-4f77-90fe-68954b26ee3c",
+ "updatedDate": "2025-01-15T10:00:00.000+00:00",
+ "updatedByUserId": "625dd2b6-b6f2-4f77-90fe-68954b26ee3c"
+ }
+ }
+ ]
+}
+
diff --git a/src/test/resources/data/configuration-entries/missing-params-response.json b/src/test/resources/data/configuration-entries/missing-params-response.json
new file mode 100644
index 00000000..d333a1ba
--- /dev/null
+++ b/src/test/resources/data/configuration-entries/missing-params-response.json
@@ -0,0 +1,17 @@
+{
+ "configs": [
+ {
+ "id": "e5555555-5555-5555-5555-555555555555",
+ "configName": "some_config",
+ "module": "mod-data-export-spring",
+ "value": "{\"type\":\"BURSAR_FEES_FINES\"}",
+ "metadata": {
+ "createdDate": "2025-01-15T10:00:00.000+00:00",
+ "createdByUserId": "625dd2b6-b6f2-4f77-90fe-68954b26ee3c",
+ "updatedDate": "2025-01-15T10:00:00.000+00:00",
+ "updatedByUserId": "625dd2b6-b6f2-4f77-90fe-68954b26ee3c"
+ }
+ }
+ ]
+}
+
diff --git a/src/test/resources/data/configuration-entries/missing-type-response.json b/src/test/resources/data/configuration-entries/missing-type-response.json
new file mode 100644
index 00000000..485de293
--- /dev/null
+++ b/src/test/resources/data/configuration-entries/missing-type-response.json
@@ -0,0 +1,17 @@
+{
+ "configs": [
+ {
+ "id": "d4444444-4444-4444-4444-444444444444",
+ "configName": "some_config",
+ "module": "mod-data-export-spring",
+ "value": "{\"exportTypeSpecificParameters\":{\"bursarFeeFines\":{}}}",
+ "metadata": {
+ "createdDate": "2025-01-15T10:00:00.000+00:00",
+ "createdByUserId": "625dd2b6-b6f2-4f77-90fe-68954b26ee3c",
+ "updatedDate": "2025-01-15T10:00:00.000+00:00",
+ "updatedByUserId": "625dd2b6-b6f2-4f77-90fe-68954b26ee3c"
+ }
+ }
+ ]
+}
+
diff --git a/src/test/resources/data/configuration-entries/valid-entry-response.json b/src/test/resources/data/configuration-entries/valid-entry-response.json
new file mode 100644
index 00000000..2b1d69a9
--- /dev/null
+++ b/src/test/resources/data/configuration-entries/valid-entry-response.json
@@ -0,0 +1,17 @@
+{
+ "configs": [
+ {
+ "id": "b2222222-2222-2222-2222-222222222222",
+ "configName": "bursar_config",
+ "module": "mod-data-export-spring",
+ "value": "{\"type\":\"BURSAR_FEES_FINES\",\"tenant\":\"diku\",\"exportTypeSpecificParameters\":{\"bursarFeeFines\":{\"daysOutstanding\":10}},\"schedulePeriod\":\"DAY\",\"scheduleFrequency\":1,\"scheduleTime\":\"12:00:00.000Z\"}",
+ "metadata": {
+ "createdDate": "2025-01-15T10:00:00.000+00:00",
+ "createdByUserId": "625dd2b6-b6f2-4f77-90fe-68954b26ee3c",
+ "updatedDate": "2025-01-15T10:00:00.000+00:00",
+ "updatedByUserId": "625dd2b6-b6f2-4f77-90fe-68954b26ee3c"
+ }
+ }
+ ]
+}
+