Skip to content

Commit

Permalink
Merge pull request #2628 from objectcomputing/feature-2543/review-lau…
Browse files Browse the repository at this point in the history
…nch-notification

Feature 2543/review launch notification
  • Loading branch information
mkimberlin authored Oct 11, 2024
2 parents 853d207 + 839846c commit 5fcb464
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 86 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand All @@ -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;
Expand Down Expand Up @@ -173,8 +182,20 @@ public ReviewPeriod update(@NotNull ReviewPeriod reviewPeriod) {
" does not have a self-review template.");
}

Set<String> allInvolved = new HashSet<>();
Set<UUID> selfRevieweeIds = new HashSet<>();
for (ReviewAssignment assignment : assignments) {
Optional<MemberProfile> reviewerProfile =
memberProfileRepository.findById(assignment.getReviewerId());
if (!reviewerProfile.isEmpty()) {
allInvolved.add(reviewerProfile.get().getWorkEmail());
}
Optional<MemberProfile> 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());
Expand All @@ -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<ReviewAssignment> reviewAssignments = reviewAssignmentRepository.findByReviewPeriodId(reviewPeriodId);
Expand All @@ -217,14 +273,20 @@ private void notifyRevieweeSupervisorsByReviewPeriod(UUID reviewPeriodId, String
List<String> 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 """
<h3>Review Assignments for Review Period '%s' are ready for your approval.</h3>\
<a href="%s/feedback/reviews?period=%s">Click here</a> 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) {
Expand Down
40 changes: 40 additions & 0 deletions server/src/main/resources/mjml/review_period_announcement.mjml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<mjml>
<mj-head>
<mj-title>%s</mj-title>
<mj-preview>It's time for performance reviews!</mj-preview>
<mj-attributes>
<mj-class name="preheader" color="#000000" font-size="11px" font-family="Ubuntu, Helvetica, Arial, sans-serif" padding="0px"></mj-class>
<mj-section background-color="#ffffff" padding="20px 20px">
</mj-attributes>
</mj-head>
<mj-body background-color="#e0f2ff">
<mj-section background-color="#2559a7">
<mj-column>
<mj-image src="https://objectcomputing.com/files/6416/4277/8012/ObjectComputingLogo_version2_white.png" alt="logo" width="150px"></mj-image>
</mj-column>
</mj-section>
<mj-hero mode="fluid-height" background-url="https://lh3.googleusercontent.com/pw/AL9nZEW6o0zSXFq6gKMdO2kpj_KZa8NLCnGq8bPoyiDFKimEuXOeuo1FsE8MhLP7ZTrSIUuf3zG4WuAQE2qLPzyDSqGAYaOhHOuWpWg2ocTjXLTbU5wUe-69JFoWbFU0gW3yDV7X5yfMollI5rq7N24vUs82=w1200-h600-no" background-color="#FFF" padding="100px 0px">
<mj-text padding="20px" font-family="Helvetica" align="center" font-size="45px" line-height="45px" font-weight="900">It's Time for Performance Reviews!</mj-text>
</mj-hero>
<mj-section background-color="#ffffff">
<mj-column>
<mj-text>
<h2>%s Starts Soon!</h2>
</mj-text>
<mj-text font-size="16px">It's nearly time for our next performance reviews. This review covers the period from %s through %s</mj-text>
<mj-text font-size="16px">Here is what to expect:
<ul>
<li><strong>%s</strong> - Self-reviews begin</li>
<li><strong>%s</strong> - Self-reviews must be completed</li>
<li><strong>%s</strong> - Reviews must be completed</li>
</ul>
</mj-text>
<mj-text font-size="16px">Help us make this a valuable experience for everyone!</mj-text>
</mj-column>
</mj-section>
<mj-section background-color="#feb672" padding="10px">
<mj-column vertical-align="top" width="100%%">
<mj-text align="center" color="#FFF" font-size="16px">Thank you for everything you do!</mj-text>
</mj-column>
</mj-section>
</mj-body>
13 changes: 13 additions & 0 deletions server/src/main/resources/mjml/supervisor_review_assignment.mjml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<mjml>
<mj-body>
<mj-section>
<mj-column>
<mj-divider border-color="#2559a7"></mj-divider>
<mj-text font-size="16px" font-family="'Helvetica Neue', Helvetica, Arial, sans-serif" color="#4d4c4f">
<h3>Review Assignments for Review Period '%s' are ready for your approval.</h3></mj-text>
<mj-text font-size="16px" font-family="'Helvetica Neue', Helvetica, Arial, sans-serif" color="#4d4c4f">
Click <a href="%s/feedback/reviews?period=%s">here</a> to review and approve reviewer assignments in the Check-Ins app.</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
Original file line number Diff line number Diff line change
@@ -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<String> 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));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
);
}

Expand Down Expand Up @@ -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()
);
}

Expand Down Expand Up @@ -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<String> 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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<String> 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());
}
}
Loading

0 comments on commit 5fcb464

Please sign in to comment.