package handlers import ( "fmt" "gestion/db" "gestion/models" "gestion/services" "gestion/utils" "io" "log" "math" "mime/multipart" "net/http" "strconv" "strings" "github.com/gabriel-vasile/mimetype" "github.com/gin-gonic/gin" ) const ( MaxFileSize = 10 * 1024 * 1024 // 10MB par fichier MaxTotalUploadSize = 50 * 1024 * 1024 // 50MB total MaxFilesPerProduct = 10 // Max 10 fichiers MaxNameLength = 200 MaxDescLength = 2000 MaxProductsPerUser = 100 // Limite pour éviter spam ) var allowedMimeTypes = map[string]bool{ "image/jpeg": true, "image/png": true, "image/gif": true, "image/webp": true, "video/mp4": true, "video/webm": true, "video/quicktime": true, } func validateProductName(name string) error { if len(name) == 0 { return fmt.Errorf("nom requis") } if len(name) > MaxNameLength { return fmt.Errorf("nom trop long (max %d caractères)", MaxNameLength) } // Sanitize if strings.Contains(name, "..") || strings.Contains(name, "/") { return fmt.Errorf("nom invalide") } return nil } func validateProductDescription(desc string) error { if len(desc) == 0 { return fmt.Errorf("description requise") } if len(desc) > MaxDescLength { return fmt.Errorf("description trop longue (max %d caractères)", MaxDescLength) } return nil } func validateStock(stock float64) error { if stock < 0 { return fmt.Errorf("stock ne peut pas être négatif") } if stock > 1000000 { return fmt.Errorf("stock trop élevé (max 1000000)") } return nil } func validatePrice(quantity float64, price float64) error { if quantity <= 0 { return fmt.Errorf("quantité doit être > 0") } if quantity > 10000 { return fmt.Errorf("quantité trop élevée (max 10000)") } if price <= 0 { return fmt.Errorf("prix doit être > 0") } if price > 100000 { return fmt.Errorf("prix trop élevé (max 100000)") } return nil } func validateUnit(unit string) error { validUnits := map[string]bool{ "u": true, // unité "kg": true, // kilogramme "g": true, // gramme "bag": true, // sac "l": true, // litre "cl": true, // centilitre "pcs": true, // pièces } if !validUnits[unit] { return fmt.Errorf("unité invalide : valeurs acceptées : u, kg, g, bag, l, cl, pcs") } return nil } func validateCategory(database *db.Database, category string) error { if category == "" { return fmt.Errorf("catégorie requise") } exists, err := database.CategoryExists(category) if err != nil { return fmt.Errorf("erreur vérification catégorie") } if !exists { return fmt.Errorf("catégorie invalide") } return nil } func validateFileMimeType(fileHeader *multipart.FileHeader) (string, error) { file, err := fileHeader.Open() if err != nil { return "", err } defer file.Close() mtype, err := mimetype.DetectReader(file) if err != nil { return "", err } mimeType := mtype.String() if idx := strings.Index(mimeType, ";"); idx != -1 { mimeType = strings.TrimSpace(mimeType[:idx]) } if !allowedMimeTypes[mimeType] { return "", fmt.Errorf("type de fichier non autorisé: %s", mimeType) } return mimeType, nil } func CreateProduct(c *gin.Context) { database := c.MustGet("database").(*db.Database) role := c.GetString("role") if role != "admin" && role != "cabine" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } username, _ := safeGetUsername(c) if err := c.Request.ParseMultipartForm(MaxTotalUploadSize); err != nil { log.Printf("❌ [CreateProduct] Formulaire trop grand: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": "Fichiers trop volumineux"}) return } name := strings.TrimSpace(c.PostForm("name")) category := strings.TrimSpace(c.PostForm("category")) description := strings.TrimSpace(c.PostForm("description")) stockStr := c.PostForm("stock") unit := strings.ToLower(strings.TrimSpace(c.PostForm("unit"))) if unit == "" { unit = "u" } if err := validateProductName(name); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := validateProductDescription(description); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } category = strings.ToLower(strings.TrimSpace(category)) category = strings.Map(func(r rune) rune { if r < 32 || r == 127 { return -1 } return r }, category) if err := validateCategory(database, category); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := validateUnit(unit); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } stock, err := strconv.ParseFloat(stockStr, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Stock invalide"}) return } if err := validateStock(stock); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } prices := []models.ProductPrice{} priceIndex := 0 for priceIndex < 100 { quantityKey := fmt.Sprintf("prices[%d][quantity]", priceIndex) priceKey := fmt.Sprintf("prices[%d][price]", priceIndex) activePriceKey := fmt.Sprintf("prices[%d][active_price]", priceIndex) quantityStr := c.PostForm(quantityKey) priceStr := c.PostForm(priceKey) if quantityStr == "" || priceStr == "" { break } quantity, err := strconv.ParseFloat(quantityStr, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Quantité invalide"}) return } price, err := strconv.ParseFloat(priceStr, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Prix invalide"}) return } if err := validatePrice(quantity, price); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } activePriceStr := c.PostForm(activePriceKey) activePrice := activePriceStr != "false" prices = append(prices, models.ProductPrice{ Quantity: quantity, Price: price, ActivePrice: activePrice, }) priceIndex++ } if len(prices) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "Au moins un prix requis"}) return } log.Printf("✅ [CreateProduct] %s crée produit: %s", username, name) comingSoon := c.PostForm("coming_soon") == "true" // ✅ CRÉER LE PRODUIT product := models.Product{ Name: name, Category: category, Description: description, Stock: stock, Unit: unit, ComingSoon: comingSoon, Prices: prices, } err = database.CreateProduct(&product) if err != nil { log.Printf("❌ [CreateProduct] Erreur DB: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur création produit"}) return } log.Printf("✅ [CreateProduct] Produit créé: ID=%d", product.ID) // ✅ TRAITER LES FICHIERS MÉDIAS AVEC SÉCURITÉ if c.Request.MultipartForm == nil || c.Request.MultipartForm.File == nil { c.JSON(http.StatusCreated, gin.H{ "success": true, "product": product, }) return } files, exists := c.Request.MultipartForm.File["media"] if !exists || len(files) == 0 { c.JSON(http.StatusCreated, gin.H{ "success": true, "product": product, }) return } if len(files) > MaxFilesPerProduct { database.DeleteProduct(product.ID) c.JSON(http.StatusBadRequest, gin.H{ "error": fmt.Sprintf("Maximum %d fichiers autorisés", MaxFilesPerProduct), }) return } log.Printf("📁 [CreateProduct] %d fichiers à traiter", len(files)) cleanProductName := cleanFileName(product.Name) uploadedMedia := []models.Media{} savedFiles := []models.Media{} storage := c.MustGet("storage").(services.Storage) var totalSize int64 = 0 for i, fileHeader := range files { if fileHeader.Size > MaxFileSize { rollbackFiles(storage, savedFiles) database.DeleteProduct(product.ID) c.JSON(http.StatusBadRequest, gin.H{ "error": fmt.Sprintf("Fichier %s trop volumineux (max %dMB)", fileHeader.Filename, MaxFileSize/(1024*1024)), }) return } totalSize += fileHeader.Size if totalSize > MaxTotalUploadSize { rollbackFiles(storage, savedFiles) database.DeleteProduct(product.ID) c.JSON(http.StatusBadRequest, gin.H{ "error": fmt.Sprintf("Taille totale dépassée (max %dMB)", MaxTotalUploadSize/(1024*1024)), }) return } log.Printf("📄 [%d/%d] Traitement: %s", i+1, len(files), fileHeader.Filename) mimeType, err := validateFileMimeType(fileHeader) if err != nil { log.Printf("❌ [CreateProduct] Type MIME invalide: %v", err) rollbackFiles(storage, savedFiles) database.DeleteProduct(product.ID) c.JSON(http.StatusBadRequest, gin.H{"error": "Type de fichier non autorisé"}) return } var mediaType string if strings.HasPrefix(mimeType, "image/") { mediaType = "image" } else if strings.HasPrefix(mimeType, "video/") { mediaType = "video" } else { rollbackFiles(storage, savedFiles) database.DeleteProduct(product.ID) c.JSON(http.StatusBadRequest, gin.H{"error": "Type de média non supporté"}) return } uniqueFileName := utils.GenerateUniqueFileName(cleanProductName, fileHeader.Filename) mediaURL, mediaKey, err := storage.Upload(fileHeader, mediaType+"s", uniqueFileName) if err != nil { log.Printf("❌ [CreateProduct] Erreur sauvegarde: %v", err) rollbackFiles(storage, savedFiles) database.DeleteProduct(product.ID) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur sauvegarde fichier"}) return } savedFiles = append(savedFiles, models.Media{URL: mediaURL, Key: mediaKey}) media := models.Media{ ProductID: product.ID, Type: mediaType, URL: mediaURL, Key: mediaKey, } if err := database.CreateMedia(&media); err != nil { log.Printf("❌ [CreateProduct] Erreur DB média: %v", err) rollbackFiles(storage, savedFiles) database.DeleteProduct(product.ID) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur création média"}) return } uploadedMedia = append(uploadedMedia, media) log.Printf("✅ [CreateProduct] Média %d créé", media.ID) } product.Media = uploadedMedia log.Printf("🎉 [CreateProduct] SUCCÈS: %d médias enregistrés", len(uploadedMedia)) c.JSON(http.StatusCreated, gin.H{ "success": true, "product": product, }) } func GetAllProducts(c *gin.Context) { database := c.MustGet("database").(*db.Database) products, err := database.GetAllProducts() if err != nil { log.Printf("❌ [GetAllProducts] Erreur: %v", err) c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "error": "Erreur récupération produits", }) return } role := c.GetString("role") if role != "admin" && role != "cabine" { products = filterActivePrices(products) } products = applyPromotions(products, database) c.JSON(http.StatusOK, gin.H{ "success": true, "data": products, "count": len(products), }) } func GetProductsByCategory(c *gin.Context) { database := c.MustGet("database").(*db.Database) category := strings.ToLower(strings.TrimSpace(c.Param("category"))) if err := validateCategory(database, category); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "error": err.Error(), }) return } products, err := database.GetProductsByCategory(category) if err != nil { log.Printf("❌ [GetProductsByCategory] Erreur: %v", err) c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "error": "Erreur récupération produits", }) return } for i := range products { media, _ := database.GetMediaByProductID(products[i].ID) products[i].Media = media } roleCtx := c.GetString("role") if roleCtx != "admin" && roleCtx != "cabine" { products = filterActivePrices(products) } products = applyPromotions(products, database) c.JSON(http.StatusOK, gin.H{ "success": true, "data": products, "count": len(products), }) } func GetProductByID(c *gin.Context) { database := c.MustGet("database").(*db.Database) id, err := strconv.Atoi(c.Param("id")) if err != nil || id <= 0 { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "error": "ID invalide", }) return } product, err := database.GetProductByID(id) if err != nil { c.JSON(http.StatusNotFound, gin.H{ "success": false, "error": "Produit non trouvé", }) return } media, _ := database.GetMediaByProductID(product.ID) product.Media = media role := c.GetString("role") if role != "admin" && role != "cabine" { filterActivepricesSingle(&product) } applyPromotionsSingle(&product, database) c.JSON(http.StatusOK, gin.H{ "success": true, "data": product, }) } func UpdateProduct(c *gin.Context) { database := c.MustGet("database").(*db.Database) role := c.GetString("role") if role != "admin" && role != "cabine" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } username, _ := safeGetUsername(c) id, err := strconv.Atoi(c.Param("id")) if err != nil || id <= 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "ID invalide"}) return } _, err = database.GetProductByID(id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Produit non trouvé"}) return } var updateData struct { Name string `json:"name"` Category string `json:"category"` Description string `json:"description"` Unit string `json:"unit"` Prices []models.ProductPrice `json:"prices"` Stock *float64 `json:"stock"` ComingSoon *bool `json:"coming_soon"` } if err := c.ShouldBindJSON(&updateData); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"}) return } if err := validateProductName(updateData.Name); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := validateProductDescription(updateData.Description); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := validateCategory(database, updateData.Category); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if updateData.Unit == "" { updateData.Unit = "u" } if err := validateUnit(updateData.Unit); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if len(updateData.Prices) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "Au moins un prix requis"}) return } for _, price := range updateData.Prices { if err := validatePrice(price.Quantity, price.Price); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } } if updateData.Stock != nil { if err := validateStock(*updateData.Stock); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } } log.Printf("🔄 [UpdateProduct] %s met à jour produit #%d", username, id) comingSoon := false if updateData.ComingSoon != nil { comingSoon = *updateData.ComingSoon } if err := database.UpdateProduct(id, updateData.Name, updateData.Category, updateData.Description, updateData.Unit, comingSoon, updateData.Prices); err != nil { log.Printf("❌ [UpdateProduct] Erreur UPDATE: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur mise à jour"}) return } if updateData.Stock != nil { if err := database.SetProductStock(id, *updateData.Stock); err != nil { log.Printf("⚠️ [UpdateProduct] Erreur mise à jour stock: %v", err) } } // ✅ RÉCUPÉRER LE PRODUIT MIS À JOUR updatedProduct, _ := database.GetProductByID(id) media, _ := database.GetMediaByProductID(id) updatedProduct.Media = media log.Printf("✅ [UpdateProduct] Produit #%d mis à jour", id) c.JSON(http.StatusOK, gin.H{ "success": true, "product": updatedProduct, }) } func UpdateStock(c *gin.Context) { database := c.MustGet("database").(*db.Database) role := c.GetString("role") if role != "admin" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } username, _ := safeGetUsername(c) id, err := strconv.Atoi(c.Param("id")) if err != nil || id <= 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "ID invalide"}) return } // ✅ VÉRIFIER QUE LE PRODUIT EXISTE _, err = database.GetProductByID(id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Produit non trouvé"}) return } var req struct { Stock float64 `json:"stock"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"}) return } if err := validateStock(req.Stock); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } reserved, err := database.GetReservedQuantityInBaskets(id) if err != nil { utils.ServerErr(c, "Erreur lecture réservations", err) return } if req.Stock+reserved < reserved { c.JSON(http.StatusBadRequest, gin.H{"error": "Stock invalide"}) return } log.Printf("🔄 [UpdateStock] %s met à jour le stock #%d (réservé en paniers: %.3f)", username, id, reserved) if err := database.SetProductStock(id, req.Stock); err != nil { log.Printf("❌ [UpdateProduct] Erreur UPDATE: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur mise à jour"}) return } updatedProduct, _ := database.GetProductByID(id) media, _ := database.GetMediaByProductID(id) updatedProduct.Media = media log.Printf("✅ [UpdateStock] le stock #%d est mis à jour", id) c.JSON(http.StatusOK, gin.H{ "success": true, "product": updatedProduct, "reserved_in_baskets": reserved, }) } func DeleteMedia(c *gin.Context) { database := c.MustGet("database").(*db.Database) s3Service := c.MustGet("s3Service").(*services.S3Service) role := c.GetString("role") if role != "admin" && role != "cabine" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } mediaID, err := strconv.Atoi(c.Param("media_id")) if err != nil || mediaID <= 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "ID invalide"}) return } media, err := database.GetMediaByID(mediaID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Média non trouvé"}) return } if err := database.DeleteMedia(mediaID); err != nil { log.Printf("❌ [DeleteMedia] Erreur suppression DB: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur suppression"}) return } if media.Key != "" { if err := s3Service.DeleteFile(media.Key); err != nil { log.Printf("⚠️ [DeleteMedia] Fichier non supprimé sur RustFS (clé: %s): %v", media.Key, err) } else { log.Printf("✅ [DeleteMedia] Fichier supprimé sur RustFS: %s", media.Key) } } else { localStorage := services.NewLocalStorage("uploads") if err := localStorage.Delete(media.URL, ""); err != nil { log.Printf("⚠️ [DeleteMedia] Fichier local non supprimé (%s): %v", media.URL, err) } else { log.Printf("✅ [DeleteMedia] Fichier local supprimé: %s", media.URL) } } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Média supprimé", }) } func UploadMedia(c *gin.Context) { database := c.MustGet("database").(*db.Database) username, err := safeGetUsername(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentification requise"}) return } role := c.GetString("role") if role != "admin" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } productID, err := strconv.Atoi(c.Param("id")) if err != nil || productID <= 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "ID produit invalide"}) return } productName, err := database.GetProductNameByID(productID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Produit non trouvé"}) return } log.Printf("📤 [UploadMedia] %s upload média pour produit #%d (%s)", username, productID, productName) fileType := c.PostForm("type") if fileType != "image" && fileType != "video" { c.JSON(http.StatusBadRequest, gin.H{"error": "Type invalide (image ou video requis)"}) return } file, err := c.FormFile("file") if err != nil { log.Printf("❌ [UploadMedia] Erreur récupération fichier: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": "Fichier manquant"}) return } const MaxFileSize = 10 * 1024 * 1024 // 10MB if file.Size > MaxFileSize { c.JSON(http.StatusBadRequest, gin.H{ "error": fmt.Sprintf("Fichier trop volumineux (max %dMB)", MaxFileSize/(1024*1024)), }) return } detectedMime, err := validateFileMimeType(file) if err != nil { log.Printf("❌ [UploadMedia] Type MIME invalide: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": "Type de fichier non autorisé"}) return } log.Printf("📋 [UploadMedia] Type MIME détecté: %s", detectedMime) if fileType == "image" && !strings.HasPrefix(detectedMime, "image/") { c.JSON(http.StatusBadRequest, gin.H{"error": "Le fichier n'est pas une image valide"}) return } if fileType == "video" && !strings.HasPrefix(detectedMime, "video/") { c.JSON(http.StatusBadRequest, gin.H{"error": "Le fichier n'est pas une vidéo valide"}) return } cleanProductName := cleanFileName(productName) uniqueFileName := utils.GenerateUniqueFileName(cleanProductName, file.Filename) storage := c.MustGet("storage").(services.Storage) folder := fileType + "s" mediaURL, mediaKey, err := storage.Upload(file, folder, uniqueFileName) if err != nil { log.Printf("❌ [UploadMedia] Erreur upload: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur upload fichier"}) return } log.Printf("✅ [UploadMedia] Fichier uploadé: %s", mediaURL) media := models.Media{ ProductID: productID, Type: fileType, URL: mediaURL, Key: mediaKey, } err = database.CreateMedia(&media) if err != nil { if delErr := storage.Delete(mediaURL, mediaKey); delErr != nil { log.Printf("⚠️ [UploadMedia] Échec rollback (%s): %v", mediaURL, delErr) } log.Printf("❌ [UploadMedia] Erreur DB: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur création média"}) return } log.Printf("✅ [UploadMedia] Média #%d créé pour produit #%d", media.ID, productID) c.JSON(http.StatusCreated, gin.H{ "success": true, "message": "Média uploadé avec succès", "media": media, "uploaded_by": gin.H{ "username": username, "role": role, }, }) } func ServeMedia(c *gin.Context) { s3Service := c.MustGet("s3Service").(*services.S3Service) key := strings.TrimPrefix(c.Param("key"), "/") if key == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Clé manquante"}) return } body, contentType, err := s3Service.GetFile(c.Request.Context(), key) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Média non trouvé"}) return } defer body.Close() c.Header("Content-Type", contentType) c.Header("Cache-Control", "public, max-age=31536000, immutable") c.Status(http.StatusOK) io.Copy(c.Writer, body) } func ActivePrice(c *gin.Context) { database := c.MustGet("database").(*db.Database) role := c.GetString("role") if role != "admin" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } id, err := strconv.Atoi(c.Param("id")) if err != nil || id <= 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "ID invalide"}) return } if err := database.AddActivePrice(id); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "Prix activé avec succès"}) } func DesActivePrice(c *gin.Context) { database := c.MustGet("database").(*db.Database) role := c.GetString("role") if role != "admin" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } id, err := strconv.Atoi(c.Param("id")) if err != nil || id <= 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "ID invalide"}) return } if err := database.DeActivePrice(id); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "Prix désactivé avec succès"}) } func DeleteProduct(c *gin.Context) { database := c.MustGet("database").(*db.Database) // ✅ VÉRIFIER LE RÔLE role := c.GetString("role") if role != "admin" && role != "cabine" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } username, _ := safeGetUsername(c) id, err := strconv.Atoi(c.Param("id")) if err != nil || id <= 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "ID invalide"}) return } log.Printf("🗑️ [DeleteProduct] %s supprime produit #%d", username, id) mediaList, err := database.GetMediaByProductID(id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur récupération médias"}) return } s3Service := c.MustGet("s3Service").(*services.S3Service) localStorage := services.NewLocalStorage("uploads") for _, media := range mediaList { if media.Key != "" { if err := s3Service.DeleteFile(media.Key); err != nil { log.Printf("⚠️ [DeleteProduct] Fichier non supprimé sur RustFS (clé: %s): %v", media.Key, err) } } else { if err := localStorage.Delete(media.URL, ""); err != nil { log.Printf("⚠️ [DeleteProduct] Erreur suppression locale: %v", err) } } } database.DeleteMediaByProductID(id) err = database.DeleteProduct(id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur suppression produit"}) return } log.Printf("✅ [DeleteProduct] Produit #%d supprimé", id) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Produit supprimé", }) } func rollbackFiles(storage services.Storage, files []models.Media) { for _, f := range files { if err := storage.Delete(f.URL, f.Key); err != nil { log.Printf("⚠️ [rollbackFiles] Erreur suppression %s: %v", f.URL, err) } } } func cleanFileName(name string) string { replacements := map[string]string{ "/": "-", "\\": "-", ":": "-", "*": "-", "?": "-", "\"": "-", "<": "-", ">": "-", "|": "-", " ": "_", } result := name for old, new := range replacements { result = strings.ReplaceAll(result, old, new) } // Limiter la longueur if len(result) > 50 { result = result[:50] } return result } func filterActivePrices(products []models.Product) []models.Product { for i := range products { activePrices := []models.ProductPrice{} for _, p := range products[i].Prices { if p.ActivePrice { activePrices = append(activePrices, p) } } products[i].Prices = activePrices } return products } func filterActivepricesSingle(product *models.Product) { activePrices := []models.ProductPrice{} for _, p := range product.Prices { if p.ActivePrice { activePrices = append(activePrices, p) } } product.Prices = activePrices } // applyPromotions annote chaque palier de prix éligible avec le prix promo // (PromoPrice/PromoPercent) si une promotion couvre ce produit/quantité — // affichage seulement, le prix catalogue (Price) n'est jamais modifié ici ; // le prix réellement facturé est recalculé indépendamment dans AddToBasket. func applyPromotions(products []models.Product, database *db.Database) []models.Product { settings, err := database.GetSettings() if err != nil || !settings.PromotionsEnabled { return products } for i := range products { for j := range products[i].Prices { pr := &products[i].Prices[j] discount, ok := db.ResolvePromotionDiscount(&settings, products[i].ID, products[i].Category, pr.Quantity) if !ok { continue } promoPrice := math.Round(pr.Price*(1-discount/100)*100) / 100 pr.PromoPrice = &promoPrice pr.PromoPercent = discount } } return products } func applyPromotionsSingle(product *models.Product, database *db.Database) { products := applyPromotions([]models.Product{*product}, database) *product = products[0] }