package db import ( "database/sql" "fmt" "gestion/models" "log" "time" ) // AddProductInBasket ajoute un produit au panier de l'utilisateur func (d *Database) AddProductInBasket(username, nameProduct string, quantity float64, category string) (*models.Panier, error) { // Rechercher le produit par son nom et catégorie (case-insensitive) var productID int productQuery := `SELECT id FROM products WHERE LOWER(name) = LOWER($1) AND LOWER(category) = LOWER($2)` err := d.QueryRow(productQuery, nameProduct, category).Scan(&productID) if err == sql.ErrNoRows { return nil, fmt.Errorf("produit '%s' non trouvé dans la catégorie '%s'", nameProduct, category) } if err != nil { return nil, fmt.Errorf("erreur lors de la recherche du produit: %w", err) } // Récupérer le prix correct selon la quantité price, err := d.GetProductPrice(nameProduct, category, quantity) if err != nil { return nil, fmt.Errorf("erreur récupération prix: %w", err) } // Vérifier si le produit existe déjà dans le panier var existingID int var existingQuantity, existingPrice float64 checkQuery := `SELECT id, quantity, price FROM baskets WHERE username = $1 AND product_id = $2` err = d.QueryRow(checkQuery, username, productID).Scan(&existingID, &existingQuantity, &existingPrice) if err == nil { // Produit déjà dans le panier : cumuler quantité et prix total de la ligne newQuantity := existingQuantity + quantity newPrice := existingPrice + price updateQuery := `UPDATE baskets SET quantity = $1, price = $2, created_at = CURRENT_TIMESTAMP WHERE id = $3 RETURNING id, username, product_id, quantity, price, created_at` var basket models.Panier err = d.QueryRow(updateQuery, newQuantity, newPrice, existingID).Scan( &basket.ID, &basket.Username, &basket.ProductID, &basket.Quantity, &basket.Price, &basket.CreatedAt, ) if err != nil { return nil, fmt.Errorf("erreur lors de la mise à jour du panier: %w", err) } return &basket, nil } // Produit non présent : l'ajouter insertQuery := `INSERT INTO baskets (username, product_id, quantity, price, created_at) VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP) RETURNING id, username, product_id, quantity, price, created_at` var basket models.Panier err = d.QueryRow(insertQuery, username, productID, quantity, price).Scan( &basket.ID, &basket.Username, &basket.ProductID, &basket.Quantity, &basket.Price, &basket.CreatedAt, ) if err != nil { return nil, fmt.Errorf("erreur lors de l'ajout au panier: %w", err) } return &basket, nil } // GetProductPriceByID récupère le prix d'un produit par son ID et quantité (NUMERIC exact) func (d *Database) GetProductPriceByID(productID int, quantity float64) (float64, error) { var price float64 // Comparaison NUMERIC précise : évite les problèmes float64 vs NUMERIC(10,3) exactQuery := ` SELECT price FROM product_prices WHERE product_id = $1 AND quantity = ROUND($2::NUMERIC, 3) LIMIT 1 ` err := d.QueryRow(exactQuery, productID, quantity).Scan(&price) if err == nil { return price, nil } // Fallback : palier inférieur le plus proche tierQuery := ` SELECT price FROM product_prices WHERE product_id = $1 AND quantity <= ROUND($2::NUMERIC, 3) ORDER BY quantity DESC LIMIT 1 ` err = d.QueryRow(tierQuery, productID, quantity).Scan(&price) if err != nil { return 0, fmt.Errorf("aucun prix trouvé pour product_id=%d qty=%.3f: %w", productID, quantity, err) } return price, nil } // GetProductStockByID récupère le stock d'un produit par son ID func (d *Database) GetProductStockByID(productID int) (float64, error) { var stock float64 err := d.QueryRow(`SELECT stock FROM products WHERE id = $1`, productID).Scan(&stock) if err != nil { return 0, fmt.Errorf("produit %d non trouvé: %w", productID, err) } return 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 existingID int var existingQuantity, existingPrice float64 checkQuery := `SELECT id, quantity, price FROM baskets WHERE username = $1 AND product_id = $2` err = d.QueryRow(checkQuery, username, productID).Scan(&existingID, &existingQuantity, &existingPrice) if err == nil { newQuantity := existingQuantity + quantity newPrice := existingPrice + price updateQuery := `UPDATE baskets SET quantity = $1, price = $2, created_at = CURRENT_TIMESTAMP WHERE id = $3 RETURNING id, username, product_id, quantity, price, created_at` var basket models.Panier err = d.QueryRow(updateQuery, newQuantity, newPrice, existingID).Scan( &basket.ID, &basket.Username, &basket.ProductID, &basket.Quantity, &basket.Price, &basket.CreatedAt, ) if err != nil { return nil, fmt.Errorf("erreur lors de la mise à jour du panier: %w", err) } return &basket, nil } insertQuery := `INSERT INTO baskets (username, product_id, quantity, price, created_at) VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP) RETURNING id, username, product_id, quantity, price, created_at` var basket models.Panier err = d.QueryRow(insertQuery, username, productID, quantity, price).Scan( &basket.ID, &basket.Username, &basket.ProductID, &basket.Quantity, &basket.Price, &basket.CreatedAt, ) if err != nil { return nil, fmt.Errorf("erreur lors de l'ajout au 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 price float64 query := ` SELECT price FROM product_prices pp INNER JOIN products p ON pp.product_id = p.id WHERE LOWER(p.name) = LOWER($1) AND LOWER(p.category) = LOWER($2) AND pp.quantity <= $3 ORDER BY pp.quantity DESC LIMIT 1 ` err := d.QueryRow(query, name, category, quantity).Scan(&price) if err != nil { return 0, fmt.Errorf("prix produit introuvable pour %f %s: %w", quantity, name, err) } return price, nil } func (d *Database) GetProductStock(name, category string) (float64, error) { var stock float64 query := `SELECT stock FROM products WHERE LOWER(name) = LOWER($1) AND LOWER(category) = LOWER($2)` err := d.QueryRow(query, name, category).Scan(&stock) if err != nil { return 0, fmt.Errorf("produit non trouvé: %w", err) } return stock, nil } func (d *Database) DecrementProductStock(name, category string, quantity float64) error { query := `UPDATE products SET stock = stock - $1 WHERE LOWER(name) = LOWER($2) AND LOWER(category) = LOWER($3) AND stock >= $1` result, err := d.Exec(query, quantity, name, category) if err != nil { return fmt.Errorf("erreur mise à jour stock: %w", err) } rows, _ := result.RowsAffected() if rows == 0 { return fmt.Errorf("stock insuffisant pour le produit") } return nil } // GetAllProductsInBasket récupère tous les produits du panier d'un utilisateur func (d *Database) GetAllProductsInBasket(username string) ([]models.Panier, error) { query := `SELECT b.id, b.username, b.product_id, b.quantity, b.price, b.created_at, p.name, p.category, p.description FROM baskets b INNER JOIN products p ON b.product_id = p.id WHERE b.username = $1 ORDER BY b.created_at DESC` rows, err := d.Query(query, username) if err != nil { return nil, fmt.Errorf("erreur lors de la récupération du panier: %w", err) } defer rows.Close() var baskets []models.Panier for rows.Next() { var basket models.Panier err := rows.Scan( &basket.ID, &basket.Username, &basket.ProductID, // ✔ FIX MAJEUR &basket.Quantity, &basket.Price, &basket.CreatedAt, &basket.ProductName, &basket.Category, &basket.Description, ) if err != nil { return nil, fmt.Errorf("erreur lors du scan du panier: %w", err) } baskets = append(baskets, basket) } if err = rows.Err(); err != nil { return nil, fmt.Errorf("erreur lors de l'itération des résultats: %w", err) } return baskets, nil } // DecrementProductStockByID décrémente le stock d'un produit par son ID de manière sécurisée (évite race condition) func (d *Database) DecrementProductStockByID(productID int, quantity float64) error { query := ` UPDATE products SET stock = stock - $1 WHERE id = $2 AND stock >= $1 ` result, err := d.Exec(query, quantity, productID) if err != nil { return fmt.Errorf("erreur lors de la mise à jour du stock: %w", err) } rowsAffected, err := result.RowsAffected() if err != nil { return fmt.Errorf("erreur lors de la vérification du stock affecté: %w", err) } if rowsAffected == 0 { return fmt.Errorf("stock insuffisant pour le produit %d", productID) } return nil } // DeleteProductFromBasket supprime un produit spécifique du panier func (d *Database) DeleteProductFromBasket(basketID int) error { query := `DELETE FROM baskets WHERE id = $1` result, err := d.Exec(query, basketID) if err != nil { return fmt.Errorf("erreur lors de la suppression du produit: %w", err) } rowsAffected, err := result.RowsAffected() if err != nil { return fmt.Errorf("erreur lors de la vérification des lignes affectées: %w", err) } if rowsAffected == 0 { return fmt.Errorf("produit non trouvé dans le panier") } return nil } // ClearBasket vide complètement le panier d'un utilisateur func (d *Database) ClearBasket(username string) error { query := `DELETE FROM baskets WHERE username = $1` _, err := d.Exec(query, username) if err != nil { return fmt.Errorf("erreur lors du vidage du panier: %w", err) } return nil } // GetBasketTotal calcule le montant total du panier d'un utilisateur // price dans baskets = prix total de la ligne (cumul des ajouts) func (d *Database) GetBasketTotal(username string) (float64, error) { query := `SELECT COALESCE(SUM(price), 0) as total FROM baskets WHERE username = $1` var total float64 err := d.QueryRow(query, username).Scan(&total) if err != nil { return 0, fmt.Errorf("erreur lors du calcul du total: %w", err) } return total, nil } // GetBasketItemCount compte le nombre d'items dans le panier func (d *Database) GetBasketItemCount(username string) (int, error) { query := `SELECT COUNT(*) FROM baskets WHERE username = $1` var count int err := d.QueryRow(query, username).Scan(&count) if err != nil { return 0, fmt.Errorf("erreur lors du comptage des items: %w", err) } return count, nil } // UpdateBasketItemQuantity met à jour la quantité d'un item du panier func (d *Database) UpdateBasketItemQuantity(basketID int, quantity float64) error { if quantity <= 0 { return fmt.Errorf("la quantité doit être supérieure à 0") } query := `UPDATE baskets SET quantity = $1, created_at = CURRENT_TIMESTAMP WHERE id = $2` result, err := d.Exec(query, quantity, basketID) if err != nil { return fmt.Errorf("erreur lors de la mise à jour de la quantité: %w", err) } rowsAffected, err := result.RowsAffected() if err != nil { return fmt.Errorf("erreur lors de la vérification des lignes affectées: %w", err) } if rowsAffected == 0 { return fmt.Errorf("produit non trouvé dans le panier") } return nil } // ExtendBasketReservations prolonge les réservations func (d *Database) ExtendBasketReservations(username string) error { query := `SELECT product_id, quantity FROM baskets WHERE username = $1` rows, err := d.Query(query, username) if err != nil { return fmt.Errorf("erreur récupération panier: %w", err) } defer rows.Close() type Item struct { ProductID int Quantity int } var items []Item for rows.Next() { var item Item if err := rows.Scan(&item.ProductID, &item.Quantity); err != nil { return fmt.Errorf("erreur scan: %w", err) } items = append(items, item) } // Vérifier stock disponible pour chaque item for _, item := range items { var stock int err := d.QueryRow(`SELECT stock FROM products WHERE id = $1`, item.ProductID).Scan(&stock) if err != nil { return fmt.Errorf("produit %d non trouvé: %w", item.ProductID, err) } if stock < item.Quantity { return fmt.Errorf("stock insuffisant pour le produit %d (demandé: %d, disponible: %d)", item.ProductID, item.Quantity, stock) } } // Prolonger les réservations de 15 minutes newReservation := time.Now().Add(15 * time.Minute) updateQuery := `UPDATE baskets SET reserved_until = $1 WHERE username = $2` _, err = d.Exec(updateQuery, newReservation, username) if 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) { query := `SELECT COUNT(*) FROM baskets WHERE username = $1 AND (reserved_until IS NULL OR reserved_until < CURRENT_TIMESTAMP)` var expiredCount int err := d.QueryRow(query, username).Scan(&expiredCount) if err != nil { return false, err } return expiredCount > 0, nil } func (d *Database) GetBasketItems(username string) ([]map[string]interface{}, error) { query := `SELECT product_id, quantity, price FROM baskets WHERE username = $1` rows, err := d.Query(query, username) if err != nil { return nil, err } defer rows.Close() var items []map[string]interface{} for rows.Next() { var productID int var quantity, price float64 if err := rows.Scan(&productID, &quantity, &price); err != nil { return nil, err } items = append(items, map[string]interface{}{ "product_id": productID, "quantity": quantity, "price": price, }) } return items, nil }