package handlers import ( "net/http" "strconv" "time" "veza-backend-api/internal/services" "github.com/gin-gonic/gin" "github.com/google/uuid" "go.uber.org/zap" ) // AnalyticsHandler gère les opérations d'analytics de lecture de tracks type AnalyticsHandler struct { analyticsService *services.AnalyticsService commonHandler *CommonHandler } // NewAnalyticsHandler crée un nouveau handler d'analytics func NewAnalyticsHandler(analyticsService *services.AnalyticsService, logger *zap.Logger) *AnalyticsHandler { return &AnalyticsHandler{ analyticsService: analyticsService, commonHandler: NewCommonHandler(logger), } } // RecordPlayRequest représente la requête pour enregistrer une lecture // MOD-P1-001: Ajout tags validate pour validation systématique type RecordPlayRequest struct { Duration int `json:"duration" binding:"required,min=1" validate:"required,min=1"` Device string `json:"device,omitempty" validate:"omitempty,max=100"` } // RecordPlay gère l'enregistrement d'une lecture de track func (h *AnalyticsHandler) RecordPlay(c *gin.Context) { trackIDStr := c.Param("id") if trackIDStr == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "track id is required"}) return } trackID, err := uuid.Parse(trackIDStr) // Changed to uuid.Parse if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid track id"}) return } var req RecordPlayRequest if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil { RespondWithAppError(c, appErr) return } // Récupérer user_id si authentifié (optionnel pour analytics anonymes) var userID *uuid.UUID if uid, ok := c.Get("user_id"); ok { if uidUUID, ok := uid.(uuid.UUID); ok { userID = &uidUUID } } // Récupérer IP address et device ipAddress := c.ClientIP() device := req.Device if device == "" { device = c.GetHeader("User-Agent") } err = h.analyticsService.RecordPlay(c.Request.Context(), trackID, userID, req.Duration, device, ipAddress) if err != nil { if err.Error() == "track not found" { c.JSON(http.StatusNotFound, gin.H{"error": "track not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } RespondSuccess(c, http.StatusOK, gin.H{"message": "play recorded"}) } // GetTrackStats gère la récupération des statistiques d'un track func (h *AnalyticsHandler) GetTrackStats(c *gin.Context) { trackIDStr := c.Param("id") if trackIDStr == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "track id is required"}) return } trackID, err := uuid.Parse(trackIDStr) // Changed to uuid.Parse if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid track id"}) return } stats, err := h.analyticsService.GetTrackStats(c.Request.Context(), trackID) if err != nil { if err.Error() == "track not found" { c.JSON(http.StatusNotFound, gin.H{"error": "track not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } RespondSuccess(c, http.StatusOK, gin.H{"stats": stats}) } // GetTopTracks gère la récupération des tracks les plus écoutés func (h *AnalyticsHandler) GetTopTracks(c *gin.Context) { // Parse limit limit := 10 if limitStr := c.Query("limit"); limitStr != "" { if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 { limit = l } else { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid limit (must be between 1 and 100)"}) return } } // Parse start_date (optionnel) var startDate *time.Time if startDateStr := c.Query("start_date"); startDateStr != "" { parsed, err := time.Parse(time.RFC3339, startDateStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid start_date format (use RFC3339)"}) return } startDate = &parsed } // Parse end_date (optionnel) var endDate *time.Time if endDateStr := c.Query("end_date"); endDateStr != "" { parsed, err := time.Parse(time.RFC3339, endDateStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid end_date format (use RFC3339)"}) return } endDate = &parsed } topTracks, err := h.analyticsService.GetTopTracks(c.Request.Context(), limit, startDate, endDate) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } RespondSuccess(c, http.StatusOK, gin.H{"tracks": topTracks}) } // GetPlaysOverTime gère la récupération des lectures sur une période func (h *AnalyticsHandler) GetPlaysOverTime(c *gin.Context) { trackIDStr := c.Param("id") if trackIDStr == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "track id is required"}) return } trackID, err := uuid.Parse(trackIDStr) // Changed to uuid.Parse if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid track id"}) return } // Parse start_date (optionnel, défaut: 30 jours) startDate := time.Now().AddDate(0, 0, -30) if startDateStr := c.Query("start_date"); startDateStr != "" { parsed, err := time.Parse(time.RFC3339, startDateStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid start_date format (use RFC3339)"}) return } startDate = parsed } // Parse end_date (optionnel, défaut: maintenant) endDate := time.Now() if endDateStr := c.Query("end_date"); endDateStr != "" { parsed, err := time.Parse(time.RFC3339, endDateStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid end_date format (use RFC3339)"}) return } endDate = parsed } // Parse interval (optionnel, défaut: day) interval := c.DefaultQuery("interval", "day") validIntervals := map[string]bool{"hour": true, "day": true, "week": true, "month": true} if !validIntervals[interval] { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid interval (must be: hour, day, week, month)"}) return } points, err := h.analyticsService.GetPlaysOverTime(c.Request.Context(), trackID, startDate, endDate, interval) if err != nil { if err.Error() == "track not found" { c.JSON(http.StatusNotFound, gin.H{"error": "track not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } RespondSuccess(c, http.StatusOK, gin.H{"points": points}) } // GetUserStats gère la récupération des statistiques d'un utilisateur func (h *AnalyticsHandler) GetUserStats(c *gin.Context) { userIDStr := c.Param("id") if userIDStr == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "user id is required"}) return } userID, err := uuid.Parse(userIDStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user id"}) return } // Vérifier que l'utilisateur peut accéder à ses propres stats var authenticatedUserID *uuid.UUID if uid, ok := c.Get("user_id"); ok { if uidUUID, ok := uid.(uuid.UUID); ok { authenticatedUserID = &uidUUID } } if authenticatedUserID != nil && *authenticatedUserID != userID { c.JSON(http.StatusForbidden, gin.H{"error": "cannot access other user's stats"}) return } stats, err := h.analyticsService.GetUserStats(c.Request.Context(), userID) if err != nil { if err.Error() == "user not found" { c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } RespondSuccess(c, http.StatusOK, gin.H{"stats": stats}) }