fix: manage stock

This commit is contained in:
Xor290
2026-05-13 11:30:22 +02:00
parent fb63855d9b
commit 65d1367f42
7 changed files with 167 additions and 186 deletions
+47 -134
View File
@@ -3,8 +3,6 @@ package db
import (
"fmt"
"gestion/models"
"log"
"time"
"gorm.io/gorm"
)
@@ -61,78 +59,6 @@ func (d *Database) AddProductInBasket(username, nameProduct string, quantity flo
return &basket, nil
}
// GetProductPriceByID récupère le prix d'un produit par son ID et quantité
func (d *Database) GetProductPriceByID(productID int, quantity float64) (float64, error) {
var result struct {
Price float64 `gorm:"column:price"`
}
err := d.GDB.Raw(`
SELECT price FROM product_prices
WHERE product_id = ? AND quantity = ROUND(?::NUMERIC, 3)
LIMIT 1`, productID, quantity).Scan(&result).Error
if err == nil && result.Price > 0 {
return result.Price, nil
}
err = d.GDB.Raw(`
SELECT price FROM product_prices
WHERE product_id = ? AND quantity <= ROUND(?::NUMERIC, 3)
ORDER BY quantity DESC LIMIT 1`, productID, quantity).Scan(&result).Error
if err != nil || result.Price == 0 {
return 0, fmt.Errorf("aucun prix trouvé pour product_id=%d qty=%.3f", productID, quantity)
}
return result.Price, nil
}
// GetProductStockByID récupère le stock d'un produit par son ID
func (d *Database) GetProductStockByID(productID int) (float64, error) {
var result struct {
Stock float64 `gorm:"column:stock"`
}
err := d.GDB.Raw(`SELECT stock FROM products WHERE id = ?`, productID).Scan(&result).Error
if err != nil {
return 0, fmt.Errorf("produit %d non trouvé: %w", productID, err)
}
return result.Stock, nil
}
// AddProductInBasketByID ajoute un produit au panier en utilisant son ID directement
func (d *Database) AddProductInBasketByID(username string, productID int, quantity float64) (*models.Panier, error) {
price, err := d.GetProductPriceByID(productID, quantity)
if err != nil {
return nil, fmt.Errorf("erreur récupération prix: %w", err)
}
var existing struct {
ID int `gorm:"column:id"`
Quantity float64 `gorm:"column:quantity"`
Price float64 `gorm:"column:price"`
}
d.GDB.Raw(`SELECT id, quantity, price FROM baskets WHERE username = ? AND product_id = ?`,
username, productID).Scan(&existing)
var basket models.Panier
if existing.ID != 0 {
newQuantity := existing.Quantity + quantity
newPrice := existing.Price + price
err = d.GDB.Raw(`
UPDATE baskets SET quantity = ?, price = ?, created_at = CURRENT_TIMESTAMP
WHERE id = ? RETURNING id, username, product_id, quantity, price, created_at`,
newQuantity, newPrice, existing.ID).Scan(&basket).Error
} else {
err = d.GDB.Raw(`
INSERT INTO baskets (username, product_id, quantity, price, created_at)
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
RETURNING id, username, product_id, quantity, price, created_at`,
username, productID, quantity, price).Scan(&basket).Error
}
if err != nil {
return nil, fmt.Errorf("erreur panier: %w", err)
}
return &basket, nil
}
// GetProductPrice récupère le prix réel d'un produit pour une quantité donnée (legacy)
func (d *Database) GetProductPrice(name, category string, quantity float64) (float64, error) {
var result struct {
@@ -195,18 +121,54 @@ func (d *Database) GetAllProductsInBasket(username string) ([]models.Panier, err
return baskets, nil
}
// DecrementProductStockByID décrémente le stock d'un produit par son ID
func (d *Database) DecrementProductStockByID(productID int, quantity float64) error {
result := d.GDB.Exec(`
UPDATE products SET stock = stock - ?
WHERE id = ? AND stock >= ?`, quantity, productID, quantity)
if result.Error != nil {
return fmt.Errorf("erreur lors de la mise à jour du stock: %w", result.Error)
func (d *Database) DecrementAndAddToBasket(username string, productID int, quantity float64) (*models.Panier, error) {
var basket models.Panier
err := d.GDB.Transaction(func(tx *gorm.DB) error {
result := tx.Exec(`UPDATE products SET stock = stock - ? WHERE id = ? AND stock >= ?`,
quantity, productID, quantity)
if result.Error != nil {
return fmt.Errorf("erreur stock: %w", result.Error)
}
if result.RowsAffected == 0 {
return fmt.Errorf("stock insuffisant")
}
var priceResult struct {
Price float64 `gorm:"column:price"`
}
if err := tx.Raw(`
SELECT price FROM product_prices
WHERE product_id = ? AND quantity <= ? AND active_price = true
ORDER BY quantity DESC LIMIT 1`,
productID, quantity).Scan(&priceResult).Error; err != nil || priceResult.Price == 0 {
return fmt.Errorf("prix introuvable pour product_id=%d qty=%.3f", productID, quantity)
}
var existing struct {
ID int `gorm:"column:id"`
Quantity float64 `gorm:"column:quantity"`
Price float64 `gorm:"column:price"`
}
tx.Raw(`SELECT id, quantity, price FROM baskets WHERE username = ? AND product_id = ?`,
username, productID).Scan(&existing)
if existing.ID != 0 {
return tx.Raw(`
UPDATE baskets SET quantity = ?, price = ?, created_at = CURRENT_TIMESTAMP
WHERE id = ? RETURNING id, username, product_id, quantity, price, created_at`,
existing.Quantity+quantity, existing.Price+priceResult.Price,
existing.ID).Scan(&basket).Error
}
return tx.Raw(`
INSERT INTO baskets (username, product_id, quantity, price, created_at)
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
RETURNING id, username, product_id, quantity, price, created_at`,
username, productID, quantity, priceResult.Price).Scan(&basket).Error
})
if err != nil {
return nil, err
}
if result.RowsAffected == 0 {
return fmt.Errorf("stock insuffisant pour le produit %d", productID)
}
return nil
return &basket, nil
}
// DeleteProductFromBasket supprime un produit spécifique du panier et restitue le stock.
@@ -329,55 +291,6 @@ func (d *Database) UpdateBasketItemQuantity(basketID int, newQuantity float64) e
})
}
// ExtendBasketReservations prolonge les réservations
func (d *Database) ExtendBasketReservations(username string) error {
var items []struct {
ProductID int `gorm:"column:product_id"`
Quantity float64 `gorm:"column:quantity"`
}
if err := d.GDB.Raw(`SELECT product_id, quantity FROM baskets WHERE username = ?`, username).Scan(&items).Error; err != nil {
return fmt.Errorf("erreur récupération panier: %w", err)
}
for _, item := range items {
var stockResult struct {
Stock float64 `gorm:"column:stock"`
}
if err := d.GDB.Raw(`SELECT stock FROM products WHERE id = ?`, item.ProductID).Scan(&stockResult).Error; err != nil {
return fmt.Errorf("produit %d non trouvé: %w", item.ProductID, err)
}
if stockResult.Stock < item.Quantity {
return fmt.Errorf("stock insuffisant pour le produit %d (demandé: %g, disponible: %g)",
item.ProductID, item.Quantity, stockResult.Stock)
}
}
newReservation := time.Now().Add(15 * time.Minute)
if err := d.GDB.Exec(`UPDATE baskets SET reserved_until = ? WHERE username = ?`,
newReservation, username).Error; err != nil {
return fmt.Errorf("erreur prolongation: %w", err)
}
log.Printf("✅ Réservations prolongées pour %s jusqu'à %s",
username, newReservation.Format("15:04:05"))
return nil
}
// CheckBasketReservations vérifie si les réservations sont expirées
func (d *Database) CheckBasketReservations(username string) (bool, error) {
var result struct {
Count int `gorm:"column:count"`
}
err := d.GDB.Raw(`
SELECT COUNT(*) as count FROM baskets
WHERE username = ? AND (reserved_until IS NULL OR reserved_until < CURRENT_TIMESTAMP)`,
username).Scan(&result).Error
if err != nil {
return false, err
}
return result.Count > 0, nil
}
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 -13
View File
@@ -179,8 +179,6 @@ func (d *Database) CheckCommandETAExistsAndValid(commandID int) bool {
}
func (d *Database) DeleteCommandAtomic(commandID int, deletedBy, role string) error {
log.Printf("🔒 [DeleteAtomic] START - cmd=%d, by=%s (%s)", commandID, deletedBy, role)
return d.GDB.Transaction(func(tx *gorm.DB) error {
var cmdResult struct {
Status string `gorm:"column:status"`
@@ -188,8 +186,8 @@ func (d *Database) DeleteCommandAtomic(commandID int, deletedBy, role string) er
LivreurAssign string `gorm:"column:livreur_assign"`
}
err := tx.Raw(`
SELECT status, username, COALESCE(livreur_assign, '') as livreur_assign
FROM commandes WHERE id = ? FOR UPDATE`, commandID).Scan(&cmdResult).Error
SELECT status, username, COALESCE(livreur_assign, '') as livreur_assign
FROM commandes WHERE id = ? FOR UPDATE`, commandID).Scan(&cmdResult).Error
if err != nil {
return err
}
@@ -199,19 +197,26 @@ func (d *Database) DeleteCommandAtomic(commandID int, deletedBy, role string) er
log.Printf("📋 [DeleteAtomic] Trouvée - status=%s, client=%s", cmdResult.Status, cmdResult.Username)
if err := 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; err != nil {
log.Printf("⚠️ [DeleteAtomic] Erreur remboursement: %v", err)
// ✅ Ne restitue le stock QUE si pas déjà fait
stockAlreadyRestored := cmdResult.Status == "cancelled" || cmdResult.Status == "approved"
if !stockAlreadyRestored {
if err := 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; err != nil {
log.Printf("⚠️ [DeleteAtomic] Erreur remboursement: %v", err)
} else {
log.Printf("✅ [DeleteAtomic] Stock remboursé (statut: %s)", cmdResult.Status)
}
} else {
log.Printf(" [DeleteAtomic] Stock remboursé")
log.Printf("⏭️ [DeleteAtomic] Stock NON restitué - statut=%s", cmdResult.Status)
}
// ✅ Log suppression
tx.Exec(`
INSERT INTO command_logs (command_id, status, message, author, created_at)
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)`,
INSERT INTO command_logs (command_id, status, message, author, created_at)
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)`,
commandID, "deleted",
fmt.Sprintf("Supprimée par %s (%s) - Ancien statut: %s", deletedBy, role, cmdResult.Status),
deletedBy)
+15 -3
View File
@@ -177,12 +177,12 @@ func (d *Database) GetProductsByCategory(category string) ([]models.Product, err
return products, nil
}
func (d *Database) UpdateProduct(productID int, name, category, description, unit string, stock float64, prices []models.ProductPrice) error {
func (d *Database) UpdateProduct(productID int, name, category, description, unit string, prices []models.ProductPrice) error {
err := d.GDB.Exec(`
UPDATE products
SET name = ?, category = ?, description = ?, stock = ?, unit = ?, updated_at = ?
SET name = ?, category = ?, description = ?, unit = ?, updated_at = ?
WHERE id = ?`,
name, category, description, stock, unit, time.Now(), productID).Error
name, category, description, unit, time.Now(), productID).Error
if err != nil {
return fmt.Errorf("erreur mise à jour produit: %w", err)
}
@@ -199,6 +199,18 @@ func (d *Database) UpdateProduct(productID int, name, category, description, uni
return nil
}
func (d *Database) SetProductStock(productID int, stock float64) error {
result := d.GDB.Exec(`UPDATE products SET stock = ?, updated_at = ? WHERE id = ?`,
stock, time.Now(), productID)
if result.Error != nil {
return fmt.Errorf("erreur mise à jour stock: %w", result.Error)
}
if result.RowsAffected == 0 {
return fmt.Errorf("produit non trouvé")
}
return nil
}
// DeleteProduct supprime un produit
func (d *Database) DeleteProduct(productID int) error {
result := d.GDB.Exec(`DELETE FROM products WHERE id = ?`, productID)
+6 -3
View File
@@ -266,9 +266,12 @@ func GetMyCancellationHistory(c *gin.Context) {
return
}
var totalPenalty int
penaltyQuery := `SELECT COALESCE(amende, 0) FROM clients WHERE username = $1`
database.QueryRow(penaltyQuery).Scan(&totalPenalty)
var penaltyResult struct {
Amende int `gorm:"column:amende"`
}
database.GDB.Raw(`SELECT COALESCE(amende, 0) as amende FROM clients WHERE username = ?`,
username).Scan(&penaltyResult)
totalPenalty := penaltyResult.Amende
c.JSON(http.StatusOK, gin.H{
"success": true,
+24 -26
View File
@@ -12,6 +12,7 @@ import (
"gestion/utils"
"log"
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
@@ -48,35 +49,32 @@ func AddProductsBasket(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Quantité invalide"})
return
}
// Si product_id fourni par le mobile, on l'utilise directement (plus fiable)
if req.ProductID > 0 || req.NameProduct == "" || req.Category == "" {
stock, err := database.GetProductStockByID(req.ProductID)
if err != nil {
log.Printf("❌ [ADD_PANIER] Produit %d non trouvé: %v", req.ProductID, err)
c.JSON(http.StatusNotFound, gin.H{"error": "Produit non trouvé"})
return
}
if stock < req.Quantity {
c.JSON(http.StatusBadRequest, gin.H{"error": "Stock insuffisant", "available": stock})
return
}
if err := database.DecrementProductStockByID(req.ProductID, req.Quantity); err != nil {
log.Printf("❌ [ADD_PANIER] Erreur décrement stock product_id=%d: %v", req.ProductID, err)
utils.ServerErr(c, "Impossible d'ajouter le produit au panier", err)
return
}
panier, err := database.AddProductInBasketByID(req.Username, req.ProductID, req.Quantity)
if err != nil {
log.Printf("❌ [ADD_PANIER] Erreur ajout product_id=%d qty=%.3f: %v", req.ProductID, req.Quantity, err)
utils.ServerErr(c, "Impossible d'ajouter le produit au panier", err)
return
}
c.JSON(http.StatusOK, gin.H{"success": true, "message": "Produit ajouté au panier avec succès", "panier": panier})
if req.ProductID <= 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "product_id requis"})
return
}
// ✅ Une seule fonction, une seule transaction atomique
panier, err := database.DecrementAndAddToBasket(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" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Stock insuffisant"})
return
}
if strings.Contains(err.Error(), "prix introuvable") {
c.JSON(http.StatusBadRequest, gin.H{"error": "Aucun prix configuré pour ce produit"})
return
}
utils.ServerErr(c, "Impossible d'ajouter le produit au panier", err)
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Produit ajouté au panier avec succès",
"panier": panier,
})
}
// ============================================
+56 -7
View File
@@ -600,7 +600,6 @@ func UpdateProduct(c *gin.Context) {
Name string `json:"name"`
Category string `json:"category"`
Description string `json:"description"`
Stock float64 `json:"stock"`
Unit string `json:"unit"`
Prices []models.ProductPrice `json:"prices"`
}
@@ -629,12 +628,8 @@ func UpdateProduct(c *gin.Context) {
if updateData.Unit == "" {
updateData.Unit = "u"
}
if err := validateUnit(updateData.Unit); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := validateStock(updateData.Stock); err != nil {
if err := validateUnit(updateData.Unit); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
@@ -653,7 +648,7 @@ func UpdateProduct(c *gin.Context) {
log.Printf("🔄 [UpdateProduct] %s met à jour produit #%d", username, id)
if err := database.UpdateProduct(id, updateData.Name, updateData.Category, updateData.Description, updateData.Unit, updateData.Stock, updateData.Prices); err != nil {
if err := database.UpdateProduct(id, updateData.Name, updateData.Category, updateData.Description, updateData.Unit, updateData.Prices); err != nil {
log.Printf("❌ [UpdateProduct] Erreur UPDATE: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur mise à jour"})
return
@@ -672,6 +667,60 @@ func UpdateProduct(c *gin.Context) {
})
}
func UpdateStock(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
role := c.GetString("role")
if role != "admin" {
c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"})
return
}
username, _ := safeGetUsername(c)
id, err := strconv.Atoi(c.Param("id"))
if err != nil || id <= 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "ID invalide"})
return
}
// ✅ VÉRIFIER QUE LE PRODUIT EXISTE
_, err = database.GetProductByID(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Produit non trouvé"})
return
}
var req struct {
Stock float64 `json:"stock"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"})
return
}
if err := validateStock(req.Stock); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
log.Printf("🔄 [UpdateStock] %s met à jour le stock #%d", username, id)
if err := database.SetProductStock(id, req.Stock); err != nil {
log.Printf("❌ [UpdateProduct] Erreur UPDATE: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur mise à jour"})
return
}
updatedProduct, _ := database.GetProductByID(id)
media, _ := database.GetMediaByProductID(id)
updatedProduct.Media = media
log.Printf("✅ [UpdateStock] le stock #%d est mis à jour", id)
c.JSON(http.StatusOK, gin.H{
"success": true,
"product": updatedProduct,
})
}
func DeleteMedia(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
+1
View File
@@ -188,6 +188,7 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
adminGroupV2.DELETE("/products/:id", handlers.DeleteProduct)
adminGroupV2.POST("/products/:id/media", handlers.UploadMedia)
adminGroupV2.DELETE("/products/:id/media/:media_id", handlers.DeleteMedia)
adminGroupV2.POST("/products/:id/stock", handlers.UpdateStock)
// ============================================
// CATÉGORIES - GESTION ADMIN
// ============================================