fix: stock manage
This commit is contained in:
@@ -43,7 +43,9 @@ func (d *Database) GetAllProductsInBasket(username string) ([]models.Panier, err
|
|||||||
return baskets, nil
|
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
|
var basket models.Panier
|
||||||
err := d.GDB.Transaction(func(tx *gorm.DB) error {
|
err := d.GDB.Transaction(func(tx *gorm.DB) error {
|
||||||
var currentStock float64
|
var currentStock float64
|
||||||
@@ -54,11 +56,6 @@ func (d *Database) DecrementAndAddToBasket(username string, productID int, quant
|
|||||||
return fmt.Errorf("stock insuffisant")
|
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 {
|
var priceResult struct {
|
||||||
Price float64 `gorm:"column:price"`
|
Price float64 `gorm:"column:price"`
|
||||||
}
|
}
|
||||||
@@ -97,38 +94,28 @@ func (d *Database) DecrementAndAddToBasket(username string, productID int, quant
|
|||||||
return &basket, nil
|
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 {
|
func (d *Database) DeleteProductFromBasket(basketID int) error {
|
||||||
return d.GDB.Transaction(func(tx *gorm.DB) error {
|
result := d.GDB.Exec(`DELETE FROM baskets WHERE id = ?`, basketID)
|
||||||
var item struct {
|
if result.Error != nil {
|
||||||
ProductID int `gorm:"column:product_id"`
|
return fmt.Errorf("erreur lors de la suppression du produit: %w", result.Error)
|
||||||
Quantity float64 `gorm:"column:quantity"`
|
}
|
||||||
}
|
if result.RowsAffected == 0 {
|
||||||
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")
|
||||||
return fmt.Errorf("produit non trouvé dans le panier")
|
}
|
||||||
}
|
return nil
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
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 {
|
return d.GDB.Transaction(func(tx *gorm.DB) error {
|
||||||
var items []struct {
|
var items []struct {
|
||||||
ProductID int `gorm:"column:product_id"`
|
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 {
|
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)
|
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(`
|
return tx.Exec(`DELETE FROM baskets WHERE username = ?`, username).Error
|
||||||
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
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
func (d *Database) GetBasketItemOwner(basketID int) (string, error) {
|
||||||
var username string
|
var username string
|
||||||
err := d.GDB.Raw(`SELECT username FROM baskets WHERE id = ?`, basketID).Scan(&username).Error
|
err := d.GDB.Raw(`SELECT username FROM baskets WHERE id = ?`, basketID).Scan(&username).Error
|
||||||
|
|||||||
@@ -93,19 +93,32 @@ func GetOrderETA(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pour pending: aucune estimation disponible
|
// Pour pending/assigned: pas encore de position livreur disponible
|
||||||
if cmdStatus == "pending" {
|
if cmdStatus == "pending" || cmdStatus == "assigned" {
|
||||||
log.Printf("⏳ [ETA] Commande en attente d'assignation - pas d'ETA")
|
log.Printf("⏳ [ETA] Commande %s - pas d'ETA disponible", cmdStatus)
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"command_id": commandID,
|
"command_id": commandID,
|
||||||
"status": cmdStatus,
|
"status": cmdStatus,
|
||||||
"eta_available": false,
|
"eta_available": false,
|
||||||
"message": "En attente d'assignation d'un livreur",
|
"message": "En attente de démarrage de la livraison",
|
||||||
})
|
})
|
||||||
return
|
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
|
// 6️⃣ VÉRIFIER LE CACHE REDIS POUR ETA
|
||||||
etaKey := fmt.Sprintf("command:eta:%d", commandID)
|
etaKey := fmt.Sprintf("command:eta:%d", commandID)
|
||||||
|
|||||||
@@ -51,8 +51,7 @@ func AddProductsBasket(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Une seule fonction, une seule transaction atomique
|
panier, err := database.AddToBasket(req.Username, req.ProductID, req.Quantity)
|
||||||
panier, err := database.DecrementAndAddToBasket(req.Username, req.ProductID, req.Quantity)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("❌ [ADD_PANIER] product_id=%d qty=%.3f: %v", req.ProductID, req.Quantity, err)
|
log.Printf("❌ [ADD_PANIER] product_id=%d qty=%.3f: %v", req.ProductID, req.Quantity, err)
|
||||||
if err.Error() == "stock insuffisant" {
|
if err.Error() == "stock insuffisant" {
|
||||||
@@ -192,10 +191,9 @@ func DeleteProductFromBasket(c *gin.Context) {
|
|||||||
|
|
||||||
log.Printf("✅ [DEL_PANIER] Article %d supprimé", req.ID)
|
log.Printf("✅ [DEL_PANIER] Article %d supprimé", req.ID)
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "Produit supprimé du panier avec succès",
|
"message": "Produit supprimé du panier avec succès",
|
||||||
"item_id": req.ID,
|
"item_id": req.ID,
|
||||||
"stock_released": true,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,9 +229,8 @@ func ClearBasket(c *gin.Context) {
|
|||||||
|
|
||||||
log.Printf("✅ [CLEAR_PANIER] Panier %s vidé: %d articles supprimés", authUsernameStr, len(baskets))
|
log.Printf("✅ [CLEAR_PANIER] Panier %s vidé: %d articles supprimés", authUsernameStr, len(baskets))
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "Panier vidé avec succès",
|
"message": "Panier vidé avec succès",
|
||||||
"stock_released": len(baskets),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,6 +427,7 @@ func ValidateBasket(c *gin.Context) {
|
|||||||
|
|
||||||
log.Printf("✅ [CHECKOUT] Commande %d créée", commandID)
|
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 {
|
if isCrypto {
|
||||||
np := c.MustGet("nowpayments").(*services.NowPaymentsClient)
|
np := c.MustGet("nowpayments").(*services.NowPaymentsClient)
|
||||||
ipnURL := fmt.Sprintf("%s/api/v1/webhooks/nowpayments", getBaseURL(c))
|
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)
|
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)
|
err = database.ClearBasketOnCheckout(usernameStr)
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
log.Printf("🧹 [CHECKOUT] Panier vidé")
|
log.Printf("🧹 [CHECKOUT] Stock décrémenté et panier vidé")
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// 4️⃣ Auto-assignation livreur (optionnel)
|
// 4️⃣ Auto-assignation livreur (optionnel)
|
||||||
|
|||||||
Reference in New Issue
Block a user