fix: stock manage

This commit is contained in:
2026-05-17 13:55:56 +02:00
parent 16792094e3
commit ac1d3d8d9f
3 changed files with 72 additions and 69 deletions
+34 -52
View File
@@ -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(&currentStock).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
+18 -5
View File
@@ -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)
+20 -12
View File
@@ -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)