Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/main/java/com/techevents/TechEventsApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
31 changes: 31 additions & 0 deletions src/main/java/com/techevents/dto/DailyEmail.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.techevents.dto;

import java.util.List;

public class DailyEmail {
private String email;
private List<String> eventTitles;

public DailyEmail() {}

public DailyEmail(String email, List<String> eventTitles) {
this.email = email;
this.eventTitles = eventTitles;
}

public String getEmail() {
return email;
}

public List<String> getEventTitles() {
return eventTitles;
}

public void setEmail(String email) {
this.email = email;
}

public void setEventTitles(List<String> eventTitles) {
this.eventTitles = eventTitles;
}
}
59 changes: 59 additions & 0 deletions src/main/java/com/techevents/jobs/DailyNotificationJob.java
Original file line number Diff line number Diff line change
@@ -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<Event> events = eventRepository.findByCreatedAtBetween(start, end);
if (events.isEmpty()) {
log.info("No events created today — skipping daily notifications.");
return;
}

List<Subscriber> 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");
}
}
17 changes: 15 additions & 2 deletions src/main/java/com/techevents/model/Event.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import jakarta.persistence.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;

@Entity
Expand All @@ -20,11 +21,20 @@ public class Event {

private String city;

@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;

@ElementCollection
private List<String> 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<String> tags) {
Expand All @@ -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() {
Expand All @@ -59,6 +70,8 @@ public List<String> getTags() {
return tags;
}


public LocalDateTime getCreatedAt() {
return createdAt;
}

}
6 changes: 6 additions & 0 deletions src/main/java/com/techevents/repository/EventRepository.java
Original file line number Diff line number Diff line change
@@ -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<Event, Long> {

List<Event> findByCreatedAtBetween(LocalDateTime start, LocalDateTime end);

}
22 changes: 22 additions & 0 deletions src/main/java/com/techevents/service/NotificationService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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<Event> events) {
List<String> 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);
}
}
}
3 changes: 2 additions & 1 deletion src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@ spring:
username: guest
password: guest


app:
rabbitmq:
queue: events.saved.queue
exchange: events.saved.exchange
routing-key: events.saved.key
notification-exchange: notifications.exchange
notification-routing-key: notifications.event.created
daily-summary-routing-key: notifications.event.daily-summary



server:
Expand Down
53 changes: 53 additions & 0 deletions src/test/java/com/techevents/jobs/DailyNotificationJobTest.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
10 changes: 6 additions & 4 deletions src/test/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
notification-routing-key: notifications.event.created
daily-summary-routing-key: notifications.event.daily-summary