diff --git a/server/src/main/java/com/objectcomputing/checkins/services/reviews/ReviewPeriodServicesImpl.java b/server/src/main/java/com/objectcomputing/checkins/services/reviews/ReviewPeriodServicesImpl.java index 798946a404..bfce29fad3 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/reviews/ReviewPeriodServicesImpl.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/reviews/ReviewPeriodServicesImpl.java @@ -10,7 +10,10 @@ import com.objectcomputing.checkins.services.feedback_request.FeedbackRequest; import com.objectcomputing.checkins.services.memberprofile.MemberProfileRepository; import com.objectcomputing.checkins.services.memberprofile.MemberProfile; +import io.micronaut.context.annotation.Value; import io.micronaut.context.env.Environment; +import io.micronaut.core.io.Readable; +import io.micronaut.core.io.IOUtils; import jakarta.inject.Named; import jakarta.inject.Singleton; import jakarta.validation.constraints.NotNull; @@ -25,6 +28,7 @@ import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; +import java.io.BufferedReader; @Singleton class ReviewPeriodServicesImpl implements ReviewPeriodServices { @@ -40,12 +44,17 @@ class ReviewPeriodServicesImpl implements ReviewPeriodServices { private final Environment environment; private final String webAddress; + @Value("classpath:mjml/supervisor_review_assignment.mjml") + private Readable supervisorReviewAssignmentTemplate; + @Value("classpath:mjml/review_period_announcement.mjml") + private Readable reviewPeriodAnnouncementTemplate; + ReviewPeriodServicesImpl(ReviewPeriodRepository reviewPeriodRepository, ReviewAssignmentRepository reviewAssignmentRepository, MemberProfileRepository memberProfileRepository, FeedbackRequestServices feedbackRequestServices, ReviewStatusTransitionValidator reviewStatusTransitionValidator, - @Named(MailJetFactory.HTML_FORMAT) EmailSender emailSender, + @Named(MailJetFactory.MJML_FORMAT) EmailSender emailSender, Environment environment, CheckInsConfiguration checkInsConfiguration) { this.reviewPeriodRepository = reviewPeriodRepository; @@ -173,8 +182,20 @@ public ReviewPeriod update(@NotNull ReviewPeriod reviewPeriod) { " does not have a self-review template."); } + Set allInvolved = new HashSet<>(); Set selfRevieweeIds = new HashSet<>(); for (ReviewAssignment assignment : assignments) { + Optional reviewerProfile = + memberProfileRepository.findById(assignment.getReviewerId()); + if (!reviewerProfile.isEmpty()) { + allInvolved.add(reviewerProfile.get().getWorkEmail()); + } + Optional revieweeProfile = + memberProfileRepository.findById(assignment.getRevieweeId()); + if (!revieweeProfile.isEmpty()) { + allInvolved.add(revieweeProfile.get().getWorkEmail()); + } + // This person is being reviewed and will need a self-review // request. selfRevieweeIds.add(assignment.getRevieweeId()); @@ -197,11 +218,46 @@ period, findCreatorId(assignment.getReviewerId()), selfReviewCloseDate); } } + + String emailContent = constructReviewPeriodAnnouncementEmail( + period.getName(), period.getPeriodStartDate(), + period.getPeriodEndDate(), period.getLaunchDate(), + period.getSelfReviewCloseDate(), period.getCloseDate() + ); + emailSender.sendEmail(null, null, "It's time for performance reviews!", emailContent, allInvolved.toArray(new String[0])); } return period; } + private String dateAsString(LocalDateTime dateTime) { + String str = String.format("%s %d, %d", + dateTime.getMonth(), + dateTime.getDayOfMonth(), + dateTime.getYear()); + return str.substring(0, 1) + str.substring(1).toLowerCase(); + } + + private String constructReviewPeriodAnnouncementEmail( + String reviewPeriodName, LocalDateTime startDate, + LocalDateTime endDate, LocalDateTime launchDate, + LocalDateTime selfReviewDate, LocalDateTime closeDate +) { + try { + return String.format(IOUtils.readText( + new BufferedReader( + reviewPeriodAnnouncementTemplate.asReader())), + reviewPeriodName, reviewPeriodName, + dateAsString(startDate), dateAsString(endDate), + dateAsString(launchDate), + dateAsString(selfReviewDate), + dateAsString(closeDate)); + } catch(Exception ex) { + LOG.error(ex.toString()); + return ""; + } + } + private void notifyRevieweeSupervisorsByReviewPeriod(UUID reviewPeriodId, String reviewPeriodName) { if (environment.getActiveNames().contains(Environments.LOCAL)) return; Set reviewAssignments = reviewAssignmentRepository.findByReviewPeriodId(reviewPeriodId); @@ -217,14 +273,20 @@ private void notifyRevieweeSupervisorsByReviewPeriod(UUID reviewPeriodId, String List supervisorEmails = memberProfileRepository.findWorkEmailByIdIn(supervisorIdsToString); // send notification to supervisors - String emailContent = constructEmailContent(reviewPeriodId, reviewPeriodName); + String emailContent = constructSupervisorEmail(reviewPeriodId, reviewPeriodName); emailSender.sendEmail(null, null, "Review Assignments Awaiting Approval", emailContent, supervisorEmails.toArray(new String[0])); } - private String constructEmailContent (UUID reviewPeriodId, String reviewPeriodName){ - return """ -

Review Assignments for Review Period '%s' are ready for your approval.

\ - Click here to review and approve reviewer assignments in the Check-Ins app.""".formatted(reviewPeriodName, webAddress, reviewPeriodId); + private String constructSupervisorEmail(UUID reviewPeriodId, String reviewPeriodName){ + try { + return String.format(IOUtils.readText( + new BufferedReader( + supervisorReviewAssignmentTemplate.asReader())), + reviewPeriodName, webAddress, reviewPeriodId); + } catch(Exception ex) { + LOG.error(ex.toString()); + return ""; + } } private void validateDates(ReviewPeriod period) { diff --git a/server/src/main/resources/mjml/review_period_announcement.mjml b/server/src/main/resources/mjml/review_period_announcement.mjml new file mode 100644 index 0000000000..db598ea458 --- /dev/null +++ b/server/src/main/resources/mjml/review_period_announcement.mjml @@ -0,0 +1,40 @@ + + + %s + It's time for performance reviews! + + + + + + + + + + + + + It's Time for Performance Reviews! + + + + +

%s Starts Soon!

+
+ It's nearly time for our next performance reviews. This review covers the period from %s through %s + Here is what to expect: +
    +
  • %s - Self-reviews begin
  • +
  • %s - Self-reviews must be completed
  • +
  • %s - Reviews must be completed
  • +
+
+ Help us make this a valuable experience for everyone! +
+
+ + + Thank you for everything you do! + + +
diff --git a/server/src/main/resources/mjml/supervisor_review_assignment.mjml b/server/src/main/resources/mjml/supervisor_review_assignment.mjml new file mode 100644 index 0000000000..151d865571 --- /dev/null +++ b/server/src/main/resources/mjml/supervisor_review_assignment.mjml @@ -0,0 +1,13 @@ + + + + + + +

Review Assignments for Review Period '%s' are ready for your approval.

+ + Click here to review and approve reviewer assignments in the Check-Ins app. +
+
+
+
diff --git a/server/src/test/java/com/objectcomputing/checkins/services/EmailHelper.java b/server/src/test/java/com/objectcomputing/checkins/services/EmailHelper.java new file mode 100644 index 0000000000..5944400376 --- /dev/null +++ b/server/src/test/java/com/objectcomputing/checkins/services/EmailHelper.java @@ -0,0 +1,25 @@ +package com.objectcomputing.checkins.services; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +public class EmailHelper { + public static void validateEmail(String action, String fromName, + String fromAddress, String subject, + String partialBody, String recipients, + List event) { + assertEquals(6, event.size()); + assertEquals(action, event.get(0)); + assertEquals(fromName, event.get(1)); + assertEquals(fromAddress, event.get(2)); + assertEquals(subject, event.get(3)); + if (partialBody != null && !partialBody.isEmpty()) { + assertTrue(event.get(4).contains(partialBody)); + } + if (recipients != null && !recipients.isEmpty()) { + assertEquals(recipients, event.get(5)); + } + } +} diff --git a/server/src/test/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestControllerTest.java index 53e08739d3..eded286109 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestControllerTest.java @@ -14,6 +14,7 @@ import com.objectcomputing.checkins.services.memberprofile.MemberProfile; import com.objectcomputing.checkins.services.reviews.ReviewPeriod; import com.objectcomputing.checkins.services.role.RoleType; +import com.objectcomputing.checkins.services.EmailHelper; import com.objectcomputing.checkins.util.Util; import io.micronaut.context.annotation.Property; import io.micronaut.core.type.Argument; @@ -187,13 +188,13 @@ void testCreateFeedbackRequestSendsEmailNow() { //verify appropriate email was sent assertTrue(response.getBody().isPresent()); assertEquals(1, emailSender.events.size()); - validateEmail("SEND_EMAIL", - fromName, - pdlMemberProfile.getWorkEmail(), - checkInsConfiguration.getApplication().getFeedback().getRequestSubject(), - "You have received a feedback request", - recipient.getWorkEmail(), - emailSender.events.getFirst() + EmailHelper.validateEmail("SEND_EMAIL", + fromName, + pdlMemberProfile.getWorkEmail(), + checkInsConfiguration.getApplication().getFeedback().getRequestSubject(), + "You have received a feedback request", + recipient.getWorkEmail(), + emailSender.events.getFirst() ); } @@ -1049,13 +1050,13 @@ void testFeedbackRequestEnableEditsSendsEmail() { // Verify appropriate email was sent assertTrue(response.getBody().isPresent()); assertEquals(1, emailSender.events.size()); - validateEmail("SEND_EMAIL", - fromName, - pdl.getWorkEmail(), - checkInsConfiguration.getApplication().getFeedback().getRequestSubject(), - "You have received edit access to a feedback request", - recipient.getWorkEmail(), - emailSender.events.getFirst() + EmailHelper.validateEmail("SEND_EMAIL", + fromName, + pdl.getWorkEmail(), + checkInsConfiguration.getApplication().getFeedback().getRequestSubject(), + "You have received edit access to a feedback request", + recipient.getWorkEmail(), + emailSender.events.getFirst() ); } @@ -1414,18 +1415,4 @@ void testGetMultipleRequesteesBySupervisor() { assertResponseEqualsEntity(feedbackReq, response.getBody().get().get(0)); assertResponseEqualsEntity(feedbackReqTwo, response.getBody().get().get(1)); } - - private void validateEmail(String action, String fromName, - String fromAddress, String subject, - String partial, String toAddress, - List event) { - assertEquals(action, event.get(0)); - assertEquals(fromName, event.get(1)); - assertEquals(fromAddress, event.get(2)); - assertEquals(subject, event.get(3)); - if (partial != null && !partial.isEmpty()) { - assertTrue(event.get(4).contains(partial)); - } - assertEquals(toAddress, event.get(5)); - } } diff --git a/server/src/test/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestTest.java b/server/src/test/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestTest.java index e6f095e6cd..aa4420298f 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestTest.java @@ -13,6 +13,7 @@ import com.objectcomputing.checkins.services.reviews.ReviewAssignmentRepository; import com.objectcomputing.checkins.services.reviews.ReviewPeriod; import com.objectcomputing.checkins.services.reviews.ReviewPeriodRepository; +import com.objectcomputing.checkins.services.EmailHelper; import io.micronaut.context.annotation.Property; import io.micronaut.core.util.StringUtils; import io.micronaut.runtime.server.EmbeddedServer; @@ -276,10 +277,10 @@ void testSendSelfReviewCompletionEmailToReviewers() { feedbackRequestServices.sendSelfReviewCompletionEmailToReviewers(feedbackRequest, reviewAssignmentsSet); // This should equal the number of review assignments. + // The order in which emails are sent is random. We will not be + // checking the recipient. assertEquals(2, emailSender.events.size()); - validateEmail("SEND_EMAIL", "null", "null", "firstName lastName has finished their self-review for Self-Review Test.", "firstName lastName has completed their self-review", - emailSender.events.getFirst() - ); + EmailHelper.validateEmail("SEND_EMAIL", "null", "null", "firstName lastName has finished their self-review for Self-Review Test.", "firstName lastName has completed their self-review", null, emailSender.events.getFirst()); } @Test @@ -325,8 +326,7 @@ void testSendSelfReviewCompletionEmailToSupervisor() { feedbackRequestServices.sendSelfReviewCompletionEmailToSupervisor(feedbackRequest); assertEquals(1, emailSender.events.size()); - validateEmail("SEND_EMAIL", "null", "null", "firstName lastName has finished their self-review for Self-Review Test.", "firstName lastName has completed their self-review", emailSender.events.getFirst() - ); + EmailHelper.validateEmail("SEND_EMAIL", "null", "null", "firstName lastName has finished their self-review for Self-Review Test.", "firstName lastName has completed their self-review", supervisorProfile.getWorkEmail(), emailSender.events.getFirst()); } @Test @@ -393,19 +393,6 @@ void testSendSelfReviewCompletionEmailToSupervisor_EmailSenderException() { assertDoesNotThrow(() -> feedbackRequestServices.sendSelfReviewCompletionEmailToSupervisor(new FeedbackRequest())); assertEquals(1, emailSender.events.size()); - validateEmail("SEND_EMAIL", "null", "null", "firstName lastName has finished their self-review.", "firstName lastName has completed their self-review", emailSender.events.getFirst() - ); - } - - private void validateEmail(String action, String fromName, - String fromAddress, String subject, - String partial, List event) { - assertEquals(action, event.get(0)); - assertEquals(fromName, event.get(1)); - assertEquals(fromAddress, event.get(2)); - assertEquals(subject, event.get(3)); - if (partial != null && !partial.isEmpty()) { - assertTrue(event.get(4).contains(partial)); - } + EmailHelper.validateEmail("SEND_EMAIL", "null", "null", "firstName lastName has finished their self-review.", "firstName lastName has completed their self-review", supervisorProfile.getWorkEmail(), emailSender.events.getFirst()); } } diff --git a/server/src/test/java/com/objectcomputing/checkins/services/pulse/PulseServicesTest.java b/server/src/test/java/com/objectcomputing/checkins/services/pulse/PulseServicesTest.java index f6d861c7fa..f3de9f012f 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/pulse/PulseServicesTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/pulse/PulseServicesTest.java @@ -10,6 +10,7 @@ import com.objectcomputing.checkins.services.settings.SettingsServices; import com.objectcomputing.checkins.services.settings.Setting; import com.objectcomputing.checkins.services.memberprofile.MemberProfile; +import com.objectcomputing.checkins.services.EmailHelper; import io.micronaut.core.util.StringUtils; import io.micronaut.context.annotation.Property; @@ -99,11 +100,11 @@ void testBiWeeklySendEmail() { pulseServices.sendPendingEmail(biWeeklyDate); assertEquals(1, emailSender.events.size()); - validateEmail("SEND_EMAIL", "null", "null", - "Check Out the Pulse Survey!", - "Please fill out your Pulse survey, if you haven't already done so.", - recipients, - emailSender.events.getFirst()); + EmailHelper.validateEmail("SEND_EMAIL", "null", "null", + "Check Out the Pulse Survey!", + "Please fill out your Pulse survey, if you haven't already done so.", + recipients, + emailSender.events.getFirst()); } @Test @@ -114,11 +115,11 @@ void testWeeklySendEmail() { pulseServices.sendPendingEmail(weeklyDate); assertEquals(1, emailSender.events.size()); - validateEmail("SEND_EMAIL", "null", "null", - "Check Out the Pulse Survey!", - "Please fill out your Pulse survey, if you haven't already done so.", - recipients, - emailSender.events.getFirst()); + EmailHelper.validateEmail("SEND_EMAIL", "null", "null", + "Check Out the Pulse Survey!", + "Please fill out your Pulse survey, if you haven't already done so.", + recipients, + emailSender.events.getFirst()); } @Test @@ -129,11 +130,11 @@ void testMonthlySendEmail() { pulseServices.sendPendingEmail(monthlyDate); assertEquals(1, emailSender.events.size()); - validateEmail("SEND_EMAIL", "null", "null", - "Check Out the Pulse Survey!", - "Please fill out your Pulse survey, if you haven't already done so.", - recipients, - emailSender.events.getFirst()); + EmailHelper.validateEmail("SEND_EMAIL", "null", "null", + "Check Out the Pulse Survey!", + "Please fill out your Pulse survey, if you haven't already done so.", + recipients, + emailSender.events.getFirst()); } @Test @@ -161,18 +162,4 @@ void testNoSendEmail() { // This should be zero because the date is not a Monday. assertEquals(0, emailSender.events.size()); } - - private void validateEmail(String action, String fromName, - String fromAddress, String subject, - String partial, String recipients, - List event) { - assertEquals(action, event.get(0)); - assertEquals(fromName, event.get(1)); - assertEquals(fromAddress, event.get(2)); - assertEquals(subject, event.get(3)); - if (partial != null && !partial.isEmpty()) { - assertTrue(event.get(4).contains(partial)); - } - assertEquals(recipients, event.get(5)); - } } diff --git a/server/src/test/java/com/objectcomputing/checkins/services/reviews/ReviewPeriodControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/reviews/ReviewPeriodControllerTest.java index 29721db685..135f0e184a 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/reviews/ReviewPeriodControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/reviews/ReviewPeriodControllerTest.java @@ -12,8 +12,10 @@ import com.objectcomputing.checkins.services.fixture.ReviewPeriodFixture; import com.objectcomputing.checkins.services.fixture.RoleFixture; import com.objectcomputing.checkins.services.memberprofile.MemberProfile; +import com.objectcomputing.checkins.services.memberprofile.MemberProfileUtils; import com.objectcomputing.checkins.services.feedback_request.FeedbackRequest; import com.objectcomputing.checkins.services.feedback_template.FeedbackTemplate; +import com.objectcomputing.checkins.services.EmailHelper; import com.objectcomputing.checkins.exceptions.BadArgException; import io.micronaut.context.annotation.Property; import io.micronaut.core.type.Argument; @@ -65,7 +67,7 @@ class ReviewPeriodControllerTest private HttpClient client; @Inject - @Named(MailJetFactory.HTML_FORMAT) + @Named(MailJetFactory.MJML_FORMAT) private MailJetFactoryReplacement.MockEmailSender emailSender; @Inject @@ -512,9 +514,11 @@ void testPUTReviewPeriodAwaitingApproval() { // expect email has been sent assertEquals(1, emailSender.events.size()); - assertEquals( - List.of("SEND_EMAIL", "null", "null", "Review Assignments Awaiting Approval", "

Review Assignments for Review Period '" + reviewPeriod.getName() + "' are ready for your approval.

Click here to review and approve reviewer assignments in the Check-Ins app.", supervisor.getWorkEmail()), - emailSender.events.getFirst() + EmailHelper.validateEmail("SEND_EMAIL", "null", "null", + "Review Assignments Awaiting Approval", + "Click here to review and approve reviewer assignments in the Check-Ins app.", + supervisor.getWorkEmail(), + emailSender.events.getFirst() ); } @@ -544,6 +548,14 @@ void testPUTReviewPeriodOpen() { FeedbackRequest feedbackRequest = requests.get(0); assertEquals(member.getId(), feedbackRequest.getRequesteeId()); assertEquals(reviewPeriod.getId(), feedbackRequest.getReviewPeriodId()); + + assertEquals(2, emailSender.events.size()); + EmailHelper.validateEmail("SEND_EMAIL", "null", "null", + "It's time for performance reviews!", + "Help us make this a valuable experience for everyone!", + member.getWorkEmail() + "," + supervisor.getWorkEmail(), + emailSender.events.get(1) + ); } @Test