package postgres import ( "context" "database/sql" "time" "chat-switchboard/models" ) type NotificationStore struct{} func NewNotificationStore() *NotificationStore { return &NotificationStore{} } func (s *NotificationStore) Create(ctx context.Context, n *models.Notification) error { return DB.QueryRowContext(ctx, ` INSERT INTO notifications (user_id, type, title, body, resource_type, resource_id, is_read) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, created_at`, n.UserID, n.Type, n.Title, n.Body, nullStr(n.ResourceType), nullStr(n.ResourceID), n.IsRead, ).Scan(&n.ID, &n.CreatedAt) } func (s *NotificationStore) ListByUser(ctx context.Context, userID string, limit, offset int, unreadOnly bool) ([]models.Notification, int, error) { if limit <= 0 { limit = 20 } // Count countQ := `SELECT COUNT(*) FROM notifications WHERE user_id = $1` args := []interface{}{userID} if unreadOnly { countQ += ` AND is_read = false` } var total int if err := DB.QueryRowContext(ctx, countQ, args...).Scan(&total); err != nil { return nil, 0, err } // Fetch q := `SELECT id, user_id, type, title, body, resource_type, resource_id, is_read, created_at FROM notifications WHERE user_id = $1` if unreadOnly { q += ` AND is_read = false` } q += ` ORDER BY created_at DESC LIMIT $2 OFFSET $3` rows, err := DB.QueryContext(ctx, q, userID, limit, offset) if err != nil { return nil, 0, err } defer rows.Close() var items []models.Notification for rows.Next() { var n models.Notification var resType, resID sql.NullString if err := rows.Scan( &n.ID, &n.UserID, &n.Type, &n.Title, &n.Body, &resType, &resID, &n.IsRead, &n.CreatedAt, ); err != nil { return nil, 0, err } n.ResourceType = NullableString(resType) n.ResourceID = NullableString(resID) items = append(items, n) } if items == nil { items = []models.Notification{} } return items, total, rows.Err() } func (s *NotificationStore) MarkRead(ctx context.Context, id, userID string) error { res, err := DB.ExecContext(ctx, `UPDATE notifications SET is_read = true WHERE id = $1 AND user_id = $2`, id, userID) if err != nil { return err } n, _ := res.RowsAffected() if n == 0 { return sql.ErrNoRows } return nil } func (s *NotificationStore) MarkAllRead(ctx context.Context, userID string) error { _, err := DB.ExecContext(ctx, `UPDATE notifications SET is_read = true WHERE user_id = $1 AND is_read = false`, userID) return err } func (s *NotificationStore) Delete(ctx context.Context, id, userID string) error { res, err := DB.ExecContext(ctx, `DELETE FROM notifications WHERE id = $1 AND user_id = $2`, id, userID) if err != nil { return err } n, _ := res.RowsAffected() if n == 0 { return sql.ErrNoRows } return nil } func (s *NotificationStore) UnreadCount(ctx context.Context, userID string) (int, error) { var count int err := DB.QueryRowContext(ctx, `SELECT COUNT(*) FROM notifications WHERE user_id = $1 AND is_read = false`, userID).Scan(&count) return count, err } func (s *NotificationStore) DeleteOlderThan(ctx context.Context, before time.Time) (int64, error) { res, err := DB.ExecContext(ctx, `DELETE FROM notifications WHERE created_at < $1`, before) if err != nil { return 0, err } return res.RowsAffected() } // nullStr returns sql.NullString for optional string fields. func nullStr(s string) sql.NullString { if s == "" { return sql.NullString{} } return sql.NullString{String: s, Valid: true} }