From 6cefa302d826c1296e828693cd5e796f817a4a2b Mon Sep 17 00:00:00 2001 From: Mrigank Khandelwal Date: Sun, 18 May 2025 20:53:33 -0400 Subject: [PATCH] feat: added daily notificaiton email system --- .../com/techevents/TechEventsApplication.java | 2 + .../java/com/techevents/dto/DailyEmail.java | 31 ++++++++++ .../techevents/jobs/DailyNotificationJob.java | 59 +++++++++++++++++++ src/main/java/com/techevents/model/Event.java | 17 +++++- .../repository/EventRepository.java | 6 ++ .../service/NotificationService.java | 22 +++++++ src/main/resources/application.yml | 3 +- .../jobs/DailyNotificationJobTest.java | 53 +++++++++++++++++ src/test/resources/application.yml | 10 ++-- 9 files changed, 196 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/techevents/dto/DailyEmail.java create mode 100644 src/main/java/com/techevents/jobs/DailyNotificationJob.java create mode 100644 src/test/java/com/techevents/jobs/DailyNotificationJobTest.java diff --git a/src/main/java/com/techevents/TechEventsApplication.java b/src/main/java/com/techevents/TechEventsApplication.java index abc6b60..9058aae 100644 --- a/src/main/java/com/techevents/TechEventsApplication.java +++ b/src/main/java/com/techevents/TechEventsApplication.java @@ -2,7 +2,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; +@EnableScheduling @SpringBootApplication public class TechEventsApplication { public static void main(String[] args) { diff --git a/src/main/java/com/techevents/dto/DailyEmail.java b/src/main/java/com/techevents/dto/DailyEmail.java new file mode 100644 index 0000000..9681af1 --- /dev/null +++ b/src/main/java/com/techevents/dto/DailyEmail.java @@ -0,0 +1,31 @@ +package com.techevents.dto; + +import java.util.List; + +public class DailyEmail { + private String email; + private List eventTitles; + + public DailyEmail() {} + + public DailyEmail(String email, List eventTitles) { + this.email = email; + this.eventTitles = eventTitles; + } + + public String getEmail() { + return email; + } + + public List getEventTitles() { + return eventTitles; + } + + public void setEmail(String email) { + this.email = email; + } + + public void setEventTitles(List eventTitles) { + this.eventTitles = eventTitles; + } +} diff --git a/src/main/java/com/techevents/jobs/DailyNotificationJob.java b/src/main/java/com/techevents/jobs/DailyNotificationJob.java new file mode 100644 index 0000000..7a46d5d --- /dev/null +++ b/src/main/java/com/techevents/jobs/DailyNotificationJob.java @@ -0,0 +1,59 @@ +package com.techevents.jobs; + +import com.techevents.model.Event; +import com.techevents.model.Subscriber; +import com.techevents.repository.EventRepository; +import com.techevents.repository.SubscriberRepository; +import com.techevents.service.NotificationService; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +@Component +public class DailyNotificationJob { + + private static final Logger log = LoggerFactory.getLogger(DailyNotificationJob.class); + + private final EventRepository eventRepository; + private final SubscriberRepository subscriberRepository; + private final NotificationService notificationService; + + public DailyNotificationJob(EventRepository eventRepository, + SubscriberRepository subscriberRepository, + NotificationService notificationService) { + this.eventRepository = eventRepository; + this.subscriberRepository = subscriberRepository; + this.notificationService = notificationService; + } + + @Scheduled(cron = "0 0 20 * * *") + public void sendDailySummaries() { + LocalDateTime start = LocalDate.now().atStartOfDay(); + LocalDateTime end = start.plusDays(1).minusNanos(1); + + List events = eventRepository.findByCreatedAtBetween(start, end); + if (events.isEmpty()) { + log.info("No events created today — skipping daily notifications."); + return; + } + + List subscribers = subscriberRepository.findAll(); + log.info("Sending daily summaries to {} subscribers", subscribers.size()); + + for (Subscriber subscriber : subscribers) { + try { + notificationService.sendNotification(subscriber, events); + } catch (Exception e) { + log.error("Failed to send summary to {}", subscriber.getEmail(), e); + } + } + + log.info("Daily summary job complete"); + } +} diff --git a/src/main/java/com/techevents/model/Event.java b/src/main/java/com/techevents/model/Event.java index 71c21c2..ced1644 100644 --- a/src/main/java/com/techevents/model/Event.java +++ b/src/main/java/com/techevents/model/Event.java @@ -2,6 +2,7 @@ import jakarta.persistence.*; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.List; @Entity @@ -20,11 +21,20 @@ public class Event { private String city; + @Column(nullable = false, updatable = false) + private LocalDateTime createdAt; + @ElementCollection private List tags; + @PrePersist + protected void onCreate() { + this.createdAt = LocalDateTime.now(); + } + // Constructors - public Event() {} + public Event() { + } public Event(String title, String description, LocalDate eventDate, String city, List tags) { @@ -33,6 +43,7 @@ public Event(String title, String description, LocalDate eventDate, String city, this.eventDate = eventDate; this.city = city; this.tags = tags; + this.createdAt = LocalDateTime.now(); } public Long getId() { @@ -59,6 +70,8 @@ public List getTags() { return tags; } - + public LocalDateTime getCreatedAt() { + return createdAt; + } } diff --git a/src/main/java/com/techevents/repository/EventRepository.java b/src/main/java/com/techevents/repository/EventRepository.java index ea89153..0f2bbb3 100644 --- a/src/main/java/com/techevents/repository/EventRepository.java +++ b/src/main/java/com/techevents/repository/EventRepository.java @@ -1,10 +1,16 @@ package com.techevents.repository; import com.techevents.model.Event; + +import java.time.LocalDateTime; +import java.util.List; + import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface EventRepository extends JpaRepository { + + List findByCreatedAtBetween(LocalDateTime start, LocalDateTime end); } diff --git a/src/main/java/com/techevents/service/NotificationService.java b/src/main/java/com/techevents/service/NotificationService.java index 3e7a92a..b48b852 100644 --- a/src/main/java/com/techevents/service/NotificationService.java +++ b/src/main/java/com/techevents/service/NotificationService.java @@ -4,7 +4,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.techevents.model.NotificationMessage; import com.techevents.model.Subscriber; +import com.techevents.dto.DailyEmail; import com.techevents.model.Event; + +import java.util.List; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.amqp.rabbit.core.RabbitTemplate; @@ -24,6 +28,8 @@ public class NotificationService { @Value("${app.rabbitmq.notification-routing-key}") private String routingKey; + @Value("${app.rabbitmq.daily-summary-routing-key}") + private String summaryRoutingKey; public NotificationService(RabbitTemplate rabbitTemplate, ObjectMapper objectMapper) { this.rabbitTemplate = rabbitTemplate; @@ -41,4 +47,20 @@ public void sendNotification(Subscriber subscriber, Event event) { log.error("Failed to serialize NotificationMessage for subscriber={}", subscriber.getEmail(), e); } } + + public void sendNotification(Subscriber subscriber, List events) { + List titles = events.stream() + .map(Event::getTitle) + .toList(); + + DailyEmail summary = new DailyEmail(subscriber.getEmail(), titles); + + try { + String payload = objectMapper.writeValueAsString(summary); + rabbitTemplate.convertAndSend(exchange, summaryRoutingKey, payload); + log.info("Published daily summary for {} to RabbitMQ", subscriber.getEmail()); + } catch (JsonProcessingException e) { + log.error("Failed to serialize daily summary for {}", subscriber.getEmail(), e); + } + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 3fbafb9..59959d8 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -26,7 +26,6 @@ spring: username: guest password: guest - app: rabbitmq: queue: events.saved.queue @@ -34,6 +33,8 @@ app: routing-key: events.saved.key notification-exchange: notifications.exchange notification-routing-key: notifications.event.created + daily-summary-routing-key: notifications.event.daily-summary + server: diff --git a/src/test/java/com/techevents/jobs/DailyNotificationJobTest.java b/src/test/java/com/techevents/jobs/DailyNotificationJobTest.java new file mode 100644 index 0000000..7c806c8 --- /dev/null +++ b/src/test/java/com/techevents/jobs/DailyNotificationJobTest.java @@ -0,0 +1,53 @@ +package com.techevents.jobs; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import static org.mockito.Mockito.*; + +import java.time.LocalDate; +import java.util.List; + + +import com.techevents.model.Event; +import com.techevents.model.Subscriber; +import com.techevents.repository.EventRepository; +import com.techevents.repository.SubscriberRepository; +import com.techevents.service.NotificationService; + +@ExtendWith(MockitoExtension.class) +class DailyNotificationJobTest { + + @Mock private EventRepository eventRepository; + @Mock private SubscriberRepository subscriberRepository; + @Mock private NotificationService notificationService; + + @InjectMocks private DailyNotificationJob job; + + @Test + void whenEventsExist_thenNotifyAllSubscribers() { + Event e1 = new Event("Title", "desc", LocalDate.now(), "City", List.of("tag")); + Subscriber s1 = new Subscriber("user1@example.com"); + Subscriber s2 = new Subscriber("user2@example.com"); + + when(eventRepository.findByCreatedAtBetween(any(), any())).thenReturn(List.of(e1)); + when(subscriberRepository.findAll()).thenReturn(List.of(s1, s2)); + + job.sendDailySummaries(); + + verify(notificationService).sendNotification(s1, List.of(e1)); + verify(notificationService).sendNotification(s2, List.of(e1)); + } + + @Test + void whenNoEvents_thenSkipNotification() { + when(eventRepository.findByCreatedAtBetween(any(), any())).thenReturn(List.of()); + + job.sendDailySummaries(); + + verifyNoInteractions(subscriberRepository); + verifyNoInteractions(notificationService); + } +} diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 60508be..fc902bd 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -16,8 +16,10 @@ spring: app: rabbitmq: - queue: test.queue - exchange: test.exchange - routing-key: test.key + queue: events.saved.queue + exchange: events.saved.exchange + routing-key: events.saved.key notification-exchange: notifications.exchange - notification-routing-key: notifications.event.created \ No newline at end of file + notification-routing-key: notifications.event.created + daily-summary-routing-key: notifications.event.daily-summary +