NotificationMessageService.java
package com.seebie.server.service;
import com.seebie.server.AppProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.MailException;
import org.springframework.mail.MailSender;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
@EnableScheduling
@Service
public class NotificationMessageService {
    private static final Logger LOG = LoggerFactory.getLogger(NotificationMessageService.class);
    private final MailSender emailSender;
    private final NotificationRetrievalService notificationRetrievalService;
    private AppProperties.Notification notificationConfig;
    private final SimpleMailMessage emailTemplate;
    @Value("${spring.mail.username}")
    private String fromEmail;
    public NotificationMessageService(NotificationRetrievalService notificationRetrievalService, MailSender emailSender, AppProperties appProperties) {
        this.notificationRetrievalService = notificationRetrievalService;
        this.emailSender = emailSender;
        this.notificationConfig = appProperties.notification();
        emailTemplate = new SimpleMailMessage();
        emailTemplate.setFrom(fromEmail);
        emailTemplate.setSubject("Missing Sleep Log");
    }
    /**
     * As opposed to setting up a separate node just for running scheduled tasks,
     * We can safely run from the webserver in a multi-node system
     * by obtaining an exclusive read-write lock on the notification records.
     * See the other Notification related classes for more technical details.
     *
     * For example:
     * If last notification was >= 24 hours ago AND the last sleep logged was >= 30 hours ago:
     * send a notification and update the latest time, user gets an email once per day until logging something.
     *
     * Use fixedDelayMinutes so that we execute the annotated method with a fixed period
     * between the end of the last invocation and the start of the next.
     * That way we avoid overlapping executions.
     */
    @Scheduled(fixedRateString="${app.notification.scanFrequencyMinutes}", timeUnit = TimeUnit.MINUTES)
    public void runOnSchedule() {
        LOG.debug("Email notifications scan is starting...");
        var listToSend = findUsersToNotify(Instant.now());
        LOG.debug("Email notifications found "+ listToSend.size() + " users to notify");
        listToSend.stream()
                .map(this::createMessage)
                .forEach(this::sendEmail);
        LOG.debug("Email notifications complete.");
    }
    public List<NotificationRequired> findUsersToNotify(Instant now) {
        var ifNotNotifiedSince = now.minus(notificationConfig.triggerAfter().lastNotified());
        var ifNotLoggedSince = now.minus(notificationConfig.triggerAfter().sleepLog());
        return notificationRetrievalService.getUsersToNotify(ifNotNotifiedSince, ifNotLoggedSince, now);
    }
    public void sendEmail(SimpleMailMessage message) {
        try {
            LOG.debug("Email notification is going out to " + Arrays.asList(message.getTo()));
            emailSender.send(message);
        }
        catch(MailException me) {
            LOG.error("Email notification failed to send for " + Arrays.asList(message.getTo()), me);
        }
    }
    public SimpleMailMessage createMessage(NotificationRequired send) {
        String text = """
        You missed recording your last sleep session. If you record it right away you won't lose your momentum!
        FYI you can control these notifications in your user settings.
        """;
        var message = new SimpleMailMessage(emailTemplate);
        message.setTo(send.email());
        message.setText(text);
        return message;
    }
}