328 lines
8.7 KiB
Go
328 lines
8.7 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"go.uber.org/zap"
|
|
"gorm.io/gorm"
|
|
"veza-backend-api/internal/models"
|
|
)
|
|
|
|
// EnhancedNotificationService provides enhanced notification management
|
|
// BE-SVC-009: Implement notification service (enhanced version)
|
|
type EnhancedNotificationService struct {
|
|
db *gorm.DB
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// NewEnhancedNotificationService creates a new enhanced notification service
|
|
func NewEnhancedNotificationService(db *gorm.DB, logger *zap.Logger) *EnhancedNotificationService {
|
|
if logger == nil {
|
|
logger = zap.NewNop()
|
|
}
|
|
return &EnhancedNotificationService{
|
|
db: db,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// NotificationParams represents parameters for querying notifications
|
|
type NotificationParams struct {
|
|
UserID uuid.UUID
|
|
Type string // Filter by notification type (empty = all types)
|
|
UnreadOnly bool // Only return unread notifications
|
|
Page int // Page number (default: 1)
|
|
Limit int // Results per page (default: 20, max: 100)
|
|
}
|
|
|
|
// NotificationListResult represents paginated notification results
|
|
type NotificationListResult struct {
|
|
Notifications []models.Notification `json:"notifications"`
|
|
Total int64 `json:"total"`
|
|
Page int `json:"page"`
|
|
Limit int `json:"limit"`
|
|
UnreadCount int64 `json:"unread_count"`
|
|
}
|
|
|
|
// CreateNotification creates a new notification
|
|
func (s *EnhancedNotificationService) CreateNotification(
|
|
ctx context.Context,
|
|
userID uuid.UUID,
|
|
notificationType, title, content, link string,
|
|
) (*models.Notification, error) {
|
|
notification := &models.Notification{
|
|
UserID: userID,
|
|
Type: notificationType,
|
|
Title: title,
|
|
Content: content,
|
|
Link: link,
|
|
Read: false,
|
|
}
|
|
|
|
if err := s.db.WithContext(ctx).Create(notification).Error; err != nil {
|
|
s.logger.Error("Failed to create notification",
|
|
zap.Error(err),
|
|
zap.String("user_id", userID.String()),
|
|
zap.String("type", notificationType),
|
|
)
|
|
return nil, fmt.Errorf("failed to create notification: %w", err)
|
|
}
|
|
|
|
s.logger.Info("Notification created",
|
|
zap.String("notification_id", notification.ID.String()),
|
|
zap.String("user_id", userID.String()),
|
|
zap.String("type", notificationType),
|
|
)
|
|
|
|
return notification, nil
|
|
}
|
|
|
|
// GetNotifications retrieves notifications with pagination and filtering
|
|
func (s *EnhancedNotificationService) GetNotifications(
|
|
ctx context.Context,
|
|
params NotificationParams,
|
|
) (*NotificationListResult, error) {
|
|
// Validate and set defaults
|
|
if params.Page < 1 {
|
|
params.Page = 1
|
|
}
|
|
if params.Limit < 1 {
|
|
params.Limit = 20
|
|
}
|
|
if params.Limit > 100 {
|
|
params.Limit = 100
|
|
}
|
|
|
|
// Build query
|
|
query := s.db.WithContext(ctx).Model(&models.Notification{}).
|
|
Where("user_id = ?", params.UserID)
|
|
|
|
// Filter by type
|
|
if params.Type != "" {
|
|
query = query.Where("type = ?", params.Type)
|
|
}
|
|
|
|
// Filter by read status
|
|
if params.UnreadOnly {
|
|
query = query.Where("read = ?", false)
|
|
}
|
|
|
|
// Get total count
|
|
var total int64
|
|
if err := query.Count(&total).Error; err != nil {
|
|
return nil, fmt.Errorf("failed to count notifications: %w", err)
|
|
}
|
|
|
|
// Get unread count
|
|
var unreadCount int64
|
|
unreadQuery := s.db.WithContext(ctx).Model(&models.Notification{}).
|
|
Where("user_id = ? AND read = ?", params.UserID, false)
|
|
if err := unreadQuery.Count(&unreadCount).Error; err != nil {
|
|
s.logger.Warn("Failed to count unread notifications", zap.Error(err))
|
|
}
|
|
|
|
// Apply pagination
|
|
offset := (params.Page - 1) * params.Limit
|
|
var notifications []models.Notification
|
|
if err := query.
|
|
Order("created_at DESC").
|
|
Offset(offset).
|
|
Limit(params.Limit).
|
|
Find(¬ifications).Error; err != nil {
|
|
return nil, fmt.Errorf("failed to get notifications: %w", err)
|
|
}
|
|
|
|
return &NotificationListResult{
|
|
Notifications: notifications,
|
|
Total: total,
|
|
Page: params.Page,
|
|
Limit: params.Limit,
|
|
UnreadCount: unreadCount,
|
|
}, nil
|
|
}
|
|
|
|
// GetNotificationByID retrieves a specific notification
|
|
func (s *EnhancedNotificationService) GetNotificationByID(
|
|
ctx context.Context,
|
|
userID, notificationID uuid.UUID,
|
|
) (*models.Notification, error) {
|
|
var notification models.Notification
|
|
if err := s.db.WithContext(ctx).
|
|
Where("id = ? AND user_id = ?", notificationID, userID).
|
|
First(¬ification).Error; err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return nil, fmt.Errorf("notification not found")
|
|
}
|
|
return nil, fmt.Errorf("failed to get notification: %w", err)
|
|
}
|
|
|
|
return ¬ification, nil
|
|
}
|
|
|
|
// MarkAsRead marks a notification as read
|
|
func (s *EnhancedNotificationService) MarkAsRead(
|
|
ctx context.Context,
|
|
userID, notificationID uuid.UUID,
|
|
) error {
|
|
now := time.Now()
|
|
result := s.db.WithContext(ctx).Model(&models.Notification{}).
|
|
Where("id = ? AND user_id = ?", notificationID, userID).
|
|
Updates(map[string]interface{}{
|
|
"read": true,
|
|
"read_at": now,
|
|
})
|
|
|
|
if result.Error != nil {
|
|
return fmt.Errorf("failed to mark notification as read: %w", result.Error)
|
|
}
|
|
|
|
if result.RowsAffected == 0 {
|
|
return fmt.Errorf("notification not found or already read")
|
|
}
|
|
|
|
s.logger.Info("Notification marked as read",
|
|
zap.String("notification_id", notificationID.String()),
|
|
zap.String("user_id", userID.String()),
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
// MarkAllAsRead marks all notifications as read for a user
|
|
func (s *EnhancedNotificationService) MarkAllAsRead(
|
|
ctx context.Context,
|
|
userID uuid.UUID,
|
|
) (int64, error) {
|
|
now := time.Now()
|
|
result := s.db.WithContext(ctx).Model(&models.Notification{}).
|
|
Where("user_id = ? AND read = ?", userID, false).
|
|
Updates(map[string]interface{}{
|
|
"read": true,
|
|
"read_at": now,
|
|
})
|
|
|
|
if result.Error != nil {
|
|
return 0, fmt.Errorf("failed to mark all notifications as read: %w", result.Error)
|
|
}
|
|
|
|
count := result.RowsAffected
|
|
s.logger.Info("All notifications marked as read",
|
|
zap.String("user_id", userID.String()),
|
|
zap.Int64("count", count),
|
|
)
|
|
|
|
return count, nil
|
|
}
|
|
|
|
// DeleteNotification deletes a notification
|
|
func (s *EnhancedNotificationService) DeleteNotification(
|
|
ctx context.Context,
|
|
userID, notificationID uuid.UUID,
|
|
) error {
|
|
result := s.db.WithContext(ctx).
|
|
Where("id = ? AND user_id = ?", notificationID, userID).
|
|
Delete(&models.Notification{})
|
|
|
|
if result.Error != nil {
|
|
return fmt.Errorf("failed to delete notification: %w", result.Error)
|
|
}
|
|
|
|
if result.RowsAffected == 0 {
|
|
return fmt.Errorf("notification not found")
|
|
}
|
|
|
|
s.logger.Info("Notification deleted",
|
|
zap.String("notification_id", notificationID.String()),
|
|
zap.String("user_id", userID.String()),
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
// DeleteAllRead deletes all read notifications for a user
|
|
func (s *EnhancedNotificationService) DeleteAllRead(
|
|
ctx context.Context,
|
|
userID uuid.UUID,
|
|
) (int64, error) {
|
|
result := s.db.WithContext(ctx).
|
|
Where("user_id = ? AND read = ?", userID, true).
|
|
Delete(&models.Notification{})
|
|
|
|
if result.Error != nil {
|
|
return 0, fmt.Errorf("failed to delete read notifications: %w", result.Error)
|
|
}
|
|
|
|
count := result.RowsAffected
|
|
s.logger.Info("All read notifications deleted",
|
|
zap.String("user_id", userID.String()),
|
|
zap.Int64("count", count),
|
|
)
|
|
|
|
return count, nil
|
|
}
|
|
|
|
// GetUnreadCount returns the count of unread notifications
|
|
func (s *EnhancedNotificationService) GetUnreadCount(
|
|
ctx context.Context,
|
|
userID uuid.UUID,
|
|
) (int64, error) {
|
|
var count int64
|
|
if err := s.db.WithContext(ctx).Model(&models.Notification{}).
|
|
Where("user_id = ? AND read = ?", userID, false).
|
|
Count(&count).Error; err != nil {
|
|
return 0, fmt.Errorf("failed to get unread count: %w", err)
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
// GetNotificationTypes returns distinct notification types for a user
|
|
func (s *EnhancedNotificationService) GetNotificationTypes(
|
|
ctx context.Context,
|
|
userID uuid.UUID,
|
|
) ([]string, error) {
|
|
var types []string
|
|
if err := s.db.WithContext(ctx).Model(&models.Notification{}).
|
|
Where("user_id = ?", userID).
|
|
Distinct("type").
|
|
Pluck("type", &types).Error; err != nil {
|
|
return nil, fmt.Errorf("failed to get notification types: %w", err)
|
|
}
|
|
|
|
return types, nil
|
|
}
|
|
|
|
// CreateNotificationBatch creates multiple notifications at once
|
|
func (s *EnhancedNotificationService) CreateNotificationBatch(
|
|
ctx context.Context,
|
|
notifications []models.Notification,
|
|
) error {
|
|
if len(notifications) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Set IDs for notifications that don't have one
|
|
for i := range notifications {
|
|
if notifications[i].ID == uuid.Nil {
|
|
notifications[i].ID = uuid.New()
|
|
}
|
|
notifications[i].Read = false
|
|
}
|
|
|
|
if err := s.db.WithContext(ctx).Create(¬ifications).Error; err != nil {
|
|
s.logger.Error("Failed to create notification batch",
|
|
zap.Error(err),
|
|
zap.Int("count", len(notifications)),
|
|
)
|
|
return fmt.Errorf("failed to create notification batch: %w", err)
|
|
}
|
|
|
|
s.logger.Info("Notification batch created",
|
|
zap.Int("count", len(notifications)),
|
|
)
|
|
|
|
return nil
|
|
}
|