Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e6d5ba4
Document endpoints
EllisD-B Jan 13, 2026
7e3dbe3
Document and cica search endpoints added
EllisD-B Jan 14, 2026
8345704
Merge branch 'master' into DTSSTCI-00
ed14537 Jan 14, 2026
aba42e8
cleanup
EllisD-B Jan 14, 2026
60a5dba
Merge branch 'master' into DTSSTCI-00
EllisD-B Jan 16, 2026
5915c5e
Merge branch 'master' into DTSSTCI-00
ed14537 Feb 16, 2026
c0e899c
Merge branch 'master' into DTSSTCI-00
scottbdavey1993 Mar 18, 2026
cc36284
Merge branch 'master' into DTSSTCI-SPIKE-1441-CD
scottbdavey1993 Mar 23, 2026
5dfbdfe
update comment
scottbdavey1993 Mar 23, 2026
04127d3
update comment
scottbdavey1993 Mar 24, 2026
620c5aa
update comment
scottbdavey1993 Mar 24, 2026
d31276e
update comment
scottbdavey1993 Mar 24, 2026
36a1b78
update import
scottbdavey1993 Mar 24, 2026
5a4e440
Merge branch 'master' into DTSSTCI-SPIKE-1441-CD
scottbdavey1993 Mar 24, 2026
6b2b8ee
port override for running locally
scottbdavey1993 Mar 25, 2026
5a5ff0d
Merge branch 'DTSSTCI-SPIKE-1441-CD' of github.com:hmcts/sptribs-case…
scottbdavey1993 Mar 25, 2026
43fb96c
comments
scottbdavey1993 Mar 26, 2026
e3fff4c
Merge branch 'master' into DTSSTCI-SPIKE-1441-CD
scottbdavey1993 Mar 26, 2026
1013427
DTSSTCI-SPIKE-1441-CD: remove applicantDocuments so there are no dupl…
Mar 26, 2026
4bfa919
Merge branch 'DTSSTCI-SPIKE-1441-CD' of https://github.com/hmcts/sptr…
Mar 26, 2026
625e641
Merge branch 'DTSSTCI-SPIKE-1441-CD' of github.com:hmcts/sptribs-case…
scottbdavey1993 Mar 30, 2026
1f7cc6a
Merge branch 'master' into DTSSTCI-SPIKE-1441-CD
scottbdavey1993 Mar 30, 2026
195c0c6
remove imports
scottbdavey1993 Mar 31, 2026
4df5eba
Merge branch 'master' into DTSSTCI-1729-BE-citizen-dashboard
scottbdavey1993 Apr 9, 2026
d98e560
Merge branch 'master' into DTSSTCI-1729-BE-citizen-dashboard
scottbdavey1993 Apr 13, 2026
f1bd054
Merge branch 'master' into DTSSTCI-1729-BE-citizen-dashboard
scottbdavey1993 Apr 16, 2026
eaa1fe2
Merge branch 'master' into DTSSTCI-1729-BE-citizen-dashboard
scottbdavey1993 Apr 21, 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
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,7 @@ tasks.withType(CftlibExec).configureEach {
}

bootWithCCD {
environment 'XUI_MO_PORT', '3003'
environment 'XUI_JURISDICTIONS', 'ST_CIC'
environment 'CASE_DOCUMENT_S2S_AUTHORISED_SERVICES', 'ccd_case_document_am_api,ccd_gw,xui_webapp,ccd_data,bulk_scan_processor,em_npa_app,dg_docassembly_api,em_stitching_api,em_ccd_orchestrator,bulk_scan_orchestrator,sptribs_case_api,sptribs_frontend'
environment 'PDF_API_URL', 'http://rpe-pdf-service-aat.service.core-compute-aat.internal'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
package uk.gov.hmcts.sptribs.controllers;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import uk.gov.hmcts.reform.authorisation.generators.AuthTokenGenerator;
import uk.gov.hmcts.sptribs.cdam.model.Document;
import uk.gov.hmcts.sptribs.common.config.WebMvcConfig;
import uk.gov.hmcts.sptribs.services.cdam.CaseDocumentClientApi;
import uk.gov.hmcts.sptribs.testutil.IdamWireMock;

import java.util.UUID;

import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static uk.gov.hmcts.sptribs.testutil.TestConstants.AUTHORIZATION;
import static uk.gov.hmcts.sptribs.testutil.TestConstants.SERVICE_AUTHORIZATION;
import static uk.gov.hmcts.sptribs.testutil.TestConstants.TEST_AUTHORIZATION_TOKEN;
import static uk.gov.hmcts.sptribs.testutil.TestConstants.TEST_SERVICE_AUTH_TOKEN;

@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
@ContextConfiguration(initializers = {IdamWireMock.PropertiesInitializer.class})
class DocumentControllerIT {

private static final String DOWNLOAD_DOCUMENT_URL = "/case/document/downloadDocument/";

@Autowired
private MockMvc mockMvc;

@MockitoBean
private WebMvcConfig webMvcConfig;

@MockitoBean
private AuthTokenGenerator authTokenGenerator;

@MockitoBean
private CaseDocumentClientApi caseDocumentClientApi;

@BeforeAll
static void setUp() {
IdamWireMock.start();
}

@AfterAll
static void tearDown() {
IdamWireMock.stopAndReset();
}

@Test
void shouldDownloadDocumentSuccessfully() throws Exception {
// Given
String fileName = "test-document.pdf";
String mimeType = "application/pdf";

Document document = new Document();
document.originalDocumentName = fileName;
document.mimeType = mimeType;
UUID documentId = UUID.randomUUID();

when(authTokenGenerator.generate()).thenReturn(TEST_SERVICE_AUTH_TOKEN);
when(caseDocumentClientApi.getDocument(
eq(TEST_AUTHORIZATION_TOKEN),
eq(TEST_SERVICE_AUTH_TOKEN),
eq(documentId)
)).thenReturn(ResponseEntity.ok(document));

byte[] documentContent = "test document content".getBytes();
when(caseDocumentClientApi.getDocumentBinary(
eq(TEST_AUTHORIZATION_TOKEN),
eq(TEST_SERVICE_AUTH_TOKEN),
eq(documentId)
)).thenReturn(ResponseEntity.ok(documentContent));

// When & Then
mockMvc.perform(get(DOWNLOAD_DOCUMENT_URL + documentId)
.header(AUTHORIZATION, TEST_AUTHORIZATION_TOKEN)
.header(SERVICE_AUTHORIZATION, TEST_SERVICE_AUTH_TOKEN))
.andExpect(status().isOk())
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, mimeType))
.andExpect(header().string("original-file-name", fileName))
.andExpect(content().bytes(documentContent));
}

@Test
void shouldReturn500WhenDocumentNotFound() throws Exception {
// Given
UUID documentId = UUID.randomUUID();

when(authTokenGenerator.generate()).thenReturn(TEST_SERVICE_AUTH_TOKEN);
when(caseDocumentClientApi.getDocument(
eq(TEST_AUTHORIZATION_TOKEN),
eq(TEST_SERVICE_AUTH_TOKEN),
eq(documentId)
)).thenReturn(ResponseEntity.ok(null));

// When & Then
mockMvc.perform(get(DOWNLOAD_DOCUMENT_URL + documentId)
.header(AUTHORIZATION, TEST_AUTHORIZATION_TOKEN)
.header(SERVICE_AUTHORIZATION, TEST_SERVICE_AUTH_TOKEN))
.andExpect(status().isInternalServerError());
}

@Test
void shouldReturn500WhenDocumentBinaryNotFound() throws Exception {
// Given
String fileName = "test-document.pdf";
String mimeType = "application/pdf";

Document document = new Document();
document.originalDocumentName = fileName;
document.mimeType = mimeType;
UUID documentId = UUID.randomUUID();

when(authTokenGenerator.generate()).thenReturn(TEST_SERVICE_AUTH_TOKEN);
when(caseDocumentClientApi.getDocument(
eq(TEST_AUTHORIZATION_TOKEN),
eq(TEST_SERVICE_AUTH_TOKEN),
eq(documentId)
)).thenReturn(ResponseEntity.ok(document));
when(caseDocumentClientApi.getDocumentBinary(
eq(TEST_AUTHORIZATION_TOKEN),
eq(TEST_SERVICE_AUTH_TOKEN),
eq(documentId)
)).thenReturn(ResponseEntity.ok(null));

// When & Then
mockMvc.perform(get(DOWNLOAD_DOCUMENT_URL + documentId)
.header(AUTHORIZATION, TEST_AUTHORIZATION_TOKEN)
.header(SERVICE_AUTHORIZATION, TEST_SERVICE_AUTH_TOKEN))
.andExpect(status().isInternalServerError());
}

@Test
void shouldReturn500WhenApiCallFails() throws Exception {
// Given
UUID documentId = UUID.randomUUID();

when(authTokenGenerator.generate()).thenReturn(TEST_SERVICE_AUTH_TOKEN);
when(caseDocumentClientApi.getDocument(
eq(TEST_AUTHORIZATION_TOKEN),
eq(TEST_SERVICE_AUTH_TOKEN),
eq(documentId)
)).thenThrow(new RuntimeException("API error"));

// When & Then
mockMvc.perform(get(DOWNLOAD_DOCUMENT_URL + documentId)
.header(AUTHORIZATION, TEST_AUTHORIZATION_TOKEN)
.header(SERVICE_AUTHORIZATION, TEST_SERVICE_AUTH_TOKEN))
.andExpect(status().isInternalServerError());
}

@Test
void shouldReturn500ForInvalidDocumentId() throws Exception {
// Given
String invalidDocumentId = "invalid-uuid-format";

// When & Then
mockMvc.perform(get(DOWNLOAD_DOCUMENT_URL + invalidDocumentId)
.header(AUTHORIZATION, TEST_AUTHORIZATION_TOKEN)
.header(SERVICE_AUTHORIZATION, TEST_SERVICE_AUTH_TOKEN))
.andExpect(status().isInternalServerError());
}

@Test
void shouldDownloadDocumentWithDifferentMimeTypes() throws Exception {
// Given
String fileName = "test-document.html";
String mimeType = "text/html";

Document document = new Document();
document.originalDocumentName = fileName;
document.mimeType = mimeType;

UUID documentId = UUID.randomUUID();

when(authTokenGenerator.generate()).thenReturn(TEST_SERVICE_AUTH_TOKEN);
when(caseDocumentClientApi.getDocument(
eq(TEST_AUTHORIZATION_TOKEN),
eq(TEST_SERVICE_AUTH_TOKEN),
eq(documentId)
)).thenReturn(ResponseEntity.ok(document));

byte[] documentContent = "<html><body>Test</body></html>".getBytes();
when(caseDocumentClientApi.getDocumentBinary(
eq(TEST_AUTHORIZATION_TOKEN),
eq(TEST_SERVICE_AUTH_TOKEN),
eq(documentId)
)).thenReturn(ResponseEntity.ok(documentContent));

// When & Then
mockMvc.perform(get(DOWNLOAD_DOCUMENT_URL + documentId)
.header(AUTHORIZATION, TEST_AUTHORIZATION_TOKEN)
.header(SERVICE_AUTHORIZATION, TEST_SERVICE_AUTH_TOKEN))
.andExpect(status().isOk())
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, mimeType))
.andExpect(header().string("original-file-name", fileName))
.andExpect(content().bytes(documentContent));
}
}







Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package uk.gov.hmcts.sptribs.ciccase.repository;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Repository;
import uk.gov.hmcts.sptribs.controllers.model.CicaCaseResponse;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import java.util.Optional;

@Slf4j
@Repository
@RequiredArgsConstructor
public class CicaCaseRepository {

private static final TypeReference<Map<String, JsonNode>> JSON_NODE_MAP = new TypeReference<>() { };

private static final String CASE_TYPE = "CriminalInjuriesCompensation";
private static final String JURISDICTION = "ST_CIC";
private static final String CICA_REFERENCE_JSON_PATH = "{editCicaCaseDetails,cicaReferenceNumber}";

private final NamedParameterJdbcTemplate namedParameterJdbcTemplate;
private final ObjectMapper objectMapper;

/**
* Finds a case by CICA reference number.
* Searches in the data JSONB column for the editCicaCaseDetails.cicaReferenceNumber field.
* Returns the most recently modified case if multiple matches are found.
*
* @param cicaReference the CICA reference number to search for (case-insensitive)
* @return Optional containing the case if found, empty otherwise
*/
public Optional<CicaCaseResponse> findByCicaReference(String cicaReference) {
log.info("Searching for case with CICA reference: {}", cicaReference);

var params = Map.of(
"cicaReference", cicaReference.toUpperCase(),
"caseType", CASE_TYPE,
"jurisdiction", JURISDICTION
);

String sql = """
SELECT
c.id,
c.reference,
c.state,
c.data::text AS case_data,
c.last_modified
FROM ccd.case_data c
WHERE c.case_type_id = :caseType
AND c.jurisdiction = :jurisdiction
AND UPPER(c.data #>> '%s') = :cicaReference
ORDER BY c.last_modified DESC
LIMIT 1
""".formatted(CICA_REFERENCE_JSON_PATH);

List<CicaCaseResponse> results = namedParameterJdbcTemplate.query(
sql,
params,
(rs, rowNum) -> mapToCicaCaseResponse(rs)
);

if (results.isEmpty()) {
log.info("No case found with CICA reference: {}", cicaReference);
return Optional.empty();
}

log.info("Found case with CICA reference: {}", cicaReference);
return Optional.of(results.get(0));
}

@SneakyThrows
private CicaCaseResponse mapToCicaCaseResponse(ResultSet rs) throws SQLException {
Long reference = rs.getObject("reference", Long.class);
String state = rs.getString("state");
String caseDataJson = rs.getString("case_data");

Map<String, JsonNode> caseData = objectMapper.readValue(caseDataJson, JSON_NODE_MAP);

return CicaCaseResponse.builder()
.id(String.valueOf(reference))
.state(state)
.data(caseData)
.build();
}
}



Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package uk.gov.hmcts.sptribs.ciccase.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import uk.gov.hmcts.sptribs.ciccase.repository.CicaCaseRepository;
import uk.gov.hmcts.sptribs.controllers.model.CicaCaseResponse;
import uk.gov.hmcts.sptribs.exception.CaseNotFoundException;

import java.util.regex.Pattern;

@Slf4j
@Service
@RequiredArgsConstructor
public class CicaCaseService {

private static final Pattern CICA_REFERENCE_PATTERN = Pattern.compile("^[XG]\\d+$", Pattern.CASE_INSENSITIVE);

private final CicaCaseRepository cicaCaseRepository;

/**
* Retrieves a case by CICA reference number.
*
* @param cicaReference the CICA reference number (must start with X or G followed by digits)
* @return the case details
* @throws CaseNotFoundException if no case is found with the given reference
* @throws IllegalArgumentException if the reference format is invalid
*/
public CicaCaseResponse getCaseByCicaReference(String cicaReference) {
log.info("Looking up case by CICA reference: {}", cicaReference);

validateCicaReferenceFormat(cicaReference);

return cicaCaseRepository.findByCicaReference(cicaReference)
.orElseThrow(() -> {
log.warn("No case found for CICA reference: {}", cicaReference);
return new CaseNotFoundException("No case found with CICA reference: " + cicaReference);
});
}

private void validateCicaReferenceFormat(String cicaReference) {
if (cicaReference == null || cicaReference.isBlank()) {
throw new IllegalArgumentException("CICA reference cannot be null or empty");
}

if (!CICA_REFERENCE_PATTERN.matcher(cicaReference).matches()) {
log.warn("Invalid CICA reference format: {}", cicaReference);
throw new IllegalArgumentException(
"Invalid CICA reference format. Reference must start with X or G followed by digits (e.g., X12345, G98765)"
);
}
}
}




Loading
Loading