Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d93ed86
add latest fta id field
gemmatalbot Dec 2, 2025
a48f448
add overdue Response
gemmatalbot Dec 4, 2025
129544f
add taskCreatedForRequest field
gemmatalbot Dec 4, 2025
385d6c2
Merge branch 'master' into SSCSCI-2275-addLatestFtaId
gemmatalbot Dec 5, 2025
2852df2
add wa token
gemmatalbot Dec 16, 2025
4365b86
add wa token
gemmatalbot Dec 16, 2025
4e53391
logs
gemmatalbot Dec 18, 2025
59f5f88
Merge branch 'master' into SSCSCI-2275-addLatestFtaId
gemmatalbot Dec 18, 2025
d96c5f2
update fta communication id
gemmatalbot Dec 19, 2025
d57770c
update overdueResponse
gemmatalbot Dec 19, 2025
894a9d3
upgrade elastic search
gemmatalbot Jan 8, 2026
da9eb64
update idamServiceTest
gemmatalbot Jan 9, 2026
dbd4cb7
business days calculator
gemmatalbot Jan 9, 2026
cf42fe1
add public method
gemmatalbot Jan 9, 2026
24cdd65
add test
gemmatalbot Jan 9, 2026
f70e81a
move CachedHolidayClient and BusinessDaysCalculatorService to uk.gov.…
hashimalisolirius Jan 16, 2026
6d871a9
move CachedHolidayClient and BusinessDaysCalculatorService to uk.gov.…
hashimalisolirius Jan 16, 2026
e775ffb
update getHolidays
gemmatalbot Jan 19, 2026
af20835
update taskCreatedForRequest type
gemmatalbot Jan 23, 2026
5af10b8
Merge branch 'master' into SSCSCI-2275-addLatestFtaId
gemmatalbot Jan 26, 2026
1b295ce
Merge branch 'master' into SSCSCI-2275-addLatestFtaId
gemmatalbot Jan 28, 2026
1ad8387
idam token config
gemmatalbot Jan 28, 2026
94f6400
idam token retry
gemmatalbot Feb 2, 2026
7f2fea3
Merge branch 'master' into SSCSCI-2275-addLatestFtaId
gemmatalbot Feb 23, 2026
209366e
Merge branch 'master' into SSCSCI-2275-addLatestFtaId
gemmatalbot Feb 25, 2026
d5a2524
Merge branch 'master' into SSCSCI-2275-addLatestFtaId
gemmatalbot Feb 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ dependencies {

implementation group: 'org.json', name: 'json', version: '20250517'

implementation group: 'org.elasticsearch', name: 'elasticsearch', version: '8.19.4'
implementation group: 'org.elasticsearch', name: 'elasticsearch', version: '8.19.9'

implementation group: 'org.springframework.retry', name: 'spring-retry', version: '2.0.12'
implementation group: 'io.github.openfeign.form', name: 'feign-form', version: '3.8.0'
Expand Down Expand Up @@ -224,6 +224,9 @@ dependencies {

implementation group: 'commons-io', name: 'commons-io', version: '2.20.0'

implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.12.0'
implementation group: 'net.objectlab.kit', name: 'datecalc-jdk8', version: '1.4.8'

implementation group: 'javax.validation', name: 'validation-api', version: '2.0.1.Final'

implementation group: 'com.networknt', name: 'json-schema-validator', version: '1.5.8'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public class CommunicationRequestDetails {
private CommunicationRequestTopic requestTopic;
private String requestMessage;
private CommunicationRequestReply requestReply;
private YesNo taskCreatedForRequest;

@Override
public String toString() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ public enum EventType {
NON_COMPLIANT_SEND_TO_INTERLOC("nonCompliantSendToInterloc", 0, false),
NOTIFICATION_SENT("notificationSent", 0, false),
NOT_LISTABLE("notListable", 0, false),
OVERDUE_FTA_RESPONSE("overdueFtaResponse", 0, false),
PANEL_UPDATE("panelUpdate", 0, false),
PAST_HEARING_BOOKED("pastHearingBooked", "pastHearingBooked", 10, true),
PERMISSION_TO_APPEAL_GRANTED("permissionToAppealGranted", 0, true),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
@JsonInclude(JsonInclude.Include.NON_NULL)
public class FtaCommunicationFields {

private String waTaskFtaCommunicationId;
private List<CommunicationRequest> ftaCommunications;
private FtaRequestType ftaRequestType;
private DynamicList ftaRequestsDl;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package uk.gov.hmcts.reform.sscs.client;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.time.LocalDate;
import java.util.HashSet;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class CachedHolidayClient {

private final OkHttpClient httpClient;
private Set<LocalDate> cachedHolidays;
private static final String HOLIDAY_API_URL = "https://www.gov.uk/bank-holidays.json";

public CachedHolidayClient(OkHttpClient httpClient) {
this.httpClient = httpClient;
}

public synchronized Set<LocalDate> getHolidays() throws IOException {
if (cachedHolidays == null) {
cachedHolidays = fetchHolidaysFromApi();
}
return cachedHolidays;
}

private Set<LocalDate> fetchHolidaysFromApi() throws IOException {
Request request = new Request.Builder().url(HOLIDAY_API_URL).build();

try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("Response unsuccessful: " + response);
}

Set<LocalDate> holidays = new HashSet<>();
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(response.body().string());

JsonNode events = root.path("england-and-wales").path("events");
for (JsonNode event : events) {
String dateStr = event.path("date").asText();
holidays.add(LocalDate.parse(dateStr));
}

return holidays;
}
}
}
52 changes: 52 additions & 0 deletions src/main/java/uk/gov/hmcts/reform/sscs/idam/IdamService.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,16 @@ public class IdamService {
@Value("${idam.oauth2.user.password}")
private String idamOauth2UserPassword;

@Value("${idam.oauth2.waUser.email}")
private String idamOauth2WaUserEmail;

@Value("${idam.oauth2.waUser.password}")
private String idamOauth2WaUserPassword;

// Tactical idam token caching solution implemented
// SSCS-5895 - will deliver the strategic caching solution
private String cachedToken;
private String cachedWaToken;

@Autowired
IdamService(AuthTokenGenerator authTokenGenerator, IdamClient idamClient) {
Expand Down Expand Up @@ -65,6 +72,12 @@ public String getIdamOauth2Token() {
return cachedToken;
}

@Retryable
public String getWaIdamOauth2Token() {
cachedWaToken = getOpenAccessTokenForWaUser();
return cachedWaToken;
}

@Retryable
public String getOpenAccessToken() {
try {
Expand All @@ -78,10 +91,24 @@ public String getOpenAccessToken() {
}
}

@Retryable
public String getOpenAccessTokenForWaUser() {
try {
log.info("Requesting Wa idam access token from Open End Point");
String accessToken = idamClient.getAccessToken(idamOauth2WaUserEmail, idamOauth2WaUserPassword);
log.info("Requesting Wa idam access token successful");
return accessToken;
} catch (Exception e) {
log.error("Requesting Wa idam token failed: " + e.getMessage());
throw e;
}
}

@Scheduled(fixedRate = ONE_HOUR)
public void evictCacheAtIntervals() {
log.info("Evicting idam token cache");
cachedToken = null;
cachedWaToken = null;
}

@Retryable(backoff = @Backoff(delay = 15000L, multiplier = 1.0, random = true))
Expand All @@ -108,4 +135,29 @@ public IdamTokens getIdamTokens() {
.roles(userDetails.getRoles())
.build();
}

@Retryable(backoff = @Backoff(delay = 15000L, multiplier = 1.0, random = true))
public IdamTokens getIdamWaTokens() {
String WaIdamOauth2Token;

if (StringUtils.isEmpty(cachedWaToken)) {
log.info("No cached Wa IDAM token found, requesting from IDAM service.");
log.info("Attempting to obtain Wa token, retry attempt {}", atomicInteger.getAndIncrement());
WaIdamOauth2Token = getWaIdamOauth2Token();
} else {
atomicInteger.set(1);
log.info("Using cached Wa IDAM token.");
WaIdamOauth2Token = cachedWaToken;
}

UserDetails waUserDetails = getUserDetails(WaIdamOauth2Token);

return IdamTokens.builder()
.idamOauth2Token(WaIdamOauth2Token)
.serviceAuthorization(generateServiceAuthorization())
.userId(waUserDetails.getId())
.email(waUserDetails.getEmail())
.roles(waUserDetails.getRoles())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package uk.gov.hmcts.reform.sscs.service;

import java.io.IOException;
import java.time.LocalDate;
import java.time.ZonedDateTime;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import net.objectlab.kit.datecalc.common.DefaultHolidayCalendar;
import net.objectlab.kit.datecalc.jdk8.LocalDateKitCalculatorsFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import uk.gov.hmcts.reform.sscs.client.CachedHolidayClient;

@Slf4j
@Service
public class BusinessDaysCalculatorService {

private final CachedHolidayClient cachedHolidayClient;

@Autowired
public BusinessDaysCalculatorService(CachedHolidayClient cachedHolidayClient) {
this.cachedHolidayClient = cachedHolidayClient;
}

public ZonedDateTime getBusinessDay(ZonedDateTime startDateTime, int numberOfBusinessDays) throws IOException {
initialiseHolidays();
LocalDate startDate = startDateTime.toLocalDate();
return calculateBusinessDay(startDate, numberOfBusinessDays, startDateTime);
}

public LocalDate getBusinessDay(LocalDate date, int numberOfBusinessDays) throws IOException {
initialiseHolidays();
return calculateBusinessDay(date, numberOfBusinessDays);
}

public LocalDate getBusinessDayInPast(LocalDate date, int numberOfBusinessDays) throws IOException {
initialiseHolidays();
return calculateBusinessDayInPast(date, numberOfBusinessDays);
}

private void initialiseHolidays() throws IOException {
Set<LocalDate> holidays = cachedHolidayClient.getHolidays();
DefaultHolidayCalendar<LocalDate> ukCalendar = new DefaultHolidayCalendar<>();
ukCalendar.setHolidays(holidays);
LocalDateKitCalculatorsFactory.getDefaultInstance().registerHolidays("UK", ukCalendar);
}

private ZonedDateTime calculateBusinessDay(LocalDate startDate, int numberOfBusinessDays, ZonedDateTime startDateTime) {
return ZonedDateTime.of(
calculateBusinessDay(startDate, numberOfBusinessDays),
startDateTime.toLocalTime(),
startDateTime.getZone()
);
}

private LocalDate calculateBusinessDay(LocalDate startDate, int numberOfBusinessDays) {
return LocalDateKitCalculatorsFactory.forwardCalculator("UK")
.setStartDate(startDate)
.moveByBusinessDays(numberOfBusinessDays)
.getCurrentBusinessDate();
}

private LocalDate calculateBusinessDayInPast(LocalDate startDate, int numberOfBusinessDays) {
return LocalDateKitCalculatorsFactory.backwardCalculator("UK")
.setStartDate(startDate)
.moveByBusinessDays(-numberOfBusinessDays)
.getCurrentBusinessDate();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package uk.gov.hmcts.reform.sscs.client;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

import java.io.IOException;
import java.time.LocalDate;
import java.util.Set;
import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

class CachedHolidayClientTest {

@Mock
private OkHttpClient httpClient;

@Mock
private Call mockCall;

@Mock
private Response response;

@Mock
private ResponseBody responseBody;

private CachedHolidayClient cachedHolidayClient;

@BeforeEach
void setUp() throws IOException {
MockitoAnnotations.openMocks(this);
when(httpClient.newCall(any(Request.class))).thenReturn(mockCall);
when(mockCall.execute()).thenReturn(response);
when(response.isSuccessful()).thenReturn(true);
when(response.body()).thenReturn(responseBody);
when(responseBody.string()).thenReturn("{\"england-and-wales\":{\"events\":[{\"date\":\"2024-12-25\"},{\"date\":\"2024-12-26\"}]}}");

cachedHolidayClient = new CachedHolidayClient(httpClient);
}

@Test
void shouldFetchHolidaysFromApi() throws IOException {
Set<LocalDate> holidays = cachedHolidayClient.getHolidays();

assertNotNull(holidays);
assertEquals(2, holidays.size());
assertTrue(holidays.contains(LocalDate.of(2024, 12, 25)));
assertTrue(holidays.contains(LocalDate.of(2024, 12, 26)));

// Verify API call was made
verify(mockCall, times(1)).execute();
}

@Test
void shouldReturnCachedHolidaysOnSubsequentCalls() throws IOException {
Set<LocalDate> holidaysFirstCall = cachedHolidayClient.getHolidays();
Set<LocalDate> holidaysSecondCall = cachedHolidayClient.getHolidays();

assertSame(holidaysFirstCall, holidaysSecondCall);

// Verify API call was made only once
verify(mockCall, times(1)).execute();
}

@Test
void shouldThrowIoExceptionForUnsuccessfulResponse() throws IOException {
when(response.isSuccessful()).thenReturn(false);

IOException exception = assertThrows(IOException.class, () -> cachedHolidayClient.getHolidays());
assertEquals("Response unsuccessful: " + response, exception.getMessage());
}

@Test
void shouldThrowIoExceptionForFailedApiCall() throws IOException {
when(mockCall.execute()).thenThrow(new IOException("Network error"));

IOException exception = assertThrows(IOException.class, () -> cachedHolidayClient.getHolidays());
assertEquals("Network error", exception.getMessage());
}
}
Loading