veza/veza-backend-api/internal/services/playlist_follow_service.go
2025-12-16 11:23:49 -05:00

168 lines
5.8 KiB
Go

package services
import (
"context"
"errors"
"fmt"
"github.com/google/uuid"
"veza-backend-api/internal/models"
"go.uber.org/zap"
"gorm.io/gorm"
)
// PlaylistFollowService gère les opérations sur les follows de playlists
// T0489: Create Playlist Follow Feature
type PlaylistFollowService struct {
db *gorm.DB
logger *zap.Logger
}
// NewPlaylistFollowService crée un nouveau service de follows de playlists
func NewPlaylistFollowService(db *gorm.DB, logger *zap.Logger) *PlaylistFollowService {
if logger == nil {
logger = zap.NewNop()
}
return &PlaylistFollowService{
db: db,
logger: logger,
}
}
// FollowPlaylist ajoute un follow d'un utilisateur sur une playlist
// MIGRATION UUID: Completée. userID et playlistID sont des UUIDs.
func (s *PlaylistFollowService) FollowPlaylist(ctx context.Context, userID uuid.UUID, playlistID uuid.UUID) error {
// Vérifier si la playlist existe
var playlist models.Playlist
if err := s.db.WithContext(ctx).First(&playlist, "id = ?", playlistID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("playlist not found")
}
return fmt.Errorf("failed to check playlist: %w", err)
}
// Vérifier si l'utilisateur est le propriétaire (ne peut pas suivre sa propre playlist)
if playlist.UserID == userID {
return errors.New("cannot follow own playlist")
}
// Vérifier si l'utilisateur suit déjà cette playlist
var existing models.PlaylistFollow
if err := s.db.WithContext(ctx).Where("user_id = ? AND playlist_id = ? AND deleted_at IS NULL", userID, playlistID).First(&existing).Error; err == nil {
// Déjà suivi, retourner nil (idempotent)
return nil
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("failed to check existing follow: %w", err)
}
// Créer le follow
follow := models.PlaylistFollow{
UserID: userID,
PlaylistID: playlistID,
}
if err := s.db.WithContext(ctx).Create(&follow).Error; err != nil {
return fmt.Errorf("failed to create follow: %w", err)
}
// Mettre à jour le compteur de followers de la playlist
if err := s.db.WithContext(ctx).Model(&playlist).UpdateColumn("follower_count", gorm.Expr("follower_count + ?", 1)).Error; err != nil {
s.logger.Warn("Failed to update playlist follower_count",
zap.String("playlist_id", playlistID.String()),
zap.Error(err),
)
// Ne pas retourner l'erreur, le follow a été créé avec succès
}
s.logger.Info("Playlist followed",
zap.String("user_id", userID.String()),
zap.String("playlist_id", playlistID.String()),
)
return nil
}
// UnfollowPlaylist supprime un follow d'un utilisateur sur une playlist
// MIGRATION UUID: Completée. userID et playlistID sont des UUIDs.
func (s *PlaylistFollowService) UnfollowPlaylist(ctx context.Context, userID uuid.UUID, playlistID uuid.UUID) error {
// Vérifier si le follow existe
var follow models.PlaylistFollow
if err := s.db.WithContext(ctx).Where("user_id = ? AND playlist_id = ? AND deleted_at IS NULL", userID, playlistID).First(&follow).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// Pas de follow à supprimer, retourner nil (idempotent)
return nil
}
return fmt.Errorf("failed to check follow: %w", err)
}
// Supprimer le follow (soft delete)
if err := s.db.WithContext(ctx).Delete(&follow).Error; err != nil {
return fmt.Errorf("failed to delete follow: %w", err)
}
// Mettre à jour le compteur de followers de la playlist
var playlist models.Playlist
if err := s.db.WithContext(ctx).First(&playlist, "id = ?", playlistID).Error; err == nil {
// Use CASE expression for SQLite compatibility (GREATEST is not supported in SQLite)
if err := s.db.WithContext(ctx).Model(&playlist).UpdateColumn("follower_count", gorm.Expr("CASE WHEN follower_count - 1 < 0 THEN 0 ELSE follower_count - 1 END")).Error; err != nil {
s.logger.Warn("Failed to update playlist follower_count",
zap.String("playlist_id", playlistID.String()),
zap.Error(err),
)
// Ne pas retourner l'erreur, le follow a été supprimé avec succès
}
}
s.logger.Info("Playlist unfollowed",
zap.String("user_id", userID.String()),
zap.String("playlist_id", playlistID.String()),
)
return nil
}
// IsFollowing vérifie si un utilisateur suit une playlist
// MIGRATION UUID: Completée. userID et playlistID sont des UUIDs.
func (s *PlaylistFollowService) IsFollowing(ctx context.Context, userID uuid.UUID, playlistID uuid.UUID) (bool, error) {
var count int64
err := s.db.WithContext(ctx).Model(&models.PlaylistFollow{}).
Where("user_id = ? AND playlist_id = ? AND deleted_at IS NULL", userID, playlistID).
Count(&count).Error
if err != nil {
return false, fmt.Errorf("failed to check follow: %w", err)
}
return count > 0, nil
}
// GetPlaylistFollowersCount retourne le nombre de followers d'une playlist
func (s *PlaylistFollowService) GetPlaylistFollowersCount(ctx context.Context, playlistID uuid.UUID) (int64, error) {
var count int64
err := s.db.WithContext(ctx).Model(&models.PlaylistFollow{}).
Where("playlist_id = ? AND deleted_at IS NULL", playlistID).
Count(&count).Error
if err != nil {
return 0, fmt.Errorf("failed to get followers count: %w", err)
}
return count, nil
}
// GetFollowedPlaylists retourne toutes les playlists suivies par un utilisateur
// T0498: Create Playlist Recommendations
func (s *PlaylistFollowService) GetFollowedPlaylists(ctx context.Context, userID uuid.UUID) ([]*models.Playlist, error) {
var playlists []*models.Playlist
err := s.db.WithContext(ctx).
Joins("INNER JOIN playlist_follows ON playlist_follows.playlist_id = playlists.id").
Where("playlist_follows.user_id = ? AND playlist_follows.deleted_at IS NULL", userID).
Preload("User").
Preload("Tracks").
Preload("Tracks.Track").
Find(&playlists).Error
if err != nil {
return nil, fmt.Errorf("failed to get followed playlists: %w", err)
}
return playlists, nil
}