Skip to content

Commit 1a5acfe

Browse files
authored
Merge pull request #2630 from objectcomputing/feature-2474/self-review-notifications
Send self-review emails at launch, 3 days to close, and 1 day to close.
2 parents fc5ee05 + d177c08 commit 1a5acfe

File tree

11 files changed

+364
-10
lines changed

11 files changed

+364
-10
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.objectcomputing.checkins.services.email;
2+
3+
import io.micronaut.data.annotation.TypeDef;
4+
import io.micronaut.data.model.DataType;
5+
import io.swagger.v3.oas.annotations.media.Schema;
6+
import jakarta.persistence.Column;
7+
import jakarta.persistence.Entity;
8+
import jakarta.persistence.Id;
9+
import jakarta.persistence.Table;
10+
import jakarta.validation.constraints.NotNull;
11+
import lombok.AllArgsConstructor;
12+
import lombok.Getter;
13+
import lombok.Setter;
14+
15+
import java.util.Objects;
16+
17+
@Setter
18+
@Getter
19+
@Entity
20+
@AllArgsConstructor
21+
@Table(name = "automated_email_sent")
22+
public class AutomatedEmail {
23+
@Id
24+
@Column(name = "id")
25+
@TypeDef(type = DataType.STRING)
26+
@Schema(description = "id of the email")
27+
private String id;
28+
29+
@Override
30+
public boolean equals(Object o) {
31+
if (this == o) return true;
32+
if (o == null || getClass() != o.getClass()) return false;
33+
AutomatedEmail email = (AutomatedEmail) o;
34+
return id.equals(email.id);
35+
}
36+
37+
@Override
38+
public int hashCode() {
39+
return id.hashCode();
40+
}
41+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.objectcomputing.checkins.services.email;
2+
3+
import io.micronaut.data.jdbc.annotation.JdbcRepository;
4+
import io.micronaut.data.model.query.builder.sql.Dialect;
5+
import io.micronaut.data.repository.CrudRepository;
6+
import jakarta.validation.Valid;
7+
import jakarta.validation.constraints.NotNull;
8+
9+
@JdbcRepository(dialect = Dialect.POSTGRES)
10+
public interface AutomatedEmailRepository extends CrudRepository<AutomatedEmail, String> {
11+
12+
@Override
13+
<S extends AutomatedEmail> S save(@Valid @NotNull S entity);
14+
15+
}

server/src/main/java/com/objectcomputing/checkins/services/pulse/PulseServicesImpl.java

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import com.objectcomputing.checkins.services.memberprofile.MemberProfileServices;
77
import com.objectcomputing.checkins.services.settings.SettingsServices;
88
import com.objectcomputing.checkins.services.settings.Setting;
9+
import com.objectcomputing.checkins.services.email.AutomatedEmail;
10+
import com.objectcomputing.checkins.services.email.AutomatedEmailRepository;
911
import com.objectcomputing.checkins.exceptions.NotFoundException;
1012

1113
import lombok.Getter;
@@ -20,6 +22,7 @@
2022

2123
import java.util.Map;
2224
import java.util.HashMap;
25+
import java.util.Optional;
2326
import java.time.LocalDate;
2427
import java.time.DayOfWeek;
2528
import java.time.temporal.ChronoUnit;
@@ -39,7 +42,7 @@ private class Frequency {
3942
private final CheckInsConfiguration checkInsConfiguration;
4043
private final MemberProfileServices memberProfileServices;
4144
private final SettingsServices settingsServices;
42-
private final Map<String, Boolean> sent = new HashMap<String, Boolean>();
45+
private final AutomatedEmailRepository automatedEmailRepository;
4346

4447
@Inject
4548
private PulseEmail email;
@@ -57,11 +60,13 @@ public PulseServicesImpl(
5760
@Named(MailJetFactory.MJML_FORMAT) EmailSender emailSender,
5861
CheckInsConfiguration checkInsConfiguration,
5962
MemberProfileServices memberProfileServices,
60-
SettingsServices settingsServices) {
63+
SettingsServices settingsServices,
64+
AutomatedEmailRepository automatedEmailRepository) {
6165
this.emailSender = emailSender;
6266
this.checkInsConfiguration = checkInsConfiguration;
6367
this.memberProfileServices = memberProfileServices;
6468
this.settingsServices = settingsServices;
69+
this.automatedEmailRepository = automatedEmailRepository;
6570
}
6671

6772
public void sendPendingEmail(LocalDate check) {
@@ -89,16 +94,18 @@ public void sendPendingEmail(LocalDate check) {
8994
do {
9095
if (start.getDayOfMonth() == check.getDayOfMonth()) {
9196
LOG.info("Check day of month matches frequency day");
92-
final String key = new StringBuilder(start.getMonth().toString())
97+
final String key = new StringBuilder("pulse")
98+
.append(start.getMonth().toString())
9399
.append("_")
94100
.append(String.valueOf(start.getDayOfMonth()))
95101
.toString();
96-
if (sent.containsKey(key)) {
97-
LOG.info("The Pulse Email has already been sent today");
98-
} else {
102+
Optional<AutomatedEmail> sent = automatedEmailRepository.findById(key);
103+
if (sent.isEmpty()) {
99104
LOG.info("Sending Pulse Email");
100105
email.send();
101-
sent.put(key, true);
106+
automatedEmailRepository.save(new AutomatedEmail(key));
107+
} else {
108+
LOG.info("The Pulse Email has already been sent today");
102109
}
103110
break;
104111
}

server/src/main/java/com/objectcomputing/checkins/services/request_notifications/CheckServicesImpl.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.objectcomputing.checkins.services.feedback_request.FeedbackRequest;
44
import com.objectcomputing.checkins.services.feedback_request.FeedbackRequestRepository;
55
import com.objectcomputing.checkins.services.feedback_request.FeedbackRequestServicesImpl;
6+
import com.objectcomputing.checkins.services.reviews.ReviewPeriodServices;
67
import com.objectcomputing.checkins.services.pulse.PulseServices;
78
import jakarta.inject.Singleton;
89
import org.slf4j.Logger;
@@ -18,13 +19,16 @@ public class CheckServicesImpl implements CheckServices {
1819
private final FeedbackRequestServicesImpl feedbackRequestServices;
1920
private final FeedbackRequestRepository feedbackRequestRepository;
2021
private final PulseServices pulseServices;
22+
private final ReviewPeriodServices reviewPeriodServices;
2123

2224
public CheckServicesImpl(FeedbackRequestServicesImpl feedbackRequestServices,
2325
FeedbackRequestRepository feedbackRequestRepository,
24-
PulseServices pulseServices) {
26+
PulseServices pulseServices,
27+
ReviewPeriodServices reviewPeriodServices) {
2528
this.feedbackRequestServices = feedbackRequestServices;
2629
this.feedbackRequestRepository = feedbackRequestRepository;
2730
this.pulseServices = pulseServices;
31+
this.reviewPeriodServices = reviewPeriodServices;
2832
}
2933

3034
@Override
@@ -38,6 +42,7 @@ public boolean sendScheduledEmails() {
3842
feedbackRequestRepository.update(req);
3943
}
4044
pulseServices.sendPendingEmail(today);
45+
reviewPeriodServices.sendNotifications(today);
4146
return true;
4247
}
4348

server/src/main/java/com/objectcomputing/checkins/services/reviews/ReviewPeriodServices.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.objectcomputing.checkins.services.reviews;
22

3+
import java.time.LocalDate;
34
import java.util.Set;
45
import java.util.UUID;
56

@@ -14,4 +15,6 @@ public interface ReviewPeriodServices {
1415
Set<ReviewPeriod> findByValue(String name, ReviewStatus reviewStatus);
1516

1617
void delete(UUID id);
18+
19+
void sendNotifications(LocalDate today);
1720
}

server/src/main/java/com/objectcomputing/checkins/services/reviews/ReviewPeriodServicesImpl.java

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@
77
import com.objectcomputing.checkins.notifications.email.EmailSender;
88
import com.objectcomputing.checkins.notifications.email.MailJetFactory;
99
import com.objectcomputing.checkins.services.feedback_request.FeedbackRequestServices;
10+
import com.objectcomputing.checkins.services.feedback_request.FeedbackRequestRepository;
1011
import com.objectcomputing.checkins.services.feedback_request.FeedbackRequest;
1112
import com.objectcomputing.checkins.services.memberprofile.MemberProfileRepository;
1213
import com.objectcomputing.checkins.services.memberprofile.MemberProfile;
14+
import com.objectcomputing.checkins.services.email.AutomatedEmail;
15+
import com.objectcomputing.checkins.services.email.AutomatedEmailRepository;
1316
import io.micronaut.context.annotation.Value;
1417
import io.micronaut.context.env.Environment;
1518
import io.micronaut.core.io.Readable;
@@ -28,43 +31,56 @@
2831
import java.util.Set;
2932
import java.util.UUID;
3033
import java.util.stream.Collectors;
34+
import java.util.Map;
35+
import java.util.HashMap;
3136
import java.io.BufferedReader;
37+
import java.time.temporal.ChronoUnit;
3238

3339
@Singleton
3440
class ReviewPeriodServicesImpl implements ReviewPeriodServices {
3541

3642
private static final Logger LOG = LoggerFactory.getLogger(ReviewPeriodServicesImpl.class);
3743

44+
private final AutomatedEmailRepository automatedEmailRepository;
3845
private final ReviewPeriodRepository reviewPeriodRepository;
3946
private final ReviewAssignmentRepository reviewAssignmentRepository;
4047
private final MemberProfileRepository memberProfileRepository;
4148
private final FeedbackRequestServices feedbackRequestServices;
49+
private final FeedbackRequestRepository feedbackRequestRepository;
4250
private final ReviewStatusTransitionValidator reviewStatusTransitionValidator;
4351
private EmailSender emailSender;
4452
private final Environment environment;
4553
private final String webAddress;
4654

55+
private enum SelfReviewDate { LAUNCH, THREE_DAYS, ONE_DAY }
56+
4757
@Value("classpath:mjml/supervisor_review_assignment.mjml")
4858
private Readable supervisorReviewAssignmentTemplate;
4959
@Value("classpath:mjml/review_period_announcement.mjml")
5060
private Readable reviewPeriodAnnouncementTemplate;
61+
@Value("classpath:mjml/self_review_reminder.mjml")
62+
private Readable selfReviewReminderTemplate;
5163

5264
ReviewPeriodServicesImpl(ReviewPeriodRepository reviewPeriodRepository,
5365
ReviewAssignmentRepository reviewAssignmentRepository,
5466
MemberProfileRepository memberProfileRepository,
5567
FeedbackRequestServices feedbackRequestServices,
68+
FeedbackRequestRepository feedbackRequestRepository,
5669
ReviewStatusTransitionValidator reviewStatusTransitionValidator,
5770
@Named(MailJetFactory.MJML_FORMAT) EmailSender emailSender,
5871
Environment environment,
59-
CheckInsConfiguration checkInsConfiguration) {
72+
CheckInsConfiguration checkInsConfiguration,
73+
AutomatedEmailRepository automatedEmailRepository) {
6074
this.reviewPeriodRepository = reviewPeriodRepository;
6175
this.reviewAssignmentRepository = reviewAssignmentRepository;
6276
this.memberProfileRepository = memberProfileRepository;
6377
this.feedbackRequestServices = feedbackRequestServices;
78+
this.feedbackRequestRepository = feedbackRequestRepository;
6479
this.reviewStatusTransitionValidator = reviewStatusTransitionValidator;
6580
this.emailSender = emailSender;
6681
this.environment = environment;
6782
this.webAddress = checkInsConfiguration.getWebAddress();
83+
this.automatedEmailRepository = automatedEmailRepository;
6884
}
6985

7086
void setEmailSender(EmailSender emailSender) {
@@ -346,4 +362,113 @@ private void createReviewRequest(ReviewPeriod period,
346362
LOG.error(ex.toString());
347363
}
348364
}
365+
366+
public void sendNotifications(LocalDate today) {
367+
List<ReviewPeriod> openPeriods =
368+
reviewPeriodRepository.findByReviewStatus(ReviewStatus.OPEN);
369+
for(ReviewPeriod openPeriod : openPeriods) {
370+
for(SelfReviewDate date : SelfReviewDate.values()) {
371+
String key = "self_review_notification" +
372+
openPeriod.getId().toString() + date.toString();
373+
Optional<AutomatedEmail> sent = automatedEmailRepository.findById(key);
374+
if (sent.isEmpty()) {
375+
LocalDateTime check;
376+
switch(date) {
377+
case SelfReviewDate.LAUNCH:
378+
check = openPeriod.getLaunchDate();
379+
break;
380+
case SelfReviewDate.THREE_DAYS:
381+
check = openPeriod.getSelfReviewCloseDate();
382+
if (check != null) {
383+
check = check.minus(3, ChronoUnit.DAYS);
384+
}
385+
break;
386+
default:
387+
case SelfReviewDate.ONE_DAY:
388+
check = openPeriod.getSelfReviewCloseDate();
389+
if (check != null) {
390+
check = check.minus(1, ChronoUnit.DAYS);
391+
}
392+
break;
393+
}
394+
395+
if (check != null) {
396+
if (today.isEqual(check.toLocalDate())) {
397+
sendSelfReviewEmail(openPeriod.getId(), date);
398+
automatedEmailRepository.save(new AutomatedEmail(key));
399+
}
400+
}
401+
}
402+
}
403+
}
404+
}
405+
406+
void sendSelfReviewEmail(UUID reviewPeriodId, SelfReviewDate date) {
407+
Optional<ReviewPeriod> reviewPeriod =
408+
reviewPeriodRepository.findById(reviewPeriodId);
409+
if (reviewPeriod.isEmpty()) {
410+
LOG.error("Unable to find review period: " + reviewPeriodId.toString());
411+
return;
412+
}
413+
414+
// Determine which subject we need to use.
415+
String subject = "";
416+
switch(date) {
417+
case SelfReviewDate.LAUNCH:
418+
subject = reviewPeriod.get().getName() + " has launched!";
419+
break;
420+
case SelfReviewDate.THREE_DAYS:
421+
subject = reviewPeriod.get().getName() +
422+
" closes in three days!";
423+
break;
424+
default:
425+
case SelfReviewDate.ONE_DAY:
426+
subject = reviewPeriod.get().getName() + " closes in one day!";
427+
break;
428+
}
429+
430+
try {
431+
// Read in the email template.
432+
String template = IOUtils.readText(
433+
new BufferedReader(
434+
selfReviewReminderTemplate.asReader()));
435+
436+
// Get the set of self-reviewer email addresses.
437+
Set<MemberProfile> recipients = new HashSet<>();
438+
String templateId = null;
439+
List<FeedbackRequest> requests =
440+
feedbackRequestRepository.findByValues(null, null, null, null,
441+
reviewPeriodId.toString(),
442+
templateId);
443+
for (FeedbackRequest request : requests) {
444+
if (request.getRecipientId().equals(request.getRequesteeId())) {
445+
Optional<MemberProfile> requesteeProfile =
446+
memberProfileRepository.findById(
447+
request.getRequesteeId());
448+
if (!requesteeProfile.isEmpty()) {
449+
recipients.add(requesteeProfile.get());
450+
}
451+
}
452+
}
453+
454+
List<String> addresses = recipients.stream()
455+
.map(p -> p.getWorkEmail()).toList();
456+
if (!addresses.isEmpty()) {
457+
// Customize the email content using the template.
458+
String content = String.format(
459+
template, webAddress,
460+
reviewPeriodId.toString(),
461+
dateAsString(reviewPeriod.get()
462+
.getSelfReviewCloseDate()),
463+
webAddress);
464+
465+
// Send out the email to everyone.
466+
emailSender.sendEmail(null, null, subject, content,
467+
addresses.toArray(
468+
new String[addresses.size()]));
469+
}
470+
} catch(Exception ex) {
471+
LOG.error("Send Self-Review Email: " + ex.toString());
472+
}
473+
}
349474
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
drop table if exists automated_email_sent;
2+
CREATE TABLE automated_email_sent(
3+
id varchar PRIMARY KEY
4+
);
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<mjml>
2+
<mj-head>
3+
<mj-title>Self-Review Reminder</mj-title>
4+
<mj-preview>Self-Reviews are due soon!</mj-preview>
5+
<mj-attributes>
6+
<mj-class name="preheader" color="#000000" font-size="11px" font-family="Ubuntu, Helvetica, Arial, sans-serif" padding="0px"></mj-class>
7+
</mj-attributes>
8+
</mj-head>
9+
<mj-body background-color="#e0f2ff">
10+
<mj-section background-color="#2559a7">
11+
<mj-column>
12+
<mj-image src="https://objectcomputing.com/files/6416/4277/8012/ObjectComputingLogo_version2_white.png" alt="logo" width="150px"></mj-image>
13+
</mj-column>
14+
</mj-section>
15+
<mj-hero mode="fluid-height" background-url="https://lh3.googleusercontent.com/pw/AL9nZEXvzBSrNroLHtqfW8W5_oM296XY7FPJqz15RNP3RBcf_XEkyZ0gn5JVkDCSTWA-loYTeVL5c-ycoAEOh_3dFBpPju1UmfGt7tLPCMFQdf5IVeHipmhyOV4fZnCWSl0n-b3tsHB4THfub4Mtknvz8R4t=w900-h600-no" background-color="#FFF" padding="100px 0px">
16+
<mj-text padding="20px" font-family="Helvetica" align="center" font-size="45px" line-height="45px" font-weight="900"> If you haven't already... </mj-text>
17+
<mj-button href="%s/feedback/self-reviews?period=%s" align="center"> FILL OUT YOUR<br/>SELF-REVIEW </mj-button>
18+
</mj-hero>
19+
<mj-section background-color="#ffffff">
20+
<mj-column>
21+
<mj-text>
22+
<h2>Self-Review Reminder!</h2>
23+
</mj-text>
24+
<mj-text font-size="16px">Greetings,</mj-text>
25+
<mj-text font-size="16px">This is a friendly reminder to complete your self-reviews by %s. <strong></strong></mj-text>
26+
<mj-text font-size="16px">To access your self-review click the button above, or visit <a href="%s">Check-Ins</a> and find "Self-Reviews" under the "Feedback" menu.</mj-text>
27+
</mj-column>
28+
</mj-section>
29+
<mj-section background-color="#feb672" padding="10px">
30+
<mj-column vertical-align="top" width="100%%">
31+
<mj-text align="center" color="#FFF" font-size="16px">Thank you for everything you do!</mj-text>
32+
</mj-column>
33+
</mj-section>
34+
</mj-body>
35+
</mjml>

0 commit comments

Comments
 (0)