fix: manage stock
This commit is contained in:
@@ -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(¤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 {
|
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 = ?`,
|
||||||
|
|||||||
@@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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":
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user