diff --git a/data-api/open-api-specification.yml b/data-api/open-api-specification.yml index 6992260..062666c 100644 --- a/data-api/open-api-specification.yml +++ b/data-api/open-api-specification.yml @@ -1097,7 +1097,7 @@ paths: schema: type: 'string' example: 'Male' - - name: 'case-reference-number' + - name: 'client-reference-number' in: 'query' schema: type: 'string' diff --git a/data-service/src/integrationTest/java/uk/gov/laa/ccms/data/repository/ClientDetailRepositoryIntegrationTest.java b/data-service/src/integrationTest/java/uk/gov/laa/ccms/data/repository/ClientDetailRepositoryIntegrationTest.java index 64bebb7..cda2e2d 100644 --- a/data-service/src/integrationTest/java/uk/gov/laa/ccms/data/repository/ClientDetailRepositoryIntegrationTest.java +++ b/data-service/src/integrationTest/java/uk/gov/laa/ccms/data/repository/ClientDetailRepositoryIntegrationTest.java @@ -1,13 +1,18 @@ package uk.gov.laa.ccms.data.repository; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.LocalDate; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; import org.springframework.test.context.jdbc.Sql; import org.springframework.test.context.jdbc.Sql.ExecutionPhase; import org.springframework.test.context.jdbc.SqlMergeMode; @@ -28,21 +33,23 @@ public class ClientDetailRepositoryIntegrationTest implements OracleIntegrationT @Autowired private ClientDetailRepository repository; - @Test - @DisplayName("Should return two notifications") - void shouldReturnTwoNotifications() { + @ParameterizedTest + @NullAndEmptySource + @DisplayName("Should return two client details") + void shouldReturnTwoClientDetails(String emptyStringInput) { // Given // When - Page result = repository.findAll(null, null, null, - null, null, null, null, + Page result = repository.findAll(emptyStringInput, emptyStringInput, + null, + emptyStringInput, emptyStringInput, emptyStringInput, emptyStringInput, PageRequest.of(0, 10)); // Then assertEquals(2L, result.getTotalElements()); } @Test - @DisplayName("Should return single notification") - void shouldReturnSingleNotification(){ + @DisplayName("Should return single client detail") + void shouldReturnSingleClientDetail(){ // Given // When Page result = repository.findAll(null, null, null, @@ -53,4 +60,222 @@ void shouldReturnSingleNotification(){ assertEquals(2, result.getTotalPages()); assertEquals(2L, result.getTotalElements()); } + + @Test + @DisplayName("Should return single client filter equals first name") + void shouldReturnSingleClientFilterEqualsFirstName(){ + // Given + String firstName = "john"; + // When + Page result = repository.findAll(firstName, + null, null, null, null, + null, null, PageRequest.of(0, 10)); + // Then + assertEquals(1L, result.getContent().size()); + assertEquals("John", result.getContent().getFirst().getFirstName()); + } + + @Test + @DisplayName("Should return multiple clients filter like first name") + void shouldReturnMultipleClientFilterLikeFirstName(){ + // Given + String firstName = "j"; + // When + Page result = repository.findAll(firstName, + null, null, null, null, + null, null, PageRequest.of(0, 10)); + // Then + assertEquals(2L, result.getContent().size()); + assertEquals("John", result.getContent().getFirst().getFirstName()); + assertEquals("Jane", result.getContent().get(1).getFirstName()); + } + + @Test + @DisplayName("Should return single client filter equals surname") + void shouldReturnSingleClientFilterEqualsSurname(){ + // Given + String surname = "doe"; + // When + Page result = repository.findAll(null, + surname, null, null, null, + null, null, PageRequest.of(0, 10)); + // Then + assertEquals(1L, result.getContent().size()); + assertEquals("Doe", result.getContent().getFirst().getSurname()); + } + + @Test + @DisplayName("Should return mulitple clients filter like surname") + void shouldReturnMultipleClientsFilterLikeSurname(){ + // Given + String surname = "oe"; + // When + Page result = repository.findAll(null, + surname, null, null, null, + null, null, PageRequest.of(0, 10)); + // Then + assertEquals(2L, result.getContent().size()); + assertEquals("Doe", result.getContent().getFirst().getSurname()); + assertEquals("Roe", result.getContent().get(1).getSurname()); + } + + @Test + @DisplayName("Should return single client filer equals date of birth") + void shouldReturnSingleClientFilterEqualsDateOfBirth(){ + // Given + LocalDate dateOfBirth = LocalDate.of(1985, 06, 15); + // When + Page result = repository.findAll(null, null, dateOfBirth, + null, null, null, null, + PageRequest.of(0, 10)); + // Then + assertEquals(1L, result.getContent().size()); + assertEquals(dateOfBirth, result.getContent().getFirst().getDateOfBirth()); + } + + @Test + @DisplayName("Should return single client equals gender") + void shouldReturnSingleClientFilterEqualsGender(){ + // Given + String gender = "male"; + // When + Page result = repository.findAll(null, null, null, + gender, null, null, null, + PageRequest.of(0, 10)); + // Then + assertEquals(1L, result.getContent().size()); + assertEquals("Male", result.getContent().getFirst().getGender()); + } + + @Test + @DisplayName("Should return single client filter equals gender alt") + void shouldReturnSingleClientFilterEqualsGenderAlt(){ + // Given + String gender = "FEMALE"; + // When + Page result = repository.findAll(null, null, + null, gender, null, null, + null, PageRequest.of(0, 10)); + // Then + assertEquals(1L, result.getContent().size()); + assertEquals("Female", result.getContent().getFirst().getGender()); + } + + @Test + @DisplayName("Should return single client filter equals client reference number") + void shouldReturnSingleClientFilterEqualsClientReferenceNumber(){ + // Given + String clientReferenceNumber = "100000000000001"; + // When + Page result = repository.findAll(null, null, + null, null, clientReferenceNumber, + null, null, PageRequest.of(0, 10)); + // Then + assertEquals(1L, result.getContent().size()); + assertEquals(100000000000001L, result.getContent().getFirst() + .getClientReferenceNumber()); + } + + @Test + @DisplayName("Should return multiple client filter like client reference number") + void shouldReturnMultipleClientFilterLikeClientReferenceNumber(){ + // Given + String clientReferenceNumber = "10000000000"; + // When + Page result = repository.findAll(null, null, + null, null, clientReferenceNumber, + null, null, PageRequest.of(0, 10)); + // Then + assertEquals(2L, result.getContent().size()); + assertEquals(100000000000001L, result.getContent().getFirst() + .getClientReferenceNumber()); + assertEquals(100000000000002L, result.getContent().get(1) + .getClientReferenceNumber()); + } + + @Test + @DisplayName("Should return single client filter equals home office reference number") + void shouldReturnSingleClientFilterEqualsHomeOfficeReferenceNumber(){ + // Given + String homeOfficeNumber = "HO123456"; + // When + Page result = repository.findAll(null, null, + null, null, null, + homeOfficeNumber, null, PageRequest.of(0, 10)); + // Then + assertEquals(1L, result.getContent().size()); + assertEquals("HO123456", result.getContent().getFirst() + .getHomeOfficeNumber()); + } + + @Test + @DisplayName("Should return multiple clients filter like home office reference number") + void shouldReturnMultipleClientsFilterLikeHomeOfficeReferenceNumber(){ + // Given + String homeOfficeNumber = "HO"; + // When + Page result = repository.findAll(null, null, + null, null, null, + homeOfficeNumber, null, PageRequest.of(0, 10)); + // Then + assertEquals(2L, result.getContent().size()); + assertEquals("HO123456", result.getContent().getFirst() + .getHomeOfficeNumber()); + assertEquals("HO987654", result.getContent().get(1) + .getHomeOfficeNumber()); + } + + @Test + @DisplayName("Should return single client filter equals national insurance number") + void shouldReturnSingleClientFilterEqualsNationalInsuranceNumber(){ + // Given + String nationalInsuranceNumber = "AB123456C"; + // When + Page result = repository.findAll(null, null, + null, null, null, + null, nationalInsuranceNumber, + PageRequest.of(0, 10)); + // Then + assertEquals(1L, result.getContent().size()); + assertEquals("AB123456C", result.getContent().getFirst() + .getNationalInsuranceNumber()); + } + + @Test + @DisplayName("Should return multiple clients filter like national insurance number") + void shouldReturnMultipleClientsFilterLikeNationalInsuranceNumber(){ + // Given + String nationalInsuranceNumber = "123"; + // When + Page result = repository.findAll(null, null, + null, null, null, + null, nationalInsuranceNumber, + PageRequest.of(0, 10)); + // Then + assertEquals(2L, result.getContent().size()); + assertEquals("AB123456C", result.getContent().getFirst() + .getNationalInsuranceNumber()); + assertEquals("CD123654E", result.getContent().get(1) + .getNationalInsuranceNumber()); + } + + @Test + @DisplayName("Should sort by first name") + void shouldSortByFirstName(){ + // Given + // When + PageRequest pageable = PageRequest.of(0, 10, + Sort.by(Sort.Order.asc("FIRSTNAME"))); + Page result = repository.findAll(null, null, + null, null, null, + null, null, + pageable); + // Then + assertEquals(2L, result.getContent().size()); + assertEquals("Jane", result.getContent().getFirst() + .getFirstName()); + assertEquals("John", result.getContent().get(1) + .getFirstName()); + } + } diff --git a/data-service/src/integrationTest/resources/sql/get_client_details_create_schema.sql b/data-service/src/integrationTest/resources/sql/get_client_details_create_schema.sql index 60b4fb7..804bfe9 100644 --- a/data-service/src/integrationTest/resources/sql/get_client_details_create_schema.sql +++ b/data-service/src/integrationTest/resources/sql/get_client_details_create_schema.sql @@ -43,7 +43,7 @@ INSERT INTO XXCCMS.XXCCMS_GET_CLIENT_DETAILS_V (CLIENT_REFERENCE_NUMBER, TITLE, CORRESPONDENCE_LANGUAGE, ETHNIC_MONITORING, NO_FIX_ABODE, NI_NUMBER, HOME_OFFICE_NUMBER) -VALUES (100000000000002, 'Ms.', 'Jane', 'Doe', 'Johnson', +VALUES (100000000000002, 'Ms.', 'Jane', 'Roe', 'Johnson', TO_DATE('1990-03-21', 'YYYY-MM-DD'), 'Female', 'CAN', 'Married', 'Phone', 'Visual Impairment', 'French', - 'Caucasian', 'No', 'CD987654E', 'HO987654'); \ No newline at end of file + 'Caucasian', 'No', 'CD123654E', 'HO987654'); \ No newline at end of file diff --git a/data-service/src/main/java/uk/gov/laa/ccms/data/controller/ClientsController.java b/data-service/src/main/java/uk/gov/laa/ccms/data/controller/ClientsController.java index 9f25eb5..e6e7335 100644 --- a/data-service/src/main/java/uk/gov/laa/ccms/data/controller/ClientsController.java +++ b/data-service/src/main/java/uk/gov/laa/ccms/data/controller/ClientsController.java @@ -50,7 +50,7 @@ public ResponseEntity getClientTransactionStatus(String trans @Override public ResponseEntity getClients(String firstName, String surname, - LocalDate dateOfBirth, String gender, String caseReferenceNumber, String homeOfficeReference, + LocalDate dateOfBirth, String gender, String clientReferenceNumber, String homeOfficeReference, String nationalInsuranceNumber, Pageable pageable) { return null; } diff --git a/data-service/src/main/java/uk/gov/laa/ccms/data/repository/BaseEntityManagerRepository.java b/data-service/src/main/java/uk/gov/laa/ccms/data/repository/BaseEntityManagerRepository.java new file mode 100644 index 0000000..b6b4189 --- /dev/null +++ b/data-service/src/main/java/uk/gov/laa/ccms/data/repository/BaseEntityManagerRepository.java @@ -0,0 +1,28 @@ +package uk.gov.laa.ccms.data.repository; + +import jakarta.persistence.EntityManager; +import java.util.StringJoiner; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; + +@RequiredArgsConstructor +public abstract class BaseEntityManagerRepository { + + protected final EntityManager entityManager; + + protected static boolean stringNotEmpty(String value) { + return value != null && !value.isEmpty(); + } + + protected static String getSortSql(Pageable pageable) { + if (pageable.getSort().isEmpty()) { + return " "; + } + + StringJoiner sortJoiner = new StringJoiner(", ", " ORDER BY ", " "); + pageable.getSort().forEach(order -> + sortJoiner.add(order.getProperty() + " " + order.getDirection().name())); + return sortJoiner.toString(); + } + +} diff --git a/data-service/src/main/java/uk/gov/laa/ccms/data/repository/CaseSearchRepository.java b/data-service/src/main/java/uk/gov/laa/ccms/data/repository/CaseSearchRepository.java index e0ca06f..7b732f7 100644 --- a/data-service/src/main/java/uk/gov/laa/ccms/data/repository/CaseSearchRepository.java +++ b/data-service/src/main/java/uk/gov/laa/ccms/data/repository/CaseSearchRepository.java @@ -5,7 +5,6 @@ import java.util.List; import java.util.Objects; import java.util.StringJoiner; -import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; @@ -24,17 +23,22 @@ *

It relies on {@link EntityManager} to execute native SQL queries and doesn't use standard * Spring Data repositories. All queries are read-only and do not modify the database state.

* + *

Extends {@link BaseEntityManagerRepository} which contains helper methods + * for helping build a SQL query and {@link EntityManager}.

+ * * @see Page * @see CaseSearch * @see EntityManager + * @see BaseEntityManagerRepository * * @author Jamie Briggs */ @Repository -@RequiredArgsConstructor -public class CaseSearchRepository { +public class CaseSearchRepository extends BaseEntityManagerRepository{ - private final EntityManager entityManager; + public CaseSearchRepository(EntityManager entityManager) { + super(entityManager); + } /** * Retrieves a paginated and filtered list of case search records based on the given parameters. @@ -95,19 +99,19 @@ private static String getFilterSql(long providerFirmPartyId, String caseReferenc // Provider firm party id sj.add("WHERE PROVIDER_FIRM_PARTY_ID = " + providerFirmPartyId); // Case reference number - if (!Objects.isNull(caseReferenceNumber) && !caseReferenceNumber.isBlank()) { + if (stringNotEmpty(caseReferenceNumber)) { sj.add("LSC_CASE_REFERENCE LIKE '%" + caseReferenceNumber + "%'"); } // Provider case reference - if (!Objects.isNull(providerCaseReference) && !providerCaseReference.isBlank()) { + if (stringNotEmpty(providerCaseReference)) { sj.add("UPPER(PROVIDER_CASE_REFERENCE) LIKE '%" + providerCaseReference.toUpperCase() + "%'"); } // Case status - if (!Objects.isNull(caseStatus) && !caseStatus.isBlank()) { + if (stringNotEmpty(caseStatus)) { sj.add("ACTUAL_CASE_STATUS = '" + caseStatus + "'"); } // Surname - if (!Objects.isNull(clientSurname) && !clientSurname.isBlank()) { + if (stringNotEmpty(clientSurname)) { sj.add("UPPER(PERSON_LAST_NAME) LIKE '%" + clientSurname.toUpperCase() + "%'"); } // Fee earner party ID diff --git a/data-service/src/main/java/uk/gov/laa/ccms/data/repository/ClientDetailRepository.java b/data-service/src/main/java/uk/gov/laa/ccms/data/repository/ClientDetailRepository.java index 4a74024..f8103ed 100644 --- a/data-service/src/main/java/uk/gov/laa/ccms/data/repository/ClientDetailRepository.java +++ b/data-service/src/main/java/uk/gov/laa/ccms/data/repository/ClientDetailRepository.java @@ -4,7 +4,8 @@ import jakarta.persistence.Query; import java.time.LocalDate; import java.util.List; -import lombok.RequiredArgsConstructor; +import java.util.Objects; +import java.util.StringJoiner; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; @@ -12,19 +13,27 @@ import uk.gov.laa.ccms.data.entity.ClientDetail; @Repository -@RequiredArgsConstructor -public class ClientDetailRepository { +public class ClientDetailRepository extends BaseEntityManagerRepository { - private final EntityManager entityManager; + + public ClientDetailRepository(EntityManager entityManager) { + super(entityManager); + } public Page findAll(final String firstName, final String surname, - final LocalDate dateOfBirth, final String gender, final String caseReferenceNumber, + final LocalDate dateOfBirth, final String gender, final String clientReferenceNumber, final String homeOfficeReference, final String nationalInsuranceNumber, final Pageable pageable){ final String searchCaseQuery = """ - SELECT * FROM XXCCMS.XXCCMS_GET_CLIENT_DETAILS_V + SELECT * FROM XXCCMS.XXCCMS_GET_CLIENT_DETAILS_V + """ + + getFilterSql(firstName, surname, dateOfBirth, gender, clientReferenceNumber, + homeOfficeReference, nationalInsuranceNumber) + + + getSortSql(pageable) + + """ OFFSET :offset ROWS FETCH NEXT :size ROWS ONLY """; @@ -36,7 +45,9 @@ public Page findAll(final String firstName, final String surname, final String countClientDetails = """ SELECT COUNT(*) FROM XXCCMS.XXCCMS_GET_CLIENT_DETAILS_V - """; + """ + + getFilterSql(firstName, surname, dateOfBirth, gender, clientReferenceNumber, + homeOfficeReference, nationalInsuranceNumber); Query countQuery = entityManager.createNativeQuery(countClientDetails); @@ -47,4 +58,45 @@ SELECT COUNT(*) FROM XXCCMS.XXCCMS_GET_CLIENT_DETAILS_V return new PageImpl<>(resultList, pageable, total); } + + private static String getFilterSql(final String firstName, final String surname, + final LocalDate dateOfBirth, final String gender, final String clientReferenceNumber, + final String homeOfficeReference, final String nationalInsuranceNumber){ + StringJoiner sj = new StringJoiner(" AND "); + // First name (Fuzzy match, case-insensitive) + if(stringNotEmpty(firstName)){ + sj.add("UPPER(FIRSTNAME) LIKE '%" + firstName.toUpperCase() + "%'"); + } + // Surname (Fuzzy match, case-insensitive) + // TODO: Should this also be filtering rows using SURNAME_AT_BIRTH column? + if(stringNotEmpty(surname)){ + sj.add("UPPER(SURNAME) LIKE '%" + surname.toUpperCase() + "%'"); + } + // Date of birth (Exact match) + if(!Objects.isNull(dateOfBirth)){ + sj.add("DATE_OF_BIRTH = TO_DATE('" + dateOfBirth + "', 'YYYY-MM-DD')"); + } + // Gender (Exact match but case-insensitive) + if(stringNotEmpty(gender)){ + sj.add("UPPER(GENDER) = '" + gender.toUpperCase() + "'"); + } + // Client reference number (Fuzzy match, case-insensitive) + if(stringNotEmpty(clientReferenceNumber)){ + sj.add("TO_CHAR(CLIENT_REFERENCE_NUMBER) LIKE '%" + + clientReferenceNumber.toUpperCase() + "%'"); + } + // Home office number (Fuzzy match, case-insensitive) + if(stringNotEmpty(homeOfficeReference)){ + sj.add("UPPER(HOME_OFFICE_NUMBER) LIKE '%" + + homeOfficeReference.toUpperCase() + "%'"); + } + // National insurance number + if(stringNotEmpty(nationalInsuranceNumber)){ + sj.add("UPPER(NI_NUMBER) LIKE '%" + + nationalInsuranceNumber.toUpperCase() + "%'"); + } + return sj.length() > 0 ? "WHERE " + sj : ""; + } + + } diff --git a/data-service/src/main/java/uk/gov/laa/ccms/data/repository/NotificationSearchRepository.java b/data-service/src/main/java/uk/gov/laa/ccms/data/repository/NotificationSearchRepository.java index 3d1b03c..a998e0f 100644 --- a/data-service/src/main/java/uk/gov/laa/ccms/data/repository/NotificationSearchRepository.java +++ b/data-service/src/main/java/uk/gov/laa/ccms/data/repository/NotificationSearchRepository.java @@ -6,7 +6,6 @@ import java.util.List; import java.util.Objects; import java.util.StringJoiner; -import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; @@ -17,19 +16,23 @@ * Repository for searching and retrieving notification records * using dynamic filters and pagination. * - *

This class interacts directly with the database view `XXCCMS_GET_NOTIF_INFO_V` + *

This class interacts directly with the database view XXCCMS_GET_NOTIF_INFO_V * to fetch records related to notifications, applying dynamic filters and paginated results. * It provides an implementation using native SQL queries to support complex filter conditions.

* + *

Extends {@link BaseEntityManagerRepository} which contains helper methods + * for helping build a SQL query and {@link EntityManager}.

+ * * @author Jamie Briggs * @see NotificationInfo * @see Pageable */ @Component -@RequiredArgsConstructor -public final class NotificationSearchRepository { +public final class NotificationSearchRepository extends BaseEntityManagerRepository { - private final EntityManager entityManager; + public NotificationSearchRepository(EntityManager entityManager) { + super(entityManager); + } /** * Retrieves a paginated list of NotificationInfo entities from the database, applying the @@ -67,11 +70,12 @@ public Page findAll(final long providerId, assignedToUserId, clientSurname, feeEarnerId, includeClosed, notificationType, assignedDateFrom, assignedDateTo) + - getSortSql(pageable) + + getSortSql(pageable) + + """ - OFFSET :offset ROWS FETCH NEXT :size ROWS ONLY + OFFSET :offset ROWS FETCH NEXT :size ROWS ONLY """; - + Query query = entityManager.createNativeQuery(searchNotificationQuery, NotificationInfo.class); query.setHint("org.hibernate.readOnly", true); query.setParameter("offset", pageable.getOffset()); @@ -139,20 +143,4 @@ private static String getFilterSql(Long providerId, return sj + " "; } - - private static boolean stringNotEmpty(String value) { - return value != null && !value.isEmpty(); - } - - private static String getSortSql(Pageable pageable) { - if (pageable.getSort().isEmpty()) { - return " "; - } - - StringJoiner sortJoiner = new StringJoiner(", ", " ORDER BY ", " "); - pageable.getSort().forEach(order -> - sortJoiner.add(order.getProperty() + " " + order.getDirection().name())); - return sortJoiner.toString(); - } - }