From 752f571afe8a0af4c39b5b97ed74b050f222990d Mon Sep 17 00:00:00 2001 From: Jarkko Pesonen <435495+jrkkp@users.noreply.github.com> Date: Tue, 19 Nov 2024 09:14:56 +0200 Subject: [PATCH] VKT(Frontend & Backend): Paytrail payment link and email sending fixes [deploy] --- .../ExaminerEnrollmentAppointmentDTO.java | 3 +- .../ExaminerEnrollmentController.java | 10 ++ .../main/java/fi/oph/vkt/model/EmailType.java | 1 + .../oph/vkt/model/EnrollmentAppointment.java | 4 + .../AbstractEnrollmentEmailService.java | 105 ++++++++++++ .../ExaminerEnrollmentEmailService.java | 61 +++++++ .../service/ExaminerEnrollmentService.java | 29 ++++ .../service/PublicEnrollmentEmailService.java | 157 +++++++----------- .../fi/oph/vkt/util/ClerkEnrollmentUtil.java | 21 ++- .../fi/oph/vkt/util/TemplateRenderer.java | 4 + .../db/changelog/db.changelog-1.0.xml | 5 + .../enrollment-appointment-auth-link.html | 79 +++++++++ .../ClerkEnrollmentAppointmentDetails.tsx | 47 +++--- ...lerkEnrollmentAppointmentDetailsFields.tsx | 112 ++++--------- .../vkt/src/interfaces/clerkEnrollment.ts | 1 + .../reducers/clerkEnrollmentAppointment.ts | 34 +++- .../redux/sagas/clerkEnrollmentAppointment.ts | 35 ++++ 17 files changed, 487 insertions(+), 221 deletions(-) create mode 100644 backend/vkt/src/main/java/fi/oph/vkt/service/AbstractEnrollmentEmailService.java create mode 100644 backend/vkt/src/main/java/fi/oph/vkt/service/ExaminerEnrollmentEmailService.java create mode 100644 backend/vkt/src/main/resources/email-templates/enrollment-appointment-auth-link.html diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerEnrollmentAppointmentDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerEnrollmentAppointmentDTO.java index 44e1c823b..feea29cc5 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerEnrollmentAppointmentDTO.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerEnrollmentAppointmentDTO.java @@ -34,6 +34,7 @@ public record ExaminerEnrollmentAppointmentDTO( @NonNull @NotBlank String lastName, @NonNull @NotNull List payments, ExaminerExamEventDTO examEvent, - ExaminerAuthLinkDTO authLink + ExaminerAuthLinkDTO authLink, + String paymentLinkUrl ) implements EnrollmentDTOSkillFields {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/examiner/ExaminerEnrollmentController.java b/backend/vkt/src/main/java/fi/oph/vkt/api/examiner/ExaminerEnrollmentController.java index bd34dff45..5f4465833 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/api/examiner/ExaminerEnrollmentController.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/examiner/ExaminerEnrollmentController.java @@ -10,6 +10,7 @@ import io.swagger.v3.oas.annotations.Operation; import jakarta.annotation.Resource; import jakarta.validation.Valid; +import java.io.IOException; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; @@ -59,6 +60,15 @@ public ExaminerEnrollmentAppointmentDTO getEnrollmentAppointment( return examinerEnrollmentService.getEnrollmentAppointment(oid, enrollmentAppointmentId); } + @PostMapping(path = "/appointment/{enrollmentAppointmentId:\\d+}/sendAuthLink", consumes = ALL_VALUE) + @Operation(tags = TAG_ENROLLMENT, summary = "Send enrollment appointment auth link") + public ExaminerEnrollmentAppointmentDTO sendEnrollmentAppointmentLink( + @PathVariable final String oid, + @PathVariable final long enrollmentAppointmentId + ) throws IOException, InterruptedException { + return examinerEnrollmentService.sendEnrollmentAppointmentLink(oid, enrollmentAppointmentId); + } + @PutMapping(path = "/appointment/{enrollmentAppointmentId:\\d+}/grades") @Operation(tags = TAG_ENROLLMENT, summary = "Update enrollment appointment grades") public ExaminerEnrollmentGradesDTO upsertEnrollmentAppointmentGrades( diff --git a/backend/vkt/src/main/java/fi/oph/vkt/model/EmailType.java b/backend/vkt/src/main/java/fi/oph/vkt/model/EmailType.java index 19ad2e8e3..21e91859f 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/model/EmailType.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/model/EmailType.java @@ -3,4 +3,5 @@ public enum EmailType { ENROLLMENT_CONFIRMATION, ENROLLMENT_TO_QUEUE_CONFIRMATION, + ENROLLMENT_APPOINTMENT_AUTH_LINK, } diff --git a/backend/vkt/src/main/java/fi/oph/vkt/model/EnrollmentAppointment.java b/backend/vkt/src/main/java/fi/oph/vkt/model/EnrollmentAppointment.java index 1eee55e24..9cd6b1f46 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/model/EnrollmentAppointment.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/model/EnrollmentAppointment.java @@ -71,6 +71,10 @@ public class EnrollmentAppointment extends EnrollmentCommon { @Column(name = "message") private String message; + @Size(max = 255) + @Column(name = "payment_link_hash", unique = true) + private String paymentLinkHash; + @Size(max = 255) @Column(name = "auth_hash", unique = true) private String authHash; diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/AbstractEnrollmentEmailService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/AbstractEnrollmentEmailService.java new file mode 100644 index 000000000..2e476589a --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/AbstractEnrollmentEmailService.java @@ -0,0 +1,105 @@ +package fi.oph.vkt.service; + +import static fi.oph.vkt.util.LocalisationUtil.localeFI; +import static fi.oph.vkt.util.LocalisationUtil.localeSV; + +import fi.oph.vkt.model.EmailType; +import fi.oph.vkt.model.Enrollment; +import fi.oph.vkt.model.EnrollmentAppointment; +import fi.oph.vkt.model.EnrollmentCommon; +import fi.oph.vkt.model.ExamEvent; +import fi.oph.vkt.model.ExamEventCommon; +import fi.oph.vkt.model.ExaminerExamEvent; +import fi.oph.vkt.model.type.ExamLanguage; +import fi.oph.vkt.service.email.EmailAttachmentData; +import fi.oph.vkt.service.email.EmailData; +import fi.oph.vkt.service.email.EmailService; +import fi.oph.vkt.util.LocalisationUtil; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class AbstractEnrollmentEmailService { + + protected void createEmail( + final EmailService emailService, + final String recipientName, + final String recipientAddress, + final String subject, + final String body, + final List attachments, + final EmailType emailType + ) { + final EmailData emailData = EmailData + .builder() + .recipientName(recipientName) + .recipientAddress(recipientAddress) + .subject(subject) + .body(body) + .attachments(attachments) + .build(); + + emailService.saveEmail(emailType, emailData); + } + + protected Map getEmailParams(final EnrollmentCommon enrollment, final ExamEventCommon examEvent) { + final Map params = new HashMap<>(Map.of()); + + if (examEvent.getLanguage() == ExamLanguage.FI) { + params.put("examLanguageFI", LocalisationUtil.translate(localeFI, "lang.finnish")); + params.put("examLanguageSV", LocalisationUtil.translate(localeSV, "lang.finnish")); + } else { + params.put("examLanguageFI", LocalisationUtil.translate(localeFI, "lang.swedish")); + params.put("examLanguageSV", LocalisationUtil.translate(localeSV, "lang.swedish")); + } + + params.put("skillsFI", getEmailParamSkills(enrollment, localeFI, params.get("examLanguageFI"))); + params.put("skillsSV", getEmailParamSkills(enrollment, localeSV, params.get("examLanguageSV"))); + + params.put("partialExamsFI", getEmailParamPartialExams(enrollment, localeFI)); + params.put("partialExamsSV", getEmailParamPartialExams(enrollment, localeSV)); + + params.put("examLevelFI", LocalisationUtil.translate(localeFI, "examLevel.excellent")); + params.put("examLevelSV", LocalisationUtil.translate(localeSV, "examLevel.excellent")); + + params.put("examDate", examEvent.getDate().format(DateTimeFormatter.ofPattern("dd.MM.yyyy"))); + + params.put("type", "enrollment"); + params.put("isFree", false); + + return params; + } + + private String getEmailParamSkills(final EnrollmentCommon enrollment, final Locale locale, final Object... args) { + return joinNonEmptyStrings( + Stream.of( + enrollment.isTextualSkill() ? LocalisationUtil.translate(locale, "skill.textual", args) : "", + enrollment.isOralSkill() ? LocalisationUtil.translate(locale, "skill.oral", args) : "", + enrollment.isUnderstandingSkill() ? LocalisationUtil.translate(locale, "skill.understanding", args) : "" + ) + ); + } + + private String getEmailParamPartialExams(final EnrollmentCommon enrollment, final Locale locale) { + return joinNonEmptyStrings( + Stream.of( + enrollment.isWritingPartialExam() ? LocalisationUtil.translate(locale, "partialExam.writing") : "", + enrollment.isReadingComprehensionPartialExam() + ? LocalisationUtil.translate(locale, "partialExam.readingComprehension") + : "", + enrollment.isSpeakingPartialExam() ? LocalisationUtil.translate(locale, "partialExam.speaking") : "", + enrollment.isSpeechComprehensionPartialExam() + ? LocalisationUtil.translate(locale, "partialExam.speechComprehension") + : "" + ) + ); + } + + private String joinNonEmptyStrings(final Stream stream) { + return stream.filter(s -> !s.isEmpty()).collect(Collectors.joining(", ")); + } +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/ExaminerEnrollmentEmailService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/ExaminerEnrollmentEmailService.java new file mode 100644 index 000000000..743f15c57 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/ExaminerEnrollmentEmailService.java @@ -0,0 +1,61 @@ +package fi.oph.vkt.service; + +import static fi.oph.vkt.util.LocalisationUtil.localeFI; +import static fi.oph.vkt.util.LocalisationUtil.localeSV; + +import fi.oph.vkt.model.EmailType; +import fi.oph.vkt.model.Enrollment; +import fi.oph.vkt.model.EnrollmentAppointment; +import fi.oph.vkt.model.Person; +import fi.oph.vkt.service.email.EmailAttachmentData; +import fi.oph.vkt.service.email.EmailService; +import fi.oph.vkt.service.receipt.ReceiptRenderer; +import fi.oph.vkt.util.ClerkEnrollmentUtil; +import fi.oph.vkt.util.LocalisationUtil; +import fi.oph.vkt.util.TemplateRenderer; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class ExaminerEnrollmentEmailService extends AbstractEnrollmentEmailService { + + private final EmailService emailService; + private final Environment environment; + private final TemplateRenderer templateRenderer; + + @Transactional + public void sendEnrollmentAppointmentAuthLink(final EnrollmentAppointment enrollment) + throws IOException, InterruptedException { + final String baseUrlAPI = environment.getRequiredProperty("app.base-url.api"); + final Map templateParams = getEmailParams(enrollment, enrollment.getExaminerExamEvent()); + final String authUrl = ClerkEnrollmentUtil.getAuthUrl(baseUrlAPI, enrollment.getId(), enrollment.getAuthHash()); + + templateParams.put("type", "enrollment"); + templateParams.put("enrollmentAuthLink", authUrl); + + final String recipientName = enrollment.getFirstName() + " " + enrollment.getLastName(); + final String recipientAddress = enrollment.getEmail(); + final String subject = String.format( + "%s | %s", + LocalisationUtil.translate(localeFI, "subject.enrollment-confirmation"), + LocalisationUtil.translate(localeSV, "subject.enrollment-confirmation") + ); + final String body = templateRenderer.renderEnrollmentAppointmentAuthLink(templateParams); + + createEmail( + emailService, + recipientName, + recipientAddress, + subject, + body, + List.of(), + EmailType.ENROLLMENT_APPOINTMENT_AUTH_LINK + ); + } +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/ExaminerEnrollmentService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/ExaminerEnrollmentService.java index f0e2462ba..2ce1271be 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/service/ExaminerEnrollmentService.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/ExaminerEnrollmentService.java @@ -17,6 +17,8 @@ import fi.oph.vkt.util.UUIDSource; import fi.oph.vkt.util.exception.APIException; import fi.oph.vkt.util.exception.APIExceptionType; +import java.io.IOException; +import java.time.LocalDateTime; import java.util.Objects; import java.util.Optional; import lombok.RequiredArgsConstructor; @@ -33,6 +35,7 @@ public class ExaminerEnrollmentService extends AbstractEnrollmentService { private final ExaminerExamEventRepository examinerExamEventRepository; private final Environment environment; private final UUIDSource uuidSource; + private final ExaminerEnrollmentEmailService examinerEnrollmentEmailService; private static void checkExaminerOid(EnrollmentAppointment enrollmentAppointment, String oid) { if (!enrollmentAppointment.getExaminer().getOid().equals(oid)) { @@ -94,6 +97,10 @@ public ExaminerEnrollmentAppointmentDTO convertToAppointment(final String oid, f enrollmentAppointment.setAuthHash(uuidSource.getRandomNonce()); } + if (enrollmentAppointment.getPaymentLinkHash() == null) { + enrollmentAppointment.setPaymentLinkHash(uuidSource.getRandomNonce()); + } + enrollmentAppointmentRepository.flush(); return ClerkEnrollmentUtil.createClerkEnrollmentAppointmentDTO(enrollmentAppointment, baseUrlAPI); @@ -214,4 +221,26 @@ private ExaminerEnrollmentGradesDTO createGradesDTO(final EnrollmentGrade enroll private EnrollmentGradeDTO createGradeDTO(final EnrollmentGradeType grade, final String comment) { return grade == null ? null : EnrollmentGradeDTO.builder().grade(grade).comment(comment).build(); } + + @Transactional + public ExaminerEnrollmentAppointmentDTO sendEnrollmentAppointmentLink( + final String oid, + final long enrollmentAppointmentId + ) throws IOException, InterruptedException { + final EnrollmentAppointment enrollmentAppointment = enrollmentAppointmentRepository.getReferenceById( + enrollmentAppointmentId + ); + final String baseUrlAPI = environment.getRequiredProperty("app.base-url.api"); + + checkExaminerOid(enrollmentAppointment, oid); + + enrollmentAppointment.setExpiresAt(LocalDateTime.now().plusDays(3)); + enrollmentAppointment.setSentAt(LocalDateTime.now()); + + examinerEnrollmentEmailService.sendEnrollmentAppointmentAuthLink(enrollmentAppointment); + + enrollmentAppointmentRepository.flush(); + + return ClerkEnrollmentUtil.createClerkEnrollmentAppointmentDTO(enrollmentAppointment, baseUrlAPI); + } } diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentEmailService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentEmailService.java index 806fdbb18..da1cf05d0 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentEmailService.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentEmailService.java @@ -6,12 +6,9 @@ import fi.oph.vkt.api.dto.FreeEnrollmentDetails; import fi.oph.vkt.model.EmailType; import fi.oph.vkt.model.Enrollment; -import fi.oph.vkt.model.ExamEvent; import fi.oph.vkt.model.Person; -import fi.oph.vkt.model.type.ExamLanguage; import fi.oph.vkt.model.type.FreeEnrollmentSource; import fi.oph.vkt.service.email.EmailAttachmentData; -import fi.oph.vkt.service.email.EmailData; import fi.oph.vkt.service.email.EmailService; import fi.oph.vkt.service.receipt.ReceiptData; import fi.oph.vkt.service.receipt.ReceiptRenderer; @@ -19,13 +16,10 @@ import fi.oph.vkt.util.LocalisationUtil; import fi.oph.vkt.util.TemplateRenderer; import java.io.IOException; -import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.Stream; import lombok.RequiredArgsConstructor; import org.springframework.core.env.Environment; import org.springframework.stereotype.Service; @@ -33,7 +27,7 @@ @Service @RequiredArgsConstructor -public class PublicEnrollmentEmailService { +public class PublicEnrollmentEmailService extends AbstractEnrollmentEmailService { private final EmailService emailService; private final Environment environment; @@ -43,7 +37,7 @@ public class PublicEnrollmentEmailService { @Transactional public void sendEnrollmentConfirmationEmail(final Enrollment enrollment) throws IOException, InterruptedException { final Person person = enrollment.getPerson(); - final Map templateParams = getEmailParams(enrollment); + final Map templateParams = getEmailParams(enrollment, enrollment.getExamEvent()); templateParams.put("type", "enrollment"); final String recipientName = person.getFirstName() + " " + person.getLastName(); @@ -62,12 +56,20 @@ public void sendEnrollmentConfirmationEmail(final Enrollment enrollment) throws ? List.of(createReceiptAttachment(enrollment, localeFI), createReceiptAttachment(enrollment, localeSV)) : List.of(); // for local development - createEmail(recipientName, recipientAddress, subject, body, attachments, EmailType.ENROLLMENT_CONFIRMATION); + createEmail( + emailService, + recipientName, + recipientAddress, + subject, + body, + attachments, + EmailType.ENROLLMENT_CONFIRMATION + ); } @Transactional public void sendEnrollmentToQueueConfirmationEmail(final Enrollment enrollment, final Person person) { - final Map templateParams = getEmailParams(enrollment); + final Map templateParams = getEmailParams(enrollment, enrollment.getExamEvent()); templateParams.put("type", "queue"); final String recipientName = person.getFirstName() + " " + person.getLastName(); @@ -80,68 +82,17 @@ public void sendEnrollmentToQueueConfirmationEmail(final Enrollment enrollment, final String body = templateRenderer.renderEnrollmentConfirmationEmailBody(templateParams); - createEmail(recipientName, recipientAddress, subject, body, List.of(), EmailType.ENROLLMENT_TO_QUEUE_CONFIRMATION); - } - - private Map getEmailParams(final Enrollment enrollment) { - final ExamEvent examEvent = enrollment.getExamEvent(); - - final Map params = new HashMap<>(Map.of()); - - if (examEvent.getLanguage() == ExamLanguage.FI) { - params.put("examLanguageFI", LocalisationUtil.translate(LocalisationUtil.localeFI, "lang.finnish")); - params.put("examLanguageSV", LocalisationUtil.translate(LocalisationUtil.localeSV, "lang.finnish")); - } else { - params.put("examLanguageFI", LocalisationUtil.translate(LocalisationUtil.localeFI, "lang.swedish")); - params.put("examLanguageSV", LocalisationUtil.translate(LocalisationUtil.localeSV, "lang.swedish")); - } - - params.put("examLevelFI", LocalisationUtil.translate(localeFI, "examLevel.excellent")); - params.put("examLevelSV", LocalisationUtil.translate(localeSV, "examLevel.excellent")); - - params.put("examDate", examEvent.getDate().format(DateTimeFormatter.ofPattern("dd.MM.yyyy"))); - - params.put("skillsFI", getEmailParamSkills(enrollment, localeFI, params.get("examLanguageFI"))); - params.put("skillsSV", getEmailParamSkills(enrollment, localeSV, params.get("examLanguageSV"))); - - params.put("partialExamsFI", getEmailParamPartialExams(enrollment, localeFI)); - params.put("partialExamsSV", getEmailParamPartialExams(enrollment, localeSV)); - - params.put("type", "enrollment"); - params.put("isFree", false); - - return params; - } - - private String getEmailParamSkills(final Enrollment enrollment, final Locale locale, final Object... args) { - return joinNonEmptyStrings( - Stream.of( - enrollment.isTextualSkill() ? LocalisationUtil.translate(locale, "skill.textual", args) : "", - enrollment.isOralSkill() ? LocalisationUtil.translate(locale, "skill.oral", args) : "", - enrollment.isUnderstandingSkill() ? LocalisationUtil.translate(locale, "skill.understanding", args) : "" - ) - ); - } - - private String getEmailParamPartialExams(final Enrollment enrollment, final Locale locale) { - return joinNonEmptyStrings( - Stream.of( - enrollment.isWritingPartialExam() ? LocalisationUtil.translate(locale, "partialExam.writing") : "", - enrollment.isReadingComprehensionPartialExam() - ? LocalisationUtil.translate(locale, "partialExam.readingComprehension") - : "", - enrollment.isSpeakingPartialExam() ? LocalisationUtil.translate(locale, "partialExam.speaking") : "", - enrollment.isSpeechComprehensionPartialExam() - ? LocalisationUtil.translate(locale, "partialExam.speechComprehension") - : "" - ) + createEmail( + emailService, + recipientName, + recipientAddress, + subject, + body, + List.of(), + EmailType.ENROLLMENT_TO_QUEUE_CONFIRMATION ); } - private String joinNonEmptyStrings(final Stream stream) { - return stream.filter(s -> !s.isEmpty()).collect(Collectors.joining(", ")); - } - private EmailAttachmentData createReceiptAttachment(final Enrollment enrollment, final Locale locale) throws IOException, InterruptedException { final ReceiptData receiptData = receiptRenderer.getReceiptData(enrollment.getId(), locale); @@ -157,26 +108,6 @@ private EmailAttachmentData createReceiptAttachment(final Enrollment enrollment, .build(); } - private void createEmail( - final String recipientName, - final String recipientAddress, - final String subject, - final String body, - final List attachments, - final EmailType emailType - ) { - final EmailData emailData = EmailData - .builder() - .recipientName(recipientName) - .recipientAddress(recipientAddress) - .subject(subject) - .body(body) - .attachments(attachments) - .build(); - - emailService.saveEmail(emailType, emailData); - } - @Transactional public void sendFreeEnrollmentConfirmationEmail( final Enrollment enrollment, @@ -184,7 +115,7 @@ public void sendFreeEnrollmentConfirmationEmail( final FreeEnrollmentDetails freeEnrollmentDetails ) { final Map templateParams = withFreeEmailParams( - getEmailParams(enrollment), + getEmailParams(enrollment, enrollment.getExamEvent()), freeEnrollmentDetails, enrollment.getFreeEnrollment().getSource(), "enrollment" @@ -199,7 +130,15 @@ public void sendFreeEnrollmentConfirmationEmail( ); final String body = templateRenderer.renderEnrollmentConfirmationEmailBody(templateParams); - createEmail(recipientName, recipientAddress, subject, body, List.of(), EmailType.ENROLLMENT_CONFIRMATION); + createEmail( + emailService, + recipientName, + recipientAddress, + subject, + body, + List.of(), + EmailType.ENROLLMENT_CONFIRMATION + ); } @Transactional @@ -209,7 +148,7 @@ public void sendFreeEnrollmentToQueueConfirmationEmail( final FreeEnrollmentDetails freeEnrollmentDetails ) { final Map templateParams = withFreeEmailParams( - getEmailParams(enrollment), + getEmailParams(enrollment, enrollment.getExamEvent()), freeEnrollmentDetails, enrollment.getFreeEnrollment().getSource(), "queue" @@ -224,7 +163,15 @@ public void sendFreeEnrollmentToQueueConfirmationEmail( ); final String body = templateRenderer.renderEnrollmentConfirmationEmailBody(templateParams); - createEmail(recipientName, recipientAddress, subject, body, List.of(), EmailType.ENROLLMENT_TO_QUEUE_CONFIRMATION); + createEmail( + emailService, + recipientName, + recipientAddress, + subject, + body, + List.of(), + EmailType.ENROLLMENT_TO_QUEUE_CONFIRMATION + ); } @Transactional @@ -234,7 +181,7 @@ public void sendPartiallyFreeEnrollmentConfirmationEmail( final FreeEnrollmentDetails freeEnrollmentDetails ) throws IOException, InterruptedException { final Map templateParams = withFreeEmailParams( - getEmailParams(enrollment), + getEmailParams(enrollment, enrollment.getExamEvent()), freeEnrollmentDetails, enrollment.getFreeEnrollment().getSource(), "enrollment" @@ -257,7 +204,15 @@ public void sendPartiallyFreeEnrollmentConfirmationEmail( ? List.of(createReceiptAttachment(enrollment, localeFI), createReceiptAttachment(enrollment, localeSV)) : List.of(); // for local development - createEmail(recipientName, recipientAddress, subject, body, attachments, EmailType.ENROLLMENT_CONFIRMATION); + createEmail( + emailService, + recipientName, + recipientAddress, + subject, + body, + attachments, + EmailType.ENROLLMENT_CONFIRMATION + ); } @Transactional @@ -267,7 +222,7 @@ public void sendPartiallyFreeEnrollmentToQueueConfirmationEmail( final FreeEnrollmentDetails freeEnrollmentDetails ) { final Map templateParams = withFreeEmailParams( - getEmailParams(enrollment), + getEmailParams(enrollment, enrollment.getExamEvent()), freeEnrollmentDetails, enrollment.getFreeEnrollment().getSource(), "queue" @@ -283,7 +238,15 @@ public void sendPartiallyFreeEnrollmentToQueueConfirmationEmail( ); final String body = templateRenderer.renderEnrollmentConfirmationEmailBody(templateParams); - createEmail(recipientName, recipientAddress, subject, body, List.of(), EmailType.ENROLLMENT_TO_QUEUE_CONFIRMATION); + createEmail( + emailService, + recipientName, + recipientAddress, + subject, + body, + List.of(), + EmailType.ENROLLMENT_TO_QUEUE_CONFIRMATION + ); } public Map withFreeEmailParams( diff --git a/backend/vkt/src/main/java/fi/oph/vkt/util/ClerkEnrollmentUtil.java b/backend/vkt/src/main/java/fi/oph/vkt/util/ClerkEnrollmentUtil.java index f72883e26..d55269b91 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/util/ClerkEnrollmentUtil.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/util/ClerkEnrollmentUtil.java @@ -150,6 +150,10 @@ public static KoskiEducationsDTO createKoskiEducationsDTO(final KoskiEducations .build(); } + public static String getAuthUrl(final String baseUrlAPI, final long id, final String hash) { + return String.format("%s/enrollment/appointment/%d/redirect/%s", baseUrlAPI, id, hash); + } + public static ExaminerEnrollmentAppointmentDTO createClerkEnrollmentAppointmentDTO( final EnrollmentAppointment enrollmentAppointment, final String baseUrlAPI @@ -164,19 +168,19 @@ public static ExaminerEnrollmentAppointmentDTO createClerkEnrollmentAppointmentD final ExaminerAuthLinkDTO examinerAuthLinkDTO = enrollmentAppointment.getAuthHash() != null ? ExaminerAuthLinkDTO .builder() - .url( - String.format( - "%s/enrollment/appointment/%d/redirect/%s", - baseUrlAPI, - enrollmentAppointment.getId(), - enrollmentAppointment.getAuthHash() - ) - ) + .url(getAuthUrl(baseUrlAPI, enrollmentAppointment.getId(), enrollmentAppointment.getAuthHash())) .expiresAt(enrollmentAppointment.getExpiresAt()) .sentAt(enrollmentAppointment.getSentAt()) .build() : null; + final String paymentLinkUrl = String.format( + "%s/enrollment/appointment/%d/redirectPayment/%s", + baseUrlAPI, + enrollmentAppointment.getId(), + enrollmentAppointment.getPaymentLinkHash() + ); + final ExaminerExamEventDTO examinerExamEventDTO = enrollmentAppointment.getExaminerExamEvent() != null ? ExaminerUtil.toExaminerExamEventWithoutEnrollmentsDTO(enrollmentAppointment.getExaminerExamEvent()) : null; @@ -203,6 +207,7 @@ public static ExaminerEnrollmentAppointmentDTO createClerkEnrollmentAppointmentD .firstName(enrollmentAppointment.getFirstName()) .lastName(enrollmentAppointment.getLastName()) .authLink(examinerAuthLinkDTO) + .paymentLinkUrl(paymentLinkUrl) .examEvent(examinerExamEventDTO) .payments(paymentDTOs) .build(); diff --git a/backend/vkt/src/main/java/fi/oph/vkt/util/TemplateRenderer.java b/backend/vkt/src/main/java/fi/oph/vkt/util/TemplateRenderer.java index 853dbb34d..a7d8175d8 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/util/TemplateRenderer.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/util/TemplateRenderer.java @@ -19,6 +19,10 @@ public String renderEnrollmentConfirmationEmailBody(final Map pa return renderTemplate("enrollment-confirmation", params, Optional.empty()); } + public String renderEnrollmentAppointmentAuthLink(final Map params) { + return renderTemplate("enrollment-appointment-auth-link", params, Optional.empty()); + } + public String renderReceipt(final Locale locale, final Map params) { return renderTemplate("receipt", params, Optional.of(locale)); } diff --git a/backend/vkt/src/main/resources/db/changelog/db.changelog-1.0.xml b/backend/vkt/src/main/resources/db/changelog/db.changelog-1.0.xml index 128b563cc..67d0eb2a8 100644 --- a/backend/vkt/src/main/resources/db/changelog/db.changelog-1.0.xml +++ b/backend/vkt/src/main/resources/db/changelog/db.changelog-1.0.xml @@ -1152,4 +1152,9 @@ + + + + + diff --git a/backend/vkt/src/main/resources/email-templates/enrollment-appointment-auth-link.html b/backend/vkt/src/main/resources/email-templates/enrollment-appointment-auth-link.html new file mode 100644 index 000000000..1b2818bf4 --- /dev/null +++ b/backend/vkt/src/main/resources/email-templates/enrollment-appointment-auth-link.html @@ -0,0 +1,79 @@ + + + +

+ Hei, +

+ +

Tunnistautumislinkki valtiohallinnon hyvän ja tyydyttävän tason tutkinnon ilmoittautumiseen

+
+ Tunnistaudu ja maksa ilmoittautuminen tästä + +
+

+ Tutkinnon kieli:
+ Tutkinnon taso:
+ Tutkintopäivä:
+ Valitsemasi taidot:
+ Valitsemasi osakokeet:
+

+
+ +

Tietoa tutkintotilaisuudesta

+ +

Tutkintotilaisuus järjestetään Opetushallituksen tiloissa osoitteessa Hakaniemenranta 6, 00530 Helsinki.

+

Tutkinnon suorittamiseen kannattaa varata koko päivä. Tutkintotilaisuus alkaa klo 9.00 kirjallisen taidon tutkinnolla ja jatkuu iltapäivällä suullisen taidon tutkinnolla. Tutkinnon päättymisaika riippuu tutkintoon osallistuvien määrästä. Saat tarkemmat ohjeet tutkintopäivän kulusta viikkoa ennen tutkintopäivää.

+

Jos sinulla on kysyttävää tutkinnosta, voit lähettää meille sähköpostia osoitteeseen kielitutkinnot@oph.fi. Ilmoitathan viipymättä, jos et pysty osallistumaan tutkintoon.

+

+ Lisätietoa tutkinnosta löydät Opetushallituksen verkkopalvelusta:
+ Valtionhallinnon kielitutkinnot (VKT) +

+
+ +

+ Älä vastaa tähän viestiin - viesti on lähetetty automaattisesti. +

+

+ Ystävällisin terveisin
+ Opetushallitus +

+ +

+ Hej, +

+ +

Tunnistautumislinkki valtiohallinnon hyvän ja tyydyttävän tason tutkinnon ilmoittautumiseen

+
+ Tunnistaudu ja maksa ilmoittautuminen tästä + +
+ +

+ Examensspråk:
+ Examensnivå:
+ Examensdatum:
+ Rätt till avgiftsfri examen: Nej
+ Förmågor som du har valt:
+ Delprov som du har valt:
+

+
+

Information om examenstillfället

+ +

Examenstillfället ordnas vid Utbildningsstyrelsen på adressen Hagnäskajen 6, 00530 Helsingfors.

+

Du bör reservera hela dagen för examen. Examenstillfället börjar klockan 9.00 med delprovet i skriftlig färdighet och fortsätter på eftermiddagen med delprovet i muntlig färdighet. Deltagarantalet avgör när examenstillfället slutar. Du får närmare anvisningar om examensdagens program en vecka före examensdagen.

+

Om du har frågor om examen, kan du kontakta oss per e-post på adressen kielitutkinnot@oph.fi. Vänligen meddela omedelbart om du inte kan delta i examen.

+

+ Närmare information om examen finns på Utbildningsstyrelsens webbplats:
+ Språkexamina för statsförvaltningen (VKT) +

+
+ +

+ Svara inte på detta meddelande, det har skickats automatiskt. +

+

+ Med vänlig hälsning
+ Utbildningsstyrelsen +

+ + diff --git a/frontend/packages/vkt/src/components/clerkEnrollment/appointment/ClerkEnrollmentAppointmentDetails.tsx b/frontend/packages/vkt/src/components/clerkEnrollment/appointment/ClerkEnrollmentAppointmentDetails.tsx index d4dd6d32a..53499ab1c 100644 --- a/frontend/packages/vkt/src/components/clerkEnrollment/appointment/ClerkEnrollmentAppointmentDetails.tsx +++ b/frontend/packages/vkt/src/components/clerkEnrollment/appointment/ClerkEnrollmentAppointmentDetails.tsx @@ -15,12 +15,10 @@ import { ClerkEnrollmentAppointment } from 'interfaces/clerkEnrollment'; import { PartialExamsAndSkills } from 'interfaces/common/enrollment'; import { ExaminerExamEvent } from 'interfaces/examinerExamEvent'; import { - resetClerkEnrollmentDetailsUpdate, + resetClerkEnrollmentDetails, updateClerkEnrollmentAppointment, } from 'redux/reducers/clerkEnrollmentAppointment'; -import { resetClerkEnrollmentStatusChange } from 'redux/reducers/clerkExamEventOverview'; -import { clerkEnrollmentDetailsSelector } from 'redux/selectors/clerkEnrollmentDetails'; -import { clerkExamEventOverviewSelector } from 'redux/selectors/clerkExamEventOverview'; +import { clerkEnrollmentAppointmentSelector } from 'redux/selectors/clerkEnrollmentAppointment'; import { EnrollmentUtils } from 'utils/enrollment'; export const ClerkEnrollmentAppointmentDetails = ({ @@ -36,11 +34,8 @@ export const ClerkEnrollmentAppointmentDetails = ({ }) => { // Redux const dispatch = useAppDispatch(); - const { status, paymentRefundStatus } = useAppSelector( - clerkEnrollmentDetailsSelector, - ); - const { clerkEnrollmentChangeStatus } = useAppSelector( - clerkExamEventOverviewSelector, + const { status, updateStatus, sendLinkStatus } = useAppSelector( + clerkEnrollmentAppointmentSelector, ); const { showToast } = useToast(); @@ -71,8 +66,7 @@ export const ClerkEnrollmentAppointmentDetails = ({ const isLoading = status === APIResponseStatus.InProgress; const resetToInitialState = useCallback(() => { - dispatch(resetClerkEnrollmentDetailsUpdate()); - dispatch(resetClerkEnrollmentStatusChange()); + dispatch(resetClerkEnrollmentDetails()); resetLocalEnrollmentDetails(); setHasLocalChanges(false); setCurrentUIMode(UIMode.View); @@ -82,14 +76,10 @@ export const ClerkEnrollmentAppointmentDetails = ({ useEffect(() => { if ( - (status === APIResponseStatus.Success && currentUIMode === UIMode.Edit) || - clerkEnrollmentChangeStatus === APIResponseStatus.Success || - paymentRefundStatus === APIResponseStatus.Success + updateStatus === APIResponseStatus.Success && + currentUIMode === UIMode.Edit ) { - const description = - clerkEnrollmentChangeStatus === APIResponseStatus.Success - ? t('toasts.enrollmentCanceled') - : t('toasts.updated'); + const description = t('toasts.updated'); showToast({ severity: Severity.Success, @@ -97,15 +87,18 @@ export const ClerkEnrollmentAppointmentDetails = ({ }); resetToInitialState(); } - }, [ - currentUIMode, - showToast, - resetToInitialState, - t, - status, - clerkEnrollmentChangeStatus, - paymentRefundStatus, - ]); + }, [currentUIMode, showToast, resetToInitialState, t, updateStatus]); + + useEffect(() => { + if (sendLinkStatus === APIResponseStatus.Success) { + const description = t('toasts.updated'); + + showToast({ + severity: Severity.Success, + description, + }); + } + }, [currentUIMode, showToast, t, sendLinkStatus]); if (!enrollmentDetails) { return null; diff --git a/frontend/packages/vkt/src/components/clerkEnrollment/appointment/ClerkEnrollmentAppointmentDetailsFields.tsx b/frontend/packages/vkt/src/components/clerkEnrollment/appointment/ClerkEnrollmentAppointmentDetailsFields.tsx index b8b8b0643..bb878b594 100644 --- a/frontend/packages/vkt/src/components/clerkEnrollment/appointment/ClerkEnrollmentAppointmentDetailsFields.tsx +++ b/frontend/packages/vkt/src/components/clerkEnrollment/appointment/ClerkEnrollmentAppointmentDetailsFields.tsx @@ -3,6 +3,7 @@ import { Divider, FormControlLabel, FormHelperTextProps, + Link, } from '@mui/material'; import { ChangeEvent, Fragment, useEffect, useState } from 'react'; import { @@ -18,12 +19,10 @@ import { import { APIResponseStatus, Color, - Severity, TextFieldTypes, TextFieldVariant, Variant, } from 'shared/enums'; -import { useDialog } from 'shared/hooks'; import { InputFieldUtils } from 'shared/utils'; import { @@ -33,11 +32,7 @@ import { useKoodistoMunicipalitiesTranslation, } from 'configs/i18n'; import { useAppDispatch, useAppSelector } from 'configs/redux'; -import { - EnrollmentAppointmentStatus, - ExamGrades, - PaymentStatus, -} from 'enums/app'; +import { EnrollmentAppointmentStatus, ExamGrades } from 'enums/app'; import { ClerkEnrollmentTextFieldEnum } from 'enums/clerkEnrollment'; import { ClerkEnrollmentAppointment, @@ -50,14 +45,10 @@ import { PartialExamsAndSkills } from 'interfaces/common/enrollment'; import { ExaminerExamEvent } from 'interfaces/examinerExamEvent'; import { resetClerkEnrollmentAppointmentGrades, + sendClerkEnrollmentAppointmentAuthLink, upsertClerkEnrollmentAppointmentGrades, } from 'redux/reducers/clerkEnrollmentAppointment'; -import { - createClerkEnrollmentPaymentLink, - setClerkPaymentRefunded, -} from 'redux/reducers/clerkEnrollmentDetails'; import { clerkEnrollmentAppointmentSelector } from 'redux/selectors/clerkEnrollmentAppointment'; -import { clerkEnrollmentDetailsSelector } from 'redux/selectors/clerkEnrollmentDetails'; import { DateTimeUtils } from 'utils/dateTime'; const CheckboxField = ({ @@ -240,43 +231,16 @@ const PaymentDetails = ({ payment }: { payment: ClerkPayment }) => { const { t } = useClerkTranslation({ keyPrefix: 'vkt.component.clerkEnrollmentDetails', }); - const translateCommon = useCommonTranslation(); - - const { showDialog } = useDialog(); - const dispatch = useAppDispatch(); - const refundLoadingStatus = useAppSelector( - clerkEnrollmentDetailsSelector, - ).paymentRefundStatus; const formatAmount = (amount: number) => { return (amount / 100).toFixed(2); }; - const handleSetRefundedButtonClick = (paymentId: number) => { - showDialog({ - title: t('payment.refundDialog.header'), - severity: Severity.Info, - description: t('payment.refundDialog.description'), - actions: [ - { - title: translateCommon('back'), - variant: Variant.Outlined, - }, - { - title: translateCommon('yes'), - variant: Variant.Contained, - action: () => dispatch(setClerkPaymentRefunded(paymentId)), - }, - ], - }); - }; - return (
{t('payment.details.status')}:{' '} {t(`paymentStatus.${payment.status}`)} - {payment.refundedAt && ` (${t('payment.details.refunded')})`} {t('payment.details.reference')}: {payment.transactionId} @@ -289,26 +253,6 @@ const PaymentDetails = ({ payment }: { payment: ClerkPayment }) => { {t('payment.details.amount')}:{' '} {formatAmount(payment.amount)} € - {payment.refundedAt ? ( - - {t('payment.details.refunded')}:{' '} - {DateTimeUtils.renderDate(payment.refundedAt)} - - ) : ( - payment.status === PaymentStatus.OK && ( -
- - {t('payment.setRefunded')} - -
- ) - )}
); }; @@ -632,10 +576,8 @@ export const ClerkEnrollmentAppointmentDetailsFields = ({ }); const translateMunicipality = useKoodistoMunicipalitiesTranslation(); const translateCommon = useCommonTranslation(); + const paymentLink = enrollment.paymentLinkUrl; const dispatch = useAppDispatch(); - const paymentLink = useAppSelector( - clerkEnrollmentDetailsSelector, - ).paymentLink; const [paymentLinkModalOpen, setPaymentLinkModalOpen] = useState(false); const [gradeModalOpen, setGradeModalOpen] = useState(false); @@ -684,6 +626,15 @@ export const ClerkEnrollmentAppointmentDetailsFields = ({ label: examEvent.location ?? '', }); + const onSendAuthLink = () => { + dispatch( + sendClerkEnrollmentAppointmentAuthLink({ + enrollmentId: enrollment.id, + oid: oid, + }), + ); + }; + // TODO Remove this flag once digital certificates are available return (
@@ -833,21 +784,6 @@ export const ClerkEnrollmentAppointmentDetailsFields = ({ {enrollment.payments.length > 0 && ( )} - {enrollment.status === - EnrollmentAppointmentStatus.AWAITING_PAYMENT && ( -
- { - setPaymentLinkModalOpen(true); - dispatch(createClerkEnrollmentPaymentLink(enrollment.id)); - }} - > - {t('payment.create')} - -
- )}
{displayPaymentHistory && (
@@ -883,13 +819,23 @@ export const ClerkEnrollmentAppointmentDetailsFields = ({
Lähetä ilmoittautumislinkki
+ + { + setPaymentLinkModalOpen(true); + }} + > + Ei mahdollisuutta tunnistautua? + + {gradeModalOpen && ( {paymentLink && (
+ + Jos asiakkaalla ei ole mahdollisuutta käyttää vahvaa + tunnistautumista, lähetä tämä suora maksulinkki. +

{t('payment.modal.link')}

- {paymentLink.url} -
-
-

{t('payment.modal.expires')}

- {DateTimeUtils.renderDateTime(paymentLink.expiresAt)} +
{paymentLink}
diff --git a/frontend/packages/vkt/src/interfaces/clerkEnrollment.ts b/frontend/packages/vkt/src/interfaces/clerkEnrollment.ts index a58cbb862..90a53faf7 100644 --- a/frontend/packages/vkt/src/interfaces/clerkEnrollment.ts +++ b/frontend/packages/vkt/src/interfaces/clerkEnrollment.ts @@ -109,6 +109,7 @@ export interface ClerkEnrollmentAppointment extends ClerkEnrollmentContact { payments: Array; person?: ClerkPerson; authLink?: ClerkAuthLink; + paymentLinkUrl?: string; examEvent?: ExaminerExamEvent; } diff --git a/frontend/packages/vkt/src/redux/reducers/clerkEnrollmentAppointment.ts b/frontend/packages/vkt/src/redux/reducers/clerkEnrollmentAppointment.ts index 0a6c5a0db..8bd425de2 100644 --- a/frontend/packages/vkt/src/redux/reducers/clerkEnrollmentAppointment.ts +++ b/frontend/packages/vkt/src/redux/reducers/clerkEnrollmentAppointment.ts @@ -9,21 +9,23 @@ import { ExaminerExamEvent } from 'interfaces/examinerExamEvent'; interface ClerkEnrollmentAppointmentState { status: APIResponseStatus; + updateStatus: APIResponseStatus; enrollment?: ClerkEnrollmentAppointment; - createStatus: APIResponseStatus; gradesStatus: APIResponseStatus; gradesSaveStatus: APIResponseStatus; examEventsStatus: APIResponseStatus; + sendLinkStatus: APIResponseStatus; examEvents: Array; grades?: ClerkEnrollmentAppointmentGrades; } const initialState: ClerkEnrollmentAppointmentState = { status: APIResponseStatus.NotStarted, - createStatus: APIResponseStatus.NotStarted, + updateStatus: APIResponseStatus.NotStarted, gradesStatus: APIResponseStatus.NotStarted, gradesSaveStatus: APIResponseStatus.NotStarted, examEventsStatus: APIResponseStatus.NotStarted, + sendLinkStatus: APIResponseStatus.NotStarted, examEvents: [], grades: { version: 0, @@ -93,10 +95,29 @@ const clerkEnrollmentAppointmentSlice = createSlice({ oid: string; }>, ) { - state.status = APIResponseStatus.InProgress; + state.updateStatus = APIResponseStatus.InProgress; + }, + storeUpdateClerkEnrollmentAppointment(state) { + state.updateStatus = APIResponseStatus.Success; }, - resetClerkEnrollmentDetailsUpdate(state) { + resetClerkEnrollmentDetails(state) { + state.updateStatus = initialState.updateStatus; state.status = initialState.status; + state.gradesSaveStatus = initialState.gradesSaveStatus; + state.examEventsStatus = initialState.examEventsStatus; + state.sendLinkStatus = initialState.sendLinkStatus; + }, + sendClerkEnrollmentAppointmentAuthLink( + state, + _action: PayloadAction<{ + enrollmentId: number; + oid: string; + }>, + ) { + state.sendLinkStatus = APIResponseStatus.InProgress; + }, + storeClerkEnrollmentAppointmentAuthLink(state) { + state.sendLinkStatus = APIResponseStatus.Success; }, loadClerkEnrollmentAppointmentGrades( state, @@ -147,10 +168,13 @@ export const { storeClerkEnrollmentAppointment, loadClerkEnrollmentAppointment, updateClerkEnrollmentAppointment, - resetClerkEnrollmentDetailsUpdate, + resetClerkEnrollmentDetails, upsertClerkEnrollmentAppointmentGrades, storeClerkEnrollmentAppointmentGrades, resetClerkEnrollmentAppointmentGrades, loadClerkEnrollmentAppointmentGrades, storeClerkEnrollmentAppointmentGradesUpsert, + sendClerkEnrollmentAppointmentAuthLink, + storeClerkEnrollmentAppointmentAuthLink, + storeUpdateClerkEnrollmentAppointment, } = clerkEnrollmentAppointmentSlice.actions; diff --git a/frontend/packages/vkt/src/redux/sagas/clerkEnrollmentAppointment.ts b/frontend/packages/vkt/src/redux/sagas/clerkEnrollmentAppointment.ts index b4512aa66..6b69206ce 100644 --- a/frontend/packages/vkt/src/redux/sagas/clerkEnrollmentAppointment.ts +++ b/frontend/packages/vkt/src/redux/sagas/clerkEnrollmentAppointment.ts @@ -16,11 +16,14 @@ import { loadClerkEnrollmentAppointmentGrades, loadExaminerExamEvents, rejectClerkEnrollmentAppointment, + sendClerkEnrollmentAppointmentAuthLink, storeClerkEnrollmentAppointment, + storeClerkEnrollmentAppointmentAuthLink, storeClerkEnrollmentAppointmentGrades, storeClerkEnrollmentAppointmentGradesUpsert, storeClerkEnrollmentAppointmentUpdate, storeExaminerExamEvents, + storeUpdateClerkEnrollmentAppointment, updateClerkEnrollmentAppointment, upsertClerkEnrollmentAppointmentGrades, } from 'redux/reducers/clerkEnrollmentAppointment'; @@ -95,6 +98,7 @@ function* updateClerkEnrollmentAppointmentSaga( apiResponse.data, ); + yield put(storeUpdateClerkEnrollmentAppointment()); yield put(storeClerkEnrollmentAppointmentUpdate(updatedEnrollment)); } catch (error) { const errorMessage = NotifierUtils.getAPIErrorMessage(error as AxiosError); @@ -150,6 +154,32 @@ function* loadClerkEnrollmentAppointmentGradesSaga( } } +function* sendClerkEnrollmentAppointmentAuthLinkSaga( + action: PayloadAction<{ + enrollmentId: number; + oid: string; + }>, +) { + try { + const { enrollmentId, oid } = action.payload; + const sendUrl = `${APIEndpoints.ExaminerEnrollmentAppointment.replace( + /:oid/, + oid, + )}/${enrollmentId}/sendAuthLink`; + + const response: AxiosResponse = + yield call(axiosInstance.post, sendUrl); + const enrollment = SerializationUtils.deserializeClerkEnrollmentAppointment( + response.data, + ); + + yield put(storeClerkEnrollmentAppointmentAuthLink()); + yield put(storeClerkEnrollmentAppointment(enrollment)); + } catch (error) { + //yield put(rejectClerkEnrollmentAppointment()); + } +} + function* loadExaminerExamEventsSaga(action: PayloadAction) { try { const oid = action.payload; @@ -181,6 +211,11 @@ export function* watchClerkEnrollmentAppointment() { loadClerkEnrollmentAppointmentGrades.type, loadClerkEnrollmentAppointmentGradesSaga, ); + yield takeLatest( + sendClerkEnrollmentAppointmentAuthLink.type, + sendClerkEnrollmentAppointmentAuthLinkSaga, + ); + yield takeLatest( upsertClerkEnrollmentAppointmentGrades.type, upsertClerkEnrollmentAppointmentGradesSaga,