veza/veza-backend-api/internal/services/notification_service_enhanced.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(&notifications).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(&notification).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 &notification, 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(&notifications).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
}