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