package db import ( "fmt" "gestion/models" "gorm.io/gorm" ) // GetActiveProductPrice retourne le prix catalogue actif pour un produit et // une quantité donnés (palier le plus proche ≤ quantity, cf. même requête que // AddToBasket) — utilisé pour calculer le prix effectif d'une récompense // "half_price_product" (50% de ce prix). func (d *Database) GetActiveProductPrice(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 <= ? AND active_price = true ORDER BY quantity DESC LIMIT 1`, productID, quantity).Scan(&result).Error if err != nil || result.Price == 0 { return 0, fmt.Errorf("prix introuvable pour product_id=%d qty=%.3f", productID, quantity) } return result.Price, nil } func (d *Database) GetProductPrice(name, category string, quantity float64) (float64, error) { var result struct { Price float64 `gorm:"column:price"` } err := d.GDB.Raw(` SELECT price FROM product_prices pp INNER JOIN products p ON pp.product_id = p.id WHERE LOWER(p.name) = LOWER(?) AND LOWER(p.category) = LOWER(?) AND pp.quantity <= ? ORDER BY pp.quantity DESC LIMIT 1`, name, category, quantity).Scan(&result).Error if err != nil || result.Price == 0 { return 0, fmt.Errorf("prix produit introuvable pour %f %s: %w", quantity, name, err) } return result.Price, nil } // GetAllProductsInBasket récupère tous les produits du panier d'un utilisateur func (d *Database) GetAllProductsInBasket(username string) ([]models.Panier, error) { var baskets []models.Panier err := d.GDB.Raw(` SELECT b.id, b.username, b.product_id, b.quantity, b.price, b.is_reward, b.created_at, p.name as product_name, p.category, p.description FROM baskets b INNER JOIN products p ON b.product_id = p.id WHERE b.username = ? ORDER BY b.created_at DESC`, username).Scan(&baskets).Error if err != nil { return nil, fmt.Errorf("erreur lors de la récupération du panier: %w", err) } return baskets, nil } // AddRewardsToBasket ajoute plusieurs produits récompense au panier (is_reward = true), // au prix fourni par l'appelant dans chaque RewardItem.Price (0 pour un produit offert, // ou le prix effectif déjà calculé pour une remise — voir handlers/points.go). // Supprime les anciens items récompense avant d'insérer les nouveaux. // Pas de vérification de stock — les récompenses sont gérées par l'admin. func (d *Database) AddRewardsToBasket(username string, items []models.RewardItem, poolKey string) ([]models.Panier, error) { var baskets []models.Panier err := d.GDB.Transaction(func(tx *gorm.DB) error { var err error baskets, err = addRewardsToBasketTx(tx, username, items, poolKey) return err }) if err != nil { return nil, err } return baskets, nil } // addRewardsToBasketTx contient la logique de remplacement des articles // récompense, factorisée pour être appelée soit seule (AddRewardsToBasket), // soit dans la même transaction qu'une autre opération (voir // ClaimPoolRewardAndAddToBasket) afin de garantir qu'une récompense n'est // jamais consommée sans que son produit soit effectivement livré. func addRewardsToBasketTx(tx *gorm.DB, username string, items []models.RewardItem, poolKey string) ([]models.Panier, error) { // Supprimer tout article récompense existant (remplacement) tx.Exec(`DELETE FROM baskets WHERE username = ? AND is_reward = true`, username) var baskets []models.Panier for _, item := range items { if item.ProductID <= 0 || item.Quantity <= 0 { continue } var productName string if err := tx.Raw(`SELECT name FROM products WHERE id = ?`, item.ProductID).Scan(&productName).Error; err != nil || productName == "" { return nil, fmt.Errorf("produit récompense introuvable (id=%d)", item.ProductID) } var basket models.Panier if err := tx.Raw(` INSERT INTO baskets (username, product_id, quantity, price, is_reward, reward_pool_key, created_at) VALUES (?, ?, ?, ?, true, ?, CURRENT_TIMESTAMP) RETURNING id, username, product_id, quantity, price, is_reward, reward_pool_key, created_at`, username, item.ProductID, item.Quantity, item.Price, poolKey).Scan(&basket).Error; err != nil { return nil, err } baskets = append(baskets, basket) } return baskets, nil } // HasOnlyRewardItems retourne true si le panier ne contient que des articles récompense. func (d *Database) HasOnlyRewardItems(username string) (bool, error) { var counts struct { Total int `gorm:"column:total"` Normal int `gorm:"column:normal"` } err := d.GDB.Raw(` SELECT COUNT(*) as total, COUNT(*) FILTER (WHERE is_reward = false) as normal FROM baskets WHERE username = ?`, username).Scan(&counts).Error if err != nil { return false, err } return counts.Total > 0 && counts.Normal == 0, nil } // 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 err := d.GDB.Transaction(func(tx *gorm.DB) error { var productInfo struct { Stock float64 `gorm:"column:stock"` Category string `gorm:"column:category"` } if err := tx.Raw(`SELECT stock, category FROM products WHERE id = ? FOR UPDATE`, productID).Scan(&productInfo).Error; err != nil { return fmt.Errorf("erreur lecture stock: %w", err) } 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) } // Une promotion active pour ce produit/quantité/catégorie s'applique // automatiquement au prix facturé — indépendamment des points de // fidélité (contrairement aux récompenses par palier). if discounted, ok := d.ApplyPromotionToPrice(productID, productInfo.Category, quantity, priceResult.Price); ok { priceResult.Price = discounted } // Offre "achetez X, Y offert" : le client reçoit une quantité // supplémentaire du même produit, gratuite, sans changer le prix déjà // calculé sur la quantité demandée — la quantité livrée/décomptée du // stock est donc supérieure à la quantité facturée. freeQuantity := d.ResolveFreeGiftQuantity(productID, productInfo.Category, quantity) deliveredQuantity := quantity + freeQuantity if productInfo.Stock < deliveredQuantity { return fmt.Errorf("stock insuffisant") } var existing struct { ID int `gorm:"column:id"` Quantity float64 `gorm:"column:quantity"` Price float64 `gorm:"column:price"` } // Chercher uniquement un item normal (non-récompense) pour ce produit tx.Raw(`SELECT id, quantity, price FROM baskets WHERE username = ? AND product_id = ? AND is_reward = false`, username, productID).Scan(&existing) if existing.ID != 0 { return tx.Raw(` UPDATE baskets SET quantity = ?, price = ?, created_at = CURRENT_TIMESTAMP WHERE id = ? AND is_reward = false RETURNING id, username, product_id, quantity, price, is_reward, created_at`, existing.Quantity+deliveredQuantity, existing.Price+priceResult.Price, existing.ID).Scan(&basket).Error } return tx.Raw(` INSERT INTO baskets (username, product_id, quantity, price, is_reward, created_at) VALUES (?, ?, ?, ?, false, CURRENT_TIMESTAMP) RETURNING id, username, product_id, quantity, price, is_reward, created_at`, username, productID, deliveredQuantity, priceResult.Price).Scan(&basket).Error }) if err != nil { return nil, err } return &basket, nil } // 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 { result := d.GDB.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. // Le stock n'est pas restitué car il n'a pas été décrémenté à l'ajout. func (d *Database) ClearBasket(username string) error { return d.GDB.Exec(`DELETE FROM baskets WHERE username = ?`, username).Error } 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 if err != nil { return "", err } if username == "" { return "", fmt.Errorf("article non trouvé") } 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) { var items []map[string]any if err := d.GDB.Raw(`SELECT product_id, quantity::float8 as quantity, price::float8 as price, is_reward FROM baskets WHERE username = ?`, username).Scan(&items).Error; err != nil { return nil, err } return items, nil }