From 4bcf73a1ad6006b04a8de55a1d7113ce314ff12f Mon Sep 17 00:00:00 2001 From: mightycox Date: Wed, 6 Nov 2024 13:38:16 -0800 Subject: [PATCH 1/3] GRAD2-2976 - Adds saga for archive student process --- .../gradstudent/constant/EventOutcome.java | 15 +- .../api/gradstudent/constant/EventType.java | 16 +- .../api/gradstudent/constant/SagaEnum.java | 5 + .../gradstudent/constant/SagaStatusEnum.java | 23 ++ .../constant/StudentStatusCodes.java | 25 ++ .../educ/api/gradstudent/constant/Topics.java | 5 +- .../messaging/MessagePublisher.java | 36 ++ .../messaging/MessageSubscriber.java | 92 +++++ .../educ/api/gradstudent/model/dc/Event.java | 13 +- .../gradstudent/model/dc/EventOutcome.java | 18 - .../api/gradstudent/model/dc/EventType.java | 18 - .../model/dc/NotificationEvent.java | 13 + .../model/dto/ArchiveStudentsSagaData.java | 21 + .../gradstudent/model/entity/SagaEntity.java | 91 +++++ .../model/entity/SagaEventStatesEntity.java | 70 ++++ .../ArchiveStudentsOrchestrator.java | 69 ++++ .../orchestrator/base/BaseOrchestrator.java | 364 ++++++++++++++++++ .../orchestrator/base/EventHandler.java | 17 + .../orchestrator/base/Orchestrator.java | 39 ++ .../orchestrator/base/SagaEventState.java | 38 ++ .../orchestrator/base/SagaStep.java | 27 ++ .../repository/SagaEventRepository.java | 36 ++ .../repository/SagaRepository.java | 18 + .../api/gradstudent/service/SagaService.java | 165 ++++++++ .../events/EventHandlerDelegatorService.java | 61 +++ .../service/events/EventHandlerService.java | 108 ++++++ .../service/events/EventPublisherService.java | 50 +++ .../educ/api/gradstudent/util/JsonUtil.java | 18 + .../1.0/V1.0.71__DDL-CREATE_SAGA-TABLES.sql | 38 ++ .../controller/BaseIntegrationTest.java | 33 +- .../DataConversionControllerTest.java | 15 +- .../ArchiveStudentsOrchestratorTest.java | 140 +++++++ .../service/CommonServiceTest.java | 14 +- .../service/DataConversionServiceTest.java | 18 +- .../service/EdwSnapshotServiceTest.java | 15 +- .../service/GradStudentServiceTest.java | 15 +- .../service/GraduationStatusServiceTest.java | 11 +- .../service/HistoryServiceTest.java | 12 +- .../JetStreamEventHandlerServiceTest.java | 16 +- .../EventHandlerDelegatorServiceTest.java | 114 ++++++ .../support/MockConfiguration.java | 16 +- .../gradstudent/support/NatsMessageImpl.java | 138 +++++++ 42 files changed, 1919 insertions(+), 147 deletions(-) create mode 100644 api/src/main/java/ca/bc/gov/educ/api/gradstudent/constant/SagaEnum.java create mode 100644 api/src/main/java/ca/bc/gov/educ/api/gradstudent/constant/SagaStatusEnum.java create mode 100644 api/src/main/java/ca/bc/gov/educ/api/gradstudent/constant/StudentStatusCodes.java create mode 100644 api/src/main/java/ca/bc/gov/educ/api/gradstudent/messaging/MessagePublisher.java create mode 100644 api/src/main/java/ca/bc/gov/educ/api/gradstudent/messaging/MessageSubscriber.java delete mode 100644 api/src/main/java/ca/bc/gov/educ/api/gradstudent/model/dc/EventOutcome.java delete mode 100644 api/src/main/java/ca/bc/gov/educ/api/gradstudent/model/dc/EventType.java create mode 100644 api/src/main/java/ca/bc/gov/educ/api/gradstudent/model/dc/NotificationEvent.java create mode 100644 api/src/main/java/ca/bc/gov/educ/api/gradstudent/model/dto/ArchiveStudentsSagaData.java create mode 100644 api/src/main/java/ca/bc/gov/educ/api/gradstudent/model/entity/SagaEntity.java create mode 100644 api/src/main/java/ca/bc/gov/educ/api/gradstudent/model/entity/SagaEventStatesEntity.java create mode 100644 api/src/main/java/ca/bc/gov/educ/api/gradstudent/orchestrator/ArchiveStudentsOrchestrator.java create mode 100644 api/src/main/java/ca/bc/gov/educ/api/gradstudent/orchestrator/base/BaseOrchestrator.java create mode 100644 api/src/main/java/ca/bc/gov/educ/api/gradstudent/orchestrator/base/EventHandler.java create mode 100644 api/src/main/java/ca/bc/gov/educ/api/gradstudent/orchestrator/base/Orchestrator.java create mode 100644 api/src/main/java/ca/bc/gov/educ/api/gradstudent/orchestrator/base/SagaEventState.java create mode 100644 api/src/main/java/ca/bc/gov/educ/api/gradstudent/orchestrator/base/SagaStep.java create mode 100644 api/src/main/java/ca/bc/gov/educ/api/gradstudent/repository/SagaEventRepository.java create mode 100644 api/src/main/java/ca/bc/gov/educ/api/gradstudent/repository/SagaRepository.java create mode 100644 api/src/main/java/ca/bc/gov/educ/api/gradstudent/service/SagaService.java create mode 100644 api/src/main/java/ca/bc/gov/educ/api/gradstudent/service/events/EventHandlerDelegatorService.java create mode 100644 api/src/main/java/ca/bc/gov/educ/api/gradstudent/service/events/EventHandlerService.java create mode 100644 api/src/main/java/ca/bc/gov/educ/api/gradstudent/service/events/EventPublisherService.java create mode 100644 api/src/main/resources/db/migration/1.0/V1.0.71__DDL-CREATE_SAGA-TABLES.sql create mode 100644 api/src/test/java/ca/bc/gov/educ/api/gradstudent/orchestrator/ArchiveStudentsOrchestratorTest.java create mode 100644 api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/events/EventHandlerDelegatorServiceTest.java create mode 100644 api/src/test/java/ca/bc/gov/educ/api/gradstudent/support/NatsMessageImpl.java diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/constant/EventOutcome.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/constant/EventOutcome.java index 6e9096975..b81cfd0f7 100644 --- a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/constant/EventOutcome.java +++ b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/constant/EventOutcome.java @@ -7,5 +7,18 @@ public enum EventOutcome { /** * Student updated event outcome. */ - GRAD_STATUS_UPDATED + GRAD_STATUS_UPDATED, + VALIDATION_SUCCESS_NO_ERROR_WARNING, + VALIDATION_SUCCESS_WITH_ERROR, + PEN_MATCH_PROCESSED, + GRAD_STATUS_FETCHED, + GRAD_STATUS_RESULTS_PROCESSED, + PEN_MATCH_RESULTS_PROCESSED, + READ_FROM_TOPIC_SUCCESS, + INITIATE_SUCCESS, + SAGA_COMPLETED, + ENROLLED_PROGRAMS_WRITTEN, + ADDITIONAL_STUDENT_ATTRIBUTES_CALCULATED, + STUDENTS_ARCHIVED, + BATCH_API_NOTIFIED } diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/constant/EventType.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/constant/EventType.java index aaf0eaff0..50b4ed66e 100644 --- a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/constant/EventType.java +++ b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/constant/EventType.java @@ -6,5 +6,19 @@ public enum EventType { GRAD_STUDENT_GRADUATED, GRAD_STUDENT_UNDO_COMPLETION, - GRAD_STUDENT_UPDATED + GRAD_STUDENT_UPDATED, + VALIDATE_SDC_STUDENT, + PROCESS_PEN_MATCH, + FETCH_GRAD_STATUS, + PROCESS_GRAD_STATUS_RESULT, + PROCESS_PEN_MATCH_RESULTS, + READ_FROM_TOPIC, + INITIATED, + MARK_SAGA_COMPLETE, + GET_PAGINATED_SCHOOLS, + WRITE_ENROLLED_PROGRAMS, + CALCULATE_ADDITIONAL_STUDENT_ATTRIBUTES, + ARCHIVE_STUDENTS, + NOTIFY_ARCHIVE_STUDENT_BATCH_COMPLETED, + ARCHIVE_STUDENTS_REQUEST } diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/constant/SagaEnum.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/constant/SagaEnum.java new file mode 100644 index 000000000..359fd6bfd --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/constant/SagaEnum.java @@ -0,0 +1,5 @@ +package ca.bc.gov.educ.api.gradstudent.constant; + +public enum SagaEnum { + ARCHIVE_STUDENTS_SAGA +} \ No newline at end of file diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/constant/SagaStatusEnum.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/constant/SagaStatusEnum.java new file mode 100644 index 000000000..85e3d08a6 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/constant/SagaStatusEnum.java @@ -0,0 +1,23 @@ +package ca.bc.gov.educ.api.gradstudent.constant; + +/** + * The enum Saga status enum. + */ +public enum SagaStatusEnum { + /** + * Started saga status enum. + */ + STARTED, + /** + * In progress saga status enum. + */ + IN_PROGRESS, + /** + * Completed saga status enum. + */ + COMPLETED, + /** + * Force stopped saga status enum. + */ + FORCE_STOPPED +} diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/constant/StudentStatusCodes.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/constant/StudentStatusCodes.java new file mode 100644 index 000000000..f56db7507 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/constant/StudentStatusCodes.java @@ -0,0 +1,25 @@ +package ca.bc.gov.educ.api.gradstudent.constant; + +import lombok.Getter; + +import java.util.Arrays; +import java.util.Optional; + +@Getter +public enum StudentStatusCodes { + CURRENT("CUR"), + ARCHIVED("ARC"), + DECEASED("DEC"), + MERGED("MER"), + TERMINATED("TER"), + PENDING_ARCHIVE("PEN"); + + private final String code; + StudentStatusCodes(String code) { + this.code = code; + } + + public static Optional findByValue(String value) { + return Arrays.stream(values()).filter(e -> Arrays.asList(e.code).contains(value)).findFirst(); + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/constant/Topics.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/constant/Topics.java index 7bc5019de..862749277 100644 --- a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/constant/Topics.java +++ b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/constant/Topics.java @@ -8,5 +8,8 @@ public enum Topics { * GradStatus events topic. */ GRAD_STATUS_EVENT_TOPIC, - GRAD_STUDENT_API_FETCH_GRAD_STATUS_TOPIC + GRAD_STUDENT_API_FETCH_GRAD_STATUS_TOPIC, + GRAD_STUDENT_API_TOPIC, + GRAD_STUDENT_ARCHIVE_STUDENTS_SAGA_TOPIC, + GRAD_BATCH_API_TOPIC, } diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/messaging/MessagePublisher.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/messaging/MessagePublisher.java new file mode 100644 index 000000000..17283e74d --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/messaging/MessagePublisher.java @@ -0,0 +1,36 @@ +package ca.bc.gov.educ.api.gradstudent.messaging; + +import io.nats.client.Connection; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.util.Optional; + +@Component +@Slf4j +public class MessagePublisher { + private final Connection connection; + + @Autowired + public MessagePublisher(final Connection con) { + this.connection = con; + } + + public void dispatchMessage(final String subject, final byte[] message) { + this.connection.publish(subject, message); + } + + public Optional requestMessage(final String subject, final byte[] message) throws InterruptedException { + log.debug("requesting from NATS on topic :: {} with payload :: {}", subject, new String(message)); + val response = this.connection.request(subject, message, Duration.ofSeconds(30)).getData(); + if (response == null || response.length == 0) { + return Optional.empty(); + } + val responseValue = new String(response); + log.debug("got response from NATS :: {}", responseValue); + return Optional.of(responseValue); + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/messaging/MessageSubscriber.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/messaging/MessageSubscriber.java new file mode 100644 index 000000000..eb0f1efda --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/messaging/MessageSubscriber.java @@ -0,0 +1,92 @@ +package ca.bc.gov.educ.api.gradstudent.messaging; + +import ca.bc.gov.educ.api.gradstudent.model.dc.Event; +import ca.bc.gov.educ.api.gradstudent.orchestrator.base.EventHandler; +import ca.bc.gov.educ.api.gradstudent.service.events.EventHandlerDelegatorService; +import ca.bc.gov.educ.api.gradstudent.util.EducGradStudentApiConstants; +import ca.bc.gov.educ.api.gradstudent.util.JsonUtil; +import ca.bc.gov.educ.api.gradstudent.util.LogHelper; +import io.nats.client.Connection; +import io.nats.client.Message; +import io.nats.client.MessageHandler; +import jakarta.annotation.PostConstruct; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static ca.bc.gov.educ.api.gradstudent.constant.Topics.GRAD_STUDENT_API_TOPIC; +import static lombok.AccessLevel.PRIVATE; + +@Component +@Slf4j +public class MessageSubscriber { + + @Getter(PRIVATE) + private final Map handlerMap = new HashMap<>(); + @Getter(PRIVATE) + private final EventHandlerDelegatorService eventHandlerDelegatorService; + private final Connection connection; + private final EducGradStudentApiConstants constants; + + @Autowired + public MessageSubscriber(final Connection con, final List eventHandlers, final EducGradStudentApiConstants constants, final EventHandlerDelegatorService eventHandlerDelegatorService) { + this.eventHandlerDelegatorService = eventHandlerDelegatorService; + this.connection = con; + eventHandlers.forEach(handler -> { + this.handlerMap.put(handler.getTopicToSubscribe(), handler); + + this.subscribeForSAGA(handler.getTopicToSubscribe(), handler); + }); + this.constants = constants; + } + + @PostConstruct + public void subscribe() { + final String queue = GRAD_STUDENT_API_TOPIC.toString().replace("_", "-"); + final var dispatcher = this.connection.createDispatcher(this.onMessage()); + dispatcher.subscribe(GRAD_STUDENT_API_TOPIC.toString(), queue); + } + + public MessageHandler onMessage() { + return (Message message) -> { + if (message != null) { + log.info("Message received subject :: {}, replyTo :: {}, subscriptionID :: {}", message.getSubject(), message.getReplyTo(), message.getSID()); + try { + final var eventString = new String(message.getData()); + LogHelper.logMessagingEventDetails(eventString, constants.isSplunkLogHelperEnabled()); + final var event = JsonUtil.getJsonObjectFromString(Event.class, eventString); + eventHandlerDelegatorService.handleEvent(event, message); + } catch (final Exception e) { + log.error("Exception ", e); + } + } + }; + } + + private static MessageHandler onMessageForSAGA(final EventHandler eventHandler) { + return (Message message) -> { + if (message != null) { + log.info("Message received subject :: {}, replyTo :: {}, subscriptionID :: {}", message.getSubject(), message.getReplyTo(), message.getSID()); + try { + final var eventString = new String(message.getData()); + final var event = JsonUtil.getJsonObjectFromString(Event.class, eventString); + eventHandler.handleEvent(event); + } catch (final Exception e) { + log.error("Exception ", e); + } + } + }; + } + + private void subscribeForSAGA(final String topic, final EventHandler eventHandler) { + this.handlerMap.computeIfAbsent(topic, k -> eventHandler); + final String queue = topic.replace("_", "-"); + final var dispatcher = this.connection.createDispatcher(MessageSubscriber.onMessageForSAGA(eventHandler)); + dispatcher.subscribe(topic, queue); + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/model/dc/Event.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/model/dc/Event.java index 442373986..809ce7ae0 100644 --- a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/model/dc/Event.java +++ b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/model/dc/Event.java @@ -1,5 +1,7 @@ package ca.bc.gov.educ.api.gradstudent.model.dc; +import ca.bc.gov.educ.api.gradstudent.constant.EventOutcome; +import ca.bc.gov.educ.api.gradstudent.constant.EventType; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.AllArgsConstructor; import lombok.Builder; @@ -36,13 +38,6 @@ public class Event { /** * The Event payload. */ - private String eventPayload; // json string - /** - * The school batch ID - */ - private String sdcSchoolBatchID; - /** - * The student ID - */ - private String sdcSchoolStudentID; + private String eventPayload; + private String batchId; } diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/model/dc/EventOutcome.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/model/dc/EventOutcome.java deleted file mode 100644 index 089243c4c..000000000 --- a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/model/dc/EventOutcome.java +++ /dev/null @@ -1,18 +0,0 @@ -package ca.bc.gov.educ.api.gradstudent.model.dc; - -/** - * The enum Event outcome. - */ -public enum EventOutcome { - VALIDATION_SUCCESS_NO_ERROR_WARNING, - VALIDATION_SUCCESS_WITH_ERROR, - PEN_MATCH_PROCESSED, - GRAD_STATUS_FETCHED, - GRAD_STATUS_RESULTS_PROCESSED, - PEN_MATCH_RESULTS_PROCESSED, - READ_FROM_TOPIC_SUCCESS, - INITIATE_SUCCESS, - SAGA_COMPLETED, - ENROLLED_PROGRAMS_WRITTEN, - ADDITIONAL_STUDENT_ATTRIBUTES_CALCULATED -} diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/model/dc/EventType.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/model/dc/EventType.java deleted file mode 100644 index 94ef33ade..000000000 --- a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/model/dc/EventType.java +++ /dev/null @@ -1,18 +0,0 @@ -package ca.bc.gov.educ.api.gradstudent.model.dc; - -/** - * The enum Event type. - */ -public enum EventType { - VALIDATE_SDC_STUDENT, - PROCESS_PEN_MATCH, - FETCH_GRAD_STATUS, - PROCESS_GRAD_STATUS_RESULT, - PROCESS_PEN_MATCH_RESULTS, - READ_FROM_TOPIC, - INITIATED, - MARK_SAGA_COMPLETE, - GET_PAGINATED_SCHOOLS, - WRITE_ENROLLED_PROGRAMS, - CALCULATE_ADDITIONAL_STUDENT_ATTRIBUTES -} diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/model/dc/NotificationEvent.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/model/dc/NotificationEvent.java new file mode 100644 index 000000000..284f3117c --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/model/dc/NotificationEvent.java @@ -0,0 +1,13 @@ +package ca.bc.gov.educ.api.gradstudent.model.dc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class NotificationEvent extends Event { + private String sagaStatus; + private String sagaName; +} diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/model/dto/ArchiveStudentsSagaData.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/model/dto/ArchiveStudentsSagaData.java new file mode 100644 index 000000000..38ce4c8e8 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/model/dto/ArchiveStudentsSagaData.java @@ -0,0 +1,21 @@ +package ca.bc.gov.educ.api.gradstudent.model.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class ArchiveStudentsSagaData { + List schoolsOfRecords; + long batchId; + String updateUser; + String studentStatusCode; +} diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/model/entity/SagaEntity.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/model/entity/SagaEntity.java new file mode 100644 index 000000000..04058219b --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/model/entity/SagaEntity.java @@ -0,0 +1,91 @@ +package ca.bc.gov.educ.api.gradstudent.model.entity; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.DynamicUpdate; +import org.hibernate.annotations.GenericGenerator; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PastOrPresent; +import jakarta.validation.constraints.Size; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.util.UUID; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +@Entity +@Table(name = "GRAD_STUDENT_SAGA") +@DynamicUpdate +public class SagaEntity { + + @Id + @GeneratedValue(generator = "UUID") + @GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator", parameters = { + @org.hibernate.annotations.Parameter(name = "uuid_gen_strategy_class", value = "org.hibernate.id.uuid.CustomVersionOneStrategy")}) + @Column(name = "SAGA_ID", unique = true, updatable = false, columnDefinition = "BINARY(16)") + UUID sagaId; + + @NotNull(message = "saga name cannot be null") + @Column(name = "SAGA_NAME") + String sagaName; + + @NotNull(message = "saga state cannot be null") + @Column(name = "SAGA_STATE") + String sagaState; + + @Column(name = "BATCH_ID") + Long batchId; + + @Lob + @Column(name = "PAYLOAD") + byte @NotNull(message = "payload cannot be null") [] payloadBytes; + + @NotNull(message = "status cannot be null") + @Column(name = "STATUS") + String status; + + @NotNull(message = "create user cannot be null") + @Column(name = "CREATE_USER", updatable = false) + @Size(max = 50) + String createUser; + + @NotNull(message = "update user cannot be null") + @Column(name = "UPDATE_USER") + @Size(max = 50) + String updateUser; + + @PastOrPresent + @Column(name = "CREATE_DATE", updatable = false) + LocalDateTime createDate; + + @PastOrPresent + @Column(name = "UPDATE_DATE") + LocalDateTime updateDate; + + @Column(name = "RETRY_COUNT") + private Integer retryCount; + + public String getPayload() { + return new String(this.getPayloadBytes(), StandardCharsets.UTF_8); + } + + public void setPayload(final String payload) { + this.setPayloadBytes(payload.getBytes(StandardCharsets.UTF_8)); + } + + public static class SagaEntityBuilder { + byte[] payloadBytes; + + public SagaEntityBuilder payload(final String payload) { + this.payloadBytes = payload.getBytes(StandardCharsets.UTF_8); + return this; + } + } + +} diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/model/entity/SagaEventStatesEntity.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/model/entity/SagaEventStatesEntity.java new file mode 100644 index 000000000..aad4d7fe1 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/model/entity/SagaEventStatesEntity.java @@ -0,0 +1,70 @@ +package ca.bc.gov.educ.api.gradstudent.model.entity; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PastOrPresent; +import jakarta.validation.constraints.Size; +import lombok.*; +import org.hibernate.annotations.DynamicUpdate; +import org.hibernate.annotations.GenericGenerator; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Setter +@Entity +@Table(name = "GRAD_STUDENT_SAGA_EVENT_STATES") +@DynamicUpdate +public class SagaEventStatesEntity { + + @Id + @GeneratedValue(generator = "UUID") + @GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator", parameters = { + @org.hibernate.annotations.Parameter(name = "uuid_gen_strategy_class", value = "org.hibernate.id.uuid.CustomVersionOneStrategy")}) + @Column(name = "SAGA_EVENT_ID", unique = true, updatable = false, columnDefinition = "BINARY(16)") + UUID sagaEventId; + + @ToString.Exclude + @EqualsAndHashCode.Exclude + @ManyToOne + @JoinColumn(name = "SAGA_ID", updatable = false, columnDefinition = "BINARY(16)") + SagaEntity saga; + + @NotNull(message = "saga_event_state cannot be null") + @Column(name = "SAGA_EVENT_STATE") + String sagaEventState; + + @NotNull(message = "saga_event_outcome cannot be null") + @Column(name = "SAGA_EVENT_OUTCOME") + String sagaEventOutcome; + + @NotNull(message = "saga_step_number cannot be null") + @Column(name = "SAGA_STEP_NUMBER") + Integer sagaStepNumber; + + @Column(name = "SAGA_EVENT_RESPONSE", length = 10485760) + String sagaEventResponse; + + @NotNull(message = "create user cannot be null") + @Column(name = "CREATE_USER", updatable = false) + @Size(max = 50) + String createUser; + + @NotNull(message = "update user cannot be null") + @Column(name = "UPDATE_USER") + @Size(max = 50) + String updateUser; + + @PastOrPresent + @Column(name = "CREATE_DATE", updatable = false) + LocalDateTime createDate; + + @PastOrPresent + @Column(name = "UPDATE_DATE") + LocalDateTime updateDate; + +} diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/orchestrator/ArchiveStudentsOrchestrator.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/orchestrator/ArchiveStudentsOrchestrator.java new file mode 100644 index 000000000..6b7cfaf03 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/orchestrator/ArchiveStudentsOrchestrator.java @@ -0,0 +1,69 @@ +package ca.bc.gov.educ.api.gradstudent.orchestrator; + +import ca.bc.gov.educ.api.gradstudent.messaging.MessagePublisher; +import ca.bc.gov.educ.api.gradstudent.model.dc.Event; +import ca.bc.gov.educ.api.gradstudent.model.dto.ArchiveStudentsSagaData; +import ca.bc.gov.educ.api.gradstudent.model.entity.SagaEntity; +import ca.bc.gov.educ.api.gradstudent.model.entity.SagaEventStatesEntity; +import ca.bc.gov.educ.api.gradstudent.orchestrator.base.BaseOrchestrator; +import ca.bc.gov.educ.api.gradstudent.service.SagaService; +import ca.bc.gov.educ.api.gradstudent.util.JsonUtil; +import com.fasterxml.jackson.core.JsonProcessingException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import static ca.bc.gov.educ.api.gradstudent.constant.EventOutcome.BATCH_API_NOTIFIED; +import static ca.bc.gov.educ.api.gradstudent.constant.EventOutcome.STUDENTS_ARCHIVED; +import static ca.bc.gov.educ.api.gradstudent.constant.EventType.ARCHIVE_STUDENTS; +import static ca.bc.gov.educ.api.gradstudent.constant.EventType.NOTIFY_ARCHIVE_STUDENT_BATCH_COMPLETED; +import static ca.bc.gov.educ.api.gradstudent.constant.SagaEnum.ARCHIVE_STUDENTS_SAGA; +import static ca.bc.gov.educ.api.gradstudent.constant.SagaStatusEnum.IN_PROGRESS; +import static ca.bc.gov.educ.api.gradstudent.constant.Topics.*; + +@Component +@Slf4j +public class ArchiveStudentsOrchestrator extends BaseOrchestrator { + + protected ArchiveStudentsOrchestrator(final SagaService sagaService, final MessagePublisher messagePublisher) { + super(sagaService, messagePublisher, ArchiveStudentsSagaData.class, ARCHIVE_STUDENTS_SAGA.toString(), GRAD_STUDENT_ARCHIVE_STUDENTS_SAGA_TOPIC.toString()); + } + + @Override + public void populateStepsToExecuteMap() { + this.stepBuilder() + .begin(ARCHIVE_STUDENTS, this::archiveStudents) + .step(ARCHIVE_STUDENTS, STUDENTS_ARCHIVED, NOTIFY_ARCHIVE_STUDENT_BATCH_COMPLETED, this::notifyBatchApi) + .end(NOTIFY_ARCHIVE_STUDENT_BATCH_COMPLETED, BATCH_API_NOTIFIED); + } + + protected void archiveStudents(final Event event, final SagaEntity saga, final ArchiveStudentsSagaData sagaData) throws JsonProcessingException { + final SagaEventStatesEntity eventStates = this.createEventState(saga, event.getEventType(), event.getEventOutcome(), event.getEventPayload()); + saga.setStatus(IN_PROGRESS.toString()); + saga.setSagaState(ARCHIVE_STUDENTS.toString()); // set current event as saga state. + this.getSagaService().updateAttachedSagaWithEvents(saga, eventStates); + + final Event nextEvent = Event.builder().sagaId(saga.getSagaId()) + .eventType(ARCHIVE_STUDENTS) + .eventPayload(JsonUtil.getJsonStringFromObject(sagaData)) + .replyTo(this.getTopicToSubscribe()) + .batchId(String.valueOf(sagaData.getBatchId())) + .build(); + this.postMessageToTopic(GRAD_STUDENT_API_TOPIC.toString(), nextEvent); + log.info("message sent to {} for {} Event. :: {}", this.getTopicToSubscribe(), nextEvent, saga.getSagaId()); + } + + protected void notifyBatchApi(final Event event, final SagaEntity saga, final ArchiveStudentsSagaData sagaData) { + final SagaEventStatesEntity eventStates = this.createEventState(saga, event.getEventType(), event.getEventOutcome(), event.getEventPayload()); + saga.setSagaState(NOTIFY_ARCHIVE_STUDENT_BATCH_COMPLETED.toString()); // set current event as saga state. + this.getSagaService().updateAttachedSagaWithEvents(saga, eventStates); + + final Event nextEvent = Event.builder() + .sagaId(saga.getSagaId()) + .replyTo(this.getTopicToSubscribe()) + .eventType(NOTIFY_ARCHIVE_STUDENT_BATCH_COMPLETED) + .batchId(String.valueOf(sagaData.getBatchId())) + .build(); + this.postMessageToTopic(String.valueOf(GRAD_BATCH_API_TOPIC), nextEvent); + log.debug("message sent to {} for {} Event. :: {}", this.getTopicToSubscribe(), nextEvent, saga.getSagaId()); + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/orchestrator/base/BaseOrchestrator.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/orchestrator/base/BaseOrchestrator.java new file mode 100644 index 000000000..b08950b62 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/orchestrator/base/BaseOrchestrator.java @@ -0,0 +1,364 @@ +package ca.bc.gov.educ.api.gradstudent.orchestrator.base; + +import ca.bc.gov.educ.api.gradstudent.constant.EventOutcome; +import ca.bc.gov.educ.api.gradstudent.constant.EventType; +import ca.bc.gov.educ.api.gradstudent.messaging.MessagePublisher; +import ca.bc.gov.educ.api.gradstudent.model.dc.Event; +import ca.bc.gov.educ.api.gradstudent.model.dc.NotificationEvent; +import ca.bc.gov.educ.api.gradstudent.model.entity.SagaEntity; +import ca.bc.gov.educ.api.gradstudent.model.entity.SagaEventStatesEntity; +import ca.bc.gov.educ.api.gradstudent.service.SagaService; +import ca.bc.gov.educ.api.gradstudent.util.JsonUtil; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.scheduling.annotation.Async; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.*; +import java.util.concurrent.TimeoutException; +import java.util.function.Predicate; + +import static ca.bc.gov.educ.api.gradstudent.constant.EventOutcome.INITIATE_SUCCESS; +import static ca.bc.gov.educ.api.gradstudent.constant.EventOutcome.SAGA_COMPLETED; +import static ca.bc.gov.educ.api.gradstudent.constant.EventType.INITIATED; +import static ca.bc.gov.educ.api.gradstudent.constant.EventType.MARK_SAGA_COMPLETE; +import static ca.bc.gov.educ.api.gradstudent.constant.SagaStatusEnum.COMPLETED; +import static lombok.AccessLevel.PROTECTED; +import static lombok.AccessLevel.PUBLIC; + +/** + * The type Base orchestrator. + * + * @param the type parameter + */ +@Slf4j +public abstract class BaseOrchestrator implements EventHandler, Orchestrator { + + protected static final String SYSTEM_IS_GOING_TO_EXECUTE_NEXT_EVENT_FOR_CURRENT_EVENT = "system is going to execute next event :: {} for current event {} and SAGA ID :: {}"; + protected static final String SELF = "SELF"; + protected final Class clazz; + protected final Map>> nextStepsToExecute = new LinkedHashMap<>(); + @Getter(PROTECTED) + private final SagaService sagaService; + @Getter(PROTECTED) + private final MessagePublisher messagePublisher; + @Getter(PUBLIC) + private final String sagaName; + @Getter(PUBLIC) + private final String topicToSubscribe; + @Setter(PROTECTED) + protected boolean shouldSendNotificationEvent = true; + + protected BaseOrchestrator(final SagaService sagaService, final MessagePublisher messagePublisher, + final Class clazz, final String sagaName, + final String topicToSubscribe) { + this.sagaService = sagaService; + this.messagePublisher = messagePublisher; + this.clazz = clazz; + this.sagaName = sagaName; + this.topicToSubscribe = topicToSubscribe; + this.populateStepsToExecuteMap(); + } + + protected List> createSingleCollectionEventState(final EventOutcome eventOutcome, final Predicate nextStepPredicate, final EventType nextEventType, final SagaStep stepToExecute) { + final List> eventStates = new ArrayList<>(); + eventStates.add(this.buildSagaEventState(eventOutcome, nextStepPredicate, nextEventType, stepToExecute)); + return eventStates; + } + + protected SagaEventState buildSagaEventState(final EventOutcome eventOutcome, final Predicate nextStepPredicate, final EventType nextEventType, final SagaStep stepToExecute) { + return SagaEventState.builder().currentEventOutcome(eventOutcome).nextStepPredicate(nextStepPredicate).nextEventType(nextEventType).stepToExecute(stepToExecute).build(); + } + + protected BaseOrchestrator registerStepToExecute(final EventType initEvent, final EventOutcome outcome, final Predicate nextStepPredicate, final EventType nextEvent, final SagaStep stepToExecute) { + if (this.nextStepsToExecute.containsKey(initEvent)) { + final List> states = this.nextStepsToExecute.get(initEvent); + states.add(this.buildSagaEventState(outcome, nextStepPredicate, nextEvent, stepToExecute)); + } else { + this.nextStepsToExecute.put(initEvent, this.createSingleCollectionEventState(outcome, nextStepPredicate, nextEvent, stepToExecute)); + } + return this; + } + + public BaseOrchestrator step(final EventType currentEvent, final EventOutcome outcome, final EventType nextEvent, final SagaStep stepToExecute) { + return this.registerStepToExecute(currentEvent, outcome, (T sagaData) -> true, nextEvent, stepToExecute); + } + + public BaseOrchestrator step(final EventType currentEvent, final EventOutcome outcome, final Predicate nextStepPredicate, final EventType nextEvent, final SagaStep stepToExecute) { + return this.registerStepToExecute(currentEvent, outcome, nextStepPredicate, nextEvent, stepToExecute); + } + + public BaseOrchestrator begin(final EventType nextEvent, final SagaStep stepToExecute) { + return this.registerStepToExecute(INITIATED, INITIATE_SUCCESS, (T sagaData) -> true, nextEvent, stepToExecute); + } + + public BaseOrchestrator begin(final Predicate nextStepPredicate, final EventType nextEvent, final SagaStep stepToExecute) { + return this.registerStepToExecute(INITIATED, INITIATE_SUCCESS, nextStepPredicate, nextEvent, stepToExecute); + } + + public void end(final EventType currentEvent, final EventOutcome outcome) { + this.registerStepToExecute(currentEvent, outcome, (T sagaData) -> true, MARK_SAGA_COMPLETE, this::markSagaComplete); + } + + public BaseOrchestrator end(final EventType currentEvent, final EventOutcome outcome, final SagaStep stepToExecute) { + return this.registerStepToExecute(currentEvent, outcome, (T sagaData) -> true, MARK_SAGA_COMPLETE, (Event event, SagaEntity saga, T sagaData) -> { + stepToExecute.apply(event, saga, sagaData); + this.markSagaComplete(event, saga, sagaData); + }); + } + + public BaseOrchestrator or() { + return this; + } + + public BaseOrchestrator stepBuilder() { + return this; + } + + /** + * this method will check if the event is not already processed. this could happen in SAGAs due to duplicate messages. + * Application should be able to handle this. + */ + protected boolean isNotProcessedEvent(final EventType currentEventType, final SagaEntity saga, final Set eventTypes) { + final EventType eventTypeInDB = EventType.valueOf(saga.getSagaState()); + final List events = new LinkedList<>(eventTypes); + final int dbEventIndex = events.indexOf(eventTypeInDB); + final int currentEventIndex = events.indexOf(currentEventType); + return currentEventIndex >= dbEventIndex; + } + + protected SagaEventStatesEntity createEventState(@NotNull final SagaEntity saga, @NotNull final EventType eventType, @NotNull final EventOutcome eventOutcome, final String eventPayload) { + final var user = this.sagaName.length() > 32 ? this.sagaName.substring(0, 32) : this.sagaName; + return SagaEventStatesEntity.builder() + .createDate(LocalDateTime.now()) + .createUser(user) + .updateDate(LocalDateTime.now()) + .updateUser(user) + .saga(saga) + .sagaEventOutcome(eventOutcome.toString()) + .sagaEventState(eventType.toString()) + .sagaStepNumber(this.calculateStep(saga)) + .sagaEventResponse(StringUtils.isBlank(eventPayload) ? "NO-PAYLOAD-IN-RESPONSE" : eventPayload) + .build(); + } + + protected void markSagaComplete(final Event event, final SagaEntity saga, final T sagaData) { + this.markSagaComplete(event, saga, sagaData, ""); + } + + protected void markSagaComplete(final Event event, final SagaEntity saga, final T sagaData, final String payloadToSubscribers) { + //Added to slow down complete write + try { + Thread.sleep(1000); + }catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + log.trace("payload is {}", sagaData); + if (this.shouldSendNotificationEvent) { + final var finalEvent = new NotificationEvent(); + BeanUtils.copyProperties(event, finalEvent); + finalEvent.setEventType(MARK_SAGA_COMPLETE); + finalEvent.setEventOutcome(SAGA_COMPLETED); + finalEvent.setSagaStatus(COMPLETED.toString()); + if(saga.getBatchId() != null) { + finalEvent.setBatchId(String.valueOf(saga.getBatchId())); + } + finalEvent.setSagaName(this.getSagaName()); + finalEvent.setEventPayload(payloadToSubscribers); + this.postMessageToTopic(this.getTopicToSubscribe(), finalEvent); + } + + final SagaEventStatesEntity sagaEventStates = this.createEventState(saga, event.getEventType(), event.getEventOutcome(), event.getEventPayload()); + saga.setSagaState(COMPLETED.toString()); + saga.setStatus(COMPLETED.toString()); + saga.setUpdateDate(LocalDateTime.now()); + this.getSagaService().updateAttachedSagaWithEvents(saga, sagaEventStates); + + } + + private int calculateStep(final SagaEntity saga) { + val sagaStates = this.getSagaService().findAllSagaStates(saga); + return (sagaStates.size() + 1); + } + + protected void postMessageToTopic(final String topicName, final Event nextEvent) { + final var eventStringOptional = JsonUtil.getJsonString(nextEvent); + if (eventStringOptional.isPresent()) { + this.getMessagePublisher().dispatchMessage(topicName, eventStringOptional.get().getBytes()); + } else { + log.error("event string is not present for :: {} :: this should not have happened", nextEvent); + } + } + + protected Optional findTheLastEventOccurred(final List eventStates) { + final int step = eventStates.stream().map(SagaEventStatesEntity::getSagaStepNumber).mapToInt(x -> x).max().orElse(0); + return eventStates.stream().filter(element -> element.getSagaStepNumber() == step).findFirst(); + } + + /** + * this method is called from the cron job , which will replay the saga process based on its current state. + */ + @Override + @Transactional + @Async("sagaRetryTaskExecutor") + public void replaySaga(final SagaEntity saga) throws IOException, InterruptedException, TimeoutException { + final var eventStates = this.getSagaService().findAllSagaStates(saga); + final var t = JsonUtil.getJsonObjectFromString(this.clazz, saga.getPayload()); + if (eventStates.isEmpty()) { //process did not start last time, lets start from beginning. + this.replayFromBeginning(saga, t); + } else { + this.replayFromLastEvent(saga, eventStates, t); + } + } + + private void replayFromLastEvent(final SagaEntity saga, final List eventStates, final T t) throws InterruptedException, TimeoutException, IOException { + val sagaEventOptional = this.findTheLastEventOccurred(eventStates); + if (sagaEventOptional.isPresent()) { + val sagaEvent = sagaEventOptional.get(); + log.trace(sagaEventOptional.toString()); + final EventType currentEvent = EventType.valueOf(sagaEvent.getSagaEventState()); + final EventOutcome eventOutcome = EventOutcome.valueOf(sagaEvent.getSagaEventOutcome()); + final Event event = Event.builder() + .eventOutcome(eventOutcome) + .eventType(currentEvent) + .eventPayload(sagaEvent.getSagaEventResponse()) + .build(); + this.findAndInvokeNextStep(saga, t, currentEvent, eventOutcome, event); + } + } + + private void findAndInvokeNextStep(final SagaEntity saga, final T t, final EventType currentEvent, final EventOutcome eventOutcome, final Event event) throws InterruptedException, TimeoutException, IOException { + final Optional> sagaEventState = this.findNextSagaEventState(currentEvent, eventOutcome, t); + if (sagaEventState.isPresent()) { + log.trace(SYSTEM_IS_GOING_TO_EXECUTE_NEXT_EVENT_FOR_CURRENT_EVENT, sagaEventState.get().getNextEventType(), event.toString(), saga.getSagaId()); + this.invokeNextEvent(event, saga, t, sagaEventState.get()); + } + } + + private void replayFromBeginning(final SagaEntity saga, final T t) throws InterruptedException, TimeoutException, IOException { + final Event event = Event.builder() + .eventOutcome(INITIATE_SUCCESS) + .eventType(INITIATED) + .build(); + this.findAndInvokeNextStep(saga, t, INITIATED, INITIATE_SUCCESS, event); + } + + @Override + @Async("subscriberExecutor") + @Transactional + public void handleEvent(@NotNull final Event event) throws InterruptedException, IOException, TimeoutException { + log.debug("Executing saga event {}", event); + if (this.sagaEventExecutionNotRequired(event)) { + log.trace("Execution is not required for this message returning EVENT is :: {}", event); + return; + } + this.broadcastSagaInitiatedMessage(event); + + log.debug("About to find saga by ID with event :: {}", event); + final var sagaOptional = this.getSagaService().findSagaById(event.getSagaId()); // system expects a saga record to be present here. + if (sagaOptional.isPresent()) { + val saga = sagaOptional.get(); + if (!COMPLETED.toString().equalsIgnoreCase(sagaOptional.get().getStatus())) {//possible duplicate message or force stop scenario check + final T sagaData = JsonUtil.getJsonObjectFromString(this.clazz, saga.getPayload()); + final var sagaEventState = this.findNextSagaEventState(event.getEventType(), event.getEventOutcome(), sagaData); + log.trace("found next event as {}", sagaEventState); + if (sagaEventState.isPresent()) { + this.process(event, saga, sagaData, sagaEventState.get()); + } else { + log.error("This should not have happened, please check that both the saga api and all the participating apis are in sync in terms of events and their outcomes. {}", event); // more explicit error message, + } + } else { + log.debug("Got message to process saga for saga ID :: {} but saga is already :: {}", saga.getSagaId(), saga.getStatus()); + } + } else { + log.error("Saga process without DB record is not expected. {}", event); + } + } + + @Override + @Transactional(propagation = Propagation.MANDATORY) + public void startSaga(@NotNull final SagaEntity saga) { + try { + log.debug("Starting saga with the following payload :: {}", saga); + this.handleEvent(Event.builder() + .eventType(INITIATED) + .eventOutcome(INITIATE_SUCCESS) + .sagaId(saga.getSagaId()) + .eventPayload(saga.getPayload()) + .build()); + } catch (final InterruptedException e) { + log.error("InterruptedException while startSaga", e); + Thread.currentThread().interrupt(); + } catch (final TimeoutException | IOException e) { + log.error("Exception while startSaga", e); + } + } + + @Override + @Transactional + public SagaEntity createSaga(@NotNull final String payload, final String userName, final long batchId) { + return this.sagaService.createSagaRecordInDB(this.sagaName, userName, payload, batchId); + } + + @Transactional + public List createSagas(final List sagaEntities) { + return this.sagaService.createSagaRecordsInDB(sagaEntities); + } + + /** + * DONT DO ANYTHING the message was broad-casted for the frontend listeners, that a saga process has initiated, completed. + * + * @param event the event object received from queue. + * @return true if this message need not be processed further. + */ + private boolean sagaEventExecutionNotRequired(@NotNull final Event event) { + return (event.getEventType() == INITIATED && event.getEventOutcome() == INITIATE_SUCCESS && SELF.equalsIgnoreCase(event.getReplyTo())) + || event.getEventType() == MARK_SAGA_COMPLETE && event.getEventOutcome() == SAGA_COMPLETED; + } + + private void broadcastSagaInitiatedMessage(@NotNull final Event event) { + if (this.shouldSendNotificationEvent && event.getEventType() == INITIATED && event.getEventOutcome() == INITIATE_SUCCESS + && !SELF.equalsIgnoreCase(event.getReplyTo())) { + final var notificationEvent = new NotificationEvent(); + BeanUtils.copyProperties(event, notificationEvent); + notificationEvent.setSagaStatus(INITIATED.toString()); + notificationEvent.setReplyTo(SELF); + notificationEvent.setSagaName(this.getSagaName()); + this.postMessageToTopic(this.getTopicToSubscribe(), notificationEvent); + } + } + + protected Optional> findNextSagaEventState(final EventType currentEvent, final EventOutcome eventOutcome, final T sagaData) { + val sagaEventStates = this.nextStepsToExecute.get(currentEvent); + return sagaEventStates == null ? Optional.empty() : sagaEventStates.stream().filter(el -> + el.getCurrentEventOutcome() == eventOutcome && el.nextStepPredicate.test(sagaData) + ).findFirst(); + } + + protected void process(@NotNull final Event event, final SagaEntity saga, final T sagaData, final SagaEventState sagaEventState) throws InterruptedException, TimeoutException, IOException { + if (!saga.getSagaState().equalsIgnoreCase(COMPLETED.toString()) + && this.isNotProcessedEvent(event.getEventType(), saga, this.nextStepsToExecute.keySet())) { + log.debug(SYSTEM_IS_GOING_TO_EXECUTE_NEXT_EVENT_FOR_CURRENT_EVENT, sagaEventState.getNextEventType(), event, saga.getSagaId()); + this.invokeNextEvent(event, saga, sagaData, sagaEventState); + } else { + log.debug("Ignoring this message as we have already processed it or it is completed. {}", event.toString()); // it is expected to receive duplicate message in saga pattern, system should be designed to handle duplicates. + } + } + + protected void invokeNextEvent(final Event event, final SagaEntity saga, final T sagaData, final SagaEventState sagaEventState) throws InterruptedException, TimeoutException, IOException { + final SagaStep stepToExecute = sagaEventState.getStepToExecute(); + stepToExecute.apply(event, saga, sagaData); + } + + public abstract void populateStepsToExecuteMap(); + +} diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/orchestrator/base/EventHandler.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/orchestrator/base/EventHandler.java new file mode 100644 index 000000000..e19a94930 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/orchestrator/base/EventHandler.java @@ -0,0 +1,17 @@ +package ca.bc.gov.educ.api.gradstudent.orchestrator.base; + +import ca.bc.gov.educ.api.gradstudent.model.dc.Event; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; + + +/** + * The interface event handler. + */ +public interface EventHandler { + + void handleEvent(Event event) throws InterruptedException, IOException, TimeoutException; + + String getTopicToSubscribe(); +} diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/orchestrator/base/Orchestrator.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/orchestrator/base/Orchestrator.java new file mode 100644 index 000000000..60e627818 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/orchestrator/base/Orchestrator.java @@ -0,0 +1,39 @@ +package ca.bc.gov.educ.api.gradstudent.orchestrator.base; + +import ca.bc.gov.educ.api.gradstudent.model.entity.SagaEntity; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +/** + * The interface Orchestrator. + */ +public interface Orchestrator { + + + /** + * Gets saga name. + * + * @return the saga name + */ + String getSagaName(); + + /** + * Start saga. + * + * @param saga the saga data + */ + void startSaga(SagaEntity saga); + + SagaEntity createSaga(String payload, String userName, long batchId); + + /** + * Replay saga. + * + * @param saga the saga + * @throws IOException the io exception + * @throws InterruptedException the interrupted exception + * @throws TimeoutException the timeout exception + */ + void replaySaga(SagaEntity saga) throws IOException, InterruptedException, TimeoutException; +} diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/orchestrator/base/SagaEventState.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/orchestrator/base/SagaEventState.java new file mode 100644 index 000000000..6a25da6dc --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/orchestrator/base/SagaEventState.java @@ -0,0 +1,38 @@ +package ca.bc.gov.educ.api.gradstudent.orchestrator.base; + +import ca.bc.gov.educ.api.gradstudent.constant.EventOutcome; +import ca.bc.gov.educ.api.gradstudent.constant.EventType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.function.Predicate; + +/** + * The type Saga event state. + * + * @param the type parameter + */ +@AllArgsConstructor +@Builder +@NoArgsConstructor +@Data +public class SagaEventState { + /** + * The Current event outcome. + */ + EventOutcome currentEventOutcome; + /** + * The function to check the next step + */ + Predicate nextStepPredicate; + /** + * The Next event type. + */ + EventType nextEventType; + /** + * The Step to execute. + */ + SagaStep stepToExecute; +} diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/orchestrator/base/SagaStep.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/orchestrator/base/SagaStep.java new file mode 100644 index 000000000..dda9e507c --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/orchestrator/base/SagaStep.java @@ -0,0 +1,27 @@ +package ca.bc.gov.educ.api.gradstudent.orchestrator.base; + +import ca.bc.gov.educ.api.gradstudent.model.dc.Event; +import ca.bc.gov.educ.api.gradstudent.model.entity.SagaEntity; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +/** + * The interface Saga step. + * + * @param the type parameter + */ +@FunctionalInterface +public interface SagaStep { + /** + * Apply. + * + * @param event the event + * @param saga the saga + * @param sagaData the saga data + * @throws InterruptedException the interrupted exception + * @throws TimeoutException the timeout exception + * @throws IOException the io exception + */ + void apply(Event event, SagaEntity saga, T sagaData) throws InterruptedException, TimeoutException, IOException; +} diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/repository/SagaEventRepository.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/repository/SagaEventRepository.java new file mode 100644 index 000000000..ce7f1653e --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/repository/SagaEventRepository.java @@ -0,0 +1,36 @@ +package ca.bc.gov.educ.api.gradstudent.repository; + + +import ca.bc.gov.educ.api.gradstudent.model.entity.SagaEntity; +import ca.bc.gov.educ.api.gradstudent.model.entity.SagaEventStatesEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * The interface Saga event repository. + */ +@Repository +public interface SagaEventRepository extends JpaRepository { + /** + * Find by saga list. + * + * @param saga the saga + * @return the list + */ + List findBySaga(SagaEntity saga); + + /** + * Find by saga and saga event outcome and saga event state and saga step number optional. + * + * @param saga the saga + * @param eventOutcome the event outcome + * @param eventState the event state + * @param stepNumber the step number + * @return the optional + */ + Optional findBySagaAndSagaEventOutcomeAndSagaEventStateAndSagaStepNumber(SagaEntity saga, String eventOutcome, String eventState, int stepNumber); +} diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/repository/SagaRepository.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/repository/SagaRepository.java new file mode 100644 index 000000000..96d21070f --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/repository/SagaRepository.java @@ -0,0 +1,18 @@ +package ca.bc.gov.educ.api.gradstudent.repository; + + +import ca.bc.gov.educ.api.gradstudent.model.entity.SagaEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.stereotype.Repository; + +import java.util.Optional; +import java.util.UUID; + +/** + * The interface Saga repository. + */ +@Repository +public interface SagaRepository extends JpaRepository, JpaSpecificationExecutor { + Optional findBySagaNameAndStatusNot(String sagaName, String status); +} diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/service/SagaService.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/service/SagaService.java new file mode 100644 index 000000000..fca80b0b4 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/service/SagaService.java @@ -0,0 +1,165 @@ +package ca.bc.gov.educ.api.gradstudent.service; + +import ca.bc.gov.educ.api.gradstudent.model.entity.SagaEventStatesEntity; +import ca.bc.gov.educ.api.gradstudent.model.entity.SagaEntity; +import ca.bc.gov.educ.api.gradstudent.repository.SagaEventRepository; +import ca.bc.gov.educ.api.gradstudent.repository.SagaRepository; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Retryable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +import static ca.bc.gov.educ.api.gradstudent.constant.EventType.INITIATED; +import static ca.bc.gov.educ.api.gradstudent.constant.SagaStatusEnum.STARTED; +import static lombok.AccessLevel.PRIVATE; + +/** + * The type Saga service. + */ +@Service +@Slf4j +public class SagaService { + /** + * The Saga repository. + */ + @Getter(AccessLevel.PRIVATE) + private final SagaRepository sagaRepository; + /** + * The Saga event repository. + */ + @Getter(PRIVATE) + private final SagaEventRepository sagaEventRepository; + + /** + * Instantiates a new Saga service. + * + * @param sagaRepository the saga repository + * @param sagaEventRepository the saga event repository + */ + @Autowired + public SagaService(final SagaRepository sagaRepository, final SagaEventRepository sagaEventRepository) { + this.sagaRepository = sagaRepository; + this.sagaEventRepository = sagaEventRepository; + } + + + /** + * Create saga record saga. + * + * @param saga the saga + * @return the saga + */ + @Transactional(propagation = Propagation.MANDATORY) + public SagaEntity createSagaRecord(final SagaEntity saga) { + return this.getSagaRepository().save(saga); + } + + /** + * Create saga records. + * + * @param sagas the sagas + * @return the saga + */ + @Transactional(propagation = Propagation.MANDATORY) + public List createSagaRecords(final List sagas) { + return this.getSagaRepository().saveAll(sagas); + } + + /** + * no need to do a get here as it is an attached entity + * first find the child record, if exist do not add. this scenario may occur in replay process, + * so dont remove this check. removing this check will lead to duplicate records in the child table. + * + * @param saga the saga object. + * @param sagaEventStates the saga event + */ + @Retryable(maxAttempts = 5, backoff = @Backoff(multiplier = 2, delay = 2000)) + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void updateAttachedSagaWithEvents(final SagaEntity saga, final SagaEventStatesEntity sagaEventStates) { + saga.setUpdateDate(LocalDateTime.now()); + this.getSagaRepository().save(saga); + val result = this.getSagaEventRepository() + .findBySagaAndSagaEventOutcomeAndSagaEventStateAndSagaStepNumber(saga, sagaEventStates.getSagaEventOutcome(), sagaEventStates.getSagaEventState(), sagaEventStates.getSagaStepNumber() - 1); //check if the previous step was same and had same outcome, and it is due to replay. + if (result.isEmpty()) { + this.getSagaEventRepository().save(sagaEventStates); + } + } + + public Optional findBySagaNameAndStatusNot(final String sagaName, final String status) { + return this.getSagaRepository().findBySagaNameAndStatusNot(sagaName, status); + } + + public Optional findSagaById(final UUID sagaId) { + return this.getSagaRepository().findById(sagaId); + } + + public List findAllSagaStates(final SagaEntity saga) { + return this.getSagaEventRepository().findBySaga(saga); + } + + @Transactional(propagation = Propagation.MANDATORY) + public void updateSagaRecord(final SagaEntity saga) { // saga here MUST be an attached entity + this.getSagaRepository().save(saga); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public SagaEntity createSagaRecordInDB(final String sagaName, final String userName, final String payload, final long batchId) { + final var saga = SagaEntity + .builder() + .payload(payload) + .batchId(batchId) + .sagaName(sagaName) + .status(STARTED.toString()) + .sagaState(INITIATED.toString()) + .createDate(LocalDateTime.now()) + .createUser(userName) + .updateUser(userName) + .updateDate(LocalDateTime.now()) + .build(); + return this.createSagaRecord(saga); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public List createSagaRecordsInDB(final List sdcSagaEntities) { + return this.createSagaRecords(sdcSagaEntities); + } + + /** + * Find all completable future. + * + * @param specs the saga specs + * @param pageNumber the page number + * @param pageSize the page size + * @param sorts the sorts + * @return the completable future + */ + @Transactional(propagation = Propagation.SUPPORTS, readOnly = true) + public CompletableFuture> findAll(final Specification specs, final Integer pageNumber, final Integer pageSize, final List sorts) { + return CompletableFuture.supplyAsync(() -> { + final Pageable paging = PageRequest.of(pageNumber, pageSize, Sort.by(sorts)); + try { + return this.sagaRepository.findAll(specs, paging); + } catch (final Exception ex) { + throw new CompletionException(ex); + } + }); + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/service/events/EventHandlerDelegatorService.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/service/events/EventHandlerDelegatorService.java new file mode 100644 index 000000000..c4499c071 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/service/events/EventHandlerDelegatorService.java @@ -0,0 +1,61 @@ +package ca.bc.gov.educ.api.gradstudent.service.events; + +import ca.bc.gov.educ.api.gradstudent.constant.EventType; +import ca.bc.gov.educ.api.gradstudent.messaging.MessagePublisher; +import ca.bc.gov.educ.api.gradstudent.model.dc.Event; +import io.nats.client.Message; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import static lombok.AccessLevel.PRIVATE; + +@Service +@Slf4j +public class EventHandlerDelegatorService { + public static final String PAYLOAD_LOG = "Payload is :: {}"; + + @Getter + private final EventHandlerService eventHandlerService; + + @Getter(PRIVATE) + private final EventPublisherService eventPublisherService; + private final MessagePublisher messagePublisher; + + @Autowired + public EventHandlerDelegatorService(final EventHandlerService eventHandlerService, final EventPublisherService eventPublisherService, final MessagePublisher messagePublisher) { + this.eventHandlerService = eventHandlerService; + this.eventPublisherService = eventPublisherService; + this.messagePublisher = messagePublisher; + } + + @Async("subscriberExecutor") + public void handleEvent(final Event event, final Message message) { + boolean isSynchronous = message.getReplyTo() != null; + try { + log.debug("Received {} from topic event :: {}", event.getEventType(), event.getSagaId()); + log.trace(PAYLOAD_LOG, event.getEventPayload()); + if (event.getEventType() == EventType.ARCHIVE_STUDENTS_REQUEST) { + var eventResponse = this.getEventHandlerService().handleArchiveStudentsRequest(event); + publishToNATS(event, message, isSynchronous, eventResponse); + } else if (event.getEventType() == EventType.ARCHIVE_STUDENTS) { + var eventResponse = this.getEventHandlerService().archiveStudents(event); + publishToNATS(event, message, isSynchronous, eventResponse); + } else { + log.debug("Silently ignoring other event :: {}", event); + } + } catch (final Exception e) { + log.error("Exception", e); + } + } + + private void publishToNATS(Event event, Message message, boolean isSynchronous, byte[] left) { + if (isSynchronous) { // sync, req/reply pattern of nats + messagePublisher.dispatchMessage(message.getReplyTo(), left); + } else { // async, pub/sub + messagePublisher.dispatchMessage(event.getReplyTo(), left); + } + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/service/events/EventHandlerService.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/service/events/EventHandlerService.java new file mode 100644 index 000000000..e4122cf98 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/service/events/EventHandlerService.java @@ -0,0 +1,108 @@ +package ca.bc.gov.educ.api.gradstudent.service.events; + +import ca.bc.gov.educ.api.gradstudent.constant.EventOutcome; +import ca.bc.gov.educ.api.gradstudent.constant.EventType; +import ca.bc.gov.educ.api.gradstudent.constant.SagaEnum; +import ca.bc.gov.educ.api.gradstudent.constant.SagaStatusEnum; +import ca.bc.gov.educ.api.gradstudent.model.dc.Event; +import ca.bc.gov.educ.api.gradstudent.model.dto.ArchiveStudentsSagaData; +import ca.bc.gov.educ.api.gradstudent.model.entity.GradStatusEvent; +import ca.bc.gov.educ.api.gradstudent.orchestrator.ArchiveStudentsOrchestrator; +import ca.bc.gov.educ.api.gradstudent.repository.GradStatusEventRepository; +import ca.bc.gov.educ.api.gradstudent.service.GraduationStatusService; +import ca.bc.gov.educ.api.gradstudent.service.SagaService; +import ca.bc.gov.educ.api.gradstudent.util.JsonUtil; +import com.fasterxml.jackson.core.JsonProcessingException; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +import static ca.bc.gov.educ.api.gradstudent.constant.EventStatus.MESSAGE_PUBLISHED; +import static ca.bc.gov.educ.api.gradstudent.util.EducGradStudentApiConstants.API_NAME; +import static org.springframework.transaction.annotation.Propagation.REQUIRES_NEW; + +/** + * The type Event handler service. + */ +@Service +@Slf4j +public class EventHandlerService { + + public static final String NO_RECORD_SAGA_ID_EVENT_TYPE = "no record found for the saga id and event type combination, processing."; + public static final String RECORD_FOUND_FOR_SAGA_ID_EVENT_TYPE = "record found for the saga id and event type combination, might be a duplicate or replay," + + " just updating the db status so that it will be polled and sent back again."; + public static final String EVENT_PAYLOAD = "event is :: {}"; + private final SagaService sagaService; + private final GradStatusEventRepository gradStatusEventRepository; + private final ArchiveStudentsOrchestrator archiveStudentsOrchestrator; + private final GraduationStatusService graduationStatusService; + + @Autowired + public EventHandlerService(final SagaService sagaService, ArchiveStudentsOrchestrator archiveStudentsOrchestrator, GradStatusEventRepository gradStatusEventRepository, GraduationStatusService graduationStatusService) { + this.sagaService = sagaService; + this.archiveStudentsOrchestrator = archiveStudentsOrchestrator; + this.gradStatusEventRepository = gradStatusEventRepository; + this.graduationStatusService = graduationStatusService; + } + + @Transactional(propagation = REQUIRES_NEW) + public byte [] handleArchiveStudentsRequest(final Event event) throws JsonProcessingException { + final ArchiveStudentsSagaData sagaData = JsonUtil.getJsonObjectFromString(ArchiveStudentsSagaData.class, event.getEventPayload()); + final var sagaInProgress = this.sagaService.findBySagaNameAndStatusNot(SagaEnum.ARCHIVE_STUDENTS_SAGA.toString(), SagaStatusEnum.COMPLETED.toString()); + if (sagaInProgress.isPresent()) { + log.trace("Archive saga is already in progress. Returning conflict for this event :: {}", event); + return "CONFLICT".getBytes(); + } + val saga = this.archiveStudentsOrchestrator.createSaga(event.getEventPayload(), API_NAME, sagaData.getBatchId()); + log.debug("Starting updateStudentDownstreamOrchestrator orchestrator :: {}", saga); + this.archiveStudentsOrchestrator.startSaga(saga); + return "SUCCESS".getBytes(); + } + + @Transactional(propagation = REQUIRES_NEW) + public byte[] archiveStudents(final Event event) throws JsonProcessingException { + var gradStatusEventOptional = this.gradStatusEventRepository.findBySagaIdAndEventType(event.getSagaId(), event.getEventType().toString()); + final GradStatusEvent gradStatusEvent; + if(gradStatusEventOptional.isEmpty()) { + log.debug(NO_RECORD_SAGA_ID_EVENT_TYPE); + log.trace(EVENT_PAYLOAD, event); + ArchiveStudentsSagaData sagaData = JsonUtil.getJsonObjectFromString(ArchiveStudentsSagaData.class, event.getEventPayload()); + this.graduationStatusService.archiveStudents(sagaData.getBatchId(), sagaData.getSchoolsOfRecords(), sagaData.getStudentStatusCode(), sagaData.getUpdateUser()); + gradStatusEvent = this.createGradStatusEventRecord(event); + } else { + log.debug(RECORD_FOUND_FOR_SAGA_ID_EVENT_TYPE); + log.trace(EVENT_PAYLOAD, event); + gradStatusEvent = gradStatusEventOptional.get(); + gradStatusEvent.setEventStatus(MESSAGE_PUBLISHED.toString()); + } + return createResponseEvent(this.gradStatusEventRepository.save(gradStatusEvent)); + } + + private GradStatusEvent createGradStatusEventRecord(final Event event) { + return GradStatusEvent.builder() + .createDate(LocalDateTime.now()) + .updateDate(LocalDateTime.now()) + .createUser(event.getEventType().toString()) + .updateUser(event.getEventType().toString()) + .eventPayload(event.getEventPayload()) + .eventType(event.getEventType().toString()) + .sagaId(event.getSagaId()) + .eventStatus(MESSAGE_PUBLISHED.toString()) + .eventOutcome(event.getEventOutcome().toString()) + .replyChannel(event.getReplyTo()) + .build(); + } + + private byte[] createResponseEvent(GradStatusEvent event) throws JsonProcessingException { + val responseEvent = Event.builder() + .sagaId(event.getSagaId()) + .eventType(EventType.valueOf(event.getEventType())) + .eventOutcome(EventOutcome.valueOf(event.getEventOutcome())) + .eventPayload(event.getEventPayload()).build(); + return JsonUtil.getJsonBytesFromObject(responseEvent); + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/service/events/EventPublisherService.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/service/events/EventPublisherService.java new file mode 100644 index 000000000..2ac407f3d --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/service/events/EventPublisherService.java @@ -0,0 +1,50 @@ +package ca.bc.gov.educ.api.gradstudent.service.events; + +import ca.bc.gov.educ.api.gradstudent.messaging.MessagePublisher; +import ca.bc.gov.educ.api.gradstudent.model.dc.Event; +import ca.bc.gov.educ.api.gradstudent.util.JsonUtil; +import com.fasterxml.jackson.core.JsonProcessingException; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import static lombok.AccessLevel.PRIVATE; + +@Service +@Slf4j +public class EventPublisherService { + + /** + * The constant RESPONDING_BACK_TO_NATS_ON_CHANNEL. + */ + public static final String RESPONDING_BACK_TO_NATS_ON_CHANNEL = "responding back to NATS on {} channel "; + + @Getter(PRIVATE) + private final MessagePublisher messagePublisher; + + @Autowired + public EventPublisherService(final MessagePublisher messagePublisher) { + this.messagePublisher = messagePublisher; + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void send(final Event event) throws JsonProcessingException { + if (event.getReplyTo() != null) { + log.debug(RESPONDING_BACK_TO_NATS_ON_CHANNEL, event.getReplyTo()); + this.getMessagePublisher().dispatchMessage(event.getReplyTo(), this.eventProcessed(event)); + } + } + + private byte[] eventProcessed(final Event gradStudentEvent) throws JsonProcessingException { + final Event event = Event.builder() + .sagaId(gradStudentEvent.getSagaId()) + .eventType(gradStudentEvent.getEventType()) + .eventOutcome(gradStudentEvent.getEventOutcome()) + .eventPayload(gradStudentEvent.getEventPayload()).build(); + return JsonUtil.getJsonStringFromObject(event).getBytes(); + } + +} diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/util/JsonUtil.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/util/JsonUtil.java index d97812bca..556db3c2e 100644 --- a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/util/JsonUtil.java +++ b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/util/JsonUtil.java @@ -2,12 +2,15 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; import java.io.IOException; +import java.util.Optional; /** * The type Json util. */ +@Slf4j public class JsonUtil { public static final ObjectMapper mapper = new ObjectMapper(); @@ -61,4 +64,19 @@ public static byte[] getJsonBytesFromObject(Object payload) throws JsonProcessin public static T getObjectFromJsonBytes(Class clazz, byte[] payload) throws IOException { return mapper.readValue(payload, clazz); } + + /** + * Get json string optional. + * + * @param payload the payload + * @return the optional + */ + public static Optional getJsonString(Object payload){ + try{ + return Optional.ofNullable(mapper.writeValueAsString(payload)); + }catch (final Exception ex){ + log.error("Exception while converting object to JSON String :: {}", payload); + } + return Optional.empty(); + } } diff --git a/api/src/main/resources/db/migration/1.0/V1.0.71__DDL-CREATE_SAGA-TABLES.sql b/api/src/main/resources/db/migration/1.0/V1.0.71__DDL-CREATE_SAGA-TABLES.sql new file mode 100644 index 000000000..64d649782 --- /dev/null +++ b/api/src/main/resources/db/migration/1.0/V1.0.71__DDL-CREATE_SAGA-TABLES.sql @@ -0,0 +1,38 @@ +CREATE TABLE GRAD_STUDENT_SAGA +( + SAGA_ID RAW(16) NOT NULL, + BATCH_ID RAW(16), + SAGA_NAME VARCHAR2(50) NOT NULL, + SAGA_STATE VARCHAR2(100) NOT NULL, + PAYLOAD BLOB NOT NULL, + STATUS VARCHAR2(20) NOT NULL, + CREATE_USER VARCHAR2(50) NOT NULL, + CREATE_DATE DATE DEFAULT SYSDATE NOT NULL, + UPDATE_USER VARCHAR2(50) NOT NULL, + UPDATE_DATE DATE DEFAULT SYSDATE NOT NULL, + RETRY_COUNT NUMBER, + CONSTRAINT GRAD_STUDENT_SAGA_PK PRIMARY KEY (SAGA_ID) +) + LOB (PAYLOAD) STORE AS PAYLOAD (TABLESPACE API_GRAD_BLOB_DATA); + +CREATE INDEX GRAD_STUDENT_SAGA_STATUS_IDX ON GRAD_STUDENT_SAGA (STATUS); +CREATE INDEX GRAD_STUDENT_SAGA_STUDENT_ID_IDX ON GRAD_STUDENT_SAGA (BATCH_ID); + +CREATE TABLE GRAD_STUDENT_SAGA_EVENT_STATES +( + SAGA_EVENT_ID RAW(16) NOT NULL, + SAGA_ID RAW(16) NOT NULL, + SAGA_EVENT_STATE VARCHAR2(100) NOT NULL, + SAGA_EVENT_OUTCOME VARCHAR2(100) NOT NULL, + SAGA_STEP_NUMBER NUMBER(4) NOT NULL, + SAGA_EVENT_RESPONSE BLOB NOT NULL, + CREATE_USER VARCHAR2(50) NOT NULL, + CREATE_DATE DATE DEFAULT SYSDATE NOT NULL, + UPDATE_USER VARCHAR2(50) NOT NULL, + UPDATE_DATE DATE DEFAULT SYSDATE NOT NULL, + CONSTRAINT GRAD_STUDENT_SAGA_EVENT_STATES_PK PRIMARY KEY (SAGA_EVENT_ID) +) + LOB (SAGA_EVENT_RESPONSE) STORE AS SAGA_EVENT_RESPONSE (TABLESPACE API_GRAD_BLOB_DATA); + +ALTER TABLE GRAD_STUDENT_SAGA_EVENT_STATES + ADD CONSTRAINT GRAD_STUDENT_SAGA_EVENT_STATES_SAGA_ID_FK FOREIGN KEY (SAGA_ID) REFERENCES GRAD_STUDENT_SAGA (SAGA_ID); diff --git a/api/src/test/java/ca/bc/gov/educ/api/gradstudent/controller/BaseIntegrationTest.java b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/controller/BaseIntegrationTest.java index 14274b09b..fe0149d51 100644 --- a/api/src/test/java/ca/bc/gov/educ/api/gradstudent/controller/BaseIntegrationTest.java +++ b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/controller/BaseIntegrationTest.java @@ -1,21 +1,32 @@ package ca.bc.gov.educ.api.gradstudent.controller; import ca.bc.gov.educ.api.gradstudent.EducGradStudentApiApplication; +import ca.bc.gov.educ.api.gradstudent.constant.StudentStatusCodes; +import ca.bc.gov.educ.api.gradstudent.model.dto.ArchiveStudentsSagaData; +import ca.bc.gov.educ.api.gradstudent.model.entity.SagaEntity; import ca.bc.gov.educ.api.gradstudent.model.entity.StudentGradeCodeEntity; import ca.bc.gov.educ.api.gradstudent.repository.StudentGradeCodeRepository; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import static ca.bc.gov.educ.api.gradstudent.constant.EventType.INITIATED; +import static ca.bc.gov.educ.api.gradstudent.constant.SagaEnum.ARCHIVE_STUDENTS_SAGA; +import static ca.bc.gov.educ.api.gradstudent.constant.SagaStatusEnum.IN_PROGRESS; +import static ca.bc.gov.educ.api.gradstudent.util.EducGradStudentApiConstants.API_NAME; + +@RunWith(SpringRunner.class) @SpringBootTest(classes = {EducGradStudentApiApplication.class}) -@ActiveProfiles("integration-test") +@ActiveProfiles("test") @AutoConfigureMockMvc public abstract class BaseIntegrationTest { @@ -44,4 +55,24 @@ public List studentGradeCodeData() { return entities; } + + protected ArchiveStudentsSagaData getArchiveStudentsSagaData() { + return ArchiveStudentsSagaData.builder() + .batchId(123456) + .updateUser("TEST") + .studentStatusCode(StudentStatusCodes.ARCHIVED.getCode()) + .build(); + } + + protected SagaEntity createMockSaga() { + return SagaEntity.builder() + .updateDate(LocalDateTime.now().minusMinutes(15)) + .createUser(API_NAME) + .updateUser(API_NAME) + .createDate(LocalDateTime.now().minusMinutes(15)) + .sagaName(ARCHIVE_STUDENTS_SAGA.toString()) + .status(IN_PROGRESS.toString()) + .sagaState(INITIATED.toString()) + .build(); + } } diff --git a/api/src/test/java/ca/bc/gov/educ/api/gradstudent/controller/DataConversionControllerTest.java b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/controller/DataConversionControllerTest.java index 6878a2a0b..80a6a8476 100644 --- a/api/src/test/java/ca/bc/gov/educ/api/gradstudent/controller/DataConversionControllerTest.java +++ b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/controller/DataConversionControllerTest.java @@ -3,12 +3,9 @@ import ca.bc.gov.educ.api.gradstudent.constant.FieldName; import ca.bc.gov.educ.api.gradstudent.constant.FieldType; import ca.bc.gov.educ.api.gradstudent.constant.TraxEventType; -import ca.bc.gov.educ.api.gradstudent.messaging.jetstream.Publisher; import ca.bc.gov.educ.api.gradstudent.model.dto.*; import ca.bc.gov.educ.api.gradstudent.service.DataConversionService; -import ca.bc.gov.educ.api.gradstudent.service.HistoryService; import ca.bc.gov.educ.api.gradstudent.util.EducGradStudentApiUtils; -import ca.bc.gov.educ.api.gradstudent.util.GradValidation; import ca.bc.gov.educ.api.gradstudent.util.ResponseHelper; import org.junit.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -33,18 +30,9 @@ public class DataConversionControllerTest { @Mock private DataConversionService dataConversionService; - @Mock - private HistoryService historyService; - @Mock ResponseHelper responseHelper; - @Mock - GradValidation validation; - - @Mock - Publisher publisher; - @InjectMocks private DataConversionController dataConversionController; @@ -148,7 +136,6 @@ public void testSaveStudentCareerProgram() { UUID gradStudentCareerProgramID = UUID.randomUUID(); UUID studentID = UUID.randomUUID(); String careerProgramCode = "Test"; - String pen = "123456789"; StudentCareerProgram studentCareerProgram = new StudentCareerProgram(); studentCareerProgram.setId(gradStudentCareerProgramID); @@ -186,7 +173,7 @@ public void testDeleteStudentCareerProgram() { Mockito.doNothing().when(dataConversionService).deleteStudentCareerProgram(careerProgramCode, studentID); Mockito.when(responseHelper.DELETE(1)).thenReturn(new ResponseEntity<>(HttpStatus.NO_CONTENT)); - var result = dataConversionController.deleteStudentCareerProgram(careerProgramCode.toString(), studentID.toString()); + var result = dataConversionController.deleteStudentCareerProgram(careerProgramCode, studentID.toString()); Mockito.verify(dataConversionService).deleteStudentCareerProgram(careerProgramCode, studentID); assertThat(result).isNotNull(); assertThat(result.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); diff --git a/api/src/test/java/ca/bc/gov/educ/api/gradstudent/orchestrator/ArchiveStudentsOrchestratorTest.java b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/orchestrator/ArchiveStudentsOrchestratorTest.java new file mode 100644 index 000000000..c18c7ddb2 --- /dev/null +++ b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/orchestrator/ArchiveStudentsOrchestratorTest.java @@ -0,0 +1,140 @@ +package ca.bc.gov.educ.api.gradstudent.orchestrator; + +import ca.bc.gov.educ.api.gradstudent.controller.BaseIntegrationTest; +import ca.bc.gov.educ.api.gradstudent.messaging.MessagePublisher; +import ca.bc.gov.educ.api.gradstudent.model.dc.Event; +import ca.bc.gov.educ.api.gradstudent.model.dto.ArchiveStudentsSagaData; +import ca.bc.gov.educ.api.gradstudent.model.entity.SagaEntity; +import ca.bc.gov.educ.api.gradstudent.repository.SagaEventRepository; +import ca.bc.gov.educ.api.gradstudent.repository.SagaRepository; +import ca.bc.gov.educ.api.gradstudent.service.SagaService; +import ca.bc.gov.educ.api.gradstudent.util.JsonUtil; +import com.fasterxml.jackson.core.JsonProcessingException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +import static ca.bc.gov.educ.api.gradstudent.constant.EventOutcome.*; +import static ca.bc.gov.educ.api.gradstudent.constant.EventType.*; +import static ca.bc.gov.educ.api.gradstudent.constant.SagaEnum.ARCHIVE_STUDENTS_SAGA; +import static ca.bc.gov.educ.api.gradstudent.constant.SagaStatusEnum.COMPLETED; +import static ca.bc.gov.educ.api.gradstudent.constant.Topics.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +class ArchiveStudentsOrchestratorTest extends BaseIntegrationTest { + + @Autowired + SagaRepository repository; + @Autowired + SagaEventRepository sagaEventRepository; + @Autowired + private SagaService sagaService; + @Autowired + private MessagePublisher messagePublisher; + @Autowired + private ArchiveStudentsOrchestrator orchestrator; + + private SagaEntity saga; + private ArchiveStudentsSagaData sagaData; + + @Captor + ArgumentCaptor eventCaptor; + + String sagaPayload; + + long batchId = 123456; + + @BeforeEach + public void setUp() throws JsonProcessingException { + MockitoAnnotations.openMocks(this); + sagaData = getArchiveStudentsSagaData(); + sagaPayload = JsonUtil.getJsonStringFromObject(sagaData); + saga = sagaService.createSagaRecordInDB(ARCHIVE_STUDENTS_SAGA.toString(), "Test", + sagaPayload, batchId); + } + + @AfterEach + public void after() { + sagaEventRepository.deleteAll(); + repository.deleteAll(); + } + + @Test + void testArchiveStudents_givenEventAndSagaData_shouldPostEventToApi() throws IOException, InterruptedException, TimeoutException { + var invocations = mockingDetails(messagePublisher).getInvocations().size(); + var event = Event.builder() + .eventType(INITIATED) + .eventOutcome(INITIATE_SUCCESS) + .sagaId(saga.getSagaId()) + .batchId(String.valueOf(batchId)) + .build(); + + orchestrator.handleEvent(event); + verify(messagePublisher, atMost(invocations + 1)).dispatchMessage(eq(GRAD_STUDENT_API_TOPIC.toString()), eventCaptor.capture()); + var newEvent = JsonUtil.getJsonObjectFromString(Event.class, new String(eventCaptor.getValue())); + assertThat(newEvent.getEventType()).isEqualTo(ARCHIVE_STUDENTS); + var archiveStudentsEvent = JsonUtil.getJsonObjectFromString(ArchiveStudentsSagaData.class, newEvent.getEventPayload()); + assertThat(archiveStudentsEvent).isEqualTo(sagaData); + + var sagaFromDB = sagaService.findSagaById(saga.getSagaId()); + assertThat(sagaFromDB).isPresent(); + assertThat(sagaFromDB.get().getSagaState()).isEqualTo(ARCHIVE_STUDENTS.toString()); + var sagaStates = sagaService.findAllSagaStates(saga); + assertThat(sagaStates).hasSize(1); + assertThat(sagaStates.get(0).getSagaEventState()).isEqualTo(INITIATED.toString()); + assertThat(sagaStates.get(0).getSagaEventOutcome()).isEqualTo(INITIATE_SUCCESS.toString()); + } + + @Test + void testNotifyBatchApi_givenEventAndSagaData_shouldPostEventToApi() throws IOException, InterruptedException, TimeoutException { + var invocations = mockingDetails(messagePublisher).getInvocations().size(); + var event = Event.builder() + .eventType(ARCHIVE_STUDENTS) + .eventOutcome(STUDENTS_ARCHIVED) + .sagaId(saga.getSagaId()) + .batchId(String.valueOf(batchId)) + .build(); + + orchestrator.handleEvent(event); + verify(messagePublisher, atMost(invocations + 1)).dispatchMessage(eq(GRAD_BATCH_API_TOPIC.toString()), eventCaptor.capture()); + var newEvent = JsonUtil.getJsonObjectFromString(Event.class, new String(eventCaptor.getValue())); + assertThat(newEvent.getEventType()).isEqualTo(NOTIFY_ARCHIVE_STUDENT_BATCH_COMPLETED); + + var sagaFromDB = sagaService.findSagaById(saga.getSagaId()); + assertThat(sagaFromDB).isPresent(); + assertThat(sagaFromDB.get().getSagaState()).isEqualTo(NOTIFY_ARCHIVE_STUDENT_BATCH_COMPLETED.toString()); + var sagaStates = sagaService.findAllSagaStates(saga); + assertThat(sagaStates).hasSize(1); + assertThat(sagaStates.get(0).getSagaEventState()).isEqualTo(ARCHIVE_STUDENTS.toString()); + assertThat(sagaStates.get(0).getSagaEventOutcome()).isEqualTo(STUDENTS_ARCHIVED.toString()); + } + + @Test + void testNotifyBatchApiResponse_givenEventAndSagaData_shouldCompleteSaga() throws IOException, InterruptedException, TimeoutException { + var event = Event.builder() + .eventType(NOTIFY_ARCHIVE_STUDENT_BATCH_COMPLETED) + .eventOutcome(BATCH_API_NOTIFIED) + .sagaId(saga.getSagaId()) + .batchId(String.valueOf(batchId)) + .build(); + + orchestrator.handleEvent(event); + + var sagaFromDB = sagaService.findSagaById(saga.getSagaId()); + assertThat(sagaFromDB).isPresent(); + assertThat(sagaFromDB.get().getSagaState()).isEqualTo(COMPLETED.toString()); + var sagaStates = sagaService.findAllSagaStates(saga); + assertThat(sagaStates).hasSize(1); + assertThat(sagaStates.get(0).getSagaEventState()).isEqualTo(NOTIFY_ARCHIVE_STUDENT_BATCH_COMPLETED.toString()); + assertThat(sagaStates.get(0).getSagaEventOutcome()).isEqualTo(BATCH_API_NOTIFIED.toString()); + } +} diff --git a/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/CommonServiceTest.java b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/CommonServiceTest.java index 97bf03060..203ba6162 100644 --- a/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/CommonServiceTest.java +++ b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/CommonServiceTest.java @@ -1,9 +1,6 @@ package ca.bc.gov.educ.api.gradstudent.service; -import ca.bc.gov.educ.api.gradstudent.messaging.NatsConnection; import ca.bc.gov.educ.api.gradstudent.messaging.jetstream.FetchGradStatusSubscriber; -import ca.bc.gov.educ.api.gradstudent.messaging.jetstream.Publisher; -import ca.bc.gov.educ.api.gradstudent.messaging.jetstream.Subscriber; import ca.bc.gov.educ.api.gradstudent.model.dto.*; import ca.bc.gov.educ.api.gradstudent.model.entity.*; import ca.bc.gov.educ.api.gradstudent.repository.*; @@ -50,21 +47,14 @@ public class CommonServiceTest { @MockBean StudentNoteRepository studentNoteRepository; @MockBean StudentStatusRepository studentStatusRepository; @MockBean HistoryActivityRepository historyActivityRepository; - @MockBean WebClient webClient; + @Autowired WebClient webClient; - @MockBean + @Autowired FetchGradStatusSubscriber fetchGradStatusSubscriber; @Mock WebClient.RequestHeadersSpec requestHeadersMock; @Mock WebClient.RequestHeadersUriSpec requestHeadersUriMock; - @Mock WebClient.RequestBodySpec requestBodyMock; - @Mock WebClient.RequestBodyUriSpec requestBodyUriMock; @Mock WebClient.ResponseSpec responseMock; - // NATS - @MockBean NatsConnection natsConnection; - @MockBean Publisher publisher; - @MockBean Subscriber subscriber; - @Before public void setUp() { openMocks(this); diff --git a/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/DataConversionServiceTest.java b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/DataConversionServiceTest.java index 9e11bc9a0..ea147c586 100644 --- a/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/DataConversionServiceTest.java +++ b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/DataConversionServiceTest.java @@ -3,10 +3,8 @@ import ca.bc.gov.educ.api.gradstudent.constant.FieldName; import ca.bc.gov.educ.api.gradstudent.constant.FieldType; import ca.bc.gov.educ.api.gradstudent.constant.TraxEventType; -import ca.bc.gov.educ.api.gradstudent.messaging.NatsConnection; +import ca.bc.gov.educ.api.gradstudent.controller.BaseIntegrationTest; import ca.bc.gov.educ.api.gradstudent.messaging.jetstream.FetchGradStatusSubscriber; -import ca.bc.gov.educ.api.gradstudent.messaging.jetstream.Publisher; -import ca.bc.gov.educ.api.gradstudent.messaging.jetstream.Subscriber; import ca.bc.gov.educ.api.gradstudent.model.dto.*; import ca.bc.gov.educ.api.gradstudent.model.entity.*; import ca.bc.gov.educ.api.gradstudent.repository.*; @@ -40,7 +38,7 @@ @RunWith(SpringRunner.class) @SpringBootTest @ActiveProfiles("test") -public class DataConversionServiceTest { +class DataConversionServiceTest extends BaseIntegrationTest { @Autowired EducGradStudentApiConstants constants; @Autowired @@ -65,10 +63,10 @@ public class DataConversionServiceTest { @MockBean GradValidation validation; - @MockBean + @Autowired WebClient webClient; - @MockBean + @Autowired FetchGradStatusSubscriber fetchGradStatusSubscriber; @Mock @@ -77,13 +75,6 @@ public class DataConversionServiceTest { @Mock WebClient.RequestBodySpec requestBodyMock; @Mock WebClient.RequestBodyUriSpec requestBodyUriMock; @Mock WebClient.ResponseSpec responseMock; - // NATS - @MockBean - NatsConnection natsConnection; - @MockBean - Publisher publisher; - @MockBean - Subscriber subscriber; @Test public void testGraduationStudentRecordAsNew() { @@ -726,7 +717,6 @@ public void testSaveStudentCareerProgramAsUpdate() { UUID gradStudentCareerProgramID = UUID.randomUUID(); UUID studentID = UUID.randomUUID(); String careerProgramCode = "Test"; - String pen = "123456789"; StudentCareerProgramEntity gradStudentCareerProgramEntity = new StudentCareerProgramEntity(); gradStudentCareerProgramEntity.setId(gradStudentCareerProgramID); diff --git a/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/EdwSnapshotServiceTest.java b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/EdwSnapshotServiceTest.java index ea5adf7dc..3994305c9 100644 --- a/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/EdwSnapshotServiceTest.java +++ b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/EdwSnapshotServiceTest.java @@ -1,9 +1,6 @@ package ca.bc.gov.educ.api.gradstudent.service; -import ca.bc.gov.educ.api.gradstudent.messaging.NatsConnection; import ca.bc.gov.educ.api.gradstudent.messaging.jetstream.FetchGradStatusSubscriber; -import ca.bc.gov.educ.api.gradstudent.messaging.jetstream.Publisher; -import ca.bc.gov.educ.api.gradstudent.messaging.jetstream.Subscriber; import ca.bc.gov.educ.api.gradstudent.model.dto.EdwGraduationSnapshot; import ca.bc.gov.educ.api.gradstudent.model.entity.EdwGraduationSnapshotEntity; import ca.bc.gov.educ.api.gradstudent.model.transformer.EDWGraduationStatusTransformer; @@ -49,20 +46,12 @@ public class EdwSnapshotServiceTest { @MockBean GradValidation validation; - @MockBean + @Autowired WebClient webClient; - @MockBean + @Autowired FetchGradStatusSubscriber fetchGradStatusSubscriber; - // NATS - @MockBean - NatsConnection natsConnection; - @MockBean - Publisher publisher; - @MockBean - Subscriber subscriber; - @Test public void testRetrieve() { Integer gradYear = 2023; diff --git a/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/GradStudentServiceTest.java b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/GradStudentServiceTest.java index 14847d5cb..98804b862 100644 --- a/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/GradStudentServiceTest.java +++ b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/GradStudentServiceTest.java @@ -1,9 +1,6 @@ package ca.bc.gov.educ.api.gradstudent.service; -import ca.bc.gov.educ.api.gradstudent.messaging.NatsConnection; import ca.bc.gov.educ.api.gradstudent.messaging.jetstream.FetchGradStatusSubscriber; -import ca.bc.gov.educ.api.gradstudent.messaging.jetstream.Publisher; -import ca.bc.gov.educ.api.gradstudent.messaging.jetstream.Subscriber; import ca.bc.gov.educ.api.gradstudent.model.dto.*; import ca.bc.gov.educ.api.gradstudent.model.entity.GraduationStudentRecordEntity; import ca.bc.gov.educ.api.gradstudent.model.entity.GraduationStudentRecordView; @@ -23,6 +20,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.data.domain.Example; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.test.context.ActiveProfiles; @@ -63,10 +61,10 @@ public class GradStudentServiceTest { @MockBean CommonService commonService; - @MockBean + @Autowired WebClient webClient; - @MockBean + @Autowired FetchGradStatusSubscriber fetchGradStatusSubscriber; @MockBean @@ -78,11 +76,6 @@ public class GradStudentServiceTest { @Mock WebClient.RequestBodyUriSpec requestBodyUriMock; @Mock WebClient.ResponseSpec responseMock; - // NATS - @MockBean NatsConnection natsConnection; - @MockBean Publisher publisher; - @MockBean Subscriber subscriber; - @Before public void setUp() { openMocks(this); @@ -247,7 +240,7 @@ public void testGetGRADStudents() { when(this.graduationStatusTransformer.transformToDTOWithModifiedProgramCompletionDate(graduationStatusEntity)).thenReturn(graduationStatus); org.springframework.data.domain.Page pagedResult = new PageImpl<>(List.of(graduationStatusEntity)); - when(this.graduationStatusRepository.findAll(any(), any(Pageable.class))).thenReturn(pagedResult); + when(this.graduationStatusRepository.findAll(any(Example.class), any(Pageable.class))).thenReturn(pagedResult); RestResponsePage response = new RestResponsePage<>(List.of(student)); final ParameterizedTypeReference> studentResponseType = new ParameterizedTypeReference<>() { diff --git a/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/GraduationStatusServiceTest.java b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/GraduationStatusServiceTest.java index 403363321..13d370b39 100644 --- a/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/GraduationStatusServiceTest.java +++ b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/GraduationStatusServiceTest.java @@ -1,10 +1,7 @@ package ca.bc.gov.educ.api.gradstudent.service; import ca.bc.gov.educ.api.gradstudent.exception.EntityNotFoundException; -import ca.bc.gov.educ.api.gradstudent.messaging.NatsConnection; import ca.bc.gov.educ.api.gradstudent.messaging.jetstream.FetchGradStatusSubscriber; -import ca.bc.gov.educ.api.gradstudent.messaging.jetstream.Publisher; -import ca.bc.gov.educ.api.gradstudent.messaging.jetstream.Subscriber; import ca.bc.gov.educ.api.gradstudent.model.dto.*; import ca.bc.gov.educ.api.gradstudent.model.dto.messaging.GraduationStudentRecordGradStatus; import ca.bc.gov.educ.api.gradstudent.model.entity.*; @@ -67,19 +64,15 @@ public class GraduationStatusServiceTest { @MockBean GraduationStudentRecordHistoryRepository graduationStudentRecordHistoryRepository; @MockBean CommonService commonService; @MockBean GradValidation validation; - @MockBean WebClient webClient; + @Autowired WebClient webClient; - @MockBean + @Autowired FetchGradStatusSubscriber fetchGradStatusSubscriber; @Mock WebClient.RequestHeadersSpec requestHeadersMock; @Mock WebClient.RequestHeadersUriSpec requestHeadersUriMock; @Mock WebClient.RequestBodySpec requestBodyMock; @Mock WebClient.RequestBodyUriSpec requestBodyUriMock; @Mock WebClient.ResponseSpec responseMock; - // NATS - @MockBean NatsConnection natsConnection; - @MockBean Publisher publisher; - @MockBean Subscriber subscriber; @MockBean GraduationStudentRecordSearchRepository graduationStudentRecordSearchRepository; diff --git a/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/HistoryServiceTest.java b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/HistoryServiceTest.java index 107bdd6e8..2c61e635f 100644 --- a/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/HistoryServiceTest.java +++ b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/HistoryServiceTest.java @@ -1,9 +1,6 @@ package ca.bc.gov.educ.api.gradstudent.service; -import ca.bc.gov.educ.api.gradstudent.messaging.NatsConnection; import ca.bc.gov.educ.api.gradstudent.messaging.jetstream.FetchGradStatusSubscriber; -import ca.bc.gov.educ.api.gradstudent.messaging.jetstream.Publisher; -import ca.bc.gov.educ.api.gradstudent.messaging.jetstream.Subscriber; import ca.bc.gov.educ.api.gradstudent.model.dto.GradSearchStudent; import ca.bc.gov.educ.api.gradstudent.model.dto.GraduationData; import ca.bc.gov.educ.api.gradstudent.model.dto.OptionalProgram; @@ -61,9 +58,9 @@ public class HistoryServiceTest { @MockBean GraduationStudentRecordRepository graduationStudentRecordRepository; @MockBean HistoryActivityRepository historyActivityRepository; @MockBean StudentOptionalProgramHistoryRepository studentOptionalProgramHistoryRepository; - @MockBean WebClient webClient; + @Autowired WebClient webClient; - @MockBean + @Autowired FetchGradStatusSubscriber fetchGradStatusSubscriber; @Mock WebClient.RequestHeadersSpec requestHeadersMock; @Mock WebClient.RequestHeadersUriSpec requestHeadersUriMock; @@ -71,11 +68,6 @@ public class HistoryServiceTest { @Mock WebClient.RequestBodyUriSpec requestBodyUriMock; @Mock WebClient.ResponseSpec responseMock; - // NATS - @MockBean NatsConnection natsConnection; - @MockBean Publisher publisher; - @MockBean Subscriber subscriber; - @Before public void setUp() { openMocks(this); diff --git a/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/JetStreamEventHandlerServiceTest.java b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/JetStreamEventHandlerServiceTest.java index ba06130e1..9de86cea0 100644 --- a/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/JetStreamEventHandlerServiceTest.java +++ b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/JetStreamEventHandlerServiceTest.java @@ -3,9 +3,6 @@ import ca.bc.gov.educ.api.gradstudent.messaging.jetstream.FetchGradStatusSubscriber; import ca.bc.gov.educ.api.gradstudent.model.dto.ChoreographedEvent; import ca.bc.gov.educ.api.gradstudent.model.entity.GradStatusEvent; -import ca.bc.gov.educ.api.gradstudent.messaging.NatsConnection; -import ca.bc.gov.educ.api.gradstudent.messaging.jetstream.Publisher; -import ca.bc.gov.educ.api.gradstudent.messaging.jetstream.Subscriber; import ca.bc.gov.educ.api.gradstudent.repository.GradStatusEventRepository; import ca.bc.gov.educ.api.gradstudent.util.EducGradStudentApiConstants; import org.junit.After; @@ -43,21 +40,12 @@ public class JetStreamEventHandlerServiceTest { @MockBean GradStatusEventRepository gradStatusEventRepository; - @MockBean + @Autowired WebClient webClient; - // NATS - @MockBean - private NatsConnection natsConnection; - - @MockBean + @Autowired FetchGradStatusSubscriber fetchGradStatusSubscriber; - @MockBean - private Publisher publisher; - - @MockBean - private Subscriber subscriber; @Before public void setUp() { diff --git a/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/events/EventHandlerDelegatorServiceTest.java b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/events/EventHandlerDelegatorServiceTest.java new file mode 100644 index 000000000..0aee200c7 --- /dev/null +++ b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/events/EventHandlerDelegatorServiceTest.java @@ -0,0 +1,114 @@ +package ca.bc.gov.educ.api.gradstudent.service.events; + +import ca.bc.gov.educ.api.gradstudent.controller.BaseIntegrationTest; +import ca.bc.gov.educ.api.gradstudent.messaging.MessagePublisher; +import ca.bc.gov.educ.api.gradstudent.messaging.jetstream.Publisher; +import ca.bc.gov.educ.api.gradstudent.model.dc.Event; +import ca.bc.gov.educ.api.gradstudent.repository.GradStatusEventRepository; +import ca.bc.gov.educ.api.gradstudent.repository.SagaEventRepository; +import ca.bc.gov.educ.api.gradstudent.repository.SagaRepository; +import ca.bc.gov.educ.api.gradstudent.support.NatsMessageImpl; +import ca.bc.gov.educ.api.gradstudent.util.JsonUtil; +import com.fasterxml.jackson.core.JsonProcessingException; +import io.nats.client.Connection; +import io.nats.client.Message; +import org.junit.After; +import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static ca.bc.gov.educ.api.gradstudent.constant.EventType.ARCHIVE_STUDENTS_REQUEST; +import static ca.bc.gov.educ.api.gradstudent.constant.Topics.GRAD_BATCH_API_TOPIC; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.verify; + +public class EventHandlerDelegatorServiceTest extends BaseIntegrationTest { + @Autowired + MessagePublisher messagePublisher; + @Autowired + Publisher publisher; + + @MockBean + Connection connection; + + @Autowired + EventHandlerDelegatorService eventHandlerDelegatorService; + + @Captor + ArgumentCaptor eventCaptor; + @Autowired + SagaRepository sagaRepository; + @Autowired + SagaEventRepository sagaEventRepository; + + @MockBean + GradStatusEventRepository gradStatusEventRepository; + + @BeforeEach + public void setUp() throws JsonProcessingException { + MockitoAnnotations.openMocks(this); + } + + @After + public void after() { + this.gradStatusEventRepository.deleteAll(); + this.sagaEventRepository.deleteAll(); + this.sagaRepository.deleteAll(); + } + + @Test + public void testHandleArchiveStudentsRequestEvent_givenValidPayload_whenSuccessfullyProcessed_shouldReturnSuccess() throws IOException { + var payload = getArchiveStudentsSagaData(); + var expectedResponse = "SUCCESS".getBytes(StandardCharsets.UTF_8); + final Event event = Event.builder() + .eventType(ARCHIVE_STUDENTS_REQUEST) + .replyTo(String.valueOf(GRAD_BATCH_API_TOPIC)) + .eventPayload(JsonUtil.getJsonStringFromObject(payload)) + .build(); + final Message message = NatsMessageImpl.builder() + .connection(this.connection) + .data(JsonUtil.getJsonBytesFromObject(event)) + .SID("SID") + .replyTo("TEST_TOPIC") + .build(); + this.eventHandlerDelegatorService.handleEvent(event, message); + verify(this.messagePublisher, atLeastOnce()).dispatchMessage(any(), this.eventCaptor.capture()); + final var replyEvent = this.eventCaptor.getValue(); + + assertThat(replyEvent).isNotNull().isEqualTo(expectedResponse); + } + + @Test + public void testHandleArchiveStudentsRequestEvent_givenArchiveAlreadyInProgress_whenSuccessfullyProcessed_shouldReturnCONFLICT() throws IOException { + var payload = getArchiveStudentsSagaData(); + var expectedResponse = "CONFLICT".getBytes(StandardCharsets.UTF_8); + final Event event = Event.builder() + .eventType(ARCHIVE_STUDENTS_REQUEST) + .replyTo(String.valueOf(GRAD_BATCH_API_TOPIC)) + .eventPayload(JsonUtil.getJsonStringFromObject(payload)) + .build(); + final Message message = NatsMessageImpl.builder() + .connection(this.connection) + .data(JsonUtil.getJsonBytesFromObject(event)) + .SID("SID") + .replyTo("TEST_TOPIC") + .build(); + var saga = createMockSaga(); + saga.setPayload(JsonUtil.getJsonStringFromObject(payload)); + this.sagaRepository.save(saga); + this.eventHandlerDelegatorService.handleEvent(event, message); + verify(this.messagePublisher, atLeastOnce()).dispatchMessage(any(), this.eventCaptor.capture()); + final var replyEvent = this.eventCaptor.getValue(); + + assertThat(replyEvent).isNotNull().isEqualTo(expectedResponse); + } +} diff --git a/api/src/test/java/ca/bc/gov/educ/api/gradstudent/support/MockConfiguration.java b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/support/MockConfiguration.java index d48efe262..f4e57d2ec 100644 --- a/api/src/test/java/ca/bc/gov/educ/api/gradstudent/support/MockConfiguration.java +++ b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/support/MockConfiguration.java @@ -1,5 +1,7 @@ package ca.bc.gov.educ.api.gradstudent.support; +import ca.bc.gov.educ.api.gradstudent.messaging.MessagePublisher; +import ca.bc.gov.educ.api.gradstudent.messaging.MessageSubscriber; import ca.bc.gov.educ.api.gradstudent.messaging.NatsConnection; import ca.bc.gov.educ.api.gradstudent.messaging.jetstream.FetchGradStatusSubscriber; import ca.bc.gov.educ.api.gradstudent.messaging.jetstream.Publisher; @@ -16,7 +18,7 @@ /** * The type Mock configuration. */ -@Profile("integration-test") +@Profile("test") @Configuration public class MockConfiguration { @@ -50,6 +52,18 @@ public Subscriber subscriber() { return Mockito.mock(Subscriber.class); } + @Bean + @Primary + public MessagePublisher messagePublisher() { + return Mockito.mock(MessagePublisher.class); + } + + @Bean + @Primary + public MessageSubscriber messageSubscriber() { + return Mockito.mock(MessageSubscriber.class); + } + @Bean @Primary public FetchGradStatusSubscriber fetchGradStatusSubscriber() { diff --git a/api/src/test/java/ca/bc/gov/educ/api/gradstudent/support/NatsMessageImpl.java b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/support/NatsMessageImpl.java new file mode 100644 index 000000000..bb7e49998 --- /dev/null +++ b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/support/NatsMessageImpl.java @@ -0,0 +1,138 @@ +package ca.bc.gov.educ.api.gradstudent.support; + +import io.nats.client.Connection; +import io.nats.client.Message; +import io.nats.client.Subscription; +import io.nats.client.impl.Headers; +import io.nats.client.impl.NatsJetStreamMetaData; +import io.nats.client.support.Status; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Duration; +import java.util.concurrent.TimeoutException; + +/** + * Support class to use for testing. + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class NatsMessageImpl implements Message { + private String subject; + private Subscription subscription; + private String replyTo; + private byte[] data; + private String SID; + private Connection connection; + + /** + * @return true if there are headers + */ + @Override + public boolean hasHeaders() { + return false; + } + + /** + * @return the headers object the message + */ + @Override + public Headers getHeaders() { + return null; + } + + /** + * @return true if there is status + */ + @Override + public boolean isStatusMessage() { + return false; + } + + /** + * @return the status object message + */ + @Override + public Status getStatus() { + return null; + } + + /** + * @return if is utf8Mode + */ + @Override + public boolean isUtf8mode() { + return false; + } + + /** + * Gets the metadata associated with a JetStream message. + * + * @return metadata or null if the message is not a JetStream message. + */ + @Override + public NatsJetStreamMetaData metaData() { + return null; + } + + /** + * ack acknowledges a JetStream messages received from a Consumer, indicating the message + * should not be received again later. + */ + @Override + public void ack() { + + } + + /** + * ack acknowledges a JetStream messages received from a Consumer, indicating the message + * should not be received again later. Duration.ZERO does not confirm the acknowledgement. + * + * @param timeout the duration to wait for an ack confirmation + * @throws TimeoutException if a timeout was specified and the NATS server does not return a response + * @throws InterruptedException if the thread is interrupted + */ + @Override + public void ackSync(final Duration timeout) throws TimeoutException, InterruptedException { + + } + + /** + * nak acknowledges a JetStream message has been received but indicates that the message + * is not completely processed and should be sent again later. + */ + @Override + public void nak() { + + } + + /** + * term prevents this message from every being delivered regardless of maxDeliverCount. + */ + @Override + public void term() { + + } + + /** + * Indicates that this message is being worked on and reset redelivery timer in the server. + */ + @Override + public void inProgress() { + + } + + /** + * Checks if a message is from Jetstream or is a standard message. + * + * @return true if the message is from JetStream. + */ + @Override + public boolean isJetStream() { + return false; + } +} From 5ca391ece2f593dbf916e0c77bb083ed7a28f1df Mon Sep 17 00:00:00 2001 From: mightycox Date: Thu, 7 Nov 2024 09:17:16 -0800 Subject: [PATCH 2/3] GRAD2-2976 - Adds more ut coverage and bugfix --- .../constant/StudentStatusCodes.java | 6 +- .../messaging/MessageSubscriber.java | 1 - .../service/events/EventHandlerService.java | 7 +- .../controller/BaseIntegrationTest.java | 12 +++- .../ArchiveStudentsOrchestratorTest.java | 4 ++ .../EventHandlerDelegatorServiceTest.java | 72 ++++++++++++++++--- 6 files changed, 85 insertions(+), 17 deletions(-) diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/constant/StudentStatusCodes.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/constant/StudentStatusCodes.java index f56db7507..ad44f20ce 100644 --- a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/constant/StudentStatusCodes.java +++ b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/constant/StudentStatusCodes.java @@ -3,6 +3,7 @@ import lombok.Getter; import java.util.Arrays; +import java.util.Objects; import java.util.Optional; @Getter @@ -11,8 +12,7 @@ public enum StudentStatusCodes { ARCHIVED("ARC"), DECEASED("DEC"), MERGED("MER"), - TERMINATED("TER"), - PENDING_ARCHIVE("PEN"); + TERMINATED("TER"); private final String code; StudentStatusCodes(String code) { @@ -20,6 +20,6 @@ public enum StudentStatusCodes { } public static Optional findByValue(String value) { - return Arrays.stream(values()).filter(e -> Arrays.asList(e.code).contains(value)).findFirst(); + return Arrays.stream(values()).filter(e -> Objects.equals(e.code, value)).findFirst(); } } diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/messaging/MessageSubscriber.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/messaging/MessageSubscriber.java index eb0f1efda..a9adcec6e 100644 --- a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/messaging/MessageSubscriber.java +++ b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/messaging/MessageSubscriber.java @@ -39,7 +39,6 @@ public MessageSubscriber(final Connection con, final List eventHan this.connection = con; eventHandlers.forEach(handler -> { this.handlerMap.put(handler.getTopicToSubscribe(), handler); - this.subscribeForSAGA(handler.getTopicToSubscribe(), handler); }); this.constants = constants; diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/service/events/EventHandlerService.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/service/events/EventHandlerService.java index e4122cf98..a53435268 100644 --- a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/service/events/EventHandlerService.java +++ b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/service/events/EventHandlerService.java @@ -21,6 +21,7 @@ import java.time.LocalDateTime; +import static ca.bc.gov.educ.api.gradstudent.constant.EventOutcome.STUDENTS_ARCHIVED; import static ca.bc.gov.educ.api.gradstudent.constant.EventStatus.MESSAGE_PUBLISHED; import static ca.bc.gov.educ.api.gradstudent.util.EducGradStudentApiConstants.API_NAME; import static org.springframework.transaction.annotation.Propagation.REQUIRES_NEW; @@ -71,7 +72,9 @@ public byte[] archiveStudents(final Event event) throws JsonProcessingException log.debug(NO_RECORD_SAGA_ID_EVENT_TYPE); log.trace(EVENT_PAYLOAD, event); ArchiveStudentsSagaData sagaData = JsonUtil.getJsonObjectFromString(ArchiveStudentsSagaData.class, event.getEventPayload()); - this.graduationStatusService.archiveStudents(sagaData.getBatchId(), sagaData.getSchoolsOfRecords(), sagaData.getStudentStatusCode(), sagaData.getUpdateUser()); + var numStudentsArchived = this.graduationStatusService.archiveStudents(sagaData.getBatchId(), sagaData.getSchoolsOfRecords(), sagaData.getStudentStatusCode(), sagaData.getUpdateUser()); + event.setEventPayload(String.valueOf(numStudentsArchived)); + event.setEventOutcome(STUDENTS_ARCHIVED); gradStatusEvent = this.createGradStatusEventRecord(event); } else { log.debug(RECORD_FOUND_FOR_SAGA_ID_EVENT_TYPE); @@ -92,7 +95,7 @@ private GradStatusEvent createGradStatusEventRecord(final Event event) { .eventType(event.getEventType().toString()) .sagaId(event.getSagaId()) .eventStatus(MESSAGE_PUBLISHED.toString()) - .eventOutcome(event.getEventOutcome().toString()) + .eventOutcome(String.valueOf(event.getEventOutcome())) .replyChannel(event.getReplyTo()) .build(); } diff --git a/api/src/test/java/ca/bc/gov/educ/api/gradstudent/controller/BaseIntegrationTest.java b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/controller/BaseIntegrationTest.java index fe0149d51..fdf42d47c 100644 --- a/api/src/test/java/ca/bc/gov/educ/api/gradstudent/controller/BaseIntegrationTest.java +++ b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/controller/BaseIntegrationTest.java @@ -3,6 +3,7 @@ import ca.bc.gov.educ.api.gradstudent.EducGradStudentApiApplication; import ca.bc.gov.educ.api.gradstudent.constant.StudentStatusCodes; import ca.bc.gov.educ.api.gradstudent.model.dto.ArchiveStudentsSagaData; +import ca.bc.gov.educ.api.gradstudent.model.entity.GraduationStudentRecordEntity; import ca.bc.gov.educ.api.gradstudent.model.entity.SagaEntity; import ca.bc.gov.educ.api.gradstudent.model.entity.StudentGradeCodeEntity; import ca.bc.gov.educ.api.gradstudent.repository.StudentGradeCodeRepository; @@ -18,6 +19,7 @@ import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import java.util.UUID; import static ca.bc.gov.educ.api.gradstudent.constant.EventType.INITIATED; import static ca.bc.gov.educ.api.gradstudent.constant.SagaEnum.ARCHIVE_STUDENTS_SAGA; @@ -60,7 +62,7 @@ protected ArchiveStudentsSagaData getArchiveStudentsSagaData() { return ArchiveStudentsSagaData.builder() .batchId(123456) .updateUser("TEST") - .studentStatusCode(StudentStatusCodes.ARCHIVED.getCode()) + .studentStatusCode(StudentStatusCodes.CURRENT.getCode()) .build(); } @@ -75,4 +77,12 @@ protected SagaEntity createMockSaga() { .sagaState(INITIATED.toString()) .build(); } + + protected GraduationStudentRecordEntity createMockGraduationStudentRecord() { + GraduationStudentRecordEntity graduationStatusEntity = new GraduationStudentRecordEntity(); + graduationStatusEntity.setStudentID(UUID.randomUUID()); + graduationStatusEntity.setPen("123456789"); + graduationStatusEntity.setStudentStatus("CUR"); + return graduationStatusEntity; + } } diff --git a/api/src/test/java/ca/bc/gov/educ/api/gradstudent/orchestrator/ArchiveStudentsOrchestratorTest.java b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/orchestrator/ArchiveStudentsOrchestratorTest.java index c18c7ddb2..0a7bdfcd2 100644 --- a/api/src/test/java/ca/bc/gov/educ/api/gradstudent/orchestrator/ArchiveStudentsOrchestratorTest.java +++ b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/orchestrator/ArchiveStudentsOrchestratorTest.java @@ -82,6 +82,8 @@ void testArchiveStudents_givenEventAndSagaData_shouldPostEventToApi() throws IOE verify(messagePublisher, atMost(invocations + 1)).dispatchMessage(eq(GRAD_STUDENT_API_TOPIC.toString()), eventCaptor.capture()); var newEvent = JsonUtil.getJsonObjectFromString(Event.class, new String(eventCaptor.getValue())); assertThat(newEvent.getEventType()).isEqualTo(ARCHIVE_STUDENTS); + assertThat(newEvent.getBatchId()).isEqualTo(String.valueOf(batchId)); + assertThat(newEvent.getSagaId()).isEqualTo(saga.getSagaId()); var archiveStudentsEvent = JsonUtil.getJsonObjectFromString(ArchiveStudentsSagaData.class, newEvent.getEventPayload()); assertThat(archiveStudentsEvent).isEqualTo(sagaData); @@ -108,6 +110,8 @@ void testNotifyBatchApi_givenEventAndSagaData_shouldPostEventToApi() throws IOEx verify(messagePublisher, atMost(invocations + 1)).dispatchMessage(eq(GRAD_BATCH_API_TOPIC.toString()), eventCaptor.capture()); var newEvent = JsonUtil.getJsonObjectFromString(Event.class, new String(eventCaptor.getValue())); assertThat(newEvent.getEventType()).isEqualTo(NOTIFY_ARCHIVE_STUDENT_BATCH_COMPLETED); + assertThat(newEvent.getBatchId()).isEqualTo(String.valueOf(batchId)); + assertThat(newEvent.getSagaId()).isEqualTo(saga.getSagaId()); var sagaFromDB = sagaService.findSagaById(saga.getSagaId()); assertThat(sagaFromDB).isPresent(); diff --git a/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/events/EventHandlerDelegatorServiceTest.java b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/events/EventHandlerDelegatorServiceTest.java index 0aee200c7..c2cc70e85 100644 --- a/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/events/EventHandlerDelegatorServiceTest.java +++ b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/events/EventHandlerDelegatorServiceTest.java @@ -1,15 +1,13 @@ package ca.bc.gov.educ.api.gradstudent.service.events; +import ca.bc.gov.educ.api.gradstudent.constant.StudentStatusCodes; import ca.bc.gov.educ.api.gradstudent.controller.BaseIntegrationTest; import ca.bc.gov.educ.api.gradstudent.messaging.MessagePublisher; import ca.bc.gov.educ.api.gradstudent.messaging.jetstream.Publisher; import ca.bc.gov.educ.api.gradstudent.model.dc.Event; -import ca.bc.gov.educ.api.gradstudent.repository.GradStatusEventRepository; -import ca.bc.gov.educ.api.gradstudent.repository.SagaEventRepository; -import ca.bc.gov.educ.api.gradstudent.repository.SagaRepository; +import ca.bc.gov.educ.api.gradstudent.repository.*; import ca.bc.gov.educ.api.gradstudent.support.NatsMessageImpl; import ca.bc.gov.educ.api.gradstudent.util.JsonUtil; -import com.fasterxml.jackson.core.JsonProcessingException; import io.nats.client.Connection; import io.nats.client.Message; import org.junit.After; @@ -20,16 +18,23 @@ import org.mockito.MockitoAnnotations; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.UUID; +import static ca.bc.gov.educ.api.gradstudent.constant.EventOutcome.STUDENTS_ARCHIVED; +import static ca.bc.gov.educ.api.gradstudent.constant.EventType.ARCHIVE_STUDENTS; import static ca.bc.gov.educ.api.gradstudent.constant.EventType.ARCHIVE_STUDENTS_REQUEST; +import static ca.bc.gov.educ.api.gradstudent.constant.SagaEnum.ARCHIVE_STUDENTS_SAGA; +import static ca.bc.gov.educ.api.gradstudent.constant.SagaStatusEnum.IN_PROGRESS; import static ca.bc.gov.educ.api.gradstudent.constant.Topics.GRAD_BATCH_API_TOPIC; +import static ca.bc.gov.educ.api.gradstudent.constant.Topics.GRAD_STUDENT_ARCHIVE_STUDENTS_SAGA_TOPIC; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.*; public class EventHandlerDelegatorServiceTest extends BaseIntegrationTest { @Autowired @@ -49,12 +54,15 @@ public class EventHandlerDelegatorServiceTest extends BaseIntegrationTest { SagaRepository sagaRepository; @Autowired SagaEventRepository sagaEventRepository; - - @MockBean + @Autowired GradStatusEventRepository gradStatusEventRepository; + @SpyBean + GraduationStudentRecordRepository gradStudentRepository; + @Autowired + GraduationStudentRecordHistoryRepository studentRecordHistoryRepository; @BeforeEach - public void setUp() throws JsonProcessingException { + public void setUp() { MockitoAnnotations.openMocks(this); } @@ -63,6 +71,8 @@ public void after() { this.gradStatusEventRepository.deleteAll(); this.sagaEventRepository.deleteAll(); this.sagaRepository.deleteAll(); + this.gradStudentRepository.deleteAll(); + this.studentRecordHistoryRepository.deleteAll(); } @Test @@ -83,7 +93,11 @@ public void testHandleArchiveStudentsRequestEvent_givenValidPayload_whenSuccessf this.eventHandlerDelegatorService.handleEvent(event, message); verify(this.messagePublisher, atLeastOnce()).dispatchMessage(any(), this.eventCaptor.capture()); final var replyEvent = this.eventCaptor.getValue(); - + var createdSagas = this.sagaRepository.findAll(); + assertThat(createdSagas).isNotEmpty().size().isEqualTo(1); + assertThat(createdSagas.get(0).getSagaState()).isEqualTo(String.valueOf(ARCHIVE_STUDENTS)); + assertThat(createdSagas.get(0).getStatus()).isEqualTo(String.valueOf(IN_PROGRESS)); + assertThat(createdSagas.get(0).getSagaName()).isEqualTo(String.valueOf(ARCHIVE_STUDENTS_SAGA)); assertThat(replyEvent).isNotNull().isEqualTo(expectedResponse); } @@ -111,4 +125,42 @@ public void testHandleArchiveStudentsRequestEvent_givenArchiveAlreadyInProgress_ assertThat(replyEvent).isNotNull().isEqualTo(expectedResponse); } + + @Test + public void testHandleArchiveStudentsEvent_givenValidPayload_whenSuccessfullyProcessed_shouldSendStudentArchivedEvent() throws IOException { + var payload = getArchiveStudentsSagaData(); + + UUID sagaId = UUID.randomUUID(); + final Event event = Event.builder() + .eventType(ARCHIVE_STUDENTS) + .replyTo(String.valueOf(GRAD_STUDENT_ARCHIVE_STUDENTS_SAGA_TOPIC)) + .eventPayload(JsonUtil.getJsonStringFromObject(payload)) + .sagaId(sagaId) + .batchId("123456") + .build(); + final Message message = NatsMessageImpl.builder() + .connection(this.connection) + .data(JsonUtil.getJsonBytesFromObject(event)) + .SID("SID") + .replyTo("TEST_TOPIC") + .build(); + + var studentRecord1 = createMockGraduationStudentRecord(); + var studentRecord2 = createMockGraduationStudentRecord(); + var studentRecord3 = createMockGraduationStudentRecord(); + var studentRecord4 = createMockGraduationStudentRecord(); + studentRecord4.setStudentStatus(StudentStatusCodes.DECEASED.getCode()); + this.gradStudentRepository.saveAll(Arrays.asList(studentRecord1, studentRecord2, studentRecord3, studentRecord4)); + + doReturn(3).when(gradStudentRepository).archiveStudents(any(), any(), anyLong(), any()); + + this.eventHandlerDelegatorService.handleEvent(event, message); + verify(this.messagePublisher, atLeastOnce()).dispatchMessage(any(), this.eventCaptor.capture()); + final var replyEvent = JsonUtil.getJsonObjectFromString(Event.class, new String(this.eventCaptor.getValue())); + assertThat(replyEvent.getSagaId()).isEqualTo(sagaId); + assertThat(replyEvent.getEventType()).isEqualTo(ARCHIVE_STUDENTS); + assertThat(replyEvent.getEventOutcome()).isEqualTo(STUDENTS_ARCHIVED); + assertThat(replyEvent.getEventPayload()).isEqualTo("3"); + assertThat(studentRecordHistoryRepository.findAll()).hasSize(3); + } } From b39dc36e1931a9bec40573c239a230d8c64b3f5b Mon Sep 17 00:00:00 2001 From: mightycox Date: Mon, 18 Nov 2024 15:15:29 -0800 Subject: [PATCH 3/3] GRAD2-2976 - Fixes and updates based on code review --- .../gradstudent/constant/EventOutcome.java | 4 +- .../GraduationStudentRecordRepository.java | 4 + .../repository/SagaEventRepository.java | 9 ++ .../repository/SagaRepository.java | 12 ++ .../scheduler/EventTaskScheduler.java | 107 ++++++++++++++++ .../scheduler/PurgeOldRecordsScheduler.java | 12 +- .../service/GraduationStatusService.java | 9 ++ .../service/events/EventHandlerService.java | 27 ++-- .../scheduler/EventTaskSchedulerTest.java | 94 ++++++++++++++ .../PurgeOldRecordsSchedulerTest.java | 117 ++++++++++++++++++ .../EventHandlerDelegatorServiceTest.java | 28 +++-- api/src/test/resources/application.yaml | 6 +- api/src/test/resources/data/test.sql | 49 -------- 13 files changed, 410 insertions(+), 68 deletions(-) create mode 100644 api/src/main/java/ca/bc/gov/educ/api/gradstudent/scheduler/EventTaskScheduler.java create mode 100644 api/src/test/java/ca/bc/gov/educ/api/gradstudent/scheduler/EventTaskSchedulerTest.java create mode 100644 api/src/test/java/ca/bc/gov/educ/api/gradstudent/scheduler/PurgeOldRecordsSchedulerTest.java diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/constant/EventOutcome.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/constant/EventOutcome.java index b81cfd0f7..86e2dc3f2 100644 --- a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/constant/EventOutcome.java +++ b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/constant/EventOutcome.java @@ -20,5 +20,7 @@ public enum EventOutcome { ENROLLED_PROGRAMS_WRITTEN, ADDITIONAL_STUDENT_ATTRIBUTES_CALCULATED, STUDENTS_ARCHIVED, - BATCH_API_NOTIFIED + BATCH_API_NOTIFIED, + FAILED_TO_START_ARCHIVE_STUDENTS_SAGA, + ARCHIVE_STUDENTS_STARTED } diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/repository/GraduationStudentRecordRepository.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/repository/GraduationStudentRecordRepository.java index b175d344f..ad875b1e9 100644 --- a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/repository/GraduationStudentRecordRepository.java +++ b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/repository/GraduationStudentRecordRepository.java @@ -143,4 +143,8 @@ void updateStudentGuidPenXrefRecord( * @param */ T findByStudentID(UUID studentId, Class type); + + Integer countBySchoolOfRecordInAndStudentStatusEquals(List schoolOfRecord, String studentStatus); + + Integer countByStudentStatusEquals(String studentStatus); } diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/repository/SagaEventRepository.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/repository/SagaEventRepository.java index ce7f1653e..fa959a112 100644 --- a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/repository/SagaEventRepository.java +++ b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/repository/SagaEventRepository.java @@ -4,8 +4,12 @@ import ca.bc.gov.educ.api.gradstudent.model.entity.SagaEntity; import ca.bc.gov.educ.api.gradstudent.model.entity.SagaEventStatesEntity; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -33,4 +37,9 @@ public interface SagaEventRepository extends JpaRepository findBySagaAndSagaEventOutcomeAndSagaEventStateAndSagaStepNumber(SagaEntity saga, String eventOutcome, String eventState, int stepNumber); + + @Transactional + @Modifying + @Query(value = "delete from GRAD_STUDENT_SAGA_EVENT_STATES e where exists(select 1 from GRAD_STUDENT_SAGA s where s.SAGA_ID = e.SAGA_ID and s.CREATE_DATE <= :createDate)", nativeQuery = true) + void deleteBySagaCreateDateBefore(LocalDateTime createDate); } diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/repository/SagaRepository.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/repository/SagaRepository.java index 96d21070f..8e807aff4 100644 --- a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/repository/SagaRepository.java +++ b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/repository/SagaRepository.java @@ -4,8 +4,13 @@ import ca.bc.gov.educ.api.gradstudent.model.entity.SagaEntity; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -15,4 +20,11 @@ @Repository public interface SagaRepository extends JpaRepository, JpaSpecificationExecutor { Optional findBySagaNameAndStatusNot(String sagaName, String status); + + @Transactional + @Modifying + @Query("delete from SagaEntity where createDate <= :createDate") + void deleteByCreateDateBefore(LocalDateTime createDate); + + List findAllByStatusIn(List statuses); } diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/scheduler/EventTaskScheduler.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/scheduler/EventTaskScheduler.java new file mode 100644 index 000000000..be98e613d --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/scheduler/EventTaskScheduler.java @@ -0,0 +1,107 @@ +package ca.bc.gov.educ.api.gradstudent.scheduler; + +import ca.bc.gov.educ.api.gradstudent.constant.SagaStatusEnum; +import ca.bc.gov.educ.api.gradstudent.model.entity.SagaEntity; +import ca.bc.gov.educ.api.gradstudent.orchestrator.base.Orchestrator; +import ca.bc.gov.educ.api.gradstudent.repository.SagaRepository; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; +import org.slf4j.MDC; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static lombok.AccessLevel.PRIVATE; + +@Slf4j +@Component +public class EventTaskScheduler { + + @Getter(PRIVATE) + private final Map sagaOrchestrators = new HashMap<>(); + @Getter(PRIVATE) + private final SagaRepository sagaRepository; + @Setter + private List statusFilters; + + private static final ObjectMapper mapper = new ObjectMapper(); + + public EventTaskScheduler(final SagaRepository sagaRepository, final List orchestrators) { + this.sagaRepository = sagaRepository; + orchestrators.forEach(orchestrator -> this.sagaOrchestrators.put(orchestrator.getSagaName(), orchestrator)); + log.info("'{}' Saga Orchestrators are loaded.", String.join(",", this.sagaOrchestrators.keySet())); + } + + @Scheduled(cron = "1 * * * * *") + @SchedulerLock(name = "REPLAY_UNCOMPLETED_SAGAS", + lockAtLeastFor = "PT50S", lockAtMostFor = "PT55S") + public void findAndProcessUncompletedSagas() { + final List sagas = this.getSagaRepository().findAllByStatusIn(this.getStatusFilters()); + if (!sagas.isEmpty()) { + this.processUncompletedSagas(sagas); + } + } + + private void processUncompletedSagas(final List sagas) { + for (val saga : sagas) { + if (saga.getCreateDate().isBefore(LocalDateTime.now().minusMinutes(1)) + && this.getSagaOrchestrators().containsKey(saga.getSagaName())) { + try { + this.setRetryCountAndLog(saga); + this.getSagaOrchestrators().get(saga.getSagaName()).replaySaga(saga); + } catch (final InterruptedException ex) { + Thread.currentThread().interrupt(); + log.error("InterruptedException while findAndProcessPendingSagaEvents :: for saga :: {} :: {}", saga, ex); + } catch (final Exception e) { + log.error("Exception while findAndProcessPendingSagaEvents :: for saga :: {} :: {}", saga, e); + } + } + } + } + + public List getStatusFilters() { + if (this.statusFilters != null && !this.statusFilters.isEmpty()) { + return this.statusFilters; + } else { + final var statuses = new ArrayList(); + statuses.add(SagaStatusEnum.IN_PROGRESS.toString()); + statuses.add(SagaStatusEnum.STARTED.toString()); + return statuses; + } + } + + private void setRetryCountAndLog(final SagaEntity saga) { + Integer retryCount = saga.getRetryCount(); + if (retryCount == null || retryCount == 0) { + retryCount = 1; + } else { + retryCount += 1; + } + saga.setRetryCount(retryCount); + this.getSagaRepository().save(saga); + logSagaRetry(saga); + } + + private static void logSagaRetry(final SagaEntity saga) { + final Map retrySagaMap = new HashMap<>(); + try { + retrySagaMap.put("sagaName", saga.getSagaName()); + retrySagaMap.put("sagaId", saga.getSagaId()); + retrySagaMap.put("retryCount", saga.getRetryCount()); + MDC.putCloseable("sagaRetry", mapper.writeValueAsString(retrySagaMap)); + log.info("Saga is being retried."); + MDC.clear(); + } catch (final Exception ex) { + log.error("Exception ", ex); + } + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/scheduler/PurgeOldRecordsScheduler.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/scheduler/PurgeOldRecordsScheduler.java index a224dc741..45ed85ac7 100644 --- a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/scheduler/PurgeOldRecordsScheduler.java +++ b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/scheduler/PurgeOldRecordsScheduler.java @@ -1,6 +1,8 @@ package ca.bc.gov.educ.api.gradstudent.scheduler; import ca.bc.gov.educ.api.gradstudent.repository.GradStatusEventRepository; +import ca.bc.gov.educ.api.gradstudent.repository.SagaEventRepository; +import ca.bc.gov.educ.api.gradstudent.repository.SagaRepository; import ca.bc.gov.educ.api.gradstudent.util.EducGradStudentApiConstants; import jakarta.transaction.Transactional; import lombok.extern.slf4j.Slf4j; @@ -15,11 +17,17 @@ @Slf4j public class PurgeOldRecordsScheduler { private final GradStatusEventRepository gradStatusEventRepository; + private final SagaRepository sagaRepository; + private final SagaEventRepository sagaEventRepository; private final EducGradStudentApiConstants constants; public PurgeOldRecordsScheduler(final GradStatusEventRepository gradStatusEventRepository, - final EducGradStudentApiConstants constants) { + final EducGradStudentApiConstants constants, + final SagaRepository sagaRepository, + final SagaEventRepository sagaEventRepository) { this.gradStatusEventRepository = gradStatusEventRepository; + this.sagaRepository = sagaRepository; + this.sagaEventRepository = sagaEventRepository; this.constants = constants; } @@ -31,6 +39,8 @@ public void purgeOldRecords() { LockAssert.assertLocked(); final LocalDateTime createDateToCompare = this.calculateCreateDateBasedOnStaleEventInDays(); this.gradStatusEventRepository.deleteByCreateDateBefore(createDateToCompare); + this.sagaEventRepository.deleteBySagaCreateDateBefore(createDateToCompare); + this.sagaRepository.deleteByCreateDateBefore(createDateToCompare); } private LocalDateTime calculateCreateDateBasedOnStaleEventInDays() { diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/service/GraduationStatusService.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/service/GraduationStatusService.java index c6fb24dfa..a02136019 100644 --- a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/service/GraduationStatusService.java +++ b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/service/GraduationStatusService.java @@ -1425,6 +1425,15 @@ public void updateStudentFlagReadyForBatchJobByStudentIDs(String batchJobType, L } } + public Integer countStudentsInSchoolOfRecordsToBeArchived(List schoolOfRecordList, String studentStatus) { + logger.debug("countStudentsToBeArchived"); + if(schoolOfRecordList != null && !schoolOfRecordList.isEmpty()) { + return graduationStatusRepository.countBySchoolOfRecordInAndStudentStatusEquals(schoolOfRecordList, studentStatus); + } else { + return graduationStatusRepository.countByStudentStatusEquals(studentStatus); + } + } + private void updateStudentFlagReadyForBatchJob(UUID studentID, String batchJobType) { logger.debug("updateStudentFlagReadyByJobType for studentID - {}", studentID); Optional optional = graduationStatusRepository.findById(studentID); diff --git a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/service/events/EventHandlerService.java b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/service/events/EventHandlerService.java index a53435268..416b38527 100644 --- a/api/src/main/java/ca/bc/gov/educ/api/gradstudent/service/events/EventHandlerService.java +++ b/api/src/main/java/ca/bc/gov/educ/api/gradstudent/service/events/EventHandlerService.java @@ -14,7 +14,6 @@ import ca.bc.gov.educ.api.gradstudent.util.JsonUtil; import com.fasterxml.jackson.core.JsonProcessingException; import lombok.extern.slf4j.Slf4j; -import lombok.val; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -54,14 +53,28 @@ public EventHandlerService(final SagaService sagaService, ArchiveStudentsOrchest public byte [] handleArchiveStudentsRequest(final Event event) throws JsonProcessingException { final ArchiveStudentsSagaData sagaData = JsonUtil.getJsonObjectFromString(ArchiveStudentsSagaData.class, event.getEventPayload()); final var sagaInProgress = this.sagaService.findBySagaNameAndStatusNot(SagaEnum.ARCHIVE_STUDENTS_SAGA.toString(), SagaStatusEnum.COMPLETED.toString()); + + final Event newEvent = Event.builder() + .sagaId(event.getSagaId()) + .eventType(event.getEventType()).build(); + + final GradStatusEvent gradStatusEvent; if (sagaInProgress.isPresent()) { log.trace("Archive saga is already in progress. Returning conflict for this event :: {}", event); - return "CONFLICT".getBytes(); + newEvent.setEventOutcome(EventOutcome.FAILED_TO_START_ARCHIVE_STUDENTS_SAGA); + newEvent.setEventPayload("CONFLICT"); + gradStatusEvent = createGradStatusEventRecord(newEvent); + } else { + Integer numStudentsToBeArchived = this.graduationStatusService.countStudentsInSchoolOfRecordsToBeArchived(sagaData.getSchoolsOfRecords(), sagaData.getStudentStatusCode()); + newEvent.setEventOutcome(EventOutcome.ARCHIVE_STUDENTS_STARTED); + newEvent.setEventPayload(String.valueOf(numStudentsToBeArchived)); + gradStatusEvent = createGradStatusEventRecord(newEvent); + + var saga = this.archiveStudentsOrchestrator.createSaga(event.getEventPayload(), API_NAME, sagaData.getBatchId()); + log.debug("Starting updateStudentDownstreamOrchestrator orchestrator :: {}", saga); + this.archiveStudentsOrchestrator.startSaga(saga); } - val saga = this.archiveStudentsOrchestrator.createSaga(event.getEventPayload(), API_NAME, sagaData.getBatchId()); - log.debug("Starting updateStudentDownstreamOrchestrator orchestrator :: {}", saga); - this.archiveStudentsOrchestrator.startSaga(saga); - return "SUCCESS".getBytes(); + return createResponseEvent(this.gradStatusEventRepository.save(gradStatusEvent)); } @Transactional(propagation = REQUIRES_NEW) @@ -101,7 +114,7 @@ private GradStatusEvent createGradStatusEventRecord(final Event event) { } private byte[] createResponseEvent(GradStatusEvent event) throws JsonProcessingException { - val responseEvent = Event.builder() + var responseEvent = Event.builder() .sagaId(event.getSagaId()) .eventType(EventType.valueOf(event.getEventType())) .eventOutcome(EventOutcome.valueOf(event.getEventOutcome())) diff --git a/api/src/test/java/ca/bc/gov/educ/api/gradstudent/scheduler/EventTaskSchedulerTest.java b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/scheduler/EventTaskSchedulerTest.java new file mode 100644 index 000000000..9cf41a0f9 --- /dev/null +++ b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/scheduler/EventTaskSchedulerTest.java @@ -0,0 +1,94 @@ +package ca.bc.gov.educ.api.gradstudent.scheduler; + +import ca.bc.gov.educ.api.gradstudent.EducGradStudentApiApplication; +import ca.bc.gov.educ.api.gradstudent.constant.SagaStatusEnum; +import ca.bc.gov.educ.api.gradstudent.model.entity.SagaEntity; +import ca.bc.gov.educ.api.gradstudent.repository.SagaEventRepository; +import ca.bc.gov.educ.api.gradstudent.repository.SagaRepository; +import lombok.val; +import net.javacrumbs.shedlock.core.LockAssert; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import java.time.LocalDateTime; + +import java.util.ArrayList; + +import static ca.bc.gov.educ.api.gradstudent.constant.EventType.INITIATED; +import static ca.bc.gov.educ.api.gradstudent.constant.SagaEnum.ARCHIVE_STUDENTS_SAGA; +import static ca.bc.gov.educ.api.gradstudent.constant.SagaStatusEnum.STARTED; +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(classes = {EducGradStudentApiApplication.class}) +@ActiveProfiles("test") +@AutoConfigureMockMvc +class EventTaskSchedulerTest { + + @Autowired + EventTaskScheduler eventTaskScheduler; + + @Autowired + SagaRepository sagaRepository; + + @Autowired + SagaEventRepository sagaEventRepository; + + private static final String PAYLOAD_STR = """ + { + "createUser": "test", + "updateUser": "test", + "batchID": "123456", + "studentStatusCode": "CUR" + }\ + """; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + final var statuses = new ArrayList(); + statuses.add(SagaStatusEnum.IN_PROGRESS.toString()); + statuses.add(SagaStatusEnum.STARTED.toString()); + this.eventTaskScheduler.setStatusFilters(statuses); + } + + @AfterEach + void cleanup(){ + sagaEventRepository.deleteAll(); + sagaRepository.deleteAll(); + } + + @Test + void testFindAndProcessUncompletedSagas_givenSagaRecordInSTARTEDStateForMoreThan5Minutes_shouldBeProcessed() { + final SagaEntity placeHolderRecord = this.createDummySagaRecord(ARCHIVE_STUDENTS_SAGA.toString()); + this.sagaRepository.save(placeHolderRecord); + LockAssert.TestHelper.makeAllAssertsPass(true); + this.eventTaskScheduler.findAndProcessUncompletedSagas(); + val sagaId = placeHolderRecord.getSagaId(); + val updatedRecordFromDB = this.sagaRepository.findById(sagaId); + assertThat(updatedRecordFromDB).isPresent(); + assertThat(updatedRecordFromDB.get().getRetryCount()).isNotNull().isPositive(); + final var eventStates = this.sagaEventRepository.findBySaga(placeHolderRecord); + assertThat(eventStates).isNotEmpty(); + } + + + private SagaEntity createDummySagaRecord(final String sagaName) { + return SagaEntity + .builder() + .payload(PAYLOAD_STR) + .sagaName(sagaName) + .status(STARTED.toString()) + .sagaState(INITIATED.toString()) + .createDate(LocalDateTime.now().minusMinutes(3)) + .createUser("test") + .updateUser("test") + .updateDate(LocalDateTime.now().minusMinutes(10)) + .build(); + } +} \ No newline at end of file diff --git a/api/src/test/java/ca/bc/gov/educ/api/gradstudent/scheduler/PurgeOldRecordsSchedulerTest.java b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/scheduler/PurgeOldRecordsSchedulerTest.java new file mode 100644 index 000000000..a0a8f8a17 --- /dev/null +++ b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/scheduler/PurgeOldRecordsSchedulerTest.java @@ -0,0 +1,117 @@ +package ca.bc.gov.educ.api.gradstudent.scheduler; + +import ca.bc.gov.educ.api.gradstudent.EducGradStudentApiApplication; +import ca.bc.gov.educ.api.gradstudent.constant.EventStatus; +import ca.bc.gov.educ.api.gradstudent.model.entity.GradStatusEvent; +import ca.bc.gov.educ.api.gradstudent.model.entity.SagaEntity; +import ca.bc.gov.educ.api.gradstudent.model.entity.SagaEventStatesEntity; +import ca.bc.gov.educ.api.gradstudent.repository.GradStatusEventRepository; +import ca.bc.gov.educ.api.gradstudent.repository.SagaEventRepository; +import ca.bc.gov.educ.api.gradstudent.repository.SagaRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import java.time.LocalDateTime; + +import static ca.bc.gov.educ.api.gradstudent.constant.SagaStatusEnum.COMPLETED; +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(classes = {EducGradStudentApiApplication.class}) +@ActiveProfiles("test") +@AutoConfigureMockMvc +class PurgeOldRecordsSchedulerTest { + + @Autowired + SagaRepository repository; + + @Autowired + SagaEventRepository sagaEventRepository; + + @Autowired + GradStatusEventRepository gradStatusEventRepository; + + @Autowired + PurgeOldRecordsScheduler purgeOldRecordsScheduler; + + + @Test + void testPurgeOldRecords_givenOldRecordsPresent_shouldBeDeleted() { + final String batchId = "123456"; + final var payload = " {\n" + + " \"createUser\": \"test\",\n" + + " \"updateUser\": \"test\",\n" + + " \"batchID\": \"" + batchId + "\",\n" + + " \"studentStatusCode\": \"CUR\"\n" + + " }"; + final var saga_today = this.getSaga(payload, LocalDateTime.now()); + final var yesterday = LocalDateTime.now().minusDays(1); + final var saga_yesterday = this.getSaga(payload, yesterday); + + this.repository.save(saga_today); + this.sagaEventRepository.save(this.getSagaEvent(saga_today, payload)); + this.gradStatusEventRepository.save(this.getServicesEvent(saga_today, payload, LocalDateTime.now())); + + this.repository.save(saga_yesterday); + this.sagaEventRepository.save(this.getSagaEvent(saga_yesterday, payload)); + this.gradStatusEventRepository.save(this.getServicesEvent(saga_yesterday, payload, yesterday)); + + this.purgeOldRecordsScheduler.purgeOldRecords(); + final var sagas = this.repository.findAll(); + assertThat(sagas).hasSize(1); + + final var sagaEvents = this.sagaEventRepository.findAll(); + assertThat(sagaEvents).hasSize(1); + + final var servicesEvents = this.gradStatusEventRepository.findAll(); + assertThat(servicesEvents).hasSize(1); + } + + + private SagaEntity getSaga(final String payload, final LocalDateTime createDateTime) { + return SagaEntity + .builder() + .payload(payload) + .sagaName("ARCHIVE_STUDENTS_SAGA") + .status(COMPLETED.toString()) + .sagaState(COMPLETED.toString()) + .createDate(createDateTime) + .createUser("GRAD_API") + .updateUser("GRAD_API") + .updateDate(createDateTime) + .build(); + } + + private SagaEventStatesEntity getSagaEvent(final SagaEntity saga, final String payload) { + return SagaEventStatesEntity + .builder() + .sagaEventResponse(payload) + .saga(saga) + .sagaEventState("ARCHIVE_STUDENTS") + .sagaStepNumber(3) + .sagaEventOutcome("STUDENTS_ARCHIVED") + .createDate(LocalDateTime.now()) + .createUser("GRAD_API") + .updateUser("GRAD_API") + .updateDate(LocalDateTime.now()) + .build(); + } + + private GradStatusEvent getServicesEvent(final SagaEntity saga, final String payload, final LocalDateTime createDateTime) { + return GradStatusEvent + .builder() + .eventPayloadBytes(payload.getBytes()) + .eventStatus(EventStatus.MESSAGE_PUBLISHED.toString()) + .eventType("ARCHIVE_STUDENTS") + .sagaId(saga.getSagaId()) + .eventOutcome("STUDENTS_ARCHIVED") + .replyChannel("TEST_CHANNEL") + .createDate(createDateTime) + .createUser("GRAD_API") + .updateUser("GRAD_API") + .updateDate(createDateTime) + .build(); + } +} diff --git a/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/events/EventHandlerDelegatorServiceTest.java b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/events/EventHandlerDelegatorServiceTest.java index c2cc70e85..8f926d3ee 100644 --- a/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/events/EventHandlerDelegatorServiceTest.java +++ b/api/src/test/java/ca/bc/gov/educ/api/gradstudent/service/events/EventHandlerDelegatorServiceTest.java @@ -21,11 +21,10 @@ import org.springframework.boot.test.mock.mockito.SpyBean; import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.UUID; -import static ca.bc.gov.educ.api.gradstudent.constant.EventOutcome.STUDENTS_ARCHIVED; +import static ca.bc.gov.educ.api.gradstudent.constant.EventOutcome.*; import static ca.bc.gov.educ.api.gradstudent.constant.EventType.ARCHIVE_STUDENTS; import static ca.bc.gov.educ.api.gradstudent.constant.EventType.ARCHIVE_STUDENTS_REQUEST; import static ca.bc.gov.educ.api.gradstudent.constant.SagaEnum.ARCHIVE_STUDENTS_SAGA; @@ -77,8 +76,14 @@ public void after() { @Test public void testHandleArchiveStudentsRequestEvent_givenValidPayload_whenSuccessfullyProcessed_shouldReturnSuccess() throws IOException { + var studentRecord1 = createMockGraduationStudentRecord(); + var studentRecord2 = createMockGraduationStudentRecord(); + var studentRecord3 = createMockGraduationStudentRecord(); + var studentRecord4 = createMockGraduationStudentRecord(); + studentRecord4.setStudentStatus(StudentStatusCodes.DECEASED.getCode()); + this.gradStudentRepository.saveAll(Arrays.asList(studentRecord1, studentRecord2, studentRecord3, studentRecord4)); + var payload = getArchiveStudentsSagaData(); - var expectedResponse = "SUCCESS".getBytes(StandardCharsets.UTF_8); final Event event = Event.builder() .eventType(ARCHIVE_STUDENTS_REQUEST) .replyTo(String.valueOf(GRAD_BATCH_API_TOPIC)) @@ -92,19 +97,23 @@ public void testHandleArchiveStudentsRequestEvent_givenValidPayload_whenSuccessf .build(); this.eventHandlerDelegatorService.handleEvent(event, message); verify(this.messagePublisher, atLeastOnce()).dispatchMessage(any(), this.eventCaptor.capture()); - final var replyEvent = this.eventCaptor.getValue(); + final var replyEvent = JsonUtil.getJsonObjectFromString(Event.class, new String(this.eventCaptor.getValue())); var createdSagas = this.sagaRepository.findAll(); + + assertThat(replyEvent.getSagaId()).isNull(); + assertThat(replyEvent.getEventType()).isEqualTo(ARCHIVE_STUDENTS_REQUEST); + assertThat(replyEvent.getEventOutcome()).isEqualTo(ARCHIVE_STUDENTS_STARTED); + assertThat(replyEvent.getEventPayload()).isEqualTo("3"); + assertThat(createdSagas).isNotEmpty().size().isEqualTo(1); assertThat(createdSagas.get(0).getSagaState()).isEqualTo(String.valueOf(ARCHIVE_STUDENTS)); assertThat(createdSagas.get(0).getStatus()).isEqualTo(String.valueOf(IN_PROGRESS)); assertThat(createdSagas.get(0).getSagaName()).isEqualTo(String.valueOf(ARCHIVE_STUDENTS_SAGA)); - assertThat(replyEvent).isNotNull().isEqualTo(expectedResponse); } @Test public void testHandleArchiveStudentsRequestEvent_givenArchiveAlreadyInProgress_whenSuccessfullyProcessed_shouldReturnCONFLICT() throws IOException { var payload = getArchiveStudentsSagaData(); - var expectedResponse = "CONFLICT".getBytes(StandardCharsets.UTF_8); final Event event = Event.builder() .eventType(ARCHIVE_STUDENTS_REQUEST) .replyTo(String.valueOf(GRAD_BATCH_API_TOPIC)) @@ -121,9 +130,12 @@ public void testHandleArchiveStudentsRequestEvent_givenArchiveAlreadyInProgress_ this.sagaRepository.save(saga); this.eventHandlerDelegatorService.handleEvent(event, message); verify(this.messagePublisher, atLeastOnce()).dispatchMessage(any(), this.eventCaptor.capture()); - final var replyEvent = this.eventCaptor.getValue(); + final var replyEvent = JsonUtil.getJsonObjectFromString(Event.class, new String(this.eventCaptor.getValue())); - assertThat(replyEvent).isNotNull().isEqualTo(expectedResponse); + assertThat(replyEvent.getSagaId()).isNull(); + assertThat(replyEvent.getEventType()).isEqualTo(ARCHIVE_STUDENTS_REQUEST); + assertThat(replyEvent.getEventOutcome()).isEqualTo(FAILED_TO_START_ARCHIVE_STUDENTS_SAGA); + assertThat(replyEvent.getEventPayload()).isEqualTo("CONFLICT"); } @Test diff --git a/api/src/test/resources/application.yaml b/api/src/test/resources/application.yaml index c949ad89b..63217fbc4 100644 --- a/api/src/test/resources/application.yaml +++ b/api/src/test/resources/application.yaml @@ -12,7 +12,6 @@ spring: url: jdbc:h2:mem:gradstudent-api-h2db;MODE=Oracle username: user password: password - data-locations: classpath:data/test.sql jpa: show-sql: true database-platform: org.hibernate.dialect.H2Dialect @@ -35,6 +34,9 @@ spring: jwt: issuer-uri: http://test jwk-set-uri: http://test + sql: + init: + data-locations: classpath:data/test.sql #Logging properties logging: @@ -99,7 +101,7 @@ cron: threshold: 100 purge-old-records: run: 0 30 0 * * * - staleInDays: 90 + staleInDays: 1 refresh-non-grad-status: run: 0 30 0 * * * diff --git a/api/src/test/resources/data/test.sql b/api/src/test/resources/data/test.sql index 73cb04460..63336f362 100644 --- a/api/src/test/resources/data/test.sql +++ b/api/src/test/resources/data/test.sql @@ -4,52 +4,3 @@ CREATE TABLE IF NOT EXISTS "STATUS_SHEDLOCK" "LOCKED_AT" DATE, "LOCKED_BY" VARCHAR(255) ) ; - -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A2E77AE0D6F','2018-EN',null,'CUR',null,null,null,'03535036','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-03','RR-MM-DD'),null,5558); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('0A614E847E271815817F093827B17540','2018-EN',null,'CUR',null,null,null,'03996527','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-03','RR-MM-DD'),null,5558); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A2E77B911F0','2018-EN',null,'CUR',null,null,null,'06299164','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-03','RR-MM-DD'),null,5558); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A323F22419E','2018-EN',null,'CUR',null,null,null,'06299043','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-03','RR-MM-DD'),null,5558); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A47C8BC66A3','2018-EN',null,'ARC',null,null,null,'06299164','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-03','RR-MM-DD'),null,5558); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('0A614E847E271815817F092C3D2A53AA','1950',null,'CUR',null,null,null,'02396738','AD',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-03','RR-MM-DD'),null,5558); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('0A614E847E271815817F08F91E986DA3','SCCP',null,'TER',null,to_date('20-06-15','RR-MM-DD'),null,'06299164','12','06299164','DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-03','RR-MM-DD'),null,5558); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A3288766499','2018-EN',null,'CUR',null,null,null,'05898002','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-03','RR-MM-DD'),null,5558); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A3217A31826','SCCP',null,'CUR',null,null,null,'02396738','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-03','RR-MM-DD'),null,5558); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('0A614E847E271815817F0919DA961819','2018-EN',null,'CUR',null,null,null,'03896927','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-03','RR-MM-DD'),null,5558); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A323F844F4F','2018-EN',null,'MER',null,null,null,'06299164','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-03','RR-MM-DD'),null,5558); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A42C7E96BC8','SCCP',null,'CUR',null,to_date('21-06-01','RR-MM-DD'),null,'03838000','12','03838000','DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-03','RR-MM-DD'),null,5558); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A44BD8D7522','2018-EN',null,'CUR',null,null,null,'03838000','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-04','RR-MM-DD'),null,5567); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A05054F5383','1950',null,'CUR',null,null,null,'03499101','AD',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-03','RR-MM-DD'),null,5558); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A32884C6380','2018-EN',null,'CUR',null,null,null,'04444000','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-03','RR-MM-DD'),null,5558); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('0A614E847E271815817F08F1304A426B','2018-EN',null,'CUR',null,null,null,'04499176','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-03','RR-MM-DD'),null,5558); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A3217AB1B25','SCCP',null,'CUR',null,to_date('20-06-01','RR-MM-DD'),null,'03535020','12','03535020','DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-03','RR-MM-DD'),null,5558); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A32CAF43238','2018-EN',null,'CUR',null,null,null,'09199085','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-03','RR-MM-DD'),null,5558); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A32CB664650','2018-EN',null,'CUR',null,null,null,'06299043','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-04','RR-MM-DD'),null,5567); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A31BFCD0DA6','2018-EN',null,'CUR',null,null,null,'06196827','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-04','RR-MM-DD'),null,5567); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A32ECD96518','2018-EN',null,'CUR',null,null,null,'04444000','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-04','RR-MM-DD'),null,5567); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A3217B71FC3','2018-EN',null,'CUR',null,null,null,'03699142','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-04','RR-MM-DD'),null,5567); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A3217BE21AD','2018-EN',null,'CUR',null,null,null,'03699142','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-04','RR-MM-DD'),null,5567); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A3C9B6C3DCF','2018-EN',null,'CUR',null,null,null,'03636175','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-04','RR-MM-DD'),null,5567); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A3288A77157','2018-EN',null,'CUR',null,null,null,'00807013','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-04','RR-MM-DD'),null,5567); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A31BFCA0CBE','1950',null,'CUR',null,null,null,'00898001','AN',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-04','RR-MM-DD'),null,5567); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A30E2C95FE2','2018-EN',null,'CUR',null,null,null,'00807013','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-04','RR-MM-DD'),null,5567); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A3C9B6C3DD0','2018-EN',null,'CUR',null,null,null,'03636175','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-04','RR-MM-DD'),null,5567); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A47897062C0','1950',null,'ARC',null,null,null,'00807013','AD',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-04','RR-MM-DD'),null,5567); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A31BF156CDC','2018-EN',null,'CUR',null,null,null,'04499191','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-04','RR-MM-DD'),null,5567); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A2E69680BF6','SCCP',null,'CUR',null,null,null,'07299073','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-04','RR-MM-DD'),null,5567); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A30E29B4D0E','SCCP',null,'CUR',null,null,null,'07299073','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-04','RR-MM-DD'),null,5567); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A32884B633D','2018-EN',null,'CUR',null,null,null,'04141062','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-04','RR-MM-DD'),null,5567); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A33553D6BE4','1950',null,'CUR',null,null,null,'02396738','AD',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-04','RR-MM-DD'),null,5567); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A3B9CD21752','2018-EN',null,'CUR',null,null,null,'04499176','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-04','RR-MM-DD'),null,5567); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('0A614E847E271815817F092C831054FA','2018-EN',null,'CUR',null,null,null,'00599156','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-04','RR-MM-DD'),null,5567); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('0A614E847E271815817F093F15011A12','2018-EN',null,'CUR',null,null,'Y','03998004','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-03','RR-MM-DD'),null,5554); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('0A614E847E271815817F091A63FA1B49','2018-EN',null,'CUR',null,null,'Y','04396924','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-03','RR-MM-DD'),null,5554); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A3C9B6D3DD2','2018-EN',null,'CUR',null,null,'Y','03636175','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-03','RR-MM-DD'),null,5554); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A2E6982160A','2018-EN',null,'CUR',null,null,'Y','05798005','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-03','RR-MM-DD'),null,5554); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A2E6D0D2FBD','2018-EN',null,'CUR',null,null,'Y','08282040','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-03','RR-MM-DD'),null,5554); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A2E69981FC8','2018-EN',null,'CUR',null,null,'Y','08298009','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-03','RR-MM-DD'),null,5554); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A3C9B6D3DD6','2018-EN',null,'CUR',null,null,'Y','03636175','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-03','RR-MM-DD'),null,5554); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A32B39207AE','1950',null,'CUR',null,null,'Y','02299077','AD',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-03','RR-MM-DD'),null,5554); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A32889D6CFE','2018-EN',null,'CUR',null,null,'Y','03838000','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-03','RR-MM-DD'),null,5554); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A31BFB70600','2018-EN',null,'CUR',null,null,'Y','03838000','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-03','RR-MM-DD'),null,5554); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A3041A43D08','2018-EN',null,'CUR',null,null,'Y','03838031','12',null,'DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-03','RR-MM-DD'),null,5554); -Insert into GRADUATION_STUDENT_RECORD (GRADUATION_STUDENT_RECORD_ID,GRADUATION_PROGRAM_CODE,GPA,STUDENT_STATUS_CODE,HONOURS_STANDING,PROGRAM_COMPLETION_DATE,RECALCULATE_GRAD_STATUS,SCHOOL_OF_RECORD,STUDENT_GRADE,SCHOOL_AT_GRADUATION,CREATE_USER,CREATE_DATE,UPDATE_USER,UPDATE_DATE,RECALCULATE_PROJECTED_GRAD,BATCH_ID) values ('AC339D7076491A2E81764A32B3900749','SCCP',null,'CUR',null,to_date('20-06-01','RR-MM-DD'),'Y','02323082','12','02323082','DATA_CONV',to_date('22-02-17','RR-MM-DD'),'API_GRAD_STUDENT',to_date('22-03-03','RR-MM-DD'),null,5554); \ No newline at end of file