Send a weekly digest email every Monday

Java · Spring Boot · intermediate · modification

Adds the weekly digest email. `WeeklyDigestJob` runs on Mondays at 08:00 Berlin time, works out the week's Monday from the clock and asks `UserRepository` for the opted-in users not yet recorded for that week. For each of them, `WeeklyDigestService.sendTo` loads last week's completed tasks and this week's open due tasks with two queries, sends one `SimpleMailMessage`, and then records a `digest_sends` row (unique per user and week). Each user is handled in its own try/catch, so one failure is logged and the run moves on; `sendWeek` returns a summary of sent, skipped and failed users. Includes the Flyway migration for `digest_sends` and a `@SpringBootTest` with a fixed clock.

Taskboard is the company's internal task tracker (every user is an employee working in Germany, so Berlin time is everyone's local time), on Spring Boot 3.3.5 (Java 21), Spring Data JPA (Hibernate 6.5) on PostgreSQL 16 with Flyway. It runs as a single instance. Existing and unchanged: `TaskboardApplication` with `@EnableScheduling`; the scheduler pool is set to 4 threads and is shared with two nightly jobs, and Spring schedules a cron task's next execution only after the current one returns. The `Clock` bean is `Clock.systemUTC()`. `User` has `id`, a unique `email` validated at signup, `displayName`, `active` and `weeklyDigest` (toggled on the settings page); users are deactivated, never deleted. `Task` has a nullable `assigneeId`, a single-line `title` of at most 200 characters, `status` (`TODO`, `IN_PROGRESS`, `DONE`), a nullable `dueDate` (`LocalDate`) and `completedAt` (`Instant`), which `complete()` sets when a task moves to `DONE` and `reopen()` clears. Up to 5,000 users opt in, and a user has at most a few hundred open tasks. Mail goes through the company SMTP relay via `spring-boot-starter-mail` (`JavaMailSender` from `spring.mail.*`, connection and read timeouts of 10 seconds; the relay usually accepts a message in a fraction of a second); `taskboard.mail.from` is an existing property. An operator runs a week again through the existing internal ops runner, which calls `WeeklyDigestJob.sendWeek` with that week's Monday, runs one job at a time, and is used only after that week's scheduled run has finished (out of scope); there is no automatic catch-up when the app is down at 08:00 on a Monday. Tests run as `@SpringBootTest` on embedded H2 with Flyway off and `ddl-auto=create-drop` (existing `src/test/resources/application.properties`).

Requirements

Files touched

--- src/main/java/com/acme/taskboard/user/UserRepository.java
 package com.acme.taskboard.user;
 
+import java.time.LocalDate;
+import java.util.List;
 import java.util.Optional;
 import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
 
 public interface UserRepository extends JpaRepository<User, Long> {
     Optional<User> findByEmail(String email);
+
+    @Query("""
+            select u.id from User u
+            where u.active = true and u.weeklyDigest = true
+              and not exists (
+                select d.id from DigestSend d
+                where d.userId = u.id and d.weekStart = :weekStart)
+            order by u.id""")
+    List<Long> findDigestRecipientIds(@Param("weekStart") LocalDate weekStart);
 }
 
--- src/main/java/com/acme/taskboard/task/TaskRepository.java
 package com.acme.taskboard.task;
 
+import java.time.Instant;
+import java.time.LocalDate;
 import java.util.List;
 import org.springframework.data.jpa.repository.JpaRepository;

Review this PR

Java practice