package handlers import ( "errors" "net/http" "strconv" "time" "veza-backend-api/internal/core/distribution" apperrors "veza-backend-api/internal/errors" "github.com/gin-gonic/gin" "github.com/google/uuid" "go.uber.org/zap" ) // DistributionHandler handles distribution HTTP endpoints (v0.12.2 F501-F510) type DistributionHandler struct { service *distribution.Service logger *zap.Logger } // NewDistributionHandler creates a new DistributionHandler func NewDistributionHandler(service *distribution.Service, logger *zap.Logger) *DistributionHandler { return &DistributionHandler{ service: service, logger: logger, } } // Submit handles POST /distributions/submit func (h *DistributionHandler) Submit(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return } var req distribution.SubmitRequest if err := c.ShouldBindJSON(&req); err != nil { RespondWithAppError(c, apperrors.NewValidationError("Invalid request: track_id, platforms, and metadata required")) return } resp, err := h.service.Submit(c.Request.Context(), userID, req) if err != nil { switch { case errors.Is(err, distribution.ErrNotEligible): RespondWithAppError(c, apperrors.NewForbiddenError("Distribution requires Creator or Premium plan")) case errors.Is(err, distribution.ErrTrackNotPublic): RespondWithAppError(c, apperrors.NewValidationError("Track must be public and belong to you")) case errors.Is(err, distribution.ErrAlreadyDistributed): RespondWithAppError(c, apperrors.NewValidationError("Track already has an active distribution")) case errors.Is(err, distribution.ErrNoPlatformsSelected): RespondWithAppError(c, apperrors.NewValidationError("At least one platform must be selected")) case errors.Is(err, distribution.ErrInvalidPlatform): RespondWithAppError(c, apperrors.NewValidationError(err.Error())) default: RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to submit distribution", err)) } return } RespondSuccess(c, http.StatusCreated, resp) } // GetDistribution handles GET /distributions/:id func (h *DistributionHandler) GetDistribution(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return } distID, err := uuid.Parse(c.Param("id")) if err != nil { RespondWithAppError(c, apperrors.NewValidationError("Invalid distribution ID")) return } dist, err := h.service.GetDistribution(c.Request.Context(), distID) if err != nil { if errors.Is(err, distribution.ErrDistributionNotFound) { RespondWithAppError(c, apperrors.NewNotFoundError("Distribution")) return } RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to get distribution", err)) return } // Ensure the distribution belongs to the requesting user if dist.CreatorID != userID { RespondWithAppError(c, apperrors.NewNotFoundError("Distribution")) return } RespondSuccess(c, http.StatusOK, dist) } // ListDistributions handles GET /distributions func (h *DistributionHandler) ListDistributions(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return } limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20")) limit = clampLimit(limit) // SECURITY(MEDIUM-004) offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0")) var trackID *uuid.UUID if tidStr := c.Query("track_id"); tidStr != "" { tid, err := uuid.Parse(tidStr) if err != nil { RespondWithAppError(c, apperrors.NewValidationError("Invalid track_id")) return } trackID = &tid } var status *distribution.DistributionStatus if statusStr := c.Query("status"); statusStr != "" { s := distribution.DistributionStatus(statusStr) status = &s } dists, total, err := h.service.ListDistributions(c.Request.Context(), userID, trackID, status, limit, offset) if err != nil { RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to list distributions", err)) return } page := (offset / max(limit, 1)) + 1 totalPages := int((total + int64(limit) - 1) / int64(limit)) RespondSuccess(c, http.StatusOK, gin.H{ "data": dists, "pagination": gin.H{ "page": page, "limit": limit, "total": total, "total_pages": totalPages, }, }) } // GetTrackDistributions handles GET /tracks/:track_id/distributions func (h *DistributionHandler) GetTrackDistributions(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return } trackID, err := uuid.Parse(c.Param("track_id")) if err != nil { RespondWithAppError(c, apperrors.NewValidationError("Invalid track ID")) return } dists, err := h.service.GetTrackDistributions(c.Request.Context(), trackID) if err != nil { RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to get track distributions", err)) return } // Filter to only show creator's own distributions var filtered []distribution.TrackDistribution for _, d := range dists { if d.CreatorID == userID { filtered = append(filtered, d) } } RespondSuccess(c, http.StatusOK, gin.H{"distributions": filtered}) } // GetStatusHistory handles GET /distributions/:id/status-history func (h *DistributionHandler) GetStatusHistory(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return } distID, err := uuid.Parse(c.Param("id")) if err != nil { RespondWithAppError(c, apperrors.NewValidationError("Invalid distribution ID")) return } // Verify ownership dist, err := h.service.GetDistribution(c.Request.Context(), distID) if err != nil { if errors.Is(err, distribution.ErrDistributionNotFound) { RespondWithAppError(c, apperrors.NewNotFoundError("Distribution")) return } RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to get distribution", err)) return } if dist.CreatorID != userID { RespondWithAppError(c, apperrors.NewNotFoundError("Distribution")) return } entries, err := h.service.GetStatusHistory(c.Request.Context(), distID) if err != nil { RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to get status history", err)) return } RespondSuccess(c, http.StatusOK, gin.H{"history": entries}) } // RemoveDistribution handles POST /distributions/:id/remove func (h *DistributionHandler) RemoveDistribution(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return } distID, err := uuid.Parse(c.Param("id")) if err != nil { RespondWithAppError(c, apperrors.NewValidationError("Invalid distribution ID")) return } if err := h.service.RemoveDistribution(c.Request.Context(), userID, distID); err != nil { switch { case errors.Is(err, distribution.ErrDistributionNotFound): RespondWithAppError(c, apperrors.NewNotFoundError("Distribution")) case errors.Is(err, distribution.ErrDistributionNotRemovable): RespondWithAppError(c, apperrors.NewValidationError("Distribution can only be removed when live")) default: RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to remove distribution", err)) } return } RespondSuccess(c, http.StatusOK, gin.H{"message": "Distribution removal requested"}) } // GetExternalRoyalties handles GET /creators/me/external-royalties func (h *DistributionHandler) GetExternalRoyalties(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return } limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20")) limit = clampLimit(limit) // SECURITY(MEDIUM-004) offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0")) var startDate, endDate *time.Time if sd := c.Query("start_date"); sd != "" { t, err := time.Parse("2006-01-02", sd) if err == nil { startDate = &t } } if ed := c.Query("end_date"); ed != "" { t, err := time.Parse("2006-01-02", ed) if err == nil { endDate = &t } } var platform *string if p := c.Query("platform"); p != "" { platform = &p } royalties, summary, err := h.service.GetExternalRoyalties(c.Request.Context(), userID, startDate, endDate, platform, limit, offset) if err != nil { RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to get royalties", err)) return } RespondSuccess(c, http.StatusOK, gin.H{ "data": royalties, "summary": summary, }) }