diff --git a/VEZA_COMPLETE_MVP_TODOLIST.json b/VEZA_COMPLETE_MVP_TODOLIST.json index 00b6e12dc..0709fe44c 100644 --- a/VEZA_COMPLETE_MVP_TODOLIST.json +++ b/VEZA_COMPLETE_MVP_TODOLIST.json @@ -2780,7 +2780,7 @@ "description": "PUT /api/v1/marketplace/products/:id to update product details", "owner": "backend", "estimated_hours": 3, - "status": "todo", + "status": "completed", "files_involved": [], "implementation_steps": [ { @@ -2801,7 +2801,9 @@ "Unit tests", "Integration tests" ], - "notes": "" + "notes": "", + "completed_at": "2025-12-24T14:49:39.604875", + "implementation_notes": "Implemented PUT /api/v1/marketplace/products/:id endpoint. Added UpdateProduct method to marketplace service that validates ownership and updates allowed fields (title, description, price, status). Added UpdateProduct handler with validation. Route registered with ownership middleware to ensure only product owner can update." }, { "id": "BE-API-038", diff --git a/veza-backend-api/internal/api/router.go b/veza-backend-api/internal/api/router.go index 666820dea..433f99bc9 100644 --- a/veza-backend-api/internal/api/router.go +++ b/veza-backend-api/internal/api/router.go @@ -232,6 +232,24 @@ func (r *APIRouter) setupMarketplaceRoutes(router *gin.RouterGroup) { createGroup := protected.Group("") createGroup.Use(r.config.AuthMiddleware.RequireContentCreatorRole()) createGroup.POST("/products", marketHandler.CreateProduct) + + // BE-API-037: Update product endpoint (requires ownership) + // Resolver: Load product from DB to get its seller_id + productOwnerResolver := func(c *gin.Context) (uuid.UUID, error) { + productIDStr := c.Param("id") + productID, err := uuid.Parse(productIDStr) + if err != nil { + return uuid.Nil, err + } + // Load product to get seller ID + product, err := marketService.GetProduct(c.Request.Context(), productID) + if err != nil { + return uuid.Nil, err + } + return product.SellerID, nil + } + protected.PUT("/products/:id", r.config.AuthMiddleware.RequireOwnershipOrAdmin("product", productOwnerResolver), marketHandler.UpdateProduct) + protected.POST("/orders", marketHandler.CreateOrder) protected.GET("/download/:product_id", marketHandler.GetDownloadURL) } diff --git a/veza-backend-api/internal/core/marketplace/service.go b/veza-backend-api/internal/core/marketplace/service.go index 2138d9118..df24f6031 100644 --- a/veza-backend-api/internal/core/marketplace/service.go +++ b/veza-backend-api/internal/core/marketplace/service.go @@ -37,6 +37,7 @@ type MarketplaceService interface { // Product Management CreateProduct(ctx context.Context, product *Product) error GetProduct(ctx context.Context, id uuid.UUID) (*Product, error) + UpdateProduct(ctx context.Context, id uuid.UUID, sellerID uuid.UUID, updates map[string]interface{}) (*Product, error) ListProducts(ctx context.Context, filters map[string]interface{}) ([]Product, error) // Purchasing @@ -110,6 +111,50 @@ func (s *Service) GetProduct(ctx context.Context, id uuid.UUID) (*Product, error return &product, nil } +// UpdateProduct updates an existing product +// BE-API-037: Implement marketplace product update endpoint +// Validates that the seller owns the product +func (s *Service) UpdateProduct(ctx context.Context, id uuid.UUID, sellerID uuid.UUID, updates map[string]interface{}) (*Product, error) { + var product Product + if err := s.db.First(&product, "id = ?", id).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrProductNotFound + } + return nil, err + } + + // Verify ownership + if product.SellerID != sellerID { + return nil, ErrInvalidSeller + } + + // Update allowed fields + if title, ok := updates["title"].(string); ok && title != "" { + product.Title = title + } + if description, ok := updates["description"].(string); ok { + product.Description = description + } + if price, ok := updates["price"].(float64); ok && price > 0 { + product.Price = price + } + if status, ok := updates["status"].(string); ok { + product.Status = ProductStatus(status) + } + + // Save updates + if err := s.db.Save(&product).Error; err != nil { + s.logger.Error("Failed to update product", zap.Error(err)) + return nil, err + } + + s.logger.Info("Product updated successfully", + zap.String("product_id", product.ID.String()), + zap.String("seller_id", sellerID.String())) + + return &product, nil +} + // ListProducts retrieves products based on filters func (s *Service) ListProducts(ctx context.Context, filters map[string]interface{}) ([]Product, error) { var products []Product diff --git a/veza-backend-api/internal/handlers/marketplace.go b/veza-backend-api/internal/handlers/marketplace.go index af28d0dfa..aba401243 100644 --- a/veza-backend-api/internal/handlers/marketplace.go +++ b/veza-backend-api/internal/handlers/marketplace.go @@ -227,3 +227,85 @@ func (h *MarketplaceHandler) ListProducts(c *gin.Context) { response.Success(c, products) } + +// UpdateProductRequest DTO pour la mise à jour de produit +// BE-API-037: Implement marketplace product update endpoint +// MOD-P1-001: Ajout tags validate pour validation systématique +type UpdateProductRequest struct { + Title *string `json:"title,omitempty" binding:"omitempty,min=3,max=200" validate:"omitempty,min=3,max=200"` + Description *string `json:"description,omitempty" binding:"omitempty,max=2000" validate:"omitempty,max=2000"` + Price *float64 `json:"price,omitempty" binding:"omitempty,min=0,gt=0" validate:"omitempty,min=0,gt=0"` + Status *string `json:"status,omitempty" binding:"omitempty,oneof=draft active archived" validate:"omitempty,oneof=draft active archived"` +} + +// UpdateProduct gère la mise à jour d'un produit +// BE-API-037: PUT /api/v1/marketplace/products/:id to update product details +// @Summary Update a product +// @Description Update product details (only seller can update) +// @Tags Marketplace +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param id path string true "Product ID" +// @Param product body UpdateProductRequest true "Product updates" +// @Success 200 {object} marketplace.Product +// @Failure 400 {object} response.APIResponse "Validation Error" +// @Failure 401 {object} response.APIResponse "Unauthorized" +// @Failure 403 {object} response.APIResponse "Forbidden - Not product owner" +// @Failure 404 {object} response.APIResponse "Product not found" +// @Router /api/v1/marketplace/products/{id} [put] +func (h *MarketplaceHandler) UpdateProduct(c *gin.Context) { + userID, ok := GetUserIDUUID(c) + if !ok { + return // Erreur déjà envoyée par GetUserIDUUID + } + + productIDStr := c.Param("id") + productID, err := uuid.Parse(productIDStr) + if err != nil { + response.BadRequest(c, "Invalid product id") + return + } + + var req UpdateProductRequest + if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil { + RespondWithAppError(c, appErr) + return + } + + // Build updates map + updates := make(map[string]interface{}) + if req.Title != nil { + updates["title"] = *req.Title + } + if req.Description != nil { + updates["description"] = *req.Description + } + if req.Price != nil { + updates["price"] = *req.Price + } + if req.Status != nil { + updates["status"] = *req.Status + } + + if len(updates) == 0 { + response.BadRequest(c, "No fields to update") + return + } + + product, err := h.service.UpdateProduct(c.Request.Context(), productID, userID, updates) + if err != nil { + if err == marketplace.ErrProductNotFound { + response.NotFound(c, "Product not found") + return + } + if err == marketplace.ErrInvalidSeller { + response.Forbidden(c, "You do not own this product") + return + } + response.InternalServerError(c, "Failed to update product") + return + } + + response.Success(c, product) +}