diff --git a/backend/gestion/db/db_basket.go b/backend/gestion/db/db_basket.go index 889af64b..2ae7f554 100644 --- a/backend/gestion/db/db_basket.go +++ b/backend/gestion/db/db_basket.go @@ -3,8 +3,6 @@ package db import ( "fmt" "gestion/models" - "log" - "time" "gorm.io/gorm" ) @@ -61,78 +59,6 @@ func (d *Database) AddProductInBasket(username, nameProduct string, quantity flo return &basket, nil } -// GetProductPriceByID récupère le prix d'un produit par son ID et quantité -func (d *Database) GetProductPriceByID(productID int, quantity float64) (float64, error) { - var result struct { - Price float64 `gorm:"column:price"` - } - - err := d.GDB.Raw(` - SELECT price FROM product_prices - WHERE product_id = ? AND quantity = ROUND(?::NUMERIC, 3) - LIMIT 1`, productID, quantity).Scan(&result).Error - if err == nil && result.Price > 0 { - return result.Price, nil - } - - err = d.GDB.Raw(` - SELECT price FROM product_prices - WHERE product_id = ? AND quantity <= ROUND(?::NUMERIC, 3) - ORDER BY quantity DESC LIMIT 1`, productID, quantity).Scan(&result).Error - if err != nil || result.Price == 0 { - return 0, fmt.Errorf("aucun prix trouvé pour product_id=%d qty=%.3f", productID, quantity) - } - return result.Price, nil -} - -// GetProductStockByID récupère le stock d'un produit par son ID -func (d *Database) GetProductStockByID(productID int) (float64, error) { - var result struct { - Stock float64 `gorm:"column:stock"` - } - err := d.GDB.Raw(`SELECT stock FROM products WHERE id = ?`, productID).Scan(&result).Error - if err != nil { - return 0, fmt.Errorf("produit %d non trouvé: %w", productID, err) - } - return result.Stock, nil -} - -// AddProductInBasketByID ajoute un produit au panier en utilisant son ID directement -func (d *Database) AddProductInBasketByID(username string, productID int, quantity float64) (*models.Panier, error) { - price, err := d.GetProductPriceByID(productID, quantity) - if err != nil { - return nil, fmt.Errorf("erreur récupération prix: %w", err) - } - - var existing struct { - ID int `gorm:"column:id"` - Quantity float64 `gorm:"column:quantity"` - Price float64 `gorm:"column:price"` - } - d.GDB.Raw(`SELECT id, quantity, price FROM baskets WHERE username = ? AND product_id = ?`, - username, productID).Scan(&existing) - - var basket models.Panier - if existing.ID != 0 { - newQuantity := existing.Quantity + quantity - newPrice := existing.Price + price - err = d.GDB.Raw(` - UPDATE baskets SET quantity = ?, price = ?, created_at = CURRENT_TIMESTAMP - WHERE id = ? RETURNING id, username, product_id, quantity, price, created_at`, - newQuantity, newPrice, existing.ID).Scan(&basket).Error - } else { - err = d.GDB.Raw(` - INSERT INTO baskets (username, product_id, quantity, price, created_at) - VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP) - RETURNING id, username, product_id, quantity, price, created_at`, - username, productID, quantity, price).Scan(&basket).Error - } - if err != nil { - return nil, fmt.Errorf("erreur panier: %w", err) - } - return &basket, nil -} - // GetProductPrice récupère le prix réel d'un produit pour une quantité donnée (legacy) func (d *Database) GetProductPrice(name, category string, quantity float64) (float64, error) { var result struct { @@ -195,18 +121,54 @@ func (d *Database) GetAllProductsInBasket(username string) ([]models.Panier, err return baskets, nil } -// DecrementProductStockByID décrémente le stock d'un produit par son ID -func (d *Database) DecrementProductStockByID(productID int, quantity float64) error { - result := d.GDB.Exec(` - UPDATE products SET stock = stock - ? - WHERE id = ? AND stock >= ?`, quantity, productID, quantity) - if result.Error != nil { - return fmt.Errorf("erreur lors de la mise à jour du stock: %w", result.Error) +func (d *Database) DecrementAndAddToBasket(username string, productID int, quantity float64) (*models.Panier, error) { + var basket models.Panier + err := d.GDB.Transaction(func(tx *gorm.DB) error { + result := tx.Exec(`UPDATE products SET stock = stock - ? WHERE id = ? AND stock >= ?`, + quantity, productID, quantity) + if result.Error != nil { + return fmt.Errorf("erreur stock: %w", result.Error) + } + if result.RowsAffected == 0 { + return fmt.Errorf("stock insuffisant") + } + + var priceResult struct { + Price float64 `gorm:"column:price"` + } + if err := tx.Raw(` + SELECT price FROM product_prices + WHERE product_id = ? AND quantity <= ? AND active_price = true + ORDER BY quantity DESC LIMIT 1`, + productID, quantity).Scan(&priceResult).Error; err != nil || priceResult.Price == 0 { + return fmt.Errorf("prix introuvable pour product_id=%d qty=%.3f", productID, quantity) + } + + var existing struct { + ID int `gorm:"column:id"` + Quantity float64 `gorm:"column:quantity"` + Price float64 `gorm:"column:price"` + } + tx.Raw(`SELECT id, quantity, price FROM baskets WHERE username = ? AND product_id = ?`, + username, productID).Scan(&existing) + + if existing.ID != 0 { + return tx.Raw(` + UPDATE baskets SET quantity = ?, price = ?, created_at = CURRENT_TIMESTAMP + WHERE id = ? RETURNING id, username, product_id, quantity, price, created_at`, + existing.Quantity+quantity, existing.Price+priceResult.Price, + existing.ID).Scan(&basket).Error + } + return tx.Raw(` + INSERT INTO baskets (username, product_id, quantity, price, created_at) + VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP) + RETURNING id, username, product_id, quantity, price, created_at`, + username, productID, quantity, priceResult.Price).Scan(&basket).Error + }) + if err != nil { + return nil, err } - if result.RowsAffected == 0 { - return fmt.Errorf("stock insuffisant pour le produit %d", productID) - } - return nil + return &basket, nil } // DeleteProductFromBasket supprime un produit spécifique du panier et restitue le stock. @@ -329,55 +291,6 @@ func (d *Database) UpdateBasketItemQuantity(basketID int, newQuantity float64) e }) } -// ExtendBasketReservations prolonge les réservations -func (d *Database) ExtendBasketReservations(username string) error { - var items []struct { - ProductID int `gorm:"column:product_id"` - Quantity float64 `gorm:"column:quantity"` - } - if err := d.GDB.Raw(`SELECT product_id, quantity FROM baskets WHERE username = ?`, username).Scan(&items).Error; err != nil { - return fmt.Errorf("erreur récupération panier: %w", err) - } - - for _, item := range items { - var stockResult struct { - Stock float64 `gorm:"column:stock"` - } - if err := d.GDB.Raw(`SELECT stock FROM products WHERE id = ?`, item.ProductID).Scan(&stockResult).Error; err != nil { - return fmt.Errorf("produit %d non trouvé: %w", item.ProductID, err) - } - if stockResult.Stock < item.Quantity { - return fmt.Errorf("stock insuffisant pour le produit %d (demandé: %g, disponible: %g)", - item.ProductID, item.Quantity, stockResult.Stock) - } - } - - newReservation := time.Now().Add(15 * time.Minute) - if err := d.GDB.Exec(`UPDATE baskets SET reserved_until = ? WHERE username = ?`, - newReservation, username).Error; err != nil { - return fmt.Errorf("erreur prolongation: %w", err) - } - - log.Printf("✅ Réservations prolongées pour %s jusqu'à %s", - username, newReservation.Format("15:04:05")) - return nil -} - -// CheckBasketReservations vérifie si les réservations sont expirées -func (d *Database) CheckBasketReservations(username string) (bool, error) { - var result struct { - Count int `gorm:"column:count"` - } - err := d.GDB.Raw(` - SELECT COUNT(*) as count FROM baskets - WHERE username = ? AND (reserved_until IS NULL OR reserved_until < CURRENT_TIMESTAMP)`, - username).Scan(&result).Error - if err != nil { - return false, err - } - return result.Count > 0, nil -} - func (d *Database) GetBasketItemOwner(basketID int) (string, error) { var username string err := d.GDB.Raw(`SELECT username FROM baskets WHERE id = ?`, basketID).Scan(&username).Error diff --git a/backend/gestion/db/db_cancel_command.go b/backend/gestion/db/db_cancel_command.go index 5736ed4b..e7cd2f36 100644 --- a/backend/gestion/db/db_cancel_command.go +++ b/backend/gestion/db/db_cancel_command.go @@ -179,8 +179,6 @@ func (d *Database) CheckCommandETAExistsAndValid(commandID int) bool { } func (d *Database) DeleteCommandAtomic(commandID int, deletedBy, role string) error { - log.Printf("🔒 [DeleteAtomic] START - cmd=%d, by=%s (%s)", commandID, deletedBy, role) - return d.GDB.Transaction(func(tx *gorm.DB) error { var cmdResult struct { Status string `gorm:"column:status"` @@ -188,8 +186,8 @@ func (d *Database) DeleteCommandAtomic(commandID int, deletedBy, role string) er LivreurAssign string `gorm:"column:livreur_assign"` } err := tx.Raw(` - SELECT status, username, COALESCE(livreur_assign, '') as livreur_assign - FROM commandes WHERE id = ? FOR UPDATE`, commandID).Scan(&cmdResult).Error + SELECT status, username, COALESCE(livreur_assign, '') as livreur_assign + FROM commandes WHERE id = ? FOR UPDATE`, commandID).Scan(&cmdResult).Error if err != nil { return err } @@ -199,19 +197,26 @@ func (d *Database) DeleteCommandAtomic(commandID int, deletedBy, role string) er log.Printf("📋 [DeleteAtomic] Trouvée - status=%s, client=%s", cmdResult.Status, cmdResult.Username) - if err := tx.Exec(` - UPDATE products p - SET stock = stock + ci.quantite, updated_at = CURRENT_TIMESTAMP - FROM command_items ci - WHERE ci.command_id = ? AND ci.product_id = p.id`, commandID).Error; err != nil { - log.Printf("⚠️ [DeleteAtomic] Erreur remboursement: %v", err) + // ✅ Ne restitue le stock QUE si pas déjà fait + stockAlreadyRestored := cmdResult.Status == "cancelled" || cmdResult.Status == "approved" + if !stockAlreadyRestored { + if err := tx.Exec(` + UPDATE products p + SET stock = stock + ci.quantite, updated_at = CURRENT_TIMESTAMP + FROM command_items ci + WHERE ci.command_id = ? AND ci.product_id = p.id`, commandID).Error; err != nil { + log.Printf("⚠️ [DeleteAtomic] Erreur remboursement: %v", err) + } else { + log.Printf("✅ [DeleteAtomic] Stock remboursé (statut: %s)", cmdResult.Status) + } } else { - log.Printf("✅ [DeleteAtomic] Stock remboursé") + log.Printf("⏭️ [DeleteAtomic] Stock NON restitué - statut=%s", cmdResult.Status) } + // ✅ Log suppression tx.Exec(` - INSERT INTO command_logs (command_id, status, message, author, created_at) - VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)`, + INSERT INTO command_logs (command_id, status, message, author, created_at) + VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)`, commandID, "deleted", fmt.Sprintf("Supprimée par %s (%s) - Ancien statut: %s", deletedBy, role, cmdResult.Status), deletedBy) diff --git a/backend/gestion/db/db_product.go b/backend/gestion/db/db_product.go index acea1534..d1fa452b 100644 --- a/backend/gestion/db/db_product.go +++ b/backend/gestion/db/db_product.go @@ -177,12 +177,12 @@ func (d *Database) GetProductsByCategory(category string) ([]models.Product, err return products, nil } -func (d *Database) UpdateProduct(productID int, name, category, description, unit string, stock float64, prices []models.ProductPrice) error { +func (d *Database) UpdateProduct(productID int, name, category, description, unit string, prices []models.ProductPrice) error { err := d.GDB.Exec(` UPDATE products - SET name = ?, category = ?, description = ?, stock = ?, unit = ?, updated_at = ? + SET name = ?, category = ?, description = ?, unit = ?, updated_at = ? WHERE id = ?`, - name, category, description, stock, unit, time.Now(), productID).Error + name, category, description, unit, time.Now(), productID).Error if err != nil { return fmt.Errorf("erreur mise à jour produit: %w", err) } @@ -199,6 +199,18 @@ func (d *Database) UpdateProduct(productID int, name, category, description, uni return nil } +func (d *Database) SetProductStock(productID int, stock float64) error { + result := d.GDB.Exec(`UPDATE products SET stock = ?, updated_at = ? WHERE id = ?`, + stock, time.Now(), productID) + if result.Error != nil { + return fmt.Errorf("erreur mise à jour stock: %w", result.Error) + } + if result.RowsAffected == 0 { + return fmt.Errorf("produit non trouvé") + } + return nil +} + // DeleteProduct supprime un produit func (d *Database) DeleteProduct(productID int) error { result := d.GDB.Exec(`DELETE FROM products WHERE id = ?`, productID) diff --git a/backend/gestion/handlers/cancel_command.go b/backend/gestion/handlers/cancel_command.go index 5dd4cb02..d0b7b840 100644 --- a/backend/gestion/handlers/cancel_command.go +++ b/backend/gestion/handlers/cancel_command.go @@ -266,9 +266,12 @@ func GetMyCancellationHistory(c *gin.Context) { return } - var totalPenalty int - penaltyQuery := `SELECT COALESCE(amende, 0) FROM clients WHERE username = $1` - database.QueryRow(penaltyQuery).Scan(&totalPenalty) + var penaltyResult struct { + Amende int `gorm:"column:amende"` + } + database.GDB.Raw(`SELECT COALESCE(amende, 0) as amende FROM clients WHERE username = ?`, + username).Scan(&penaltyResult) + totalPenalty := penaltyResult.Amende c.JSON(http.StatusOK, gin.H{ "success": true, diff --git a/backend/gestion/handlers/panier.go b/backend/gestion/handlers/panier.go index 38ddc1db..342458a7 100644 --- a/backend/gestion/handlers/panier.go +++ b/backend/gestion/handlers/panier.go @@ -12,6 +12,7 @@ import ( "gestion/utils" "log" "net/http" + "strings" "github.com/gin-gonic/gin" ) @@ -48,35 +49,32 @@ func AddProductsBasket(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "Quantité invalide"}) return } - - // Si product_id fourni par le mobile, on l'utilise directement (plus fiable) - if req.ProductID > 0 || req.NameProduct == "" || req.Category == "" { - stock, err := database.GetProductStockByID(req.ProductID) - if err != nil { - log.Printf("❌ [ADD_PANIER] Produit %d non trouvé: %v", req.ProductID, err) - c.JSON(http.StatusNotFound, gin.H{"error": "Produit non trouvé"}) - return - } - if stock < req.Quantity { - c.JSON(http.StatusBadRequest, gin.H{"error": "Stock insuffisant", "available": stock}) - return - } - if err := database.DecrementProductStockByID(req.ProductID, req.Quantity); err != nil { - log.Printf("❌ [ADD_PANIER] Erreur décrement stock product_id=%d: %v", req.ProductID, err) - utils.ServerErr(c, "Impossible d'ajouter le produit au panier", err) - return - } - panier, err := database.AddProductInBasketByID(req.Username, req.ProductID, req.Quantity) - if err != nil { - log.Printf("❌ [ADD_PANIER] Erreur ajout product_id=%d qty=%.3f: %v", req.ProductID, req.Quantity, err) - utils.ServerErr(c, "Impossible d'ajouter le produit au panier", err) - return - } - - c.JSON(http.StatusOK, gin.H{"success": true, "message": "Produit ajouté au panier avec succès", "panier": panier}) + if req.ProductID <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "product_id requis"}) return } + // ✅ Une seule fonction, une seule transaction atomique + panier, err := database.DecrementAndAddToBasket(req.Username, req.ProductID, req.Quantity) + if err != nil { + log.Printf("❌ [ADD_PANIER] product_id=%d qty=%.3f: %v", req.ProductID, req.Quantity, err) + if err.Error() == "stock insuffisant" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Stock insuffisant"}) + return + } + if strings.Contains(err.Error(), "prix introuvable") { + c.JSON(http.StatusBadRequest, gin.H{"error": "Aucun prix configuré pour ce produit"}) + return + } + utils.ServerErr(c, "Impossible d'ajouter le produit au panier", err) + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "Produit ajouté au panier avec succès", + "panier": panier, + }) } // ============================================ diff --git a/backend/gestion/handlers/product.go b/backend/gestion/handlers/product.go index b8d29e10..e9d7edd5 100644 --- a/backend/gestion/handlers/product.go +++ b/backend/gestion/handlers/product.go @@ -600,7 +600,6 @@ func UpdateProduct(c *gin.Context) { Name string `json:"name"` Category string `json:"category"` Description string `json:"description"` - Stock float64 `json:"stock"` Unit string `json:"unit"` Prices []models.ProductPrice `json:"prices"` } @@ -629,12 +628,8 @@ func UpdateProduct(c *gin.Context) { if updateData.Unit == "" { updateData.Unit = "u" } - if err := validateUnit(updateData.Unit); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - if err := validateStock(updateData.Stock); err != nil { + if err := validateUnit(updateData.Unit); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } @@ -653,7 +648,7 @@ func UpdateProduct(c *gin.Context) { log.Printf("🔄 [UpdateProduct] %s met à jour produit #%d", username, id) - if err := database.UpdateProduct(id, updateData.Name, updateData.Category, updateData.Description, updateData.Unit, updateData.Stock, updateData.Prices); err != nil { + if err := database.UpdateProduct(id, updateData.Name, updateData.Category, updateData.Description, updateData.Unit, updateData.Prices); err != nil { log.Printf("❌ [UpdateProduct] Erreur UPDATE: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur mise à jour"}) return @@ -672,6 +667,60 @@ func UpdateProduct(c *gin.Context) { }) } +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 + } + log.Printf("🔄 [UpdateStock] %s met à jour le stock #%d", username, id) + + 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, + }) + +} + func DeleteMedia(c *gin.Context) { database := c.MustGet("database").(*db.Database) diff --git a/backend/gestion/routes/routes.go b/backend/gestion/routes/routes.go index bd737ec9..08d422ea 100644 --- a/backend/gestion/routes/routes.go +++ b/backend/gestion/routes/routes.go @@ -188,6 +188,7 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services adminGroupV2.DELETE("/products/:id", handlers.DeleteProduct) adminGroupV2.POST("/products/:id/media", handlers.UploadMedia) adminGroupV2.DELETE("/products/:id/media/:media_id", handlers.DeleteMedia) + adminGroupV2.POST("/products/:id/stock", handlers.UpdateStock) // ============================================ // CATÉGORIES - GESTION ADMIN // ============================================