Skip to content

Commit 82b3cea

Browse files
committed
feat(notifications): add best time to join notifications
- Add notifyOnBestTime field to NotificationPreference entity - Create scheduler to check queues hourly and send push notifications when waiting count < 3 - Respect per‑queue preferences and 24‑hour cooldown - Add frontend checkbox in preference modals - Include unit tests for scheduler logic
1 parent a30e26a commit 82b3cea

10 files changed

Lines changed: 1149 additions & 58 deletions

File tree

backend/backend/src/main/java/com/queueless/backend/controller/NotificationPreferenceController.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ public ResponseEntity<NotificationPreferenceDTO> updatePreference(
6161
dto.getNotifyBeforeMinutes(),
6262
dto.getNotifyOnStatusChange(),
6363
dto.getNotifyOnEmergencyApproval(),
64-
dto.getEnabled()
64+
dto.getEnabled(),
65+
dto.getNotifyOnBestTime()
6566
);
6667
return ResponseEntity.ok(NotificationPreferenceDTO.fromEntity(saved));
6768
}

backend/backend/src/main/java/com/queueless/backend/dto/NotificationPreferenceDTO.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public class NotificationPreferenceDTO {
1818
private Boolean notifyOnStatusChange;
1919
private Boolean notifyOnEmergencyApproval;
2020
private Boolean enabled;
21-
21+
private Boolean notifyOnBestTime;
2222
public static NotificationPreferenceDTO fromEntity(NotificationPreference pref) {
2323
NotificationPreferenceDTO dto = new NotificationPreferenceDTO();
2424
dto.setUserId(pref.getUserId());
@@ -27,6 +27,7 @@ public static NotificationPreferenceDTO fromEntity(NotificationPreference pref)
2727
dto.setNotifyOnStatusChange(pref.getNotifyOnStatusChange());
2828
dto.setNotifyOnEmergencyApproval(pref.getNotifyOnEmergencyApproval());
2929
dto.setEnabled(pref.getEnabled());
30+
dto.setNotifyOnBestTime(pref.getNotifyOnBestTime() != null ? pref.getNotifyOnBestTime() : false);
3031
return dto;
3132
}
3233
}

backend/backend/src/main/java/com/queueless/backend/model/NotificationPreference.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,7 @@ public class NotificationPreference {
3636

3737
private LocalDateTime createdAt;
3838
private LocalDateTime updatedAt;
39+
private Boolean notifyOnBestTime;
40+
// Add this field
41+
private LocalDateTime lastBestTimeNotificationSent;
3942
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package com.queueless.backend.scheduler;
2+
3+
import com.queueless.backend.model.NotificationPreference;
4+
import com.queueless.backend.model.Queue;
5+
import com.queueless.backend.model.User;
6+
import com.queueless.backend.repository.NotificationPreferenceRepository;
7+
import com.queueless.backend.repository.QueueRepository;
8+
import com.queueless.backend.repository.UserRepository;
9+
import com.queueless.backend.service.FcmService;
10+
import lombok.RequiredArgsConstructor;
11+
import lombok.extern.slf4j.Slf4j;
12+
import org.springframework.scheduling.annotation.Scheduled;
13+
import org.springframework.stereotype.Component;
14+
15+
import java.time.LocalDateTime;
16+
import java.util.List;
17+
import java.util.Map;
18+
import java.util.stream.Collectors;
19+
20+
@Slf4j
21+
@Component
22+
@RequiredArgsConstructor
23+
public class BestTimeNotificationScheduler {
24+
25+
private final QueueRepository queueRepository;
26+
private final NotificationPreferenceRepository preferenceRepository;
27+
private final UserRepository userRepository;
28+
private final FcmService fcmService;
29+
30+
// Threshold for "best time" – waiting tokens count below which we notify
31+
private static final int BEST_TIME_THRESHOLD = 3;
32+
33+
@Scheduled(cron = "0 0 * * * *") // every hour at minute 0
34+
public void checkBestTimeNotifications() {
35+
log.info("Starting best time notification check...");
36+
List<Queue> activeQueues = queueRepository.findByIsActive(true);
37+
38+
for (Queue queue : activeQueues) {
39+
try {
40+
processQueue(queue);
41+
} catch (Exception e) {
42+
log.error("Error processing queue {} for best time notifications", queue.getId(), e);
43+
}
44+
}
45+
}
46+
47+
private void processQueue(Queue queue) {
48+
// Get current waiting count
49+
long waitingCount = queue.getTokens().stream()
50+
.filter(t -> "WAITING".equals(t.getStatus()))
51+
.count();
52+
53+
// If waiting count is above threshold, nothing to do
54+
if (waitingCount >= BEST_TIME_THRESHOLD) {
55+
return;
56+
}
57+
58+
// Get all preferences for this queue where notifyOnBestTime = true
59+
List<NotificationPreference> preferences = preferenceRepository.findByQueueId(queue.getId()).stream()
60+
.filter(p -> Boolean.TRUE.equals(p.getNotifyOnBestTime()))
61+
.filter(p -> Boolean.TRUE.equals(p.getEnabled())) // only if preferences are enabled
62+
.collect(Collectors.toList());
63+
64+
if (preferences.isEmpty()) {
65+
return;
66+
}
67+
68+
// Group by user for efficient user lookup
69+
Map<String, NotificationPreference> prefByUserId = preferences.stream()
70+
.collect(Collectors.toMap(NotificationPreference::getUserId, p -> p));
71+
72+
// Fetch all users at once
73+
List<User> users = userRepository.findAllById(prefByUserId.keySet());
74+
LocalDateTime now = LocalDateTime.now();
75+
76+
for (User user : users) {
77+
NotificationPreference pref = prefByUserId.get(user.getId());
78+
79+
// Avoid sending too often: check last sent time (e.g., at least 24 hours apart)
80+
if (pref.getLastBestTimeNotificationSent() != null &&
81+
pref.getLastBestTimeNotificationSent().isAfter(now.minusHours(24))) {
82+
continue;
83+
}
84+
85+
// Send push notification if user has FCM tokens
86+
if (user.getFcmTokens() != null && !user.getFcmTokens().isEmpty()) {
87+
String title = "Queue is now short!";
88+
String body = String.format("The queue for %s currently has only %d people waiting. Great time to join!",
89+
queue.getServiceName(), waitingCount);
90+
91+
fcmService.sendMulticast(user.getFcmTokens(), title, body, queue.getId());
92+
93+
// Update last sent time
94+
pref.setLastBestTimeNotificationSent(now);
95+
preferenceRepository.save(pref);
96+
97+
log.info("Best time notification sent to user {} for queue {} (waiting {})",
98+
user.getId(), queue.getId(), waitingCount);
99+
}
100+
}
101+
}
102+
}

backend/backend/src/main/java/com/queueless/backend/service/NotificationPreferenceService.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ public NotificationPreference createOrUpdatePreference(String userId, String que
2828
Integer notifyBeforeMinutes,
2929
Boolean notifyOnStatusChange,
3030
Boolean notifyOnEmergencyApproval,
31-
Boolean enabled) {
31+
Boolean enabled,
32+
Boolean notifyOnBestTime) {
3233
// Validate user and queue exist
3334
userService.getUserById(userId);
3435
queueService.getQueueById(queueId);
@@ -42,6 +43,7 @@ public NotificationPreference createOrUpdatePreference(String userId, String que
4243
preference.setNotifyOnStatusChange(notifyOnStatusChange);
4344
preference.setNotifyOnEmergencyApproval(notifyOnEmergencyApproval);
4445
preference.setEnabled(enabled != null ? enabled : true);
46+
preference.setNotifyOnBestTime(notifyOnBestTime != null ? notifyOnBestTime : false);
4547

4648
if (preference.getCreatedAt() == null) {
4749
preference.setCreatedAt(LocalDateTime.now());

0 commit comments

Comments
 (0)