diff --git a/.gitignore b/.gitignore
index e3ce445..bdba2a5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -93,3 +93,7 @@ test-results/
.idea
/.java-version
+
+# Project
+data-api/src/main/resources/application-secret.yml
+data-service/src/main/resources/application-secret.yml
diff --git a/data-api/open-api-specification.yml b/data-api/open-api-specification.yml
index 3895a3e..863c668 100644
--- a/data-api/open-api-specification.yml
+++ b/data-api/open-api-specification.yml
@@ -974,6 +974,78 @@ paths:
description: 'Forbidden'
'500':
description: 'Internal server error'
+ /notifications:
+ get:
+ tags:
+ - notifications
+ summary: 'Get Notifications'
+ operationId: 'getNotifications'
+ x-spring-paginated: true
+ parameters:
+ - name: 'case-reference-number'
+ in: 'query'
+ schema:
+ type: 'string'
+ example: '1234567890'
+ - name: 'provider-case-reference'
+ in: 'query'
+ schema:
+ type: 'string'
+ example: "1234567890"
+ - name: 'assigned-to-user-id'
+ in: 'query'
+ schema:
+ type: 'string'
+ example: 'abc123'
+ - name: 'client-surname'
+ in: 'query'
+ schema:
+ type: 'string'
+ example: 'smith'
+ - name: 'fee-earner-id'
+ in: 'query'
+ schema:
+ type: 'integer'
+ example: 1234567890
+ - name: 'include-closed'
+ in: 'query'
+ schema:
+ type: boolean
+ default: true
+ - name: 'notification-type'
+ in: 'query'
+ schema:
+ type: 'string'
+ example: 'A'
+ - name: 'date-from'
+ in: 'query'
+ schema:
+ type: 'string'
+ example: "2017-01-01"
+ format: date
+ - name: 'date-to'
+ in: 'query'
+ schema:
+ type: 'string'
+ example: "2017-01-01"
+ format: date
+ responses:
+ '200':
+ description: 'Successful operation'
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/notifications"
+ '400':
+ description: 'Bad request'
+ '401':
+ description: 'Unauthorized'
+ '403':
+ description: 'Forbidden'
+ '404':
+ description: 'Not found'
+ '500':
+ description: 'Internal server error'
components:
securitySchemes:
@@ -1577,6 +1649,109 @@ components:
properties:
case_reference_number:
type: 'string'
+ notifications:
+ allOf:
+ - $ref: "#/components/schemas/page"
+ type: object
+ properties:
+ content:
+ type: array
+ default: [ ]
+ items:
+ $ref: '#/components/schemas/notification'
+ notification:
+ type: 'object'
+ properties:
+ #client_reference_number:
+ # type: 'string'
+ case_reference_number:
+ type: 'string'
+ provider_case_reference_number:
+ type: 'string'
+ user:
+ $ref: '#/components/schemas/userDetail'
+ client_name:
+ type: 'string'
+ category_of_law:
+ type: 'string'
+ fee_earner:
+ type: 'string'
+ notification_id:
+ type: 'string'
+ provider_firm_id:
+ type: 'string'
+ subject:
+ type: 'string'
+ assign_date:
+ type: 'string'
+ format: date
+ due_date:
+ type: 'string'
+ format: date
+ notification_type:
+ type: 'string'
+ status:
+ type: 'string'
+ notes:
+ type: 'array'
+ items:
+ $ref: '#/components/schemas/note'
+ evidence_allowed:
+ type: 'boolean'
+ notification_open_indicator:
+ type: 'boolean'
+ attached_documents:
+ type: 'array'
+ items:
+ $ref: '#/components/schemas/document'
+ uploaded_documents:
+ type: 'array'
+ items:
+ $ref: '#/components/schemas/document'
+ available_responses:
+ type: array
+ items:
+ type: 'string'
+ note:
+ type: 'object'
+ properties:
+ notes_id:
+ type: 'string'
+ user:
+ $ref: '#/components/schemas/userDetail'
+ date:
+ type: 'string'
+ format: date
+ message:
+ type: 'string'
+ baseDocument:
+ type: 'object'
+ properties:
+ document_type:
+ type: 'string'
+ file_extension:
+ type: 'string'
+ text:
+ type: 'string'
+ document:
+ allOf:
+ - $ref: "#/components/schemas/baseDocument"
+ type: 'object'
+ properties:
+ document_id:
+ type: 'string'
+ title:
+ type: 'string'
+ channel:
+ type: 'string'
+ document_link:
+ type: 'string'
+ file_data:
+ type: 'string'
+ status:
+ type: 'string'
+ status_description:
+ type: 'string'
page:
type: 'object'
properties:
diff --git a/data-service/build.gradle b/data-service/build.gradle
index 5b02dfb..2355c36 100644
--- a/data-service/build.gradle
+++ b/data-service/build.gradle
@@ -20,6 +20,8 @@ dependencies {
implementation 'org.mapstruct:mapstruct:1.6.3'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.6.3'
+ implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml'
+
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.mockito:mockito-core:5.14.2'
testImplementation 'org.junit.jupiter:junit-jupiter:5.11.4'
diff --git a/data-service/src/integrationTest/java/uk/gov/laa/ccms/data/repository/NotificationRepositoryIntegrationTest.java b/data-service/src/integrationTest/java/uk/gov/laa/ccms/data/repository/NotificationRepositoryIntegrationTest.java
new file mode 100644
index 0000000..3e17e14
--- /dev/null
+++ b/data-service/src/integrationTest/java/uk/gov/laa/ccms/data/repository/NotificationRepositoryIntegrationTest.java
@@ -0,0 +1,364 @@
+package uk.gov.laa.ccms.data.repository;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+import java.time.LocalDate;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.test.context.ActiveProfiles;
+import uk.gov.laa.ccms.data.entity.Notification;
+import uk.gov.laa.ccms.data.repository.specification.NotificationSpecification;
+
+@DataJpaTest
+@ActiveProfiles("h2-test")
+@DisplayName("Notification Repository Integration Test")
+public class NotificationRepositoryIntegrationTest {
+
+ @Autowired
+ private NotificationRepository notificationRepository;
+
+ @PersistenceContext
+ private EntityManager entityManager;
+
+ private Notification n1;
+ private Notification n2;
+
+ @BeforeEach
+ void setUp() {
+ // Insert test data into the in-memory database
+ n1 = Notification.builder().notificationId(1L)
+ .lscCaseRefReference("1001")
+ .providerCaseReference("2001")
+ .assignedTo("JBriggs")
+ .personFirstName("Jamie")
+ .personLastName("Briggs")
+ .feeEarnerPartyId(3001L)
+ .isOpen(true)
+ .actionNotificationInd("N")
+ .dateAssigned(LocalDate.of(2025, 1, 1))
+ .build();
+ n2 = Notification.builder().notificationId(2L)
+ .lscCaseRefReference("1002")
+ .providerCaseReference("2002")
+ .assignedTo("SMonday")
+ .personFirstName("Ski")
+ .personLastName("Bri-Monday")
+ .feeEarnerPartyId(3002L)
+ .isOpen(false)
+ .actionNotificationInd("O")
+ .dateAssigned(LocalDate.of(2026, 1, 1))
+ .build();
+ // Use entityManager as NotificationRepository extends ReadOnlyRepository.
+ entityManager.persist(n1);
+ entityManager.persist(n2);
+ }
+
+ @Test
+ @DisplayName("Should get all Notifications")
+ void shouldGetAllNotifications(){
+ // Given
+ Specification spec = NotificationSpecification.withFilters(
+ null,
+ null,
+ null,
+ null,
+ null,
+ true,
+ null,
+ null,
+ null);
+ // When
+ Page result = notificationRepository.findAll(spec, Pageable.unpaged());
+ // Then
+ assertEquals(2, result.getTotalElements());
+ assertEquals(true, result.getContent().contains(n1));
+ assertEquals(true, result.getContent().contains(n2));
+ }
+
+ @Test
+ @DisplayName("Should filter by case reference number")
+ void shouldFilterByCaseReferenceNumber(){
+ // Given
+ Specification spec = NotificationSpecification.withFilters(
+ "1001",
+ null,
+ null,
+ null,
+ null,
+ true,
+ null,
+ null,
+ null);
+ // When
+ Page result = notificationRepository.findAll(spec, Pageable.unpaged());
+ // Then
+ assertEquals(1, result.getTotalElements());
+ assertEquals(true, result.getContent().contains(n1));
+ }
+
+ @Test
+ @DisplayName("Should filter by similar case reference number")
+ void shouldFilterBySimilarCaseReferenceNumber(){
+ // Given
+ Specification spec = NotificationSpecification.withFilters(
+ "100",
+ null,
+ null,
+ null,
+ null,
+ true,
+ null,
+ null,
+ null);
+ // When
+ Page result = notificationRepository.findAll(spec, Pageable.unpaged());
+ // Then
+ assertEquals(2, result.getTotalElements());
+ assertEquals(true, result.getContent().contains(n1));
+ assertEquals(true, result.getContent().contains(n2));
+ }
+
+ @Test
+ @DisplayName("Should filter by provider case reference number")
+ void shouldFilterByProviderCaseReferenceNumber(){
+ // Given
+ Specification spec = NotificationSpecification.withFilters(
+ null,
+ "2001",
+ null,
+ null,
+ null,
+ true,
+ null,
+ null,
+ null);
+ // When
+ Page result = notificationRepository.findAll(spec, Pageable.unpaged());
+ // Then
+ assertEquals(1, result.getTotalElements());
+ assertEquals(true, result.getContent().contains(n1));
+ }
+
+ @Test
+ @DisplayName("Should filter by similar provider case reference number")
+ void shouldFilterBySimilarProviderCaseReferenceNumber(){
+ // Given
+ Specification spec = NotificationSpecification.withFilters(
+ null,
+ "200",
+ null,
+ null,
+ null,
+ true,
+ null,
+ null,
+ null);
+ // When
+ Page result = notificationRepository.findAll(spec, Pageable.unpaged());
+ // Then
+ assertEquals(2, result.getTotalElements());
+ assertEquals(true, result.getContent().contains(n1));
+ assertEquals(true, result.getContent().contains(n2));
+ }
+
+ @Test
+ @DisplayName("Should filter by assigned to user ID")
+ void shouldFilterByAssignedToUserID(){
+ // Given
+ Specification spec = NotificationSpecification.withFilters(
+ null,
+ null,
+ "JBriggs",
+ null,
+ null,
+ true,
+ null,
+ null,
+ null);
+ // When
+ Page result = notificationRepository.findAll(spec, Pageable.unpaged());
+ // Then
+ assertEquals(1, result.getTotalElements());
+ assertEquals(true, result.getContent().contains(n1));
+ }
+
+ @Test
+ @DisplayName("Should filter by user surname")
+ void shouldFilterByUserSurname(){
+ // Given
+ Specification spec = NotificationSpecification.withFilters(
+ null,
+ null,
+ null,
+ "Briggs",
+ null,
+ true,
+ null,
+ null,
+ null);
+ // When
+ Page result = notificationRepository.findAll(spec, Pageable.unpaged());
+ // Then
+ assertEquals(1, result.getTotalElements());
+ assertEquals(true, result.getContent().contains(n1));
+ }
+
+ @Test
+ @DisplayName("Should filter by like user surname")
+ void shouldFilterByLikeUserSurname(){
+ // Given
+ Specification spec = NotificationSpecification.withFilters(
+ null,
+ null,
+ null,
+ "Bri",
+ null,
+ true,
+ null,
+ null,
+ null);
+ // When
+ Page result = notificationRepository.findAll(spec, Pageable.unpaged());
+ // Then
+ assertEquals(2, result.getTotalElements());
+ assertEquals(true, result.getContent().contains(n1));
+ assertEquals(true, result.getContent().contains(n2));
+ }
+
+ @Test
+ @DisplayName("Should filter by fee earner ID")
+ void shouldFilterByFeeEarnerID(){
+ // Given
+ Specification spec = NotificationSpecification.withFilters(
+ null,
+ null,
+ null,
+ null,
+ 3001,
+ true,
+ null,
+ null,
+ null);
+ // When
+ Page result = notificationRepository.findAll(spec, Pageable.unpaged());
+ // Then
+ assertEquals(1, result.getTotalElements());
+ assertEquals(true, result.getContent().contains(n1));
+ }
+
+ @Test
+ @DisplayName("Should filter by notification type")
+ void shouldFilterByNotificationType(){
+ // Given
+ Specification spec = NotificationSpecification.withFilters(
+ null,
+ null,
+ null,
+ null,
+ null,
+ false, "N",
+ null,
+ null);
+ // When
+ Page result = notificationRepository.findAll(spec, Pageable.unpaged());
+ // Then
+ assertEquals(1, result.getTotalElements());
+ assertEquals(true, result.getContent().contains(n1));
+ }
+
+ @Test
+ @DisplayName("Should filter by date from")
+ void shouldFilterByDateFrom(){
+ // Given
+ Specification spec = NotificationSpecification.withFilters(
+ null,
+ null,
+ null,
+ null,
+ null,
+ true,
+ null,
+ LocalDate.of(2025, 2, 1),
+ null);
+ // When
+ Page result = notificationRepository.findAll(spec, Pageable.unpaged());
+ // Then
+ assertEquals(1, result.getTotalElements());
+ assertEquals(true, result.getContent().contains(n2));
+ }
+
+ @Test
+ @DisplayName("Should filter by date from inclusive")
+ void shouldFilterByDateFromInclusive(){
+ // Given
+ Specification spec = NotificationSpecification.withFilters(
+ null,
+ null,
+ null,
+ null,
+ null,
+ true,
+ null,
+ LocalDate.of(2024, 1, 1),
+ null);
+ // When
+ Page result = notificationRepository.findAll(spec, Pageable.unpaged());
+ // Then
+ assertEquals(2, result.getTotalElements());
+ assertEquals(true, result.getContent().contains(n1));
+ assertEquals(true, result.getContent().contains(n2));
+ }
+
+ @Test
+ @DisplayName("Should filter by date to")
+ void shouldFilterByDateTo(){
+ // Given
+ Specification spec = NotificationSpecification.withFilters(
+ null,
+ null,
+ null,
+ null,
+ null,
+ true,
+ null,
+ null,
+ LocalDate.of(2025, 12, 1));
+ // When
+ Page result = notificationRepository.findAll(spec, Pageable.unpaged());
+ // Then
+ assertEquals(1, result.getTotalElements());
+ assertEquals(true, result.getContent().contains(n1));
+ }
+
+ @Test
+ @DisplayName("Should filter by date to inclusive")
+ void shouldFilterByDateToInclusive(){
+ // Given
+ Specification spec = NotificationSpecification.withFilters(
+ null,
+ null,
+ null,
+ null,
+ null,
+ true,
+ null,
+ null,
+ LocalDate.of(2026, 1, 1));
+ // When
+ Page result = notificationRepository.findAll(spec, Pageable.unpaged());
+ // Then
+ assertEquals(2, result.getTotalElements());
+ assertEquals(true, result.getContent().contains(n1));
+ assertEquals(true, result.getContent().contains(n2));
+ }
+
+
+}
diff --git a/data-service/src/integrationTest/resources/application-h2-test.yml b/data-service/src/integrationTest/resources/application-h2-test.yml
new file mode 100644
index 0000000..2021e75
--- /dev/null
+++ b/data-service/src/integrationTest/resources/application-h2-test.yml
@@ -0,0 +1,6 @@
+spring.datasource.url=jdbc:h2:mem:testdb
+spring.datasource.driver-class-name=org.h2.Driver
+spring.datasource.username=sa
+spring.datasource.password=password
+spring.jpa.hibernate.ddl-auto=create-drop
+spring.jpa.show-sql=true
\ No newline at end of file
diff --git a/data-service/src/main/java/uk/gov/laa/ccms/data/controller/NotificationsController.java b/data-service/src/main/java/uk/gov/laa/ccms/data/controller/NotificationsController.java
index 7d34b1a..9b27175 100644
--- a/data-service/src/main/java/uk/gov/laa/ccms/data/controller/NotificationsController.java
+++ b/data-service/src/main/java/uk/gov/laa/ccms/data/controller/NotificationsController.java
@@ -1,10 +1,14 @@
package uk.gov.laa.ccms.data.controller;
+import java.time.LocalDate;
+import java.util.Optional;
import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RestController;
import uk.gov.laa.ccms.data.api.NotificationsApi;
import uk.gov.laa.ccms.data.model.NotificationSummary;
+import uk.gov.laa.ccms.data.model.Notifications;
import uk.gov.laa.ccms.data.service.NotificationService;
/**
@@ -22,6 +26,48 @@ public class NotificationsController implements NotificationsApi {
private final NotificationService notificationService;
+ /**
+ * Retrieves a list of notifications based on various search criteria.
+ *
+ * @param caseReferenceNumber the case reference number to filter notifications
+ * @param providerCaseReference the provider-specific case reference to filter notifications
+ * @param assignedToUserId the user ID to filter notifications assigned to a specific user
+ * @param clientSurname the client's surname to filter notifications for a specific client
+ * @param feeEarnerId the ID of the fee earner to filter notifications associated with them
+ * @param includeClosed a flag to indicate whether to include closed notifications in the results
+ * @param notificationType the type of notifications to filter by
+ * @param dateFrom the starting date to filter notifications by a specific date range
+ * @param dateTo the ending date to filter notifications by a specific date range
+ * @param pageable the pagination and sorting information for the result set
+ * @return a {@code ResponseEntity} containing the retrieved list of notifications if found,
+ * or a {@code ResponseEntity} with HTTP status 404 if no notifications are found
+ */
+ @Override
+ public ResponseEntity getNotifications(String caseReferenceNumber,
+ String providerCaseReference, String assignedToUserId, String clientSurname,
+ Integer feeEarnerId, Boolean includeClosed, String notificationType, LocalDate dateFrom,
+ LocalDate dateTo, Pageable pageable) {
+ Optional notifications = notificationService.getNotifications(
+ caseReferenceNumber,
+ providerCaseReference,
+ assignedToUserId,
+ clientSurname,
+ feeEarnerId,
+ Boolean.TRUE.equals(includeClosed),
+ notificationType,
+ dateFrom,
+ dateTo,
+ pageable);
+ return notifications.map(ResponseEntity::ok).orElse(ResponseEntity.notFound().build());
+ }
+
+ /**
+ * Retrieves a summary of user notifications for the specified login ID.
+ *
+ * @param loginId the login ID of the user for whom the notification summary is to be retrieved
+ * @return a {@code ResponseEntity} containing the {@code NotificationSummary} if found,
+ * or a {@code ResponseEntity} with HTTP status 404 if no summary is available
+ */
@Override
public ResponseEntity getUserNotificationSummary(String loginId) {
return notificationService.getUserNotificationSummary(loginId).map(ResponseEntity::ok)
diff --git a/data-service/src/main/java/uk/gov/laa/ccms/data/entity/Notification.java b/data-service/src/main/java/uk/gov/laa/ccms/data/entity/Notification.java
new file mode 100644
index 0000000..56fa671
--- /dev/null
+++ b/data-service/src/main/java/uk/gov/laa/ccms/data/entity/Notification.java
@@ -0,0 +1,114 @@
+package uk.gov.laa.ccms.data.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.Lob;
+import jakarta.persistence.Table;
+import java.time.LocalDate;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.hibernate.annotations.Immutable;
+
+/**
+ * Represents a notification entity from the "XXCCMS_GET_NOTIFICATIONS_V" database view.
+ *
+ * This entity captures details about notifications, such as the user it is assigned to,
+ * associated case references, client information, deadlines, and related metadata.
+ * It provides essential fields to track the status, associated parties,
+ * supporting documents, and notes.
+ *
+ * The class is immutable, and its instances can be created using the builder pattern.
+ *
+ * @author Jamie Briggs
+ */
+@Entity
+@Table(name = "XXCCMS_GET_NOTIFICATIONS_V")
+@Getter
+@Builder
+@Immutable
+@AllArgsConstructor
+@RequiredArgsConstructor
+public class Notification {
+
+ @Id
+ private long notificationId;
+
+ @Column(name = "ASSIGNED_TO", length = 360)
+ private String assignedTo;
+
+ @Column(name = "USER_LOGIN_ID", length = 100)
+ private String userLoginId;
+
+ @Column(name = "PROVIDERFIRM_ID", nullable = false)
+ private long providerFirmId;
+
+ @Column(name = "CLIENT_PARTY_ID")
+ private Long clientPartyId;
+
+ @Column(name = "LSC_CASE_REF_REFERENCE", length = 360)
+ private String lscCaseRefReference;
+
+ @Column(name = "PROVIDER_CASE_REFERENCE", length = 150)
+ private String providerCaseReference;
+
+ @Column(name = "CLIENT_NAME", length = 301)
+ private String clientName;
+
+ @Column(name = "CATEGORY_OF_LAW", length = 150)
+ private String categoryOfLaw;
+
+ @Column(name = "FEE_EARNER", length = 360)
+ private String feeEarner;
+
+ @Column(name = "FEE_EARNER_PARTY_ID")
+ private Long feeEarnerPartyId;
+
+ @Column(name = "NOTIFICATION_SUBJECT", length = 320)
+ private String notificationSubject;
+
+ @Column(name = "DATE_ASSIGNED")
+ private LocalDate dateAssigned;
+
+ @Column(name = "DUE_DATE")
+ private LocalDate dueDate;
+
+ @Column(name = "ACTION_NOTIFICATION_IND", length = 150)
+ private String actionNotificationInd;
+
+ @Column(name = "STATUS", length = 150)
+ private String status;
+
+ @Column(name = "EVIDENCE_ALLOWED_IND", length = 5)
+ private String evidenceAllowedInd;
+
+ @Column(name = "IS_OPEN")
+ private Boolean isOpen;
+
+ @Column(name = "ASSIGNED_TO_PARTY_ID", precision = 15, scale = 0)
+ private Long assignedToPartyId;
+
+ @Column(name = "PERSON_FIRST_NAME", length = 150)
+ private String personFirstName;
+
+ @Column(name = "PERSON_LAST_NAME", length = 150)
+ private String personLastName;
+
+ @Lob
+ @Column(name = "NOTES")
+ private String notes;
+
+ @Lob
+ @Column(name = "UPLOADED_DOCUMENTS")
+ private String uploadedDocuments;
+
+ @Lob
+ @Column(name = "ATTACHED_DOCUMENTS")
+ private String attachedDocuments;
+
+ @Lob
+ @Column(name = "AVAILABLE_RESPONSES")
+ private String availableResponses;
+}
diff --git a/data-service/src/main/java/uk/gov/laa/ccms/data/mapper/NotificationsMapper.java b/data-service/src/main/java/uk/gov/laa/ccms/data/mapper/NotificationsMapper.java
new file mode 100644
index 0000000..fa74434
--- /dev/null
+++ b/data-service/src/main/java/uk/gov/laa/ccms/data/mapper/NotificationsMapper.java
@@ -0,0 +1,194 @@
+package uk.gov.laa.ccms.data.mapper;
+
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.dataformat.xml.XmlMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import java.util.Collections;
+import java.util.List;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.Named;
+import org.springframework.data.domain.Page;
+import uk.gov.laa.ccms.data.entity.Notification;
+import uk.gov.laa.ccms.data.mapper.xml.AttachedDocumentXml;
+import uk.gov.laa.ccms.data.mapper.xml.AttachedDocumentsXml;
+import uk.gov.laa.ccms.data.mapper.xml.AvailableResponsesXml;
+import uk.gov.laa.ccms.data.mapper.xml.NoteXml;
+import uk.gov.laa.ccms.data.mapper.xml.NotesXml;
+import uk.gov.laa.ccms.data.mapper.xml.UploadedDocumentXml;
+import uk.gov.laa.ccms.data.mapper.xml.UploadedDocumentsXml;
+import uk.gov.laa.ccms.data.model.Document;
+import uk.gov.laa.ccms.data.model.Note;
+import uk.gov.laa.ccms.data.model.Notifications;
+import uk.gov.laa.ccms.data.model.UserDetail;
+
+/**
+ * Interface responsible for mapping notification-related data
+ * entities and models to one another. This interface utilizes MapStruct
+ * for transformation and supports advanced mappings with custom conversion logic.
+ *
+ * @see Notifications
+ * @see Notification
+ * @see Page
+ * @author Jamie Briggs
+ */
+@Mapper(componentModel = "spring")
+public interface NotificationsMapper {
+
+ /**
+ * Maps a Page of Notification objects to a Notifications object.
+ *
+ * @param notificationPage a Page containing Notification entities to be mapped
+ * @return a Notifications object containing the mapped notifications along
+ * with pagination details
+ */
+ Notifications mapToNotificationsList(Page notificationPage);
+
+ /**
+ * Maps a Notification object to a {@link uk.gov.laa.ccms.data.model.Notification} object.
+ *
+ * @param notification the source Notification object to be mapped
+ * @return the mapped uk.gov.laa.ccms.data.model.Notification object
+ */
+ @Mapping(target = "notes", source = "notes", qualifiedByName = "formatNotes")
+ @Mapping(target = "uploadedDocuments", source = "uploadedDocuments",
+ qualifiedByName = "formatUploadedDocuments")
+ @Mapping(target = "attachedDocuments", source = "attachedDocuments",
+ qualifiedByName = "formatAttachedDocuments")
+ @Mapping(target = "availableResponses", source = "availableResponses",
+ qualifiedByName = "formatResponses")
+ @Mapping(target = "user.loginId", source = "userLoginId")
+ @Mapping(target = "user.username", source = "assignedTo")
+ @Mapping(target = "subject", source = "notificationSubject")
+ @Mapping(target = "assignDate", source = "dateAssigned")
+ @Mapping(target = "providerCaseReferenceNumber", source = "providerCaseReference")
+ @Mapping(target = "notificationType", source = "actionNotificationInd")
+ @Mapping(target = "notificationOpenIndicator", source = "isOpen")
+ @Mapping(target = "evidenceAllowed", source = "evidenceAllowedInd")
+ @Mapping(target = "caseReferenceNumber", source = "lscCaseRefReference")
+ uk.gov.laa.ccms.data.model.Notification mapToNotification(Notification notification);
+
+ /**
+ * Maps a String containing XML representation of notes to a List of Note objects.
+ *
+ * @param notesString the XML String representing the notes
+ * @return a List of Note objects derived from the XML String or
+ * an empty list if the input is null or empty
+ * @throws JsonProcessingException if an error occurs during the processing of the XML String
+ */
+ @Named("formatNotes")
+ static List mapToNoteList(String notesString) throws JsonProcessingException {
+ XmlMapper xmlMapper = getXmlMapper();
+ if (notesString == null || notesString.isEmpty()) {
+ return Collections.emptyList();
+ }
+ List noteXmls = xmlMapper.readValue(notesString, NotesXml.class).notes();
+
+ // Return empty list if notes is null
+ if (noteXmls == null) {
+ return Collections.emptyList();
+ }
+
+ return noteXmls.stream().map(
+ x -> new Note().notesId(x.noteId()).user(new UserDetail().username(x.noteBy()))
+ .date(x.date()).message(x.message())).toList();
+ }
+
+ /**
+ * Maps a String containing the XML representation of uploaded documents to
+ * a List of {@link Document} objects.
+ *
+ * @param uploadedDocuments the XML String representing the uploaded documents
+ * @return a List of Document objects derived from the XML String or an
+ * empty list if the input is null or empty
+ * @throws JsonProcessingException if an error occurs during the processing of the XML String
+ */
+ @Named("formatUploadedDocuments")
+ static List mapToFormattedDocuments(String uploadedDocuments)
+ throws JsonProcessingException {
+ XmlMapper xmlMapper = getXmlMapper();
+ if (uploadedDocuments == null || uploadedDocuments.isEmpty()) {
+ return Collections.emptyList();
+ }
+ List uploadedDocumentsXmls = xmlMapper.readValue(uploadedDocuments,
+ UploadedDocumentsXml.class).uploadedDocuments();
+
+ // Return empty list if uploadedDocumentsXmls is null
+ if (uploadedDocumentsXmls == null) {
+ return Collections.emptyList();
+ }
+
+ return uploadedDocumentsXmls.stream().map(
+ x -> new Document().documentId(x.documentId())
+ .documentType(x.documentType())
+ .channel(x.documentChannel())
+ .text(x.text()))
+ .toList();
+ }
+
+ /**
+ * Maps a String containing the XML representation of attached documents to a
+ * List of {@link Document} objects.
+ *
+ * @param attachedDocuments the XML String representing the attached documents
+ * @return a List of Document objects derived from the XML String or an empty
+ * list if the input is null or empty
+ * @throws JsonProcessingException if an error occurs during the processing of the XML String
+ */
+ @Named("formatAttachedDocuments")
+ static List mapToAttachedDocuments(String attachedDocuments)
+ throws JsonProcessingException {
+ XmlMapper xmlMapper = getXmlMapper();
+ if (attachedDocuments == null || attachedDocuments.isEmpty()) {
+ return Collections.emptyList();
+ }
+ List attachedDocumentXmls = xmlMapper.readValue(attachedDocuments,
+ AttachedDocumentsXml.class).attachedDocuments();
+
+ // Return empty list if uploadedDocumentsXmls is null
+ if (attachedDocumentXmls == null) {
+ return Collections.emptyList();
+ }
+
+ return attachedDocumentXmls.stream().map(
+ x -> new Document()
+ .documentId(x.documentId())
+ .title(x.attachmentTitle())
+ .text(x.text()))
+ .toList();
+ }
+
+ /**
+ * Maps a String containing XML representation of available responses
+ * to a {@link List} of String objects.
+ *
+ * @param availableResponses the XML String representing the available responses
+ * @return a List of String objects derived from the XML String or an empty
+ * list if the input is null or empty
+ * @throws JsonProcessingException if an error occurs during the processing
+ * of the XML String
+ */
+ @Named("formatResponses")
+ static List mapToAvailableResponses(String availableResponses)
+ throws JsonProcessingException {
+ XmlMapper xmlMapper = getXmlMapper();
+ if (availableResponses == null || availableResponses.isEmpty()) {
+ return Collections.emptyList();
+ }
+ List responses = xmlMapper.readValue(availableResponses,
+ AvailableResponsesXml.class).responses();
+ return responses == null ? Collections.emptyList() : responses;
+ }
+
+ /**
+ * Creates and configures an instance of XmlMapper.
+ *
+ * @return an XmlMapper instance configured with the Java Time Module
+ */
+ private static XmlMapper getXmlMapper() {
+ XmlMapper xmlMapper = new XmlMapper();
+ xmlMapper.registerModule(new JavaTimeModule());
+ return xmlMapper;
+ }
+}
diff --git a/data-service/src/main/java/uk/gov/laa/ccms/data/mapper/xml/AttachedDocumentXml.java b/data-service/src/main/java/uk/gov/laa/ccms/data/mapper/xml/AttachedDocumentXml.java
new file mode 100644
index 0000000..1aab34a
--- /dev/null
+++ b/data-service/src/main/java/uk/gov/laa/ccms/data/mapper/xml/AttachedDocumentXml.java
@@ -0,0 +1,22 @@
+package uk.gov.laa.ccms.data.mapper.xml;
+
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+
+/**
+ * Represents a single attached document in XML format.
+ *
+ * This record is primarily utilized for mapping XML data to Java objects
+ * using the Jackson data format library for XML deserialization/serialization.
+ *
+ * @param documentId A unique identifier for the document.
+ * @param attachmentTitle The title of the attached document.
+ * @param text Text describing the document.
+ * @see AttachedDocumentsXml
+ * @author Jamie Briggs
+ */
+public record AttachedDocumentXml(
+ @JacksonXmlProperty(localName = "document_id") String documentId,
+ @JacksonXmlProperty(localName = "ATTACHMENT_TITLE") String attachmentTitle,
+ @JacksonXmlProperty(localName = "Text") String text) {
+
+}
diff --git a/data-service/src/main/java/uk/gov/laa/ccms/data/mapper/xml/AttachedDocumentsXml.java b/data-service/src/main/java/uk/gov/laa/ccms/data/mapper/xml/AttachedDocumentsXml.java
new file mode 100644
index 0000000..6c1053a
--- /dev/null
+++ b/data-service/src/main/java/uk/gov/laa/ccms/data/mapper/xml/AttachedDocumentsXml.java
@@ -0,0 +1,32 @@
+package uk.gov.laa.ccms.data.mapper.xml;
+
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+import java.util.List;
+
+/**
+ * Represents a collection of attached documents in XML format.
+ * Each document is represented as an instance of the AttachedDocumentXml record.
+ *
+ * The XML structure for this record is defined such that the "Documents" element contains
+ * a list of attached documents without an additional wrapper element.
+ *
+ * This class is primarily used for mapping XML data to Java objects
+ * using the Jackson data format library.
+ *
+ * An attached document typically includes properties such as:
+ *
+ * - Document ID: A unique identifier for the document.
+ * - Attachment title: The title of the attached document.
+ * - Text: Text describing the document
+ *
+ *
+ * @param attachedDocuments A list of metadata for documents which were attached
+ * @see AttachedDocumentXml
+ * @author Jamie Briggs
+ */
+public record AttachedDocumentsXml(@JacksonXmlElementWrapper(useWrapping = false)
+ @JacksonXmlProperty(localName = "Documents")
+ List attachedDocuments) {
+
+}
diff --git a/data-service/src/main/java/uk/gov/laa/ccms/data/mapper/xml/AvailableResponsesXml.java b/data-service/src/main/java/uk/gov/laa/ccms/data/mapper/xml/AvailableResponsesXml.java
new file mode 100644
index 0000000..792b9a4
--- /dev/null
+++ b/data-service/src/main/java/uk/gov/laa/ccms/data/mapper/xml/AvailableResponsesXml.java
@@ -0,0 +1,24 @@
+package uk.gov.laa.ccms.data.mapper.xml;
+
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+import java.util.List;
+
+/**
+ * Represents a collection of available responses in XML format.
+ *
+ * This record maps to an XML structure where the "Response" elements are provided as a list
+ * without any additional wrapping element. Each entry in the list corresponds to an individual
+ * response.
+ *
+ * It is primarily used for mapping XML data to Java objects
+ * using the Jackson data format library.
+ *
+ * @param responses A list of responses available
+ * @author Jamie Briggs
+ */
+public record AvailableResponsesXml(@JacksonXmlElementWrapper(useWrapping = false)
+ @JacksonXmlProperty(localName = "Response")
+ List responses) {
+
+}
diff --git a/data-service/src/main/java/uk/gov/laa/ccms/data/mapper/xml/NoteXml.java b/data-service/src/main/java/uk/gov/laa/ccms/data/mapper/xml/NoteXml.java
new file mode 100644
index 0000000..0c0e652
--- /dev/null
+++ b/data-service/src/main/java/uk/gov/laa/ccms/data/mapper/xml/NoteXml.java
@@ -0,0 +1,26 @@
+package uk.gov.laa.ccms.data.mapper.xml;
+
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+import java.time.LocalDate;
+
+/**
+ * Represents a single note in XML format.
+ *
+ * This record is used for mapping between XML data and Java objects when handling notes, using
+ * the Jackson XML data format library. The properties of this record correspond to fields
+ * in the XML representation.
+ *
+ * @param noteId A unique identifier for the note.
+ * @param noteBy The author of the note.
+ * @param date The date the note was created.
+ * @param message The contents of the note.
+ * @see NotesXml
+ * @author Jamie Briggs
+ */
+public record NoteXml(@JacksonXmlProperty(localName = "note_id") String noteId,
+ @JacksonXmlProperty(localName = "note_by") String noteBy,
+ @JacksonXmlProperty(localName = "date")
+ LocalDate date,
+ @JacksonXmlProperty(localName = "Message") String message) {
+
+}
diff --git a/data-service/src/main/java/uk/gov/laa/ccms/data/mapper/xml/NotesXml.java b/data-service/src/main/java/uk/gov/laa/ccms/data/mapper/xml/NotesXml.java
new file mode 100644
index 0000000..865c749
--- /dev/null
+++ b/data-service/src/main/java/uk/gov/laa/ccms/data/mapper/xml/NotesXml.java
@@ -0,0 +1,33 @@
+package uk.gov.laa.ccms.data.mapper.xml;
+
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+import java.util.List;
+
+/**
+ * Represents a collection of notes in XML format.
+ *
+ * The XML structure for this record is defined such that the "notes" element contains
+ * a list of attached notes without an additional wrapper element.
+ *
+ * This class is primarily used for mapping XML data to Java objects
+ * using the Jackson data format library.
+ *
+ * A note typically includes properties such as:
+ *
+ * - Note ID: A unique identifier for the note.
+ * - Note By: The author of the note.
+ * - Date: The date the note was created.
+ * - Message: The contents of the note
+ *
+ *
+ * @see NoteXml
+ * @param notes A list of notes.
+ * @author Jamie Briggs
+ */
+public record NotesXml(
+ @JacksonXmlElementWrapper(useWrapping = false)
+ @JacksonXmlProperty(localName = "Note")
+ List notes) {
+
+}
diff --git a/data-service/src/main/java/uk/gov/laa/ccms/data/mapper/xml/UploadedDocumentXml.java b/data-service/src/main/java/uk/gov/laa/ccms/data/mapper/xml/UploadedDocumentXml.java
new file mode 100644
index 0000000..fb8a874
--- /dev/null
+++ b/data-service/src/main/java/uk/gov/laa/ccms/data/mapper/xml/UploadedDocumentXml.java
@@ -0,0 +1,26 @@
+package uk.gov.laa.ccms.data.mapper.xml;
+
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+
+/**
+ * Represents a single uploaded document in XML format.
+ *
+ * This record is used for mapping XML data to Java objects when handling uploaded documents.
+ * Each field in this record corresponds to a specific element or attribute in the XML data.
+ *
+ * This record is typically part of a collection represented
+ * by {@link UploadedDocumentsXml}, which aggregates multiple uploaded documents.
+ *
+ * @param documentId A unique identifier for the document.
+ * @param documentType The type or category of the document.
+ * @param documentChannel The source in which the document was uplaoded from.
+ * @param text Text describing the document.
+ * @see UploadedDocumentsXml
+ */
+public record UploadedDocumentXml(
+ @JacksonXmlProperty(localName = "document_id") String documentId,
+ @JacksonXmlProperty(localName = "document_type") String documentType,
+ @JacksonXmlProperty(localName = "document_channel") String documentChannel,
+ @JacksonXmlProperty(localName = "Text") String text) {
+
+}
diff --git a/data-service/src/main/java/uk/gov/laa/ccms/data/mapper/xml/UploadedDocumentsXml.java b/data-service/src/main/java/uk/gov/laa/ccms/data/mapper/xml/UploadedDocumentsXml.java
new file mode 100644
index 0000000..9c9ab99
--- /dev/null
+++ b/data-service/src/main/java/uk/gov/laa/ccms/data/mapper/xml/UploadedDocumentsXml.java
@@ -0,0 +1,34 @@
+package uk.gov.laa.ccms.data.mapper.xml;
+
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+import java.util.List;
+
+/**
+ * Represents a collection of uploaded documents in XML format.
+ *
+ * This record is used for deserializing and serializing XML data where the "Documents" element
+ * contains a list of uploaded documents. Each uploaded document is represented as an instance
+ * of the UploadedDocumentXml record.
+ *
+ * The Jackson XML data format library is utilized to map XML structures to Java objects
+ * and vice versa. The "useWrapping" property is set to false, indicating
+ * that the "Documents" elements are provided as a flat list without an extra wrapper.
+ *
+ * An uploaded document typically includes properties such as:
+ *
+ * - Document ID: A unique identifier for the document.
+ * - Document Type: The type or category of the document.
+ * - Document Channel: The source in which the document was uploaded from
+ * - Text: Text describing the document.
+ *
+ *
+ * @param uploadedDocuments A list of metadata for documents which were uploaded.
+ * @see UploadedDocumentXml
+ * @author Jamie Briggs
+ */
+public record UploadedDocumentsXml(@JacksonXmlElementWrapper(useWrapping = false)
+ @JacksonXmlProperty(localName = "Documents")
+ List uploadedDocuments) {
+
+}
diff --git a/data-service/src/main/java/uk/gov/laa/ccms/data/repository/NotificationCountRepository.java b/data-service/src/main/java/uk/gov/laa/ccms/data/repository/NotificationCountRepository.java
index 307273e..0538c7e 100644
--- a/data-service/src/main/java/uk/gov/laa/ccms/data/repository/NotificationCountRepository.java
+++ b/data-service/src/main/java/uk/gov/laa/ccms/data/repository/NotificationCountRepository.java
@@ -9,8 +9,8 @@
* Repository interface for accessing {@link NotificationCount} entities.
*
* This repository extends the {@link ReadOnlyRepository} interface, it supports read-only
- * operations for the {@code NotificationCount} entity, with the primary key of type
- * {@code String}.
+ * operations for the {@link NotificationCount} entity, with the primary key of type
+ * {@link String}.
*
* @author Jamie Briggs
* @see NotificationCount
diff --git a/data-service/src/main/java/uk/gov/laa/ccms/data/repository/NotificationRepository.java b/data-service/src/main/java/uk/gov/laa/ccms/data/repository/NotificationRepository.java
new file mode 100644
index 0000000..06c342b
--- /dev/null
+++ b/data-service/src/main/java/uk/gov/laa/ccms/data/repository/NotificationRepository.java
@@ -0,0 +1,26 @@
+package uk.gov.laa.ccms.data.repository;
+
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.stereotype.Repository;
+import uk.gov.laa.ccms.data.entity.Notification;
+
+
+/**
+ * Repository interface for accessing {@link Notification} entities.
+ *
+ * This repository extends the {@link ReadOnlyRepository} interface,
+ * it supports read-only operations for the {@link Notification} entity.
+ * This repository also extends {@link JpaSpecificationExecutor}, which
+ * allows the use of ${@link org.springframework.data.jpa.domain.Specification}
+ * to filter easier.
+ *
+ * @author Jamie Briggs
+ * @see Notification
+ * @see ReadOnlyRepository
+ * @see org.springframework.data.jpa.domain.Specification
+ */
+@Repository
+public interface NotificationRepository extends ReadOnlyRepository,
+ JpaSpecificationExecutor {
+
+}
diff --git a/data-service/src/main/java/uk/gov/laa/ccms/data/repository/specification/NotificationSpecification.java b/data-service/src/main/java/uk/gov/laa/ccms/data/repository/specification/NotificationSpecification.java
new file mode 100644
index 0000000..47d7957
--- /dev/null
+++ b/data-service/src/main/java/uk/gov/laa/ccms/data/repository/specification/NotificationSpecification.java
@@ -0,0 +1,108 @@
+package uk.gov.laa.ccms.data.repository.specification;
+
+import jakarta.persistence.criteria.Predicate;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.List;
+import org.springframework.data.jpa.domain.Specification;
+import uk.gov.laa.ccms.data.entity.Notification;
+
+/**
+ * A utility class for creating specifications to filter and query notifications.
+ *
+ * The {@link NotificationSpecification} class provides mechanisms to construct dynamic
+ * query criteria using the JPA Specification API. It allows filtering of
+ * {@link Notification} entities based on various attributes such as case reference number,
+ * provider case reference, assigned user, client surname, and more.
+ *
+ * The filters include:
+ *
+ * - Case reference number (partial matching support).
+ * - Provider case reference (partial matching support).
+ * - Assigned user ID.
+ * - Client surname (partial matching support).
+ * - Fee earner ID.
+ * - Should include closed notifications.
+ * - Notification type.
+ * - Date range (start and end dates).
+ *
+ *
+ * This allows querying for notifications based on multiple combinations of these filters,
+ * with all specified conditions combined.
+ *
+ * @see Notification
+ * @see Specification
+ * @see uk.gov.laa.ccms.data.repository.NotificationRepository
+ * @author Jamie Briggs
+ */
+public class NotificationSpecification {
+
+ /**
+ * Private constructor to prevent instantiation of the NotificationSpecification class.
+ */
+ private NotificationSpecification() {}
+
+ /**
+ * Builds a {@link Specification} for filtering {@link Notification} entities based
+ * on various criteria. The method dynamically constructs filter
+ * conditions for the provided filter parameters.
+ *
+ * @param caseReferenceNumber the case reference number to filter by (optional).
+ * @param providerCaseReference the provider case reference to filter by (optional).
+ * @param assignedToUserId the user ID assigned to the notification (optional).
+ * @param clientSurname the client's surname to filter by (optional).
+ * @param feeEarnerId the ID of the fee earner to filter by (optional).
+ * @param includeClosed a flag to include closed notifications in the result set.
+ * @param notificationType the type of notification to filter by (optional).
+ * @param dateFrom the starting date for filtering notifications by the date assigned (inclusive).
+ * @param dateTo the ending date for filtering notifications by the date assigned (inclusive).
+ * @return a {@link Specification} object encapsulating the
+ * filtering logic for {@link Notification} entities.
+ */
+ public static Specification withFilters(
+ String caseReferenceNumber,
+ String providerCaseReference, String assignedToUserId, String clientSurname,
+ Integer feeEarnerId, boolean includeClosed, String notificationType, LocalDate dateFrom,
+ LocalDate dateTo
+ ) {
+ return (root, query, criteriaBuilder) -> {
+ List predicates = new ArrayList<>();
+
+ // Add predicates for each filter only if they are non-null
+ if (caseReferenceNumber != null) {
+ predicates.add(criteriaBuilder.like(root.get("lscCaseRefReference"),
+ "%" + caseReferenceNumber + "%"));
+ }
+ if (providerCaseReference != null) {
+ predicates.add(criteriaBuilder.like(root.get("providerCaseReference"),
+ "%" + providerCaseReference + "%"));
+ }
+ if (assignedToUserId != null) {
+ predicates.add(criteriaBuilder.equal(root.get("assignedTo"), assignedToUserId));
+ }
+ if (clientSurname != null) {
+ predicates.add(criteriaBuilder.like(root.get("personLastName"), "%" + clientSurname + "%"));
+ }
+ if (feeEarnerId != null) {
+ predicates.add(criteriaBuilder.equal(root.get("feeEarnerPartyId"), feeEarnerId));
+ }
+ if (!includeClosed) {
+ predicates.add(criteriaBuilder.isTrue(root.get("isOpen")));
+ }
+ if (notificationType != null) {
+ predicates.add(criteriaBuilder.equal(root.get("actionNotificationInd"), notificationType));
+ }
+ if (dateFrom != null) {
+ predicates.add(criteriaBuilder.greaterThanOrEqualTo(root.get("dateAssigned"), dateFrom));
+ }
+ if (dateTo != null) {
+ predicates.add(criteriaBuilder.lessThanOrEqualTo(root.get("dateAssigned"), dateTo));
+ }
+
+ // Combine all predicates with AND
+ return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
+ };
+ }
+
+
+}
diff --git a/data-service/src/main/java/uk/gov/laa/ccms/data/service/NotificationService.java b/data-service/src/main/java/uk/gov/laa/ccms/data/service/NotificationService.java
index 6c3b5ee..feb9c2a 100644
--- a/data-service/src/main/java/uk/gov/laa/ccms/data/service/NotificationService.java
+++ b/data-service/src/main/java/uk/gov/laa/ccms/data/service/NotificationService.java
@@ -1,15 +1,23 @@
package uk.gov.laa.ccms.data.service;
+import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+import uk.gov.laa.ccms.data.entity.Notification;
import uk.gov.laa.ccms.data.entity.NotificationCount;
import uk.gov.laa.ccms.data.mapper.NotificationSummaryMapper;
+import uk.gov.laa.ccms.data.mapper.NotificationsMapper;
import uk.gov.laa.ccms.data.model.NotificationSummary;
+import uk.gov.laa.ccms.data.model.Notifications;
import uk.gov.laa.ccms.data.repository.NotificationCountRepository;
+import uk.gov.laa.ccms.data.repository.NotificationRepository;
+import uk.gov.laa.ccms.data.repository.specification.NotificationSpecification;
/**
* Service class responsible for handling notification-related operations.
@@ -27,6 +35,8 @@ public class NotificationService {
private final NotificationCountRepository notificationCountRepository;
private final NotificationSummaryMapper notificationSummaryMapper;
+ private final NotificationRepository notificationRepository;
+ private final NotificationsMapper notificationsMapper;
private final UserService userService;
/**
@@ -47,4 +57,38 @@ public Optional getUserNotificationSummary(String userId) {
}
return Optional.empty();
}
+
+ /**
+ * Retrieves a paginated list of notifications.
+ *
+ * @param caseReferenceNumber the case reference number to filter by (optional).
+ * @param providerCaseReference the provider case reference to filter by (optional).
+ * @param assignedToUserId the user ID assigned to the notification (optional).
+ * @param clientSurname the client's surname to filter by (optional).
+ * @param feeEarnerId the ID of the fee earner to filter by (optional).
+ * @param includeClosed a flag to include closed notifications in the result set.
+ * @param notificationType the type of notification to filter by (optional).
+ * @param dateFrom the starting date for filtering notifications by the date assigned (inclusive).
+ * @param dateTo the ending date for filtering notifications by the date assigned (inclusive).
+ * @param pageable the pageable to describe the requested pagination format.
+ * @return a paginated list of notifications.
+ */
+ public Optional getNotifications(String caseReferenceNumber,
+ String providerCaseReference, String assignedToUserId, String clientSurname,
+ Integer feeEarnerId, boolean includeClosed, String notificationType, LocalDate dateFrom,
+ LocalDate dateTo, Pageable pageable) {
+ Page byAssignedTo = notificationRepository.findAll(
+ NotificationSpecification.withFilters(caseReferenceNumber,
+ providerCaseReference,
+ assignedToUserId,
+ clientSurname,
+ feeEarnerId,
+ includeClosed,
+ notificationType,
+ dateFrom,
+ dateTo),
+ pageable);
+ Notifications notifications = notificationsMapper.mapToNotificationsList(byAssignedTo);
+ return Optional.ofNullable(notifications);
+ }
}
diff --git a/data-service/src/test/java/uk/gov/laa/ccms/data/controller/NotificationsControllerTest.java b/data-service/src/test/java/uk/gov/laa/ccms/data/controller/NotificationsControllerTest.java
index 1cd349d..16a7f03 100644
--- a/data-service/src/test/java/uk/gov/laa/ccms/data/controller/NotificationsControllerTest.java
+++ b/data-service/src/test/java/uk/gov/laa/ccms/data/controller/NotificationsControllerTest.java
@@ -14,14 +14,18 @@
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
+import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.web.PageableHandlerMethodArgumentResolver;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.context.WebApplicationContext;
import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper;
+import uk.gov.laa.ccms.data.model.Notification;
import uk.gov.laa.ccms.data.model.NotificationSummary;
+import uk.gov.laa.ccms.data.model.Notifications;
import uk.gov.laa.ccms.data.service.NotificationService;
@ExtendWith({SpringExtension.class})
@@ -44,7 +48,9 @@ class NotificationsControllerTest {
@BeforeEach
public void setup() {
- mockMvc = standaloneSetup(notificationsController).build();
+ mockMvc = standaloneSetup(notificationsController)
+ .setCustomArgumentResolvers(new PageableHandlerMethodArgumentResolver())
+ .build();
objectMapper = new ObjectMapper();
}
@@ -74,4 +80,31 @@ void getUserNotificationSummary_notFound() throws Exception {
.andDo(print())
.andExpect(status().isNotFound());
}
+
+ @Test
+ @DisplayName("getNotifications: Returns data")
+ void getNotifications_returnsData() throws Exception {
+ //Given
+ Notifications expected = new Notifications().addContentItem(new Notification().notificationId("123"));
+ when(notificationService.getNotifications(Mockito.any(), Mockito.any(), Mockito.any(),
+ Mockito.any(), Mockito.any(), Mockito.anyBoolean(), Mockito.any(), Mockito.any(),
+ Mockito.any(), Mockito.any())).thenReturn(Optional.of(
+ expected));
+ // Then
+ String jsonContent = objectMapper.writeValueAsString(expected);
+ this.mockMvc.perform(get("/notifications"))
+ .andDo(print())
+ .andExpect(status().isOk())
+ .andExpect(content().json(jsonContent));
+ }
+
+ @Test
+ @DisplayName("getNotifications: Not found")
+ void getNotifications_notFound() throws Exception {
+ //Given
+ // Then
+ this.mockMvc.perform(get("/notifications"))
+ .andDo(print())
+ .andExpect(status().isNotFound());
+ }
}
\ No newline at end of file
diff --git a/data-service/src/test/java/uk/gov/laa/ccms/data/mapper/NotificationsMapperImplTest.java b/data-service/src/test/java/uk/gov/laa/ccms/data/mapper/NotificationsMapperImplTest.java
new file mode 100644
index 0000000..7f9bd1f
--- /dev/null
+++ b/data-service/src/test/java/uk/gov/laa/ccms/data/mapper/NotificationsMapperImplTest.java
@@ -0,0 +1,307 @@
+package uk.gov.laa.ccms.data.mapper;
+
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.time.LocalDate;
+import java.util.Arrays;
+import java.util.Collections;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.PageRequest;
+import uk.gov.laa.ccms.data.entity.Notification;
+import uk.gov.laa.ccms.data.model.Document;
+import uk.gov.laa.ccms.data.model.Note;
+import uk.gov.laa.ccms.data.model.Notifications;
+
+@DisplayName("NotificationsMapperImpl Test")
+public class NotificationsMapperImplTest {
+
+ NotificationsMapperImpl mapper = new NotificationsMapperImpl();
+
+ @Test
+ @DisplayName("Should map page attributes")
+ void shouldMapPageAttributes(){
+ // Given
+ Page input = new PageImpl<>(Collections.emptyList(),
+ PageRequest.of(1, 100), 150);
+ // When
+ Notifications result = mapper.mapToNotificationsList(input);
+ // Then
+ assertEquals(1, result.getNumber());
+ assertEquals(100, result.getSize());
+ assertEquals(2, result.getTotalPages());
+ assertEquals(150, result.getTotalElements());
+ }
+
+ @Test
+ @DisplayName("Should map single notification excluding Lob objects")
+ void shouldMapSingleNotificationExcludingLobObjects(){
+ // Given
+ Notification notification = Notification.builder()
+ .assignedTo("User Name")
+ .userLoginId("User Login Id")
+ .clientName("Client Name")
+ .categoryOfLaw("Category Of Law")
+ .feeEarner("Fee Earner")
+ .notificationId(1L)
+ .providerFirmId(2L)
+ .notificationSubject("Notification Subject")
+ .dateAssigned(LocalDate.of(2024, 1, 1))
+ .dueDate(LocalDate.of(2025, 2, 1))
+ .status("Status")
+ .evidenceAllowedInd("true")
+ .isOpen(true)
+ .actionNotificationInd("N")
+ .lscCaseRefReference("LSC Case Ref")
+ .providerCaseReference("Provider Case Ref")
+ .clientPartyId(3L)
+ .feeEarnerPartyId(4L)
+ .assignedToPartyId(5L)
+ .build();
+ Page input = new PageImpl<>(Arrays.asList(notification),
+ PageRequest.of(0, 1), 1);
+ // When
+ Notifications result = mapper.mapToNotificationsList(input);
+ // Then
+ uk.gov.laa.ccms.data.model.Notification notificationResult = result.getContent().get(0);
+ assertEquals("User Login Id", notificationResult.getUser().getLoginId());
+ assertEquals("User Name", notificationResult.getUser().getUsername());
+ assertEquals("Client Name", notificationResult.getClientName());
+ assertEquals("Category Of Law", notificationResult.getCategoryOfLaw());
+ assertEquals("Fee Earner", notificationResult.getFeeEarner());
+ assertEquals("1", notificationResult.getNotificationId());
+ assertEquals("2", notificationResult.getProviderFirmId());
+ assertEquals("Notification Subject", notificationResult.getSubject());
+ assertEquals(LocalDate.of(2024, 1, 1), notificationResult.getAssignDate());
+ assertEquals(LocalDate.of(2025, 2, 1), notificationResult.getDueDate());
+ assertEquals("N", notificationResult.getNotificationType());
+ assertEquals("Status", notificationResult.getStatus());
+ assertEquals(true, notificationResult.getEvidenceAllowed());
+ assertEquals(true, notificationResult.getNotificationOpenIndicator());
+ assertEquals("Provider Case Ref",notificationResult.getProviderCaseReferenceNumber());
+ assertEquals("LSC Case Ref", notificationResult.getCaseReferenceNumber());
+ assertEquals(true, notificationResult.getEvidenceAllowed());
+ }
+
+
+ @Test
+ @DisplayName("Should map notes")
+ void shouldMapNotes(){
+ // Given
+ String noteContent =
+ """
+
+
+ 78405222
+ LAA
+ 2024-12-12
+ 12/12/2024 09:16***PENNY.WALL@SWITALSKIS.COM*** Comments:
+
+ LAA Case Reference Number:test
+ Message for Provider:
+
+
+
+
+ 78405224
+ LAA
+ 2024-12-12
+ Unable to send message to conducting solicitor - No live case with the
+ reference test currently exists. Please either check your records to
+ ensure you have quoted the correct case reference or check with your conducting
+ solicitor to ensure that this case has been granted.
+
+
+
+ """;
+ Notification notification = Notification.builder()
+ .notes(noteContent)
+ .build();
+ Page input = new PageImpl<>(Arrays.asList(notification),
+ PageRequest.of(0, 1), 1);
+ // When
+ Notifications result = mapper.mapToNotificationsList(input);
+ // Then
+ uk.gov.laa.ccms.data.model.Notification notificationResult = result.getContent().get(0);
+ Note noteOne = notificationResult.getNotes().get(0);
+ Note noteTwo = notificationResult.getNotes().get(1);
+ assertEquals("78405222", noteOne.getNotesId());
+ assertEquals("LAA", noteOne.getUser().getUsername());
+ assertEquals("""
+ 12/12/2024 09:16***PENNY.WALL@SWITALSKIS.COM*** Comments:
+
+ LAA Case Reference Number:test
+ Message for Provider:
+
+ """.strip(), noteOne.getMessage().strip());
+ assertEquals(LocalDate.of(2024, 12, 12), noteTwo.getDate());
+ assertEquals("78405224", noteTwo.getNotesId());
+ assertEquals("LAA", noteTwo.getUser().getUsername());
+ assertEquals("""
+ Unable to send message to conducting solicitor - No live case with the
+ reference test currently exists. Please either check your records to
+ ensure you have quoted the correct case reference or check with your conducting
+ solicitor to ensure that this case has been granted.""".strip(), noteTwo.getMessage().strip());
+ assertEquals(LocalDate.of(2024, 12, 12), noteTwo.getDate());
+ }
+
+ @Test
+ @DisplayName("Should map empty notes")
+ void shouldMapEmptyNotes(){
+ // Given
+ String noteContent = "";
+ Notification notification = Notification.builder()
+ .notes(noteContent)
+ .build();
+ Page input = new PageImpl<>(Arrays.asList(notification),
+ PageRequest.of(0, 1), 1);
+ // When
+ Notifications result = mapper.mapToNotificationsList(input);
+ // Then
+ assertTrue(result.getContent().get(0).getNotes().isEmpty());
+ }
+
+
+ @Test
+ @DisplayName("Should map uploaded documents")
+ void shouldMapUploadedDocuments(){
+ String uploadedDocumentsString =
+ """
+
+
+ 33373866
+ COURT_ORD
+ E
+ 123
+
+
+ 33373867
+ COURT_ORD
+ E
+ 123
+
+
+ """;
+ Notification notification = Notification.builder()
+ .uploadedDocuments(uploadedDocumentsString)
+ .build();
+ Page input = new PageImpl<>(Arrays.asList(notification),
+ PageRequest.of(0, 1), 1);
+ // When
+ Notifications result = mapper.mapToNotificationsList(input);
+ // Then
+ uk.gov.laa.ccms.data.model.Notification notificationResult = result.getContent().get(0);
+ Document documentOne = notificationResult.getUploadedDocuments().get(0);
+ Document documentTwo = notificationResult.getUploadedDocuments().get(1);
+ assertEquals("33373866", documentOne.getDocumentId());
+ assertEquals("COURT_ORD", documentOne.getDocumentType());
+ assertEquals("E", documentOne.getChannel());
+ assertEquals("123", documentOne.getText());
+ assertEquals("33373867", documentTwo.getDocumentId());
+ assertEquals("COURT_ORD", documentTwo.getDocumentType());
+ assertEquals("E", documentTwo.getChannel());
+ assertEquals("123", documentTwo.getText());
+ }
+
+ @Test
+ @DisplayName("Should map empty uploaded documents")
+ void shouldMapEmptyUploadedDocuments(){
+ // Given
+ String uploadedDocumentsContent = "";
+ Notification notification = Notification.builder()
+ .uploadedDocuments(uploadedDocumentsContent)
+ .build();
+ Page input = new PageImpl<>(Arrays.asList(notification),
+ PageRequest.of(0, 1), 1);
+ // When
+ Notifications result = mapper.mapToNotificationsList(input);
+ // Then
+ assertTrue(result.getContent().get(0).getUploadedDocuments().isEmpty());
+ }
+
+ @Test
+ @DisplayName("Should map attached documents")
+ void shouldMapAttachedDocuments(){
+ String attachedDocuments =
+ """
+
+
+ 3426023
+ Outbound Letter
+ Bill assessment outcome - sol
+
+
+ """;
+ Notification notification = Notification.builder()
+ .attachedDocuments(attachedDocuments)
+ .build();
+ Page input = new PageImpl<>(Arrays.asList(notification),
+ PageRequest.of(0, 1), 1);
+ // When
+ Notifications result = mapper.mapToNotificationsList(input);
+ // Then
+ uk.gov.laa.ccms.data.model.Notification notificationResult = result.getContent().get(0);
+ Document documentOne = notificationResult.getAttachedDocuments().get(0);
+ assertEquals("3426023", documentOne.getDocumentId());
+ assertEquals("Outbound Letter", documentOne.getTitle());
+ assertEquals("Bill assessment outcome - sol", documentOne.getText());
+ }
+
+ @Test
+ @DisplayName("Should map empty attached documents")
+ void shouldMapEmptyAttachedDocuments(){
+ // Given
+ String attachedDocumentsContent = "";
+ Notification notification = Notification.builder()
+ .attachedDocuments(attachedDocumentsContent)
+ .build();
+ Page input = new PageImpl<>(Arrays.asList(notification),
+ PageRequest.of(0, 1), 1);
+ // When
+ Notifications result = mapper.mapToNotificationsList(input);
+ // Then
+ assertTrue(result.getContent().get(0).getAttachedDocuments().isEmpty());
+ }
+
+ @Test
+ @DisplayName("Should map available responses")
+ void shouldMapAvailableResponses(){
+ String attachedDocuments =
+ """
+
+ Read
+
+ """;
+ Notification notification = Notification.builder()
+ .availableResponses(attachedDocuments)
+ .build();
+ Page input = new PageImpl<>(Arrays.asList(notification),
+ PageRequest.of(0, 1), 1);
+ // When
+ Notifications result = mapper.mapToNotificationsList(input);
+ // Then
+ uk.gov.laa.ccms.data.model.Notification notificationResult = result.getContent().get(0);
+ String response = notificationResult.getAvailableResponses().get(0);
+ assertEquals("Read", response);
+ }
+
+ @Test
+ @DisplayName("Should map empty available responses")
+ void shouldMapEmptyAvailableResponses(){
+ String attachedDocuments =
+ "";
+ Notification notification = Notification.builder()
+ .availableResponses(attachedDocuments)
+ .build();
+ Page input = new PageImpl<>(Arrays.asList(notification),
+ PageRequest.of(0, 1), 1);
+ // When
+ Notifications result = mapper.mapToNotificationsList(input);
+ // Then
+ assertTrue(result.getContent().get(0).getAvailableResponses().isEmpty());
+ }
+}
diff --git a/data-service/src/test/java/uk/gov/laa/ccms/data/service/NotificationServiceTest.java b/data-service/src/test/java/uk/gov/laa/ccms/data/service/NotificationServiceTest.java
new file mode 100644
index 0000000..53dcc80
--- /dev/null
+++ b/data-service/src/test/java/uk/gov/laa/ccms/data/service/NotificationServiceTest.java
@@ -0,0 +1,130 @@
+package uk.gov.laa.ccms.data.service;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+import java.time.LocalDate;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.domain.Specification;
+import uk.gov.laa.ccms.data.entity.Notification;
+import uk.gov.laa.ccms.data.entity.NotificationCount;
+import uk.gov.laa.ccms.data.mapper.NotificationSummaryMapper;
+import uk.gov.laa.ccms.data.mapper.NotificationsMapper;
+import uk.gov.laa.ccms.data.model.NotificationSummary;
+import uk.gov.laa.ccms.data.model.Notifications;
+import uk.gov.laa.ccms.data.model.UserDetail;
+import uk.gov.laa.ccms.data.repository.NotificationCountRepository;
+import uk.gov.laa.ccms.data.repository.NotificationRepository;
+
+@ExtendWith(MockitoExtension.class)
+@DisplayName("Notification service test")
+class NotificationServiceTest {
+
+ private NotificationService notificationService;
+ @Mock
+ private NotificationCountRepository notificationCountRepository;
+ @Mock
+ private NotificationSummaryMapper notificationSummaryMapper;
+ @Mock
+ private NotificationRepository notificationRepository;
+ @Mock
+ private NotificationsMapper notificationsMapper;
+ @Mock
+ private UserService userService;
+
+
+ @BeforeEach
+ void setup() {
+ notificationService = new NotificationService(notificationCountRepository,
+ notificationSummaryMapper, notificationRepository, notificationsMapper, userService);
+ }
+
+ @Test
+ @DisplayName("getUserNotificationSummary(): Returns notification summary")
+ void getUserNotificationSummary_returnsNotificationSummary() {
+ // Given
+ String userId = "123456";
+ when(userService.getUser(userId)).thenReturn(Optional.of(new UserDetail()));
+ List notificationCounts = List.of(new NotificationCount());
+ when(notificationCountRepository.findAllByIdUserLoginId(userId)).thenReturn(notificationCounts);
+ NotificationSummary expected = new NotificationSummary();
+ when(notificationSummaryMapper.toNotificationSummary(notificationCounts)).thenReturn(expected);
+ // When
+ Optional userNotificationSummary = notificationService.getUserNotificationSummary(
+ userId);
+ // Then
+ assertEquals(expected, userNotificationSummary.get());
+ }
+
+ @Test
+ @DisplayName("getUserNotificationSummary(): User not found")
+ void getUserNotificationSummary_userNotFound() {
+ // Given
+ String userId = "123456";
+ // When
+ Optional userNotificationSummary = notificationService.getUserNotificationSummary(
+ userId);
+ // Then
+ assertFalse(userNotificationSummary.isPresent());
+ }
+
+ @Test
+ @DisplayName("getNotifications(): Returns data")
+ void getNotifications_returnsData() {
+ // Given
+ PageImpl repositoryResult = new PageImpl<>(Collections.singletonList(new Notification()));
+ when(notificationRepository.findAll(any(Specification.class), any(Pageable.class)))
+ .thenReturn(
+ repositoryResult);
+ Notifications expected = new Notifications().size(1);
+ when(notificationsMapper.mapToNotificationsList(repositoryResult)).thenReturn(
+ expected
+ );
+ // When
+ Optional result = notificationService.getNotifications(
+ "Case Ref",
+ "Prov case ref",
+ "Assigned user id",
+ "surname",
+ 123,
+ true,
+ "type",
+ LocalDate.of(2000, 1, 1),
+ LocalDate.of(2024, 1 ,1), Pageable.unpaged());
+ // Then
+ assertTrue(result.isPresent());
+ assertEquals(expected, result.get());
+ }
+
+ @Test
+ @DisplayName("getNotifications(): No data found")
+ void getNotifications_noDataFound() {
+ // Given
+ // When
+ Optional result = notificationService.getNotifications(
+ "Case Ref",
+ "Prov case ref",
+ "Assigned user id",
+ "surname",
+ 123,
+ true,
+ "type",
+ LocalDate.of(2000, 1, 1),
+ LocalDate.of(2024, 1 ,1), Pageable.unpaged());
+ // Then
+ assertFalse(result.isPresent());
+ }
+}
\ No newline at end of file