diff --git a/backend/gestion/db/db_basket.go b/backend/gestion/db/db_basket.go index 9142dad5..3c8c1ee8 100644 --- a/backend/gestion/db/db_basket.go +++ b/backend/gestion/db/db_basket.go @@ -46,14 +46,18 @@ func (d *Database) GetAllProductsInBasket(username string) ([]models.Panier, err 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) + var currentStock float64 + if err := tx.Raw(`SELECT stock FROM products WHERE id = ? FOR UPDATE`, productID).Scan(¤tStock).Error; err != nil { + return fmt.Errorf("erreur lecture stock: %w", err) + } + if currentStock < quantity { + return fmt.Errorf("stock insuffisant") + } + + result := tx.Exec(`UPDATE products SET stock = stock - ? WHERE id = ?`, quantity, productID) 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"` @@ -100,7 +104,7 @@ func (d *Database) DeleteProductFromBasket(basketID int) error { ProductID int `gorm:"column:product_id"` Quantity float64 `gorm:"column:quantity"` } - if err := tx.Raw(`SELECT product_id, quantity FROM baskets WHERE id = ?`, basketID).Scan(&item).Error; err != nil { + if err := tx.Raw(`SELECT product_id, quantity FROM baskets WHERE id = ? FOR UPDATE`, basketID).Scan(&item).Error; err != nil { return fmt.Errorf("produit non trouvé dans le panier") } if item.ProductID == 0 { @@ -126,6 +130,17 @@ func (d *Database) DeleteProductFromBasket(basketID int) error { // ClearBasket vide complètement le panier d'un utilisateur et restitue les stocks. func (d *Database) ClearBasket(username string) error { return d.GDB.Transaction(func(tx *gorm.DB) error { + var items []struct { + ProductID int `gorm:"column:product_id"` + Quantity float64 `gorm:"column:quantity"` + } + if err := tx.Raw(`SELECT product_id, quantity FROM baskets WHERE username = ? FOR UPDATE`, username).Scan(&items).Error; err != nil { + return fmt.Errorf("erreur lecture panier: %w", err) + } + if len(items) == 0 { + return nil + } + if err := tx.Exec(` UPDATE products p SET stock = stock + b.quantity @@ -157,6 +172,36 @@ func (d *Database) GetBasketItemOwner(basketID int) (string, error) { return username, nil } +// GetUnavailableBasketItems retourne les noms des produits du panier dont tous les prix ont été désactivés. +func (d *Database) GetUnavailableBasketItems(username string) ([]string, error) { + var names []string + err := d.GDB.Raw(` + SELECT DISTINCT p.name + FROM baskets b + INNER JOIN products p ON b.product_id = p.id + WHERE b.username = ? + AND NOT EXISTS ( + SELECT 1 FROM product_prices pp + WHERE pp.product_id = b.product_id + AND pp.quantity <= b.quantity + AND pp.active_price = true + )`, username).Scan(&names).Error + if err != nil { + return nil, fmt.Errorf("erreur vérification disponibilité: %w", err) + } + return names, nil +} + +// GetReservedQuantityInBaskets retourne la somme des quantités d'un produit dans tous les paniers actifs. +func (d *Database) GetReservedQuantityInBaskets(productID int) (float64, error) { + var total float64 + err := d.GDB.Raw(`SELECT COALESCE(SUM(quantity), 0) FROM baskets WHERE product_id = ?`, productID).Scan(&total).Error + if err != nil { + return 0, fmt.Errorf("erreur lecture réservations panier: %w", err) + } + return total, nil +} + func (d *Database) GetBasketItems(username string) ([]map[string]any, error) { var items []map[string]any if err := d.GDB.Raw(`SELECT product_id, quantity::float8 as quantity, price::float8 as price FROM baskets WHERE username = ?`, diff --git a/backend/gestion/db/db_cancel_command.go b/backend/gestion/db/db_cancel_command.go index e7cd2f36..40c23c03 100644 --- a/backend/gestion/db/db_cancel_command.go +++ b/backend/gestion/db/db_cancel_command.go @@ -86,10 +86,9 @@ func (d *Database) CancelCommandAtomic(commandID int, username, reason string, f 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("⚠️ [CancelAtomic] Erreur remboursement stock: %v", err) - } else { - log.Printf("✅ [CancelAtomic] Stock remboursé") + return fmt.Errorf("erreur remboursement stock: %w", err) } + log.Printf("✅ [CancelAtomic] Stock remboursé") if err := tx.Exec(` UPDATE clients @@ -321,3 +320,13 @@ func (d *Database) AddClientPenalty(username string, points int) error { return nil } + +func (d *Database) RestoreCommandStock(commandID int) error { + return d.GDB.Transaction(func(tx *gorm.DB) error { + return 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 + }) +} diff --git a/backend/gestion/db/db_command_items.go b/backend/gestion/db/db_command_items.go index 1ab4d8ce..259eda3e 100644 --- a/backend/gestion/db/db_command_items.go +++ b/backend/gestion/db/db_command_items.go @@ -81,10 +81,10 @@ func validateItemStatus(status string) error { status = strings.ToLower(strings.TrimSpace(status)) if !slices.Contains(validStatuses, status) { - return fmt.Errorf("...") + return fmt.Errorf("statut invalide: %s", status) } - return fmt.Errorf("statut invalide: %s", status) + return nil } // ============================================ @@ -310,18 +310,21 @@ func (d *Database) DeleteCommandItem(commandID, itemID int) error { return err } - // Récupérer le prix et la quantité avant suppression pour mettre à jour le total var result struct { - Prix float64 `gorm:"column:prix"` - Quantite float64 `gorm:"column:quantite"` + Prix float64 `gorm:"column:prix"` + Quantite float64 `gorm:"column:quantite"` + ProductID int `gorm:"column:product_id"` } - if err := d.GDB.Raw(`SELECT prix, quantite FROM command_items WHERE id = ? AND command_id = ?`, itemID, commandID).Scan(&result).Error; err != nil { + if err := d.GDB.Raw(`SELECT prix, quantite, product_id FROM command_items WHERE id = ? AND command_id = ?`, itemID, commandID).Scan(&result).Error; err != nil { return fmt.Errorf("erreur vérification item: %w", err) } if result.Prix == 0 && result.Quantite == 0 { return fmt.Errorf("item %d non trouvé dans la commande %d", itemID, commandID) } + var cmdStatus string + d.GDB.Raw(`SELECT status FROM commandes WHERE id = ?`, commandID).Scan(&cmdStatus) + // Supprimer l'item if err := d.GDB.Exec(`DELETE FROM command_items WHERE id = ?`, itemID).Error; err != nil { log.Printf("❌ Erreur DELETE command_items: %v", err) @@ -336,6 +339,17 @@ func (d *Database) DeleteCommandItem(commandID, itemID int) error { log.Printf("⚠️ [DeleteCommandItem] Erreur maj total commande: %v", err) } + // Restaurer le stock si la commande n'est pas déjà terminée + noRestoreStatuses := []string{"cancelled", "approved", "livre"} + if result.ProductID != 0 && !slices.Contains(noRestoreStatuses, cmdStatus) { + if err := d.GDB.Exec(`UPDATE products SET stock = stock + ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, + result.Quantite, result.ProductID).Error; err != nil { + log.Printf("⚠️ [DeleteCommandItem] Erreur restauration stock: %v", err) + } else { + log.Printf("✅ [DeleteCommandItem] Stock restauré: +%.3f pour produit %d", result.Quantite, result.ProductID) + } + } + return nil } diff --git a/backend/gestion/db/db_payments.go b/backend/gestion/db/db_payments.go index 83f94ba1..5975223e 100644 --- a/backend/gestion/db/db_payments.go +++ b/backend/gestion/db/db_payments.go @@ -3,7 +3,6 @@ package db import ( "fmt" "gestion/models" - "log" "gorm.io/gorm" ) @@ -67,6 +66,17 @@ func (d *Database) ActivateCryptoCommand(commandID int) error { func (d *Database) CancelCryptoCommand(commandID int) error { return d.GDB.Transaction(func(tx *gorm.DB) error { + var cmdStatus string + if err := tx.Raw(`SELECT status FROM commandes WHERE id = ? FOR UPDATE`, commandID).Scan(&cmdStatus).Error; err != nil { + return err + } + if cmdStatus == "" { + return fmt.Errorf("commande non trouvée") + } + if cmdStatus != "pending_payment" { + return fmt.Errorf("commande non annulable (statut: %s)", cmdStatus) + } + type item struct { ProductID int Quantite float64 @@ -77,9 +87,9 @@ func (d *Database) CancelCryptoCommand(commandID int) error { } for _, it := range items { if err := tx.Exec(`UPDATE products SET stock = stock + ? WHERE id = ?`, it.Quantite, it.ProductID).Error; err != nil { - log.Printf("[CANCEL CRYPTO] erreur restauration stock produit %d: %v", it.ProductID, err) + return fmt.Errorf("erreur restauration stock produit %d: %w", it.ProductID, err) } } - return tx.Exec(`UPDATE commandes SET status = 'cancelled', updated_at = NOW() WHERE id = ? AND status = 'pending_payment'`, commandID).Error + return tx.Exec(`UPDATE commandes SET status = 'cancelled', updated_at = NOW() WHERE id = ?`, commandID).Error }) } diff --git a/backend/gestion/handlers/commands.go b/backend/gestion/handlers/commands.go index 68063d03..68a23b1d 100644 --- a/backend/gestion/handlers/commands.go +++ b/backend/gestion/handlers/commands.go @@ -1119,6 +1119,19 @@ func UpdateCommandStatusAdmin(c *gin.Context) { return } + if req.Status == "cancelled" { + current, errCmd := database.GetCommandByID(commandID) + if errCmd == nil { + currentStatus, _ := current["status"].(string) + alreadyDone := currentStatus == "cancelled" || currentStatus == "approved" || currentStatus == "livre" + if !alreadyDone { + if err := database.RestoreCommandStock(commandID); err != nil { + log.Printf("⚠️ [STATUS_ADMIN] Erreur restauration stock cmd %d: %v", commandID, err) + } + } + } + } + if err := database.UpdateCommandStatus(commandID, req.Status); err != nil { utils.ServerErr(c, "Impossible de mettre à jour le statut", err) return diff --git a/backend/gestion/handlers/deleviry.go b/backend/gestion/handlers/deleviry.go index 04e12972..4cf150a4 100644 --- a/backend/gestion/handlers/deleviry.go +++ b/backend/gestion/handlers/deleviry.go @@ -392,8 +392,12 @@ func UpdateDeliveryStatus(c *gin.Context) { database.CompleteDeliveryAndProcessNext(usernameStr, commandID) case "cancelled": - // Annulation par le livreur - Nettoyer la queue log.Printf("🚫 Livraison annulée par livreur - Nettoyage queue cmd %d", commandID) + if err := database.RestoreCommandStock(commandID); err != nil { + log.Printf("⚠️ [STATUS_LIVREUR] Erreur restauration stock cmd %d: %v", commandID, err) + } else { + log.Printf("✅ [STATUS_LIVREUR] Stock restauré pour cmd %d", commandID) + } database.CompleteDeliveryAndProcessNext(usernameStr, commandID) case "arrived": diff --git a/backend/gestion/handlers/panier.go b/backend/gestion/handlers/panier.go index 342458a7..3b1d7930 100644 --- a/backend/gestion/handlers/panier.go +++ b/backend/gestion/handlers/panier.go @@ -13,6 +13,7 @@ import ( "log" "net/http" "strings" + "time" "github.com/gin-gonic/gin" ) @@ -266,6 +267,14 @@ func ValidateBasket(c *gin.Context) { } usernameStr := username.(string) + lockKey := fmt.Sprintf("checkout_lock:%s", usernameStr) + locked, errLock := db.Redis.SetNX(db.RedisCtx, lockKey, "1", 30*time.Second).Result() + if errLock != nil || !locked { + c.JSON(http.StatusConflict, gin.H{"error": "Un checkout est déjà en cours pour ce compte"}) + return + } + defer db.Redis.Del(db.RedisCtx, lockKey) + var req struct { DeliveryAddress string `json:"delivery_address" binding:"required"` UseReferralBalance bool `json:"use_referral_balance"` @@ -394,6 +403,21 @@ func ValidateBasket(c *gin.Context) { log.Printf("✅ [CHECKOUT] Crédit parrainage -%.2f€ débité pour %s", referralUsed, usernameStr) } + // Vérifier que tous les produits du panier ont encore un prix actif + unavailable, err := database.GetUnavailableBasketItems(usernameStr) + if err != nil { + utils.ServerErr(c, "Erreur vérification produits", err) + return + } + if len(unavailable) > 0 { + log.Printf("❌ [CHECKOUT] Produits sans prix actif: %v", unavailable) + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Certains produits de votre panier ne sont plus disponibles", + "products": unavailable, + }) + return + } + // Vérification option crypto isCrypto := req.PaymentMethod == "crypto" if isCrypto { diff --git a/backend/gestion/handlers/product.go b/backend/gestion/handlers/product.go index e9d7edd5..0bee9888 100644 --- a/backend/gestion/handlers/product.go +++ b/backend/gestion/handlers/product.go @@ -701,7 +701,18 @@ func UpdateStock(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - log.Printf("🔄 [UpdateStock] %s met à jour le stock #%d", username, id) + + 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) @@ -715,8 +726,9 @@ func UpdateStock(c *gin.Context) { log.Printf("✅ [UpdateStock] le stock #%d est mis à jour", id) c.JSON(http.StatusOK, gin.H{ - "success": true, - "product": updatedProduct, + "success": true, + "product": updatedProduct, + "reserved_in_baskets": reserved, }) }