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 }