diff --git a/backend/gestion/db/db_basket.go b/backend/gestion/db/db_basket.go index 3c8c1ee8..371c2d40 100644 --- a/backend/gestion/db/db_basket.go +++ b/backend/gestion/db/db_basket.go @@ -43,7 +43,9 @@ func (d *Database) GetAllProductsInBasket(username string) ([]models.Panier, err return baskets, nil } -func (d *Database) DecrementAndAddToBasket(username string, productID int, quantity float64) (*models.Panier, error) { +// AddToBasket vérifie le stock disponible et ajoute l'article au panier. +// Le stock n'est pas décrémenté ici — il l'est uniquement au checkout. +func (d *Database) AddToBasket(username string, productID int, quantity float64) (*models.Panier, error) { var basket models.Panier err := d.GDB.Transaction(func(tx *gorm.DB) error { var currentStock float64 @@ -54,11 +56,6 @@ func (d *Database) DecrementAndAddToBasket(username string, productID int, quant 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) - } - var priceResult struct { Price float64 `gorm:"column:price"` } @@ -97,38 +94,28 @@ func (d *Database) DecrementAndAddToBasket(username string, productID int, quant return &basket, nil } -// DeleteProductFromBasket supprime un produit spécifique du panier et restitue le stock. +// DeleteProductFromBasket supprime un produit spécifique du panier. +// Le stock n'est pas restitué car il n'a pas été décrémenté à l'ajout. func (d *Database) DeleteProductFromBasket(basketID int) error { - return d.GDB.Transaction(func(tx *gorm.DB) error { - var item struct { - ProductID int `gorm:"column:product_id"` - Quantity float64 `gorm:"column:quantity"` - } - 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 { - return fmt.Errorf("produit non trouvé dans le panier") - } - - if err := tx.Exec(`UPDATE products SET stock = stock + ? WHERE id = ?`, - item.Quantity, item.ProductID).Error; err != nil { - return fmt.Errorf("erreur restitution stock: %w", err) - } - - result := tx.Exec(`DELETE FROM baskets WHERE id = ?`, basketID) - if result.Error != nil { - return fmt.Errorf("erreur lors de la suppression du produit: %w", result.Error) - } - if result.RowsAffected == 0 { - return fmt.Errorf("produit non trouvé dans le panier") - } - return nil - }) + result := d.GDB.Exec(`DELETE FROM baskets WHERE id = ?`, basketID) + if result.Error != nil { + return fmt.Errorf("erreur lors de la suppression du produit: %w", result.Error) + } + if result.RowsAffected == 0 { + return fmt.Errorf("produit non trouvé dans le panier") + } + return nil } -// ClearBasket vide complètement le panier d'un utilisateur et restitue les stocks. +// ClearBasket vide complètement le panier d'un utilisateur. +// Le stock n'est pas restitué car il n'a pas été décrémenté à l'ajout. func (d *Database) ClearBasket(username string) error { + return d.GDB.Exec(`DELETE FROM baskets WHERE username = ?`, username).Error +} + +// ClearBasketOnCheckout décrémente le stock pour chaque article du panier puis vide le panier. +// C'est ici que le stock est effectivement consommé, au moment de la validation de la commande. +func (d *Database) ClearBasketOnCheckout(username string) error { return d.GDB.Transaction(func(tx *gorm.DB) error { var items []struct { ProductID int `gorm:"column:product_id"` @@ -137,29 +124,24 @@ func (d *Database) ClearBasket(username string) error { 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 + + for _, item := range items { + var currentStock float64 + if err := tx.Raw(`SELECT stock FROM products WHERE id = ? FOR UPDATE`, item.ProductID).Scan(¤tStock).Error; err != nil { + return fmt.Errorf("erreur lecture stock produit %d: %w", item.ProductID, err) + } + if currentStock < item.Quantity { + return fmt.Errorf("stock insuffisant pour le produit %d", item.ProductID) + } + if err := tx.Exec(`UPDATE products SET stock = stock - ? WHERE id = ?`, item.Quantity, item.ProductID).Error; err != nil { + return fmt.Errorf("erreur décrémentation stock produit %d: %w", item.ProductID, err) + } } - if err := tx.Exec(` - UPDATE products p - SET stock = stock + b.quantity - FROM baskets b - WHERE b.username = ? AND b.product_id = p.id`, username).Error; err != nil { - return fmt.Errorf("erreur restitution stock: %w", err) - } - if err := tx.Exec(`DELETE FROM baskets WHERE username = ?`, username).Error; err != nil { - return fmt.Errorf("erreur lors du vidage du panier: %w", err) - } - return nil + return tx.Exec(`DELETE FROM baskets WHERE username = ?`, username).Error }) } -// ClearBasketOnCheckout vide le panier après commande validée SANS restituer le stock. -func (d *Database) ClearBasketOnCheckout(username string) error { - return d.GDB.Exec(`DELETE FROM baskets WHERE username = ?`, username).Error -} - 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/handlers/eta.go b/backend/gestion/handlers/eta.go index 96ed5ba2..d385db66 100644 --- a/backend/gestion/handlers/eta.go +++ b/backend/gestion/handlers/eta.go @@ -93,19 +93,32 @@ func GetOrderETA(c *gin.Context) { return } - // Pour pending: aucune estimation disponible - if cmdStatus == "pending" { - log.Printf("⏳ [ETA] Commande en attente d'assignation - pas d'ETA") + // Pour pending/assigned: pas encore de position livreur disponible + if cmdStatus == "pending" || cmdStatus == "assigned" { + log.Printf("⏳ [ETA] Commande %s - pas d'ETA disponible", cmdStatus) c.JSON(http.StatusOK, gin.H{ "success": true, "command_id": commandID, "status": cmdStatus, "eta_available": false, - "message": "En attente d'assignation d'un livreur", + "message": "En attente de démarrage de la livraison", }) return } - // Pour assigned/en_route/arrived: calcul ETA réel via position du livreur + + // Pour arrived: livreur sur place, ETA non pertinent + if cmdStatus == "arrived" { + log.Printf("ℹ️ [ETA] Commande arrived - livreur déjà sur place") + c.JSON(http.StatusOK, gin.H{ + "success": true, + "command_id": commandID, + "status": cmdStatus, + "eta_available": false, + "message": "Le livreur est arrivé à destination", + }) + return + } + // Pour en_route: calcul ETA réel via position du livreur // 6️⃣ VÉRIFIER LE CACHE REDIS POUR ETA etaKey := fmt.Sprintf("command:eta:%d", commandID) diff --git a/backend/gestion/handlers/panier.go b/backend/gestion/handlers/panier.go index 8892eec9..c8620bba 100644 --- a/backend/gestion/handlers/panier.go +++ b/backend/gestion/handlers/panier.go @@ -51,8 +51,7 @@ func AddProductsBasket(c *gin.Context) { return } - // ✅ Une seule fonction, une seule transaction atomique - panier, err := database.DecrementAndAddToBasket(req.Username, req.ProductID, req.Quantity) + panier, err := database.AddToBasket(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" { @@ -192,10 +191,9 @@ func DeleteProductFromBasket(c *gin.Context) { log.Printf("✅ [DEL_PANIER] Article %d supprimé", req.ID) c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "Produit supprimé du panier avec succès", - "item_id": req.ID, - "stock_released": true, + "success": true, + "message": "Produit supprimé du panier avec succès", + "item_id": req.ID, }) } @@ -231,9 +229,8 @@ func ClearBasket(c *gin.Context) { log.Printf("✅ [CLEAR_PANIER] Panier %s vidé: %d articles supprimés", authUsernameStr, len(baskets)) c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "Panier vidé avec succès", - "stock_released": len(baskets), + "success": true, + "message": "Panier vidé avec succès", }) } @@ -430,6 +427,7 @@ func ValidateBasket(c *gin.Context) { log.Printf("✅ [CHECKOUT] Commande %d créée", commandID) + // Pour le paiement crypto, le panier/stock sera décrémenté à la confirmation du webhook NowPayments. if isCrypto { np := c.MustGet("nowpayments").(*services.NowPaymentsClient) ipnURL := fmt.Sprintf("%s/api/v1/webhooks/nowpayments", getBaseURL(c)) @@ -481,14 +479,24 @@ func ValidateBasket(c *gin.Context) { go database.NotifyAllAdminCabine(commandID, usernameStr, req.DeliveryAddress) // ============================================ - // 3️⃣ Vider le panier (sans restituer le stock — déjà déduit à l'ajout) + // 3️⃣ Décrémenter le stock et vider le panier // ============================================ err = database.ClearBasketOnCheckout(usernameStr) if err != nil { - utils.ServerErr(c, "Impossible de vider le panier", err) + // Stock insuffisant au moment du checkout (concurrent) → annuler la commande + if strings.Contains(err.Error(), "stock insuffisant") { + _ = database.CancelCryptoCommand(commandID) + if referralUsed > 0 { + _ = database.CreditClientReferral(usernameStr, referralUsed) + } + log.Printf("❌ [CHECKOUT] Stock insuffisant au moment de la validation: %v", err) + c.JSON(http.StatusBadRequest, gin.H{"error": "Désolé ! Tu as trop attendu pour passer commande! Le stock ou le produit n'est plus disponible, repasse commande"}) + return + } + utils.ServerErr(c, "Impossible de valider le panier", err) return } - log.Printf("🧹 [CHECKOUT] Panier vidé") + log.Printf("🧹 [CHECKOUT] Stock décrémenté et panier vidé") // ============================================ // 4️⃣ Auto-assignation livreur (optionnel)