feat(sprint7): Phase 1 — notifications enhancement + push infrastructure
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
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
package de.cannamanage.domain.entity;
|
||||
|
||||
import de.cannamanage.domain.enums.DevicePlatform;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Push notification device token — stores Web Push subscriptions and mobile push tokens.
|
||||
* A user can have multiple device tokens (multi-device support, max 10 per user).
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "device_tokens", uniqueConstraints = {
|
||||
@UniqueConstraint(columnNames = {"user_id", "token"})
|
||||
})
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class DeviceToken extends AbstractTenantEntity {
|
||||
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private UUID userId;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "platform", nullable = false, length = 20)
|
||||
private DevicePlatform platform;
|
||||
|
||||
@Column(name = "token", nullable = false, columnDefinition = "TEXT")
|
||||
private String token;
|
||||
|
||||
@Column(name = "device_name", length = 100)
|
||||
private String deviceName;
|
||||
|
||||
@Column(name = "last_used_at")
|
||||
private Instant lastUsedAt;
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package de.cannamanage.domain.entity;
|
||||
|
||||
import de.cannamanage.domain.enums.NotificationChannel;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Per-user notification channel preference.
|
||||
* Controls which delivery channels are enabled for a user.
|
||||
* IN_APP is always enabled and cannot be disabled.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "notification_preferences", uniqueConstraints = {
|
||||
@UniqueConstraint(columnNames = {"user_id", "channel"})
|
||||
})
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class NotificationPreference extends AbstractTenantEntity {
|
||||
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private UUID userId;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "channel", nullable = false, length = 20)
|
||||
private NotificationChannel channel;
|
||||
|
||||
@Column(name = "enabled", nullable = false)
|
||||
private boolean enabled;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private Instant updatedAt;
|
||||
|
||||
@PrePersist
|
||||
@PreUpdate
|
||||
void onUpdate() {
|
||||
this.updatedAt = Instant.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package de.cannamanage.domain.entity;
|
||||
|
||||
import de.cannamanage.domain.enums.TargetType;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Tracks a notification send operation (admin broadcast or targeted send).
|
||||
* One NotificationSend creates many individual Notification records.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "notification_sends", indexes = {
|
||||
@Index(name = "idx_notification_sends_tenant", columnList = "tenant_id, sent_at DESC")
|
||||
})
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class NotificationSend extends AbstractTenantEntity {
|
||||
|
||||
@Column(name = "title", nullable = false)
|
||||
private String title;
|
||||
|
||||
@Column(name = "message", nullable = false, columnDefinition = "TEXT")
|
||||
private String message;
|
||||
|
||||
@Column(name = "link", length = 500)
|
||||
private String link;
|
||||
|
||||
@Column(name = "author_id", nullable = false)
|
||||
private UUID authorId;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "target_type", nullable = false, length = 20)
|
||||
private TargetType targetType;
|
||||
|
||||
@Column(name = "target_count", nullable = false)
|
||||
private int targetCount;
|
||||
|
||||
@Column(name = "read_count", nullable = false)
|
||||
private int readCount;
|
||||
|
||||
@Column(name = "sent_at", nullable = false)
|
||||
private Instant sentAt;
|
||||
|
||||
@PrePersist
|
||||
void onPersist() {
|
||||
if (this.sentAt == null) {
|
||||
this.sentAt = Instant.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package de.cannamanage.domain.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Join table entity: links a NotificationSend to individual recipients and their notifications.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "notification_send_recipients")
|
||||
@IdClass(NotificationSendRecipientId.class)
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class NotificationSendRecipient {
|
||||
|
||||
@Id
|
||||
@Column(name = "send_id", nullable = false)
|
||||
private UUID sendId;
|
||||
|
||||
@Id
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private UUID userId;
|
||||
|
||||
@Column(name = "notification_id")
|
||||
private UUID notificationId;
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package de.cannamanage.domain.entity;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Composite primary key for NotificationSendRecipient.
|
||||
*/
|
||||
public class NotificationSendRecipientId implements Serializable {
|
||||
|
||||
private UUID sendId;
|
||||
private UUID userId;
|
||||
|
||||
public NotificationSendRecipientId() {}
|
||||
|
||||
public NotificationSendRecipientId(UUID sendId, UUID userId) {
|
||||
this.sendId = sendId;
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
NotificationSendRecipientId that = (NotificationSendRecipientId) o;
|
||||
return Objects.equals(sendId, that.sendId) && Objects.equals(userId, that.userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(sendId, userId);
|
||||
}
|
||||
}
|
||||
@@ -40,5 +40,21 @@ public enum AuditEventType {
|
||||
SUBSCRIPTION_STARTED,
|
||||
SUBSCRIPTION_CANCELED,
|
||||
PAYMENT_RECEIVED,
|
||||
PAYMENT_FAILED
|
||||
PAYMENT_FAILED,
|
||||
|
||||
// Sprint 7 — Notification events
|
||||
NOTIFICATION_BROADCAST_SENT,
|
||||
|
||||
// Sprint 7 — Info Board events
|
||||
INFO_BOARD_POST_CREATED,
|
||||
INFO_BOARD_POST_EDITED,
|
||||
INFO_BOARD_POST_PINNED,
|
||||
INFO_BOARD_POST_ARCHIVED,
|
||||
|
||||
// Sprint 7 — Forum events
|
||||
FORUM_TOPIC_CREATED,
|
||||
FORUM_TOPIC_LOCKED,
|
||||
FORUM_TOPIC_DELETED,
|
||||
FORUM_REPLY_DELETED,
|
||||
FORUM_REPORT_RESOLVED
|
||||
}
|
||||
|
||||
@@ -6,5 +6,8 @@ package de.cannamanage.domain.enums;
|
||||
public enum ConsentType {
|
||||
DATA_PROCESSING,
|
||||
MARKETING,
|
||||
ANALYTICS
|
||||
ANALYTICS,
|
||||
// Sprint 7 — Push notification consent (GDPR Art. 7(1))
|
||||
NOTIFICATION_PUSH,
|
||||
NOTIFICATION_EMAIL
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package de.cannamanage.domain.enums;
|
||||
|
||||
/**
|
||||
* Platform types for push notification device tokens.
|
||||
*/
|
||||
public enum DevicePlatform {
|
||||
WEB,
|
||||
IOS,
|
||||
ANDROID
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package de.cannamanage.domain.enums;
|
||||
|
||||
/**
|
||||
* Delivery channels for notifications.
|
||||
* IN_APP is always enabled and cannot be disabled.
|
||||
*/
|
||||
public enum NotificationChannel {
|
||||
IN_APP,
|
||||
EMAIL,
|
||||
WEB_PUSH,
|
||||
MOBILE_PUSH
|
||||
}
|
||||
@@ -7,5 +7,10 @@ public enum NotificationType {
|
||||
QUOTA_WARNING,
|
||||
BATCH_RECALLED,
|
||||
DISTRIBUTION_RECORDED,
|
||||
SUBSCRIPTION_EXPIRING
|
||||
SUBSCRIPTION_EXPIRING,
|
||||
// Sprint 7:
|
||||
ADMIN_MESSAGE,
|
||||
INFO_BOARD_POST,
|
||||
FORUM_REPLY,
|
||||
FORUM_MENTION
|
||||
}
|
||||
|
||||
@@ -13,5 +13,9 @@ public enum StaffPermission {
|
||||
VIEW_STOCK,
|
||||
RECORD_STOCK_IN,
|
||||
VIEW_COMPLIANCE_REPORT,
|
||||
MANAGE_GROW_CALENDAR
|
||||
MANAGE_GROW_CALENDAR,
|
||||
// Sprint 7:
|
||||
SEND_NOTIFICATIONS,
|
||||
MANAGE_INFO_BOARD,
|
||||
MODERATE_FORUM
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package de.cannamanage.domain.enums;
|
||||
|
||||
/**
|
||||
* Target audience for a notification send.
|
||||
*/
|
||||
public enum TargetType {
|
||||
ALL,
|
||||
SELECTED
|
||||
}
|
||||
Reference in New Issue
Block a user