package services import ( "context" "database/sql" "fmt" "github.com/google/uuid" "veza-backend-api/internal/database" "go.uber.org/zap" ) // NotificationService handles notification operations type NotificationService struct { db *database.Database logger *zap.Logger pushService *PushService // optional, for N1.2 Web Push } // Notification represents a notification type Notification struct { ID uuid.UUID `json:"id" db:"id"` UserID uuid.UUID `json:"user_id" db:"user_id"` Type string `json:"type" db:"type"` Title string `json:"title" db:"title"` Content string `json:"content" db:"content"` Link string `json:"link" db:"link"` Read bool `json:"read" db:"read"` CreatedAt string `json:"created_at" db:"created_at"` } // NewNotificationService creates a new notification service func NewNotificationService(db *database.Database, logger *zap.Logger) *NotificationService { return &NotificationService{ db: db, logger: logger, } } // SetPushService injects the push service for Web Push (N1.2) func (ns *NotificationService) SetPushService(ps *PushService) { ns.pushService = ps } // CreateNotification creates a new notification and optionally sends Web Push (N1.2) func (ns *NotificationService) CreateNotification(userID uuid.UUID, notificationType, title, content, link string) error { ctx := context.Background() _, err := ns.db.ExecContext(ctx, ` INSERT INTO notifications (user_id, type, title, content, link) VALUES ($1, $2, $3, $4, $5) `, userID, notificationType, title, content, link) if err != nil { return fmt.Errorf("failed to create notification: %w", err) } // N1.2: Send Web Push if enabled and user has subscriptions if ns.pushService != nil { prefs, err := ns.GetPreferences(userID) if err != nil { ns.logger.Warn("failed to get push preferences", zap.Error(err)) return nil } var shouldPush bool switch notificationType { case "follow": shouldPush = prefs.PushFollow case "like": shouldPush = prefs.PushLike case "comment": shouldPush = prefs.PushComment case "new_message", "message": shouldPush = prefs.PushMessage case "user_mentioned", "mention": shouldPush = prefs.PushMention default: shouldPush = false } if shouldPush { if err := ns.pushService.SendPushToUser(ctx, userID, title, content, link); err != nil { ns.logger.Warn("failed to send push notification", zap.Error(err), zap.String("user_id", userID.String())) } } } return nil } // GetNotifications retrieves notifications for a user func (ns *NotificationService) GetNotifications(userID uuid.UUID, unreadOnly bool) ([]Notification, error) { ctx := context.Background() query := ` SELECT id, user_id, type, title, content, link, read, created_at FROM notifications WHERE user_id = $1 ` args := []interface{}{userID} if unreadOnly { query += " AND read = FALSE" } query += " ORDER BY created_at DESC LIMIT 50" rows, err := ns.db.QueryContext(ctx, query, args...) if err != nil { return nil, fmt.Errorf("failed to get notifications: %w", err) } defer rows.Close() var notifications []Notification for rows.Next() { var notification Notification if err := rows.Scan( ¬ification.ID, ¬ification.UserID, ¬ification.Type, ¬ification.Title, ¬ification.Content, ¬ification.Link, ¬ification.Read, ¬ification.CreatedAt, ); err != nil { continue } notifications = append(notifications, notification) } return notifications, nil } // MarkAsRead marks a notification as read func (ns *NotificationService) MarkAsRead(userID uuid.UUID, notificationID uuid.UUID) error { ctx := context.Background() _, err := ns.db.ExecContext(ctx, ` UPDATE notifications SET read = TRUE WHERE id = $1 AND user_id = $2 `, notificationID, userID) if err != nil { return fmt.Errorf("failed to mark notification as read: %w", err) } return nil } // MarkAllAsRead marks all notifications as read for a user func (ns *NotificationService) MarkAllAsRead(userID uuid.UUID) error { ctx := context.Background() _, err := ns.db.ExecContext(ctx, ` UPDATE notifications SET read = TRUE WHERE user_id = $1 AND read = FALSE `, userID) if err != nil { return fmt.Errorf("failed to mark all notifications as read: %w", err) } return nil } // GetUnreadCount returns the count of unread notifications func (ns *NotificationService) GetUnreadCount(userID uuid.UUID) (int, error) { ctx := context.Background() var count int err := ns.db.QueryRowContext(ctx, ` SELECT COUNT(*) FROM notifications WHERE user_id = $1 AND read = FALSE `, userID).Scan(&count) if err != nil { return 0, fmt.Errorf("failed to get unread count: %w", err) } return count, nil } // DeleteNotification deletes a notification func (ns *NotificationService) DeleteNotification(userID uuid.UUID, notificationID uuid.UUID) error { ctx := context.Background() _, err := ns.db.ExecContext(ctx, ` DELETE FROM notifications WHERE id = $1 AND user_id = $2 `, notificationID, userID) if err != nil { return fmt.Errorf("failed to delete notification: %w", err) } return nil } // DeleteAllNotifications deletes all notifications for a user func (ns *NotificationService) DeleteAllNotifications(userID uuid.UUID) error { ctx := context.Background() _, err := ns.db.ExecContext(ctx, ` DELETE FROM notifications WHERE user_id = $1 `, userID) if err != nil { return fmt.Errorf("failed to delete all notifications: %w", err) } return nil } // NotificationPrefs represents notification preferences (N1.3) type NotificationPrefs struct { PushFollow bool `json:"push_follow"` PushLike bool `json:"push_like"` PushComment bool `json:"push_comment"` PushMessage bool `json:"push_message"` PushMention bool `json:"push_mention"` } // GetPreferences returns notification preferences for a user func (ns *NotificationService) GetPreferences(userID uuid.UUID) (*NotificationPrefs, error) { ctx := context.Background() prefs := &NotificationPrefs{PushFollow: true, PushLike: true, PushComment: true, PushMessage: true, PushMention: true} err := ns.db.QueryRowContext(ctx, ` SELECT push_follow, push_like, push_comment, push_message, push_mention FROM notification_preferences WHERE user_id = $1 `, userID).Scan(&prefs.PushFollow, &prefs.PushLike, &prefs.PushComment, &prefs.PushMessage, &prefs.PushMention) if err == nil { return prefs, nil } if err != sql.ErrNoRows { return nil, fmt.Errorf("failed to get preferences: %w", err) } return prefs, nil } // UpdatePreferences updates notification preferences func (ns *NotificationService) UpdatePreferences(userID uuid.UUID, pushFollow, pushLike, pushComment, pushMessage, pushMention *bool) error { ctx := context.Background() _, err := ns.db.ExecContext(ctx, ` INSERT INTO notification_preferences (user_id, push_follow, push_like, push_comment, push_message, push_mention, updated_at) VALUES ($1, COALESCE($2, true), COALESCE($3, true), COALESCE($4, true), COALESCE($5, true), COALESCE($6, true), NOW()) ON CONFLICT (user_id) DO UPDATE SET push_follow = CASE WHEN $2 IS NOT NULL THEN $2 ELSE notification_preferences.push_follow END, push_like = CASE WHEN $3 IS NOT NULL THEN $3 ELSE notification_preferences.push_like END, push_comment = CASE WHEN $4 IS NOT NULL THEN $4 ELSE notification_preferences.push_comment END, push_message = CASE WHEN $5 IS NOT NULL THEN $5 ELSE notification_preferences.push_message END, push_mention = CASE WHEN $6 IS NOT NULL THEN $6 ELSE notification_preferences.push_mention END, updated_at = NOW() `, userID, pushFollow, pushLike, pushComment, pushMessage, pushMention) return err }