diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/constants/EventType.java b/api/src/main/java/ca/bc/gov/educ/eas/api/constants/EventType.java index 1e191de..d040ea0 100644 --- a/api/src/main/java/ca/bc/gov/educ/eas/api/constants/EventType.java +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/constants/EventType.java @@ -4,6 +4,7 @@ public enum EventType { INITIATED, //Default. Used by BaseOrchestrator. MARK_SAGA_COMPLETE, //Default. Used by BaseOrchestrator. GET_PAGINATED_SCHOOLS, + GET_MERGED_STUDENT_IDS, CREATE_STUDENT_REGISTRATION, GET_STUDENT_REGISTRATION, PUBLISH_STUDENT_REGISTRATION, diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/constants/TopicsEnum.java b/api/src/main/java/ca/bc/gov/educ/eas/api/constants/TopicsEnum.java index 03145af..a309b92 100644 --- a/api/src/main/java/ca/bc/gov/educ/eas/api/constants/TopicsEnum.java +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/constants/TopicsEnum.java @@ -5,5 +5,6 @@ public enum TopicsEnum { EAS_API_TOPIC, INSTITUTE_API_TOPIC, EAS_EVENTS_TOPIC, + PEN_SERVICES_API_TOPIC, CREATE_STUDENT_REGISTRATION_SAGA_TOPIC } diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/controller/v1/StudentMergeController.java b/api/src/main/java/ca/bc/gov/educ/eas/api/controller/v1/StudentMergeController.java new file mode 100644 index 0000000..3942051 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/controller/v1/StudentMergeController.java @@ -0,0 +1,24 @@ +package ca.bc.gov.educ.eas.api.controller.v1; + +import ca.bc.gov.educ.eas.api.endpoint.v1.StudentMergeEndpoint; +import ca.bc.gov.educ.eas.api.service.v1.StudentMergeService; +import ca.bc.gov.educ.eas.api.struct.v1.StudentMerge; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@Slf4j +@RestController +public class StudentMergeController implements StudentMergeEndpoint { + private final StudentMergeService studentMergeService; + + public StudentMergeController(StudentMergeService studentMergeService){ + this.studentMergeService = studentMergeService; + } + + @Override + public List getMergedStudentsForDateRange(String createDateStart, String createDateEnd){ + return studentMergeService.getMergedStudentsForDateRange(createDateStart, createDateEnd); + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/endpoint/v1/StudentMergeEndpoint.java b/api/src/main/java/ca/bc/gov/educ/eas/api/endpoint/v1/StudentMergeEndpoint.java new file mode 100644 index 0000000..f3a291f --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/endpoint/v1/StudentMergeEndpoint.java @@ -0,0 +1,25 @@ +package ca.bc.gov.educ.eas.api.endpoint.v1; + +import ca.bc.gov.educ.eas.api.constants.v1.URL; +import ca.bc.gov.educ.eas.api.struct.v1.StudentMerge; +import com.fasterxml.jackson.core.JsonProcessingException; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.List; + +@RequestMapping(URL.BASE_URL + "/student-merges") +public interface StudentMergeEndpoint { + + @GetMapping() + @PreAuthorize("hasAuthority('SCOPE_READ_EAS_STUDENT')") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK")}) + @Tag(name = "PEN Records", description = "Endpoints to retrieve PEN records.") + List getMergedStudentsForDateRange(@RequestParam(name = "createDateStart") String createDateStart, @RequestParam(name = "createDateEnd") String createDateEnd) throws JsonProcessingException; + +} \ No newline at end of file diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/rest/RestUtils.java b/api/src/main/java/ca/bc/gov/educ/eas/api/rest/RestUtils.java index 8f275bc..6ec8c92 100644 --- a/api/src/main/java/ca/bc/gov/educ/eas/api/rest/RestUtils.java +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/rest/RestUtils.java @@ -7,6 +7,7 @@ import ca.bc.gov.educ.eas.api.properties.ApplicationProperties; import ca.bc.gov.educ.eas.api.struct.Event; import ca.bc.gov.educ.eas.api.struct.external.institute.v1.*; +import ca.bc.gov.educ.eas.api.struct.v1.StudentMerge; import ca.bc.gov.educ.eas.api.util.JsonUtil; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; @@ -45,6 +46,8 @@ public class RestUtils { private final Map facilityTypeCodesMap = new ConcurrentHashMap<>(); private final Map schoolCategoryCodesMap = new ConcurrentHashMap<>(); public static final String PAGE_SIZE = "pageSize"; + public static final String CREATE_DATE_START = "createDateStart"; + public static final String CREATE_DATE_END = "createDateEnd"; private final WebClient webClient; private final MessagePublisher messagePublisher; private final ObjectMapper objectMapper = new ObjectMapper(); @@ -324,4 +327,25 @@ public List getAllSchoolList(UUID correlationID) { throw new EasAPIRuntimeException(NATS_TIMEOUT + correlationID + ex.getMessage()); } } + + public List getMergedStudentsForDateRange(UUID correlationID, String createDateStart, String createDateEnd) { + try { + final TypeReference refEventResponse = new TypeReference<>() {}; + final TypeReference> refMergedStudentResponse = new TypeReference<>() {}; + Object event = Event.builder().sagaId(correlationID).eventType(EventType.GET_MERGED_STUDENT_IDS).eventPayload(CREATE_DATE_START.concat("=").concat(createDateStart).concat("&").concat(CREATE_DATE_END).concat("=").concat(createDateEnd)).build(); + val responseMessage = this.messagePublisher.requestMessage(TopicsEnum.PEN_SERVICES_API_TOPIC.toString(), JsonUtil.getJsonBytesFromObject(event)).completeOnTimeout(null, 120, TimeUnit.SECONDS).get(); + if (responseMessage == null) { + log.error("Received null response from PEN SERVICES API for correlationID: {}", correlationID); + throw new EasAPIRuntimeException(NATS_TIMEOUT + correlationID); + } else { + val eventResponse = objectMapper.readValue(responseMessage.getData(), refEventResponse); + return objectMapper.readValue(eventResponse.getEventPayload(), refMergedStudentResponse); + } + + } catch (final Exception ex) { + log.error("Error occurred calling PEN SERVICES API service :: " + ex.getMessage()); + Thread.currentThread().interrupt(); + throw new EasAPIRuntimeException(ex.getMessage()); + } + } } diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/service/v1/StudentMergeService.java b/api/src/main/java/ca/bc/gov/educ/eas/api/service/v1/StudentMergeService.java new file mode 100644 index 0000000..efc1fe0 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/service/v1/StudentMergeService.java @@ -0,0 +1,28 @@ +package ca.bc.gov.educ.eas.api.service.v1; + +import ca.bc.gov.educ.eas.api.rest.RestUtils; +import ca.bc.gov.educ.eas.api.struct.v1.StudentMerge; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.UUID; + +@Service +@Slf4j +public class StudentMergeService { + + private final RestUtils restUtils; + + @Autowired + public StudentMergeService(RestUtils restUtils){ + this.restUtils = restUtils; + } + + public List getMergedStudentsForDateRange(String createDateStart, String createDateEnd){ + UUID correlationID = UUID.randomUUID(); + log.info("Fetching student merge records created between {} and {} with correlation ID: {}", createDateStart, createDateEnd, correlationID); + return restUtils.getMergedStudentsForDateRange(correlationID, createDateStart, createDateEnd); + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/eas/api/struct/v1/StudentMerge.java b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/v1/StudentMerge.java new file mode 100644 index 0000000..778c077 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/eas/api/struct/v1/StudentMerge.java @@ -0,0 +1,53 @@ +package ca.bc.gov.educ.eas.api.struct.v1; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.io.Serializable; + +/** + * The type Student merge. + */ +@EqualsAndHashCode(callSuper = true) +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class StudentMerge extends BaseRequest implements Serializable { + /** + * The constant serialVersionUID. + */ + private static final long serialVersionUID = 1L; + + /** + * The Student merge id. + */ + @NotNull(message = "Student Merge ID cannot be null") + String studentMergeID; + /** + * The Student id. + */ + @NotNull(message = "Student ID cannot be null.") + String studentID; + /** + * The Merge student id. + */ + @NotNull(message = "Merge Student ID cannot be null.") + String mergeStudentID; + /** + * The Student merge direction code. + */ + @NotNull(message = "Student Merge Direction Code cannot be null.") + String studentMergeDirectionCode; + /** + * The Student merge source code. + */ + @NotNull(message = "Student Merge Source Code cannot be null.") + String studentMergeSourceCode; +} diff --git a/api/src/test/java/ca/bc/gov/educ/eas/api/rest/RestUtilsTest.java b/api/src/test/java/ca/bc/gov/educ/eas/api/rest/RestUtilsTest.java index 6193961..a3e2120 100644 --- a/api/src/test/java/ca/bc/gov/educ/eas/api/rest/RestUtilsTest.java +++ b/api/src/test/java/ca/bc/gov/educ/eas/api/rest/RestUtilsTest.java @@ -1,5 +1,6 @@ package ca.bc.gov.educ.eas.api.rest; +import ca.bc.gov.educ.eas.api.exception.EasAPIRuntimeException; import ca.bc.gov.educ.eas.api.messaging.MessagePublisher; import ca.bc.gov.educ.eas.api.properties.ApplicationProperties; import ca.bc.gov.educ.eas.api.struct.external.institute.v1.District; @@ -14,6 +15,9 @@ import org.springframework.web.reactive.function.client.WebClient; import java.util.*; +import java.util.concurrent.CompletableFuture; + +import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -274,4 +278,36 @@ void testGetSchoolFromMincodeMap_WhenApiCallSucceeds_ShouldReturnSchool() { assertEquals(school1, result.get()); } + @Test + void testGetAllMergedStudentsInRange_WhenRequestTimesOut_ShouldThrowEASAPIRuntimeException() { + UUID correlationID = UUID.randomUUID(); + String createStartDate = "2024-02-01T00:00:00"; + String createEndDate = "2024-09-01T00:00:00"; + + when(messagePublisher.requestMessage(anyString(), any(byte[].class))) + .thenReturn(CompletableFuture.completedFuture(null)); + + EasAPIRuntimeException exception = assertThrows( + EasAPIRuntimeException.class, + () -> restUtils.getMergedStudentsForDateRange(correlationID, createStartDate, createEndDate) + ); + + assertEquals(RestUtils.NATS_TIMEOUT + correlationID, exception.getMessage()); + } + + @Test + void testGetMergedStudentsInRange_WhenExceptionOccurs_ShouldThrowEASAPIRuntimeException() { + UUID correlationID = UUID.randomUUID(); + String createStartDate = "2024-02-01T00:00:00"; + String createEndDate = "2024-09-01T00:00:00"; + Exception mockException = new Exception("exception"); + + when(messagePublisher.requestMessage(anyString(), any(byte[].class))) + .thenReturn(CompletableFuture.failedFuture(mockException)); + + assertThrows( + EasAPIRuntimeException.class, + () -> restUtils.getMergedStudentsForDateRange(correlationID, createStartDate, createEndDate) + ); + } } diff --git a/api/src/test/java/ca/bc/gov/educ/eas/api/service/v1/StudentMergeServiceTest.java b/api/src/test/java/ca/bc/gov/educ/eas/api/service/v1/StudentMergeServiceTest.java new file mode 100644 index 0000000..1def7d3 --- /dev/null +++ b/api/src/test/java/ca/bc/gov/educ/eas/api/service/v1/StudentMergeServiceTest.java @@ -0,0 +1,61 @@ +package ca.bc.gov.educ.eas.api.service.v1; + +import ca.bc.gov.educ.eas.api.exception.EasAPIRuntimeException; +import ca.bc.gov.educ.eas.api.rest.RestUtils; +import ca.bc.gov.educ.eas.api.struct.v1.StudentMerge; +import lombok.val; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +class StudentMergeServiceTest { + + @Mock + private RestUtils restUtils; + + @InjectMocks + private StudentMergeService studentMergeService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void testGetMergedStudentsForDateRange_ShouldReturnMergedStudents() { + String createDateStart = "2024-02-01T00:00:00"; + String createDateEnd = "2024-09-01T00:00:00"; + List mockMergedStudents = List.of( + StudentMerge.builder().studentMergeID(UUID.randomUUID().toString()).studentID(UUID.randomUUID().toString()).mergeStudentID(UUID.randomUUID().toString()).studentMergeDirectionCode("FROM").studentMergeSourceCode("MI").build(), + StudentMerge.builder().studentMergeID(UUID.randomUUID().toString()).studentID(UUID.randomUUID().toString()).mergeStudentID(UUID.randomUUID().toString()).studentMergeDirectionCode("TO").studentMergeSourceCode("API").build() + ); + when(restUtils.getMergedStudentsForDateRange(any(UUID.class), eq(createDateStart), eq(createDateEnd))) + .thenReturn(mockMergedStudents); + + val result = studentMergeService.getMergedStudentsForDateRange(createDateStart, createDateEnd); + + assertEquals(mockMergedStudents, result); + verify(restUtils, times(1)).getMergedStudentsForDateRange(any(UUID.class), eq(createDateStart), eq(createDateEnd)); // Use any(UUID.class) here + } + + @Test + void testGetMergedStudentsForDateRange_WhenRestUtilsThrowsException_ShouldThrowEasAPIRuntimeException() { + String createDateStart = "2024-02-01T00:00:00"; + String createDateEnd = "2024-09-01T00:00:00"; + + when(restUtils.getMergedStudentsForDateRange(any(UUID.class), eq(createDateStart), eq(createDateEnd))) + .thenThrow(new EasAPIRuntimeException("PEN Services API failed")); + + assertThrows(EasAPIRuntimeException.class, () -> studentMergeService.getMergedStudentsForDateRange(createDateStart, createDateEnd)); + verify(restUtils, times(1)).getMergedStudentsForDateRange(any(UUID.class), eq(createDateStart), eq(createDateEnd)); // Use any(UUID.class) here + } +} \ No newline at end of file