fix: manage stock

This commit is contained in:
2026-05-14 15:31:38 +02:00
parent 426f4b42ee
commit 0fe8a06bfa
8 changed files with 153 additions and 22 deletions
+51 -6
View File
@@ -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) { func (d *Database) DecrementAndAddToBasket(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 {
result := tx.Exec(`UPDATE products SET stock = stock - ? WHERE id = ? AND stock >= ?`, var currentStock float64
quantity, productID, quantity) if err := tx.Raw(`SELECT stock FROM products WHERE id = ? FOR UPDATE`, productID).Scan(&currentStock).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 { if result.Error != nil {
return fmt.Errorf("erreur stock: %w", result.Error) return fmt.Errorf("erreur stock: %w", result.Error)
} }
if result.RowsAffected == 0 {
return fmt.Errorf("stock insuffisant")
}
var priceResult struct { var priceResult struct {
Price float64 `gorm:"column:price"` Price float64 `gorm:"column:price"`
@@ -100,7 +104,7 @@ func (d *Database) DeleteProductFromBasket(basketID int) error {
ProductID int `gorm:"column:product_id"` ProductID int `gorm:"column:product_id"`
Quantity float64 `gorm:"column:quantity"` 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") return fmt.Errorf("produit non trouvé dans le panier")
} }
if item.ProductID == 0 { 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. // ClearBasket vide complètement le panier d'un utilisateur et restitue les stocks.
func (d *Database) ClearBasket(username string) error { func (d *Database) ClearBasket(username string) error {
return d.GDB.Transaction(func(tx *gorm.DB) 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(` if err := tx.Exec(`
UPDATE products p UPDATE products p
SET stock = stock + b.quantity SET stock = stock + b.quantity
@@ -157,6 +172,36 @@ func (d *Database) GetBasketItemOwner(basketID int) (string, error) {
return username, nil 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) { func (d *Database) GetBasketItems(username string) ([]map[string]any, error) {
var items []map[string]any 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 = ?`, if err := d.GDB.Raw(`SELECT product_id, quantity::float8 as quantity, price::float8 as price FROM baskets WHERE username = ?`,
+12 -3
View File
@@ -86,10 +86,9 @@ func (d *Database) CancelCommandAtomic(commandID int, username, reason string, f
SET stock = stock + ci.quantite, updated_at = CURRENT_TIMESTAMP SET stock = stock + ci.quantite, updated_at = CURRENT_TIMESTAMP
FROM command_items ci FROM command_items ci
WHERE ci.command_id = ? AND ci.product_id = p.id`, commandID).Error; err != nil { WHERE ci.command_id = ? AND ci.product_id = p.id`, commandID).Error; err != nil {
log.Printf("⚠️ [CancelAtomic] Erreur remboursement stock: %v", err) return fmt.Errorf("erreur remboursement stock: %w", err)
} else {
log.Printf("✅ [CancelAtomic] Stock remboursé")
} }
log.Printf("✅ [CancelAtomic] Stock remboursé")
if err := tx.Exec(` if err := tx.Exec(`
UPDATE clients UPDATE clients
@@ -321,3 +320,13 @@ func (d *Database) AddClientPenalty(username string, points int) error {
return nil 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
})
}
+18 -4
View File
@@ -81,10 +81,10 @@ func validateItemStatus(status string) error {
status = strings.ToLower(strings.TrimSpace(status)) status = strings.ToLower(strings.TrimSpace(status))
if !slices.Contains(validStatuses, 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 return err
} }
// Récupérer le prix et la quantité avant suppression pour mettre à jour le total
var result struct { var result struct {
Prix float64 `gorm:"column:prix"` Prix float64 `gorm:"column:prix"`
Quantite float64 `gorm:"column:quantite"` 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) return fmt.Errorf("erreur vérification item: %w", err)
} }
if result.Prix == 0 && result.Quantite == 0 { if result.Prix == 0 && result.Quantite == 0 {
return fmt.Errorf("item %d non trouvé dans la commande %d", itemID, commandID) 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 // Supprimer l'item
if err := d.GDB.Exec(`DELETE FROM command_items WHERE id = ?`, itemID).Error; err != nil { if err := d.GDB.Exec(`DELETE FROM command_items WHERE id = ?`, itemID).Error; err != nil {
log.Printf("❌ Erreur DELETE command_items: %v", err) 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) 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 return nil
} }
+13 -3
View File
@@ -3,7 +3,6 @@ package db
import ( import (
"fmt" "fmt"
"gestion/models" "gestion/models"
"log"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -67,6 +66,17 @@ func (d *Database) ActivateCryptoCommand(commandID int) error {
func (d *Database) CancelCryptoCommand(commandID int) error { func (d *Database) CancelCryptoCommand(commandID int) error {
return d.GDB.Transaction(func(tx *gorm.DB) 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 { type item struct {
ProductID int ProductID int
Quantite float64 Quantite float64
@@ -77,9 +87,9 @@ func (d *Database) CancelCryptoCommand(commandID int) error {
} }
for _, it := range items { for _, it := range items {
if err := tx.Exec(`UPDATE products SET stock = stock + ? WHERE id = ?`, it.Quantite, it.ProductID).Error; err != nil { 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
}) })
} }
+13
View File
@@ -1119,6 +1119,19 @@ func UpdateCommandStatusAdmin(c *gin.Context) {
return 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 { if err := database.UpdateCommandStatus(commandID, req.Status); err != nil {
utils.ServerErr(c, "Impossible de mettre à jour le statut", err) utils.ServerErr(c, "Impossible de mettre à jour le statut", err)
return return
+5 -1
View File
@@ -392,8 +392,12 @@ func UpdateDeliveryStatus(c *gin.Context) {
database.CompleteDeliveryAndProcessNext(usernameStr, commandID) database.CompleteDeliveryAndProcessNext(usernameStr, commandID)
case "cancelled": case "cancelled":
// Annulation par le livreur - Nettoyer la queue
log.Printf("🚫 Livraison annulée par livreur - Nettoyage queue cmd %d", commandID) 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) database.CompleteDeliveryAndProcessNext(usernameStr, commandID)
case "arrived": case "arrived":
+24
View File
@@ -13,6 +13,7 @@ import (
"log" "log"
"net/http" "net/http"
"strings" "strings"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -266,6 +267,14 @@ func ValidateBasket(c *gin.Context) {
} }
usernameStr := username.(string) 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 { var req struct {
DeliveryAddress string `json:"delivery_address" binding:"required"` DeliveryAddress string `json:"delivery_address" binding:"required"`
UseReferralBalance bool `json:"use_referral_balance"` 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) 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 // Vérification option crypto
isCrypto := req.PaymentMethod == "crypto" isCrypto := req.PaymentMethod == "crypto"
if isCrypto { if isCrypto {
+13 -1
View File
@@ -701,7 +701,18 @@ func UpdateStock(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return 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 { if err := database.SetProductStock(id, req.Stock); err != nil {
log.Printf("❌ [UpdateProduct] Erreur UPDATE: %v", err) log.Printf("❌ [UpdateProduct] Erreur UPDATE: %v", err)
@@ -717,6 +728,7 @@ func UpdateStock(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
"product": updatedProduct, "product": updatedProduct,
"reserved_in_baskets": reserved,
}) })
} }