From 3bad50769aefeb3e1f1f0ec07d10a89b13156f31 Mon Sep 17 00:00:00 2001 From: Kriti Jain Date: Mon, 23 Feb 2026 10:47:43 -0600 Subject: [PATCH 1/7] MODEXPW-613 : Use GET /locale to get tenant language & locale setting selections --- .../CirculationLogItemProcessor.java | 44 +------------ .../org/folio/dew/client/LocaleClient.java | 64 +++++++++++++++++++ .../org/folio/dew/CirculationLogTest.java | 3 + src/test/resources/mappings/locale.json | 17 +++++ src/test/resources/output/invalid_file.csv | 0 5 files changed, 87 insertions(+), 41 deletions(-) create mode 100644 src/main/java/org/folio/dew/client/LocaleClient.java create mode 100644 src/test/resources/mappings/locale.json create mode 100644 src/test/resources/output/invalid_file.csv diff --git a/src/main/java/org/folio/dew/batch/circulationlog/CirculationLogItemProcessor.java b/src/main/java/org/folio/dew/batch/circulationlog/CirculationLogItemProcessor.java index e69e3d530..d6f2d39ed 100644 --- a/src/main/java/org/folio/dew/batch/circulationlog/CirculationLogItemProcessor.java +++ b/src/main/java/org/folio/dew/batch/circulationlog/CirculationLogItemProcessor.java @@ -1,12 +1,9 @@ package org.folio.dew.batch.circulationlog; -import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; -import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.StringUtils; +import org.folio.dew.client.LocaleClient; import org.folio.dew.client.ServicePointClient; -import org.folio.dew.client.SettingsClient; import org.folio.dew.domain.dto.CirculationLogExportFormat; import org.folio.dew.domain.dto.LogRecord; import org.folio.dew.domain.dto.LogRecordItemsInner; @@ -27,12 +24,10 @@ @Component @StepScope @RequiredArgsConstructor -@Log4j2 public class CirculationLogItemProcessor implements ItemProcessor { private final ServicePointClient servicePointClient; - private final SettingsClient settingsClient; - private final ObjectMapper objectMapper; + private final LocaleClient localeClient; private Map servicePointMap; private SimpleDateFormat format; @@ -67,45 +62,12 @@ public void initStep(StepExecution stepExecution) { private void initTenantSpecificDateFormat() { if (format != null) return; - String timezoneId = fetchTimezone(); - var dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm"); - dateFormat.setTimeZone(TimeZone.getTimeZone(timezoneId)); + dateFormat.setTimeZone(TimeZone.getTimeZone(localeClient.getLocaleSettings().getZoneId())); format = dateFormat; } - @SneakyThrows - @SuppressWarnings("unchecked") - private String fetchTimezone() { - try { - final Map tenantLocaleSettings = - settingsClient.getSettings("scope==stripes-core.prefs.manage and key==tenantLocaleSettings"); - - var resultInfo = (Map) tenantLocaleSettings.get("resultInfo"); - var totalRecords = (Integer) resultInfo.get("totalRecords"); - - if (totalRecords > 0) { - var items = (List>) tenantLocaleSettings.get("items"); - var settingsEntry = items.get(0); - var value = settingsEntry.get("value"); - - if (value instanceof String valueStr) { - return valueStr; - } else { - var jsonObject = objectMapper.valueToTree(value); - if (jsonObject.has("timezone")) { - return jsonObject.get("timezone").asText(); - } - } - } - } catch (Exception e) { - log.warn("Failed to fetch timezone from mod-settings: {}", e.getMessage()); - } - - return "UTC"; - } - private void fetchServicePoints() { if (servicePointMap != null) return; diff --git a/src/main/java/org/folio/dew/client/LocaleClient.java b/src/main/java/org/folio/dew/client/LocaleClient.java new file mode 100644 index 000000000..c852efda9 --- /dev/null +++ b/src/main/java/org/folio/dew/client/LocaleClient.java @@ -0,0 +1,64 @@ +package org.folio.dew.client; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import feign.FeignException; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.folio.dew.config.feign.FeignClientConfiguration; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.GetMapping; + +import java.time.DateTimeException; +import java.time.ZoneId; + +/** + * Client for the /locale API. + * + * @implNote This is a separate class from the internal LocaleClientRaw interface because Feign clients must be interfaces, disallowing any injection. + */ +@Log4j2 +@Component +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class LocaleClient { + + private final ObjectMapper objectMapper; + private final LocaleClientRaw underlyingClient; + + /** + * Provides raw access to the /locale API. + */ + @FeignClient(name = "locale", configuration = FeignClientConfiguration.class) + interface LocaleClientRaw { + @GetMapping + String getLocaleSettings(); + } + + public record LocaleSettings( + String locale, + String currency, + String timezone, + String numberingSystem + ) { + public ZoneId getZoneId() { + try { + return ZoneId.of(timezone); + } catch (DateTimeException e) { + log.error("Invalid timezone '{}', defaulting to UTC.", timezone, e); + return ZoneId.of("UTC"); + } + } + } + + public LocaleSettings getLocaleSettings() { + try { + String response = underlyingClient.getLocaleSettings(); + return objectMapper.readValue(response, LocaleSettings.class); + } catch (JsonProcessingException | FeignException | NullPointerException e) { + log.error("Failed to retrieve locale information. Defaulting to en-US, USD, UTC, latn.", e); + return new LocaleSettings("en-US", "USD", "UTC", "latn"); + } + } +} diff --git a/src/test/java/org/folio/dew/CirculationLogTest.java b/src/test/java/org/folio/dew/CirculationLogTest.java index faa021ea9..d7172de5e 100644 --- a/src/test/java/org/folio/dew/CirculationLogTest.java +++ b/src/test/java/org/folio/dew/CirculationLogTest.java @@ -26,6 +26,7 @@ import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching; import static org.assertj.core.api.Assertions.assertThat; import static org.folio.dew.domain.dto.JobParameterNames.CIRCULATION_LOG_FILE_NAME; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -59,6 +60,8 @@ void circulationLogJobTest() throws Exception { getRequestedFor( urlEqualTo( "/audit-data/circulation/logs?query&offset=0&limit=0"))); + wireMockServer.verify(getRequestedFor(urlEqualTo("/locale"))); + wireMockServer.verify(0, getRequestedFor(urlMatching("/settings/entries\\?query=.*tenantLocaleSettings.*"))); } @Test diff --git a/src/test/resources/mappings/locale.json b/src/test/resources/mappings/locale.json new file mode 100644 index 000000000..3685c6a7d --- /dev/null +++ b/src/test/resources/mappings/locale.json @@ -0,0 +1,17 @@ +{ + "mappings": [ + { + "request": { + "method": "GET", + "urlPattern": "/locale" + }, + "response": { + "status": 200, + "body": "{\n \"locale\": \"en-US\",\n \"currency\": \"USD\",\n \"timezone\": \"Pacific/Yap\",\n \"numberingSystem\": \"latn\"\n}", + "headers": { + "Content-Type": "application/json" + } + } + } + ] +} diff --git a/src/test/resources/output/invalid_file.csv b/src/test/resources/output/invalid_file.csv new file mode 100644 index 000000000..e69de29bb From 23ffe074bfad07b40d3a9f179e2e6dc07808b5f7 Mon Sep 17 00:00:00 2001 From: Kriti Jain Date: Mon, 23 Feb 2026 10:59:07 -0600 Subject: [PATCH 2/7] re --- src/test/resources/output/invalid_file.csv | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/resources/output/invalid_file.csv b/src/test/resources/output/invalid_file.csv index e69de29bb..e51a188a4 100644 --- a/src/test/resources/output/invalid_file.csv +++ b/src/test/resources/output/invalid_file.csv @@ -0,0 +1,2 @@ +this,is,not,the,expected,export +1,2,3 From a96b7adefb7eb4476205238a60fabf252a64b68e Mon Sep 17 00:00:00 2001 From: Kriti Jain Date: Mon, 23 Feb 2026 11:09:52 -0600 Subject: [PATCH 3/7] re --- .../CirculationLogItemProcessorTest.java | 96 +++++++++++++++++++ .../folio/dew/client/LocaleClientTest.java | 69 +++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 src/test/java/org/folio/dew/batch/circulationlog/CirculationLogItemProcessorTest.java create mode 100644 src/test/java/org/folio/dew/client/LocaleClientTest.java diff --git a/src/test/java/org/folio/dew/batch/circulationlog/CirculationLogItemProcessorTest.java b/src/test/java/org/folio/dew/batch/circulationlog/CirculationLogItemProcessorTest.java new file mode 100644 index 000000000..fbfe80700 --- /dev/null +++ b/src/test/java/org/folio/dew/batch/circulationlog/CirculationLogItemProcessorTest.java @@ -0,0 +1,96 @@ +package org.folio.dew.batch.circulationlog; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.util.Arrays; +import java.util.List; +import org.folio.dew.client.LocaleClient; +import org.folio.dew.client.ServicePointClient; +import org.folio.dew.domain.dto.ActionType; +import org.folio.dew.domain.dto.LogRecord; +import org.folio.dew.domain.dto.LogRecordItemsInner; +import org.folio.dew.domain.dto.LoggedObjectType; +import org.folio.dew.domain.dto.ServicePoint; +import org.folio.dew.domain.dto.Servicepoints; +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; + +@ExtendWith(MockitoExtension.class) +class CirculationLogItemProcessorTest { + + @Mock + private ServicePointClient servicePointClient; + @Mock + private LocaleClient localeClient; + + @InjectMocks + private CirculationLogItemProcessor processor; + + @Test + void shouldUseLocaleTimezoneAndReuseCachedValuesAcrossInit() throws Exception { + var servicepoints = new Servicepoints(); + servicepoints.setServicepoints(List.of(new ServicePoint().id("sp-1").name("Main service point"))); + when(servicePointClient.get("name<>null", 1000)).thenReturn(servicepoints); + when(localeClient.getLocaleSettings()).thenReturn(new LocaleClient.LocaleSettings("en-US", "USD", "America/New_York", "latn")); + + processor.initStep(null); + processor.initStep(null); + + verify(servicePointClient, times(1)).get("name<>null", 1000); + verify(localeClient, times(1)).getLocaleSettings(); + + var item = new LogRecord() + .userBarcode("u-1") + .description("desc") + .action(ActionType.CHECK_OUT) + .date(java.util.Date.from(Instant.parse("2024-01-01T00:00:00Z"))) + .servicePointId("sp-1") + ._object(LoggedObjectType.LOAN) + .source("SYSTEM") + .items(Arrays.asList( + new LogRecordItemsInner().itemBarcode("bc-1"), + new LogRecordItemsInner().itemBarcode(" "), + null, + new LogRecordItemsInner().itemBarcode("bc-2"))); + + var result = processor.process(item); + + assertThat(result.getServicePointId()).isEqualTo("Main service point"); + assertThat(result.getDate()).isEqualTo("2023-12-31 19:00"); + assertThat(result.getItems()).isEqualTo("bc-1,bc-2"); + assertThat(result.getAction()).isEqualTo("Check out"); + assertThat(result.getObjectField()).isEqualTo("Loan"); + } + + @Test + void shouldHandleEmptyServicePoints() throws Exception { + var servicepoints = new Servicepoints(); + servicepoints.setServicepoints(List.of()); + when(servicePointClient.get("name<>null", 1000)).thenReturn(servicepoints); + when(localeClient.getLocaleSettings()).thenReturn(new LocaleClient.LocaleSettings("en-US", "USD", "UTC", "latn")); + + processor.initStep(null); + + var item = new LogRecord() + .userBarcode("u-1") + .description("desc") + .action(ActionType.CHECK_IN) + .date(java.util.Date.from(Instant.parse("2024-01-01T00:00:00Z"))) + .servicePointId("missing") + ._object(LoggedObjectType.LOAN) + .source("SYSTEM") + .items(List.of(new LogRecordItemsInner().itemBarcode("bc-1"))); + + var result = processor.process(item); + + assertThat(result.getServicePointId()).isNull(); + assertThat(result.getDate()).isEqualTo("2024-01-01 00:00"); + } +} diff --git a/src/test/java/org/folio/dew/client/LocaleClientTest.java b/src/test/java/org/folio/dew/client/LocaleClientTest.java new file mode 100644 index 000000000..7740ba6e8 --- /dev/null +++ b/src/test/java/org/folio/dew/client/LocaleClientTest.java @@ -0,0 +1,69 @@ +package org.folio.dew.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.databind.ObjectMapper; +import feign.FeignException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class LocaleClientTest { + + private static final String LOCALE_JSON = """ + { + "locale": "en-US", + "currency": "USD", + "timezone": "America/New_York", + "numberingSystem": "latn" + } + """; + + @InjectMocks + private LocaleClient localeClient; + + @Mock + private LocaleClient.LocaleClientRaw underlyingClient; + + @Spy + private ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void shouldReturnLocaleSettingsWhenResponseIsValid() { + when(underlyingClient.getLocaleSettings()).thenReturn(LOCALE_JSON); + + var expected = new LocaleClient.LocaleSettings("en-US", "USD", "America/New_York", "latn"); + assertThat(localeClient.getLocaleSettings()).isEqualTo(expected); + assertThat(localeClient.getLocaleSettings().getZoneId().getId()).isEqualTo("America/New_York"); + } + + @Test + void shouldReturnDefaultSettingsWhenClientThrowsException() { + when(underlyingClient.getLocaleSettings()) + .thenThrow(new FeignException.Unauthorized("", mock(feign.Request.class), null, null)); + + var expectedDefault = new LocaleClient.LocaleSettings("en-US", "USD", "UTC", "latn"); + assertThat(localeClient.getLocaleSettings()).isEqualTo(expectedDefault); + } + + @Test + void shouldReturnDefaultSettingsWhenJsonIsInvalid() { + when(underlyingClient.getLocaleSettings()).thenReturn("invalid json"); + + var expectedDefault = new LocaleClient.LocaleSettings("en-US", "USD", "UTC", "latn"); + assertThat(localeClient.getLocaleSettings()).isEqualTo(expectedDefault); + } + + @Test + void shouldFallbackToUtcWhenTimezoneIsInvalid() { + var settings = new LocaleClient.LocaleSettings("en-US", "USD", "not-a-timezone", "latn"); + + assertThat(settings.getZoneId().getId()).isEqualTo("UTC"); + } +} From 3426a9d41b18c991d966ed484a3610a5e7f75f07 Mon Sep 17 00:00:00 2001 From: Kriti Jain Date: Mon, 23 Feb 2026 11:22:42 -0600 Subject: [PATCH 4/7] Enforce CirculationLogTest order --- .../org/folio/dew/CirculationLogTest.java | 70 ++++++++++++++ .../CirculationLogItemProcessorTest.java | 96 ------------------- .../folio/dew/client/LocaleClientTest.java | 69 ------------- 3 files changed, 70 insertions(+), 165 deletions(-) delete mode 100644 src/test/java/org/folio/dew/batch/circulationlog/CirculationLogItemProcessorTest.java delete mode 100644 src/test/java/org/folio/dew/client/LocaleClientTest.java diff --git a/src/test/java/org/folio/dew/CirculationLogTest.java b/src/test/java/org/folio/dew/CirculationLogTest.java index d7172de5e..40a118040 100644 --- a/src/test/java/org/folio/dew/CirculationLogTest.java +++ b/src/test/java/org/folio/dew/CirculationLogTest.java @@ -7,7 +7,10 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.MethodOrderer; import org.springframework.batch.core.ExitStatus; import org.springframework.batch.core.Job; import org.springframework.batch.core.JobExecution; @@ -24,6 +27,8 @@ import java.util.Date; import java.util.UUID; +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching; @@ -33,6 +38,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) class CirculationLogTest extends BaseBatchTest { @Autowired private Job getCirculationLogJob; @@ -45,6 +51,7 @@ static void beforeAll() { } @Test + @Order(1) @DisplayName("Run CirculationLogJob successfully") void circulationLogJobTest() throws Exception { JobLauncherTestUtils testLauncher = createTestLauncher(getCirculationLogJob); @@ -65,6 +72,7 @@ void circulationLogJobTest() throws Exception { } @Test + @Order(2) @DisplayName("Check that date setting in 24 hours format instead of 12h and test pass successfully") void successfulSetDateIn24hFormatInsteadOf12hTest() throws ParseException { CirculationLogExportFormat circulationLogExportFormat = new CirculationLogExportFormat(); @@ -117,6 +125,7 @@ private JobParameters prepareJobParameters() { } @Test + @Order(3) @DisplayName("Fail test when circulation log output file content does not match expected") void circulationLogJobFileMismatchTest() throws Exception { JobLauncherTestUtils testLauncher = createTestLauncher(getCirculationLogJob); @@ -131,4 +140,65 @@ void circulationLogJobFileMismatchTest() throws Exception { ); assertThat(thrown.getMessage()).contains("Files are not identical!"); } + + @Test + @Order(90) + @DisplayName("Run CirculationLogJob successfully when /locale returns unauthorized") + void circulationLogJobUsesDefaultLocaleWhenLocaleApiUnauthorized() throws Exception { + wireMockServer.stubFor(get(urlEqualTo("/locale")) + .atPriority(3) + .willReturn(aResponse().withStatus(401))); + + JobLauncherTestUtils testLauncher = createTestLauncher(getCirculationLogJob); + JobExecution jobExecution = testLauncher.launchJob(prepareJobParameters()); + + assertThat(jobExecution.getExitStatus()).isEqualTo(ExitStatus.COMPLETED); + wireMockServer.verify(getRequestedFor(urlEqualTo("/locale"))); + wireMockServer.verify(0, getRequestedFor(urlMatching("/settings/entries\\?query=.*tenantLocaleSettings.*"))); + } + + @Test + @Order(91) + @DisplayName("Run CirculationLogJob successfully when /locale returns invalid json") + void circulationLogJobUsesDefaultLocaleWhenLocaleApiReturnsInvalidJson() throws Exception { + wireMockServer.stubFor(get(urlEqualTo("/locale")) + .atPriority(2) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("invalid json"))); + + JobLauncherTestUtils testLauncher = createTestLauncher(getCirculationLogJob); + JobExecution jobExecution = testLauncher.launchJob(prepareJobParameters()); + + assertThat(jobExecution.getExitStatus()).isEqualTo(ExitStatus.COMPLETED); + wireMockServer.verify(getRequestedFor(urlEqualTo("/locale"))); + wireMockServer.verify(0, getRequestedFor(urlMatching("/settings/entries\\?query=.*tenantLocaleSettings.*"))); + } + + @Test + @Order(92) + @DisplayName("Run CirculationLogJob successfully when /locale returns invalid timezone") + void circulationLogJobUsesUtcWhenLocaleApiReturnsInvalidTimezone() throws Exception { + wireMockServer.stubFor(get(urlEqualTo("/locale")) + .atPriority(1) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(""" + { + "locale": "en-US", + "currency": "USD", + "timezone": "invalid-timezone", + "numberingSystem": "latn" + } + """))); + + JobLauncherTestUtils testLauncher = createTestLauncher(getCirculationLogJob); + JobExecution jobExecution = testLauncher.launchJob(prepareJobParameters()); + + assertThat(jobExecution.getExitStatus()).isEqualTo(ExitStatus.COMPLETED); + wireMockServer.verify(getRequestedFor(urlEqualTo("/locale"))); + wireMockServer.verify(0, getRequestedFor(urlMatching("/settings/entries\\?query=.*tenantLocaleSettings.*"))); + } } diff --git a/src/test/java/org/folio/dew/batch/circulationlog/CirculationLogItemProcessorTest.java b/src/test/java/org/folio/dew/batch/circulationlog/CirculationLogItemProcessorTest.java deleted file mode 100644 index fbfe80700..000000000 --- a/src/test/java/org/folio/dew/batch/circulationlog/CirculationLogItemProcessorTest.java +++ /dev/null @@ -1,96 +0,0 @@ -package org.folio.dew.batch.circulationlog; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.time.Instant; -import java.util.Arrays; -import java.util.List; -import org.folio.dew.client.LocaleClient; -import org.folio.dew.client.ServicePointClient; -import org.folio.dew.domain.dto.ActionType; -import org.folio.dew.domain.dto.LogRecord; -import org.folio.dew.domain.dto.LogRecordItemsInner; -import org.folio.dew.domain.dto.LoggedObjectType; -import org.folio.dew.domain.dto.ServicePoint; -import org.folio.dew.domain.dto.Servicepoints; -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; - -@ExtendWith(MockitoExtension.class) -class CirculationLogItemProcessorTest { - - @Mock - private ServicePointClient servicePointClient; - @Mock - private LocaleClient localeClient; - - @InjectMocks - private CirculationLogItemProcessor processor; - - @Test - void shouldUseLocaleTimezoneAndReuseCachedValuesAcrossInit() throws Exception { - var servicepoints = new Servicepoints(); - servicepoints.setServicepoints(List.of(new ServicePoint().id("sp-1").name("Main service point"))); - when(servicePointClient.get("name<>null", 1000)).thenReturn(servicepoints); - when(localeClient.getLocaleSettings()).thenReturn(new LocaleClient.LocaleSettings("en-US", "USD", "America/New_York", "latn")); - - processor.initStep(null); - processor.initStep(null); - - verify(servicePointClient, times(1)).get("name<>null", 1000); - verify(localeClient, times(1)).getLocaleSettings(); - - var item = new LogRecord() - .userBarcode("u-1") - .description("desc") - .action(ActionType.CHECK_OUT) - .date(java.util.Date.from(Instant.parse("2024-01-01T00:00:00Z"))) - .servicePointId("sp-1") - ._object(LoggedObjectType.LOAN) - .source("SYSTEM") - .items(Arrays.asList( - new LogRecordItemsInner().itemBarcode("bc-1"), - new LogRecordItemsInner().itemBarcode(" "), - null, - new LogRecordItemsInner().itemBarcode("bc-2"))); - - var result = processor.process(item); - - assertThat(result.getServicePointId()).isEqualTo("Main service point"); - assertThat(result.getDate()).isEqualTo("2023-12-31 19:00"); - assertThat(result.getItems()).isEqualTo("bc-1,bc-2"); - assertThat(result.getAction()).isEqualTo("Check out"); - assertThat(result.getObjectField()).isEqualTo("Loan"); - } - - @Test - void shouldHandleEmptyServicePoints() throws Exception { - var servicepoints = new Servicepoints(); - servicepoints.setServicepoints(List.of()); - when(servicePointClient.get("name<>null", 1000)).thenReturn(servicepoints); - when(localeClient.getLocaleSettings()).thenReturn(new LocaleClient.LocaleSettings("en-US", "USD", "UTC", "latn")); - - processor.initStep(null); - - var item = new LogRecord() - .userBarcode("u-1") - .description("desc") - .action(ActionType.CHECK_IN) - .date(java.util.Date.from(Instant.parse("2024-01-01T00:00:00Z"))) - .servicePointId("missing") - ._object(LoggedObjectType.LOAN) - .source("SYSTEM") - .items(List.of(new LogRecordItemsInner().itemBarcode("bc-1"))); - - var result = processor.process(item); - - assertThat(result.getServicePointId()).isNull(); - assertThat(result.getDate()).isEqualTo("2024-01-01 00:00"); - } -} diff --git a/src/test/java/org/folio/dew/client/LocaleClientTest.java b/src/test/java/org/folio/dew/client/LocaleClientTest.java deleted file mode 100644 index 7740ba6e8..000000000 --- a/src/test/java/org/folio/dew/client/LocaleClientTest.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.folio.dew.client; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.fasterxml.jackson.databind.ObjectMapper; -import feign.FeignException; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Spy; -import org.mockito.junit.jupiter.MockitoExtension; - -@ExtendWith(MockitoExtension.class) -class LocaleClientTest { - - private static final String LOCALE_JSON = """ - { - "locale": "en-US", - "currency": "USD", - "timezone": "America/New_York", - "numberingSystem": "latn" - } - """; - - @InjectMocks - private LocaleClient localeClient; - - @Mock - private LocaleClient.LocaleClientRaw underlyingClient; - - @Spy - private ObjectMapper objectMapper = new ObjectMapper(); - - @Test - void shouldReturnLocaleSettingsWhenResponseIsValid() { - when(underlyingClient.getLocaleSettings()).thenReturn(LOCALE_JSON); - - var expected = new LocaleClient.LocaleSettings("en-US", "USD", "America/New_York", "latn"); - assertThat(localeClient.getLocaleSettings()).isEqualTo(expected); - assertThat(localeClient.getLocaleSettings().getZoneId().getId()).isEqualTo("America/New_York"); - } - - @Test - void shouldReturnDefaultSettingsWhenClientThrowsException() { - when(underlyingClient.getLocaleSettings()) - .thenThrow(new FeignException.Unauthorized("", mock(feign.Request.class), null, null)); - - var expectedDefault = new LocaleClient.LocaleSettings("en-US", "USD", "UTC", "latn"); - assertThat(localeClient.getLocaleSettings()).isEqualTo(expectedDefault); - } - - @Test - void shouldReturnDefaultSettingsWhenJsonIsInvalid() { - when(underlyingClient.getLocaleSettings()).thenReturn("invalid json"); - - var expectedDefault = new LocaleClient.LocaleSettings("en-US", "USD", "UTC", "latn"); - assertThat(localeClient.getLocaleSettings()).isEqualTo(expectedDefault); - } - - @Test - void shouldFallbackToUtcWhenTimezoneIsInvalid() { - var settings = new LocaleClient.LocaleSettings("en-US", "USD", "not-a-timezone", "latn"); - - assertThat(settings.getZoneId().getId()).isEqualTo("UTC"); - } -} From 200f3238117a12ecdac57b0290566f8297479bdc Mon Sep 17 00:00:00 2001 From: Kriti Jain Date: Mon, 23 Feb 2026 11:30:33 -0600 Subject: [PATCH 5/7] Adjust CirculationLogTest setup --- .../org/folio/dew/CirculationLogTest.java | 74 ++++++++++--------- 1 file changed, 40 insertions(+), 34 deletions(-) diff --git a/src/test/java/org/folio/dew/CirculationLogTest.java b/src/test/java/org/folio/dew/CirculationLogTest.java index 40a118040..bc38bb60c 100644 --- a/src/test/java/org/folio/dew/CirculationLogTest.java +++ b/src/test/java/org/folio/dew/CirculationLogTest.java @@ -1,16 +1,15 @@ package org.folio.dew; +import com.github.tomakehurst.wiremock.stubbing.StubMapping; import org.apache.commons.io.FileUtils; import org.folio.dew.domain.dto.JobParameterNames; import org.folio.dew.domain.dto.ExportType; import org.folio.dew.domain.dto.CirculationLogExportFormat; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestMethodOrder; -import org.junit.jupiter.api.MethodOrderer; import org.springframework.batch.core.ExitStatus; import org.springframework.batch.core.Job; import org.springframework.batch.core.JobExecution; @@ -38,7 +37,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue; -@TestMethodOrder(MethodOrderer.OrderAnnotation.class) class CirculationLogTest extends BaseBatchTest { @Autowired private Job getCirculationLogJob; @@ -50,8 +48,12 @@ static void beforeAll() { setUpTenant(NON_CONSORTIUM_TENANT); } + @BeforeEach + void beforeEach() { + wireMockServer.resetRequests(); + } + @Test - @Order(1) @DisplayName("Run CirculationLogJob successfully") void circulationLogJobTest() throws Exception { JobLauncherTestUtils testLauncher = createTestLauncher(getCirculationLogJob); @@ -72,7 +74,6 @@ void circulationLogJobTest() throws Exception { } @Test - @Order(2) @DisplayName("Check that date setting in 24 hours format instead of 12h and test pass successfully") void successfulSetDateIn24hFormatInsteadOf12hTest() throws ParseException { CirculationLogExportFormat circulationLogExportFormat = new CirculationLogExportFormat(); @@ -125,7 +126,6 @@ private JobParameters prepareJobParameters() { } @Test - @Order(3) @DisplayName("Fail test when circulation log output file content does not match expected") void circulationLogJobFileMismatchTest() throws Exception { JobLauncherTestUtils testLauncher = createTestLauncher(getCirculationLogJob); @@ -142,45 +142,48 @@ void circulationLogJobFileMismatchTest() throws Exception { } @Test - @Order(90) @DisplayName("Run CirculationLogJob successfully when /locale returns unauthorized") void circulationLogJobUsesDefaultLocaleWhenLocaleApiUnauthorized() throws Exception { - wireMockServer.stubFor(get(urlEqualTo("/locale")) + StubMapping localeStub = wireMockServer.stubFor(get(urlEqualTo("/locale")) .atPriority(3) .willReturn(aResponse().withStatus(401))); - - JobLauncherTestUtils testLauncher = createTestLauncher(getCirculationLogJob); - JobExecution jobExecution = testLauncher.launchJob(prepareJobParameters()); - - assertThat(jobExecution.getExitStatus()).isEqualTo(ExitStatus.COMPLETED); - wireMockServer.verify(getRequestedFor(urlEqualTo("/locale"))); - wireMockServer.verify(0, getRequestedFor(urlMatching("/settings/entries\\?query=.*tenantLocaleSettings.*"))); + try { + JobLauncherTestUtils testLauncher = createTestLauncher(getCirculationLogJob); + JobExecution jobExecution = testLauncher.launchJob(prepareJobParameters()); + + assertThat(jobExecution.getExitStatus()).isEqualTo(ExitStatus.COMPLETED); + wireMockServer.verify(getRequestedFor(urlEqualTo("/locale"))); + wireMockServer.verify(0, getRequestedFor(urlMatching("/settings/entries\\?query=.*tenantLocaleSettings.*"))); + } finally { + wireMockServer.removeStubMapping(localeStub); + } } @Test - @Order(91) @DisplayName("Run CirculationLogJob successfully when /locale returns invalid json") void circulationLogJobUsesDefaultLocaleWhenLocaleApiReturnsInvalidJson() throws Exception { - wireMockServer.stubFor(get(urlEqualTo("/locale")) + StubMapping localeStub = wireMockServer.stubFor(get(urlEqualTo("/locale")) .atPriority(2) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody("invalid json"))); - - JobLauncherTestUtils testLauncher = createTestLauncher(getCirculationLogJob); - JobExecution jobExecution = testLauncher.launchJob(prepareJobParameters()); - - assertThat(jobExecution.getExitStatus()).isEqualTo(ExitStatus.COMPLETED); - wireMockServer.verify(getRequestedFor(urlEqualTo("/locale"))); - wireMockServer.verify(0, getRequestedFor(urlMatching("/settings/entries\\?query=.*tenantLocaleSettings.*"))); + try { + JobLauncherTestUtils testLauncher = createTestLauncher(getCirculationLogJob); + JobExecution jobExecution = testLauncher.launchJob(prepareJobParameters()); + + assertThat(jobExecution.getExitStatus()).isEqualTo(ExitStatus.COMPLETED); + wireMockServer.verify(getRequestedFor(urlEqualTo("/locale"))); + wireMockServer.verify(0, getRequestedFor(urlMatching("/settings/entries\\?query=.*tenantLocaleSettings.*"))); + } finally { + wireMockServer.removeStubMapping(localeStub); + } } @Test - @Order(92) @DisplayName("Run CirculationLogJob successfully when /locale returns invalid timezone") void circulationLogJobUsesUtcWhenLocaleApiReturnsInvalidTimezone() throws Exception { - wireMockServer.stubFor(get(urlEqualTo("/locale")) + StubMapping localeStub = wireMockServer.stubFor(get(urlEqualTo("/locale")) .atPriority(1) .willReturn(aResponse() .withStatus(200) @@ -193,12 +196,15 @@ void circulationLogJobUsesUtcWhenLocaleApiReturnsInvalidTimezone() throws Except "numberingSystem": "latn" } """))); - - JobLauncherTestUtils testLauncher = createTestLauncher(getCirculationLogJob); - JobExecution jobExecution = testLauncher.launchJob(prepareJobParameters()); - - assertThat(jobExecution.getExitStatus()).isEqualTo(ExitStatus.COMPLETED); - wireMockServer.verify(getRequestedFor(urlEqualTo("/locale"))); - wireMockServer.verify(0, getRequestedFor(urlMatching("/settings/entries\\?query=.*tenantLocaleSettings.*"))); + try { + JobLauncherTestUtils testLauncher = createTestLauncher(getCirculationLogJob); + JobExecution jobExecution = testLauncher.launchJob(prepareJobParameters()); + + assertThat(jobExecution.getExitStatus()).isEqualTo(ExitStatus.COMPLETED); + wireMockServer.verify(getRequestedFor(urlEqualTo("/locale"))); + wireMockServer.verify(0, getRequestedFor(urlMatching("/settings/entries\\?query=.*tenantLocaleSettings.*"))); + } finally { + wireMockServer.removeStubMapping(localeStub); + } } } From 45670b079e5172466cd365c894deea5db5e016f6 Mon Sep 17 00:00:00 2001 From: Kriti Jain Date: Mon, 23 Feb 2026 13:13:18 -0600 Subject: [PATCH 6/7] re --- descriptors/ModuleDescriptor-template.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index e07efb192..d6e8a153e 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -153,6 +153,10 @@ { "id": "settings", "version": "1.2" + }, + { + "id": "locale", + "version": "1.0" } ], "optional": [ @@ -249,7 +253,8 @@ "users.collection.get", "transfers.collection.get", "inventory-storage.service-points.collection.get", - "instance-authority-links.authority-statistics.collection.get" + "instance-authority-links.authority-statistics.collection.get", + "locale.item.get" ] }, { From 7d9ab06077f32a01b01b36bbccb38ec4fceaa933 Mon Sep 17 00:00:00 2001 From: Kriti Jain Date: Mon, 23 Feb 2026 14:02:43 -0600 Subject: [PATCH 7/7] Add logging for circulation log --- .../CirculationLogItemProcessor.java | 14 ++++++++++- .../org/folio/dew/client/LocaleClient.java | 25 ++++++++++++++----- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/folio/dew/batch/circulationlog/CirculationLogItemProcessor.java b/src/main/java/org/folio/dew/batch/circulationlog/CirculationLogItemProcessor.java index d6f2d39ed..e00b2d2a7 100644 --- a/src/main/java/org/folio/dew/batch/circulationlog/CirculationLogItemProcessor.java +++ b/src/main/java/org/folio/dew/batch/circulationlog/CirculationLogItemProcessor.java @@ -1,6 +1,7 @@ package org.folio.dew.batch.circulationlog; import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.StringUtils; import org.folio.dew.client.LocaleClient; import org.folio.dew.client.ServicePointClient; @@ -24,6 +25,7 @@ @Component @StepScope @RequiredArgsConstructor +@Log4j2 public class CirculationLogItemProcessor implements ItemProcessor { private final ServicePointClient servicePointClient; @@ -62,8 +64,18 @@ public void initStep(StepExecution stepExecution) { private void initTenantSpecificDateFormat() { if (format != null) return; + var localeSettings = localeClient.getLocaleSettings(); + var zoneId = localeSettings.getZoneId(); + + log.info("Initializing circulation log date format with locale settings: locale='{}', timezone='{}', currency='{}', numberingSystem='{}', resolvedZoneId='{}'.", + localeSettings.locale(), + localeSettings.timezone(), + localeSettings.currency(), + localeSettings.numberingSystem(), + zoneId.getId()); + var dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm"); - dateFormat.setTimeZone(TimeZone.getTimeZone(localeClient.getLocaleSettings().getZoneId())); + dateFormat.setTimeZone(TimeZone.getTimeZone(zoneId)); format = dateFormat; } diff --git a/src/main/java/org/folio/dew/client/LocaleClient.java b/src/main/java/org/folio/dew/client/LocaleClient.java index c852efda9..ae55ca79a 100644 --- a/src/main/java/org/folio/dew/client/LocaleClient.java +++ b/src/main/java/org/folio/dew/client/LocaleClient.java @@ -45,8 +45,8 @@ public record LocaleSettings( public ZoneId getZoneId() { try { return ZoneId.of(timezone); - } catch (DateTimeException e) { - log.error("Invalid timezone '{}', defaulting to UTC.", timezone, e); + } catch (DateTimeException | NullPointerException e) { + log.error("Invalid or missing timezone '{}', defaulting to UTC.", timezone, e); return ZoneId.of("UTC"); } } @@ -55,10 +55,23 @@ public ZoneId getZoneId() { public LocaleSettings getLocaleSettings() { try { String response = underlyingClient.getLocaleSettings(); - return objectMapper.readValue(response, LocaleSettings.class); - } catch (JsonProcessingException | FeignException | NullPointerException e) { - log.error("Failed to retrieve locale information. Defaulting to en-US, USD, UTC, latn.", e); - return new LocaleSettings("en-US", "USD", "UTC", "latn"); + LocaleSettings settings = objectMapper.readValue(response, LocaleSettings.class); + log.info("Retrieved tenant locale settings from /locale: locale='{}', timezone='{}', currency='{}', numberingSystem='{}'.", + settings.locale(), settings.timezone(), settings.currency(), settings.numberingSystem()); + return settings; + } catch (FeignException e) { + log.warn("Failed to call /locale (status={}): {}. Falling back to defaults.", e.status(), e.getMessage()); + return defaultLocaleSettings(); + } catch (JsonProcessingException e) { + log.warn("Failed to parse /locale response. Falling back to defaults.", e); + return defaultLocaleSettings(); + } catch (NullPointerException e) { + log.warn("Missing locale settings payload from /locale. Falling back to defaults.", e); + return defaultLocaleSettings(); } } + + private LocaleSettings defaultLocaleSettings() { + return new LocaleSettings("en-US", "USD", "UTC", "latn"); + } }