706a6e257b
Phase 1 (Notification Enhancement): - Extended NotificationType enum (ADMIN_MESSAGE, INFO_BOARD_POST, FORUM_REPLY, FORUM_MENTION) - Extended StaffPermission enum (SEND_NOTIFICATIONS, MANAGE_INFO_BOARD, MODERATE_FORUM) - Extended AuditEventType with Sprint 7 events - Flyway V11: notification_sends + notification_send_recipients tables - NotificationSend + NotificationSendRecipient entities - NotificationSendRepository + NotificationSendRecipientRepository - Extended NotificationService with sendBroadcast() and sendToSelected() - NotificationComposeController (POST /compose, GET /sends) - ComposeNotificationRequest DTO Phase 1B (Push Infrastructure): - Flyway V12: device_tokens + notification_preferences tables - DeviceToken entity + DevicePlatform enum - NotificationPreference entity + NotificationChannel enum - DeviceTokenRepository + NotificationPreferenceRepository - DeviceRegistrationService (register/unregister/list devices, max 10 per user) - NotificationPreferenceService (get/create defaults, update, IN_APP always on) - NotificationDispatchService (multi-channel fan-out: WebSocket, Web Push, FCM, Email) - WebPushSender (VAPID-based, simplified for MVP) - FcmPushSender (graceful degradation if not configured) - PushPayload DTO - DeviceRegistrationController (POST/GET/DELETE /devices, GET /vapid-key) - NotificationPreferenceController (GET/PUT /preferences) - ConsentType extended (NOTIFICATION_PUSH, NOTIFICATION_EMAIL) - TargetType enum (ALL, SELECTED) Frontend: - Updated sw.js with push event handler + notification click handler - push-subscription.ts (subscribeToPush, unsubscribe, permission helpers) - notification-compose.ts service (compose, sends, devices, preferences APIs) - i18n keys (de.json + en.json) for compose, preferences, push, devices Configuration: - application-docker.properties: VAPID + FCM push config properties - MemberRepository: added findAllActiveUserIds() for broadcast
168 lines
6.3 KiB
Java
168 lines
6.3 KiB
Java
package de.cannamanage.service;
|
|
|
|
import de.cannamanage.domain.entity.Notification;
|
|
import de.cannamanage.domain.entity.NotificationSend;
|
|
import de.cannamanage.domain.entity.NotificationSendRecipient;
|
|
import de.cannamanage.domain.enums.NotificationType;
|
|
import de.cannamanage.domain.enums.TargetType;
|
|
import de.cannamanage.service.repository.MemberRepository;
|
|
import de.cannamanage.service.repository.NotificationRepository;
|
|
import de.cannamanage.service.repository.NotificationSendRepository;
|
|
import de.cannamanage.service.repository.NotificationSendRecipientRepository;
|
|
import lombok.RequiredArgsConstructor;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
|
import java.time.Instant;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.UUID;
|
|
|
|
/**
|
|
* Notification service — persists notifications and delivers in real-time via WebSocket.
|
|
* Extended in Sprint 7 with broadcast and targeted send capabilities.
|
|
*/
|
|
@Slf4j
|
|
@Service
|
|
@RequiredArgsConstructor
|
|
public class NotificationService {
|
|
|
|
private final NotificationRepository notificationRepository;
|
|
private final NotificationSendRepository notificationSendRepository;
|
|
private final NotificationSendRecipientRepository sendRecipientRepository;
|
|
private final MemberRepository memberRepository;
|
|
private final SimpMessagingTemplate messagingTemplate;
|
|
private final NotificationDispatchService dispatchService;
|
|
|
|
/**
|
|
* Send a notification: persist to DB + push via WebSocket.
|
|
*/
|
|
@Transactional
|
|
public Notification sendNotification(UUID userId, NotificationType type, String title, String message, String link) {
|
|
var notification = new Notification(userId, type, title, message, link);
|
|
notification = notificationRepository.save(notification);
|
|
|
|
// Push to user's WebSocket queue
|
|
pushToWebSocket(notification);
|
|
|
|
// Dispatch to additional channels (Web Push, FCM, Email) asynchronously
|
|
dispatchService.dispatch(notification, userId);
|
|
|
|
log.debug("Notification sent to user {}: {} - {}", userId, type, title);
|
|
return notification;
|
|
}
|
|
|
|
/**
|
|
* Send a broadcast notification to ALL active members in the current tenant.
|
|
*/
|
|
@Transactional
|
|
public NotificationSend sendBroadcast(String title, String message, String link, UUID authorId) {
|
|
// Get all active member user IDs in the current tenant
|
|
var memberUserIds = memberRepository.findAllActiveUserIds();
|
|
|
|
return sendToRecipients(title, message, link, authorId, memberUserIds, TargetType.ALL);
|
|
}
|
|
|
|
/**
|
|
* Send a targeted notification to selected members.
|
|
*/
|
|
@Transactional
|
|
public NotificationSend sendToSelected(String title, String message, String link, UUID authorId, List<UUID> recipientIds) {
|
|
return sendToRecipients(title, message, link, authorId, recipientIds, TargetType.SELECTED);
|
|
}
|
|
|
|
private NotificationSend sendToRecipients(String title, String message, String link, UUID authorId,
|
|
List<UUID> recipientIds, TargetType targetType) {
|
|
// Create the send record
|
|
var send = NotificationSend.builder()
|
|
.title(title)
|
|
.message(message)
|
|
.link(link)
|
|
.authorId(authorId)
|
|
.targetType(targetType)
|
|
.targetCount(recipientIds.size())
|
|
.readCount(0)
|
|
.sentAt(Instant.now())
|
|
.build();
|
|
send = notificationSendRepository.save(send);
|
|
|
|
// Create individual notifications for each recipient
|
|
var recipients = new ArrayList<NotificationSendRecipient>();
|
|
for (UUID userId : recipientIds) {
|
|
var notification = new Notification(userId, NotificationType.ADMIN_MESSAGE, title, message, link);
|
|
notification = notificationRepository.save(notification);
|
|
|
|
recipients.add(NotificationSendRecipient.builder()
|
|
.sendId(send.getId())
|
|
.userId(userId)
|
|
.notificationId(notification.getId())
|
|
.build());
|
|
|
|
// Push via WebSocket
|
|
pushToWebSocket(notification);
|
|
|
|
// Dispatch to push channels asynchronously
|
|
dispatchService.dispatch(notification, userId);
|
|
}
|
|
|
|
sendRecipientRepository.saveAll(recipients);
|
|
|
|
log.info("Broadcast sent: {} recipients, type={}, title='{}'", recipientIds.size(), targetType, title);
|
|
return send;
|
|
}
|
|
|
|
private void pushToWebSocket(Notification notification) {
|
|
messagingTemplate.convertAndSendToUser(
|
|
notification.getUserId().toString(),
|
|
"/queue/notifications",
|
|
Map.of(
|
|
"id", notification.getId(),
|
|
"type", notification.getType().name(),
|
|
"title", notification.getTitle(),
|
|
"message", notification.getMessage(),
|
|
"link", notification.getLink() != null ? notification.getLink() : "",
|
|
"read", notification.isRead(),
|
|
"createdAt", notification.getCreatedAt().toString()
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get the last 10 notifications for a user (unread first).
|
|
*/
|
|
@Transactional(readOnly = true)
|
|
public List<Notification> getRecentNotifications(UUID userId) {
|
|
return notificationRepository.findTop10ByUserIdOrderByReadAscCreatedAtDesc(userId);
|
|
}
|
|
|
|
/**
|
|
* Get unread count for badge display.
|
|
*/
|
|
@Transactional(readOnly = true)
|
|
public long getUnreadCount(UUID userId) {
|
|
return notificationRepository.countByUserIdAndReadFalse(userId);
|
|
}
|
|
|
|
/**
|
|
* Mark a single notification as read.
|
|
*/
|
|
@Transactional
|
|
public void markAsRead(UUID notificationId) {
|
|
notificationRepository.findById(notificationId).ifPresent(n -> {
|
|
n.setRead(true);
|
|
notificationRepository.save(n);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Mark all notifications as read for a user.
|
|
*/
|
|
@Transactional
|
|
public int markAllAsRead(UUID userId) {
|
|
return notificationRepository.markAllAsRead(userId);
|
|
}
|
|
}
|