258 lines
9.9 KiB
Go
258 lines
9.9 KiB
Go
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)
|
|
}
|
|
if productInfo.Stock < quantity {
|
|
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)
|
|
}
|
|
// 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
|
|
}
|
|
|
|
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+quantity, 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, quantity, 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
|
|
}
|