From 3bf3f5e605802230e07ea7c8a369c82e7d6f9a09 Mon Sep 17 00:00:00 2001 From: Xor290 Date: Sat, 28 Mar 2026 17:00:00 +0100 Subject: [PATCH] chore: update id order --- backend/gestion/db/db_address.go | 60 +- backend/gestion/db/db_alert.go | 159 +-- backend/gestion/db/db_basket.go | 513 ++++------ backend/gestion/db/db_cancel_command.go | 451 ++++----- backend/gestion/db/db_categories.go | 68 +- backend/gestion/db/db_clients.go | 665 ++++++------- backend/gestion/db/db_command_items.go | 299 +++--- backend/gestion/db/db_command_priority.go | 253 ++--- backend/gestion/db/db_commands.go | 917 ++++++++---------- backend/gestion/db/db_delivery.go | 383 +++----- backend/gestion/db/db_delivery_issues.go | 90 +- backend/gestion/db/db_delivery_manage.go | 184 +--- backend/gestion/db/db_delivery_mode.go | 28 +- backend/gestion/db/db_history.go | 216 +---- backend/gestion/db/db_init.go | 43 +- backend/gestion/db/db_jwt_tokens.go | 186 ++-- backend/gestion/db/db_media.go | 207 +--- backend/gestion/db/db_notifications.go | 54 +- backend/gestion/db/db_payments.go | 119 +-- backend/gestion/db/db_product.go | 195 ++-- backend/gestion/db/db_product_price.go | 72 +- backend/gestion/db/db_referral.go | 80 +- backend/gestion/db/db_sanction_client.go | 59 +- backend/gestion/db/db_settings.go | 126 ++- backend/gestion/db/db_telegram.go | 54 +- backend/gestion/db/db_users.go | 183 +--- backend/gestion/db/helpers.go | 8 + backend/gestion/go.mod | 8 + backend/gestion/go.sum | 17 + backend/gestion/handlers/auth.go | 4 +- backend/gestion/handlers/cancel_command.go | 2 +- backend/gestion/handlers/deleviry.go | 2 +- backend/gestion/handlers/history.go | 5 +- backend/gestion/main.go | 30 - backend/gestion/models/address.go | 12 +- backend/gestion/models/alert.go | 16 +- backend/gestion/models/client.go | 31 +- backend/gestion/models/livreur.go | 22 +- backend/gestion/models/media.go | 12 +- backend/gestion/models/payment.go | 24 +- backend/gestion/models/product.go | 34 +- backend/gestion/models/user.go | 14 +- backend/gestion/workers/cron_auto_assign.go | 13 +- mobile/src/api/api_types.ts | 1 + .../src/screens/client/OrderDetailsScreen.tsx | 2 +- .../src/screens/client/OrderHistoryScreen.tsx | 2 +- .../screens/client/OrderTrackingScreen.tsx | 2 +- mobile/src/screens/client/ProfileScreen.tsx | 115 ++- 48 files changed, 2347 insertions(+), 3693 deletions(-) create mode 100644 backend/gestion/db/helpers.go diff --git a/backend/gestion/db/db_address.go b/backend/gestion/db/db_address.go index c8b0ddcb..b600d9da 100644 --- a/backend/gestion/db/db_address.go +++ b/backend/gestion/db/db_address.go @@ -1,67 +1,47 @@ package db import ( - "database/sql" - "errors" "fmt" "gestion/models" ) func (d *Database) CheckAddress(addressByUser *models.Command) error { var correction models.Address - err := d.DB.QueryRow( - `SELECT invalid_address, correct_address FROM adresse_correction WHERE invalid_address = $1`, - addressByUser.DeliveryAddress, - ).Scan(&correction.InvalidAddress, &correction.CorrectAddress) - if errors.Is(err, sql.ErrNoRows) { - return nil + result := d.GDB.Where("invalid_address = ?", addressByUser.DeliveryAddress).First(&correction) + if result.Error != nil { + if isNotFound(result.Error) { + return nil + } + return fmt.Errorf("checkAddress: %w", result.Error) } - if err != nil { - return fmt.Errorf("checkAddress: %w", err) - } - var correctedAddress = correction.CorrectAddress - addressByUser.DeliveryAddress = correctedAddress - return fmt.Errorf("Adresse invalide %s", correctedAddress) + addressByUser.DeliveryAddress = correction.CorrectAddress + return fmt.Errorf("Adresse invalide %s", correction.CorrectAddress) } func (d *Database) AddAddress(CorrectAddressByAdmin string, InvalidAddressByAdmin string) error { - _, err := d.DB.Exec( - `INSERT INTO adresse_correction - (invalid_address, correct_address) - VALUES ($1, $2)`, - InvalidAddressByAdmin, CorrectAddressByAdmin, - ) - if err != nil { + address := models.Address{ + InvalidAddress: InvalidAddressByAdmin, + CorrectAddress: CorrectAddressByAdmin, + } + if err := d.GDB.Create(&address).Error; err != nil { return fmt.Errorf("addAddress: %w", err) } return nil } func (d *Database) DeleteAddress(InvalidAddressByAdmin string, CorrectAddressByAdmin string) error { - _, err := d.DB.Exec( - `DELETE FROM adresse_correction WHERE invalid_address = $1 AND correct_address = $2`, - InvalidAddressByAdmin, CorrectAddressByAdmin, - ) - if err != nil { - return fmt.Errorf("deleteAddress: %w", err) + result := d.GDB.Where("invalid_address = ? AND correct_address = ?", InvalidAddressByAdmin, CorrectAddressByAdmin). + Delete(&models.Address{}) + if result.Error != nil { + return fmt.Errorf("deleteAddress: %w", result.Error) } return nil } func (d *Database) AllAddress() ([]models.Address, error) { - rows, err := d.DB.Query("SELECT invalid_address, correct_address FROM adresse_correction") - if err != nil { + var addresses []models.Address + if err := d.GDB.Find(&addresses).Error; err != nil { return nil, fmt.Errorf("getAllAddress: %w", err) } - defer rows.Close() - - var addresses []models.Address - for rows.Next() { - var a models.Address - if err := rows.Scan(&a.InvalidAddress, &a.CorrectAddress); err != nil { - return nil, fmt.Errorf("getAllAddress scan: %w", err) - } - addresses = append(addresses, a) - } - return addresses, rows.Err() + return addresses, nil } diff --git a/backend/gestion/db/db_alert.go b/backend/gestion/db/db_alert.go index 51ec11fe..63950898 100644 --- a/backend/gestion/db/db_alert.go +++ b/backend/gestion/db/db_alert.go @@ -6,182 +6,75 @@ import ( ) func (d *Database) CreateAlert(username string, message string) (models.AlertPolicy, error) { - - query := ` - INSERT INTO alerte_policy (username, status, message) - VALUES ($1, 'true', $2) - RETURNING id, username, status, message, created_at, updated_at - ` - var alert models.AlertPolicy - err := d.QueryRow(query, username, message).Scan(&alert.ID, &alert.Username, &alert.Status, &alert.Message, &alert.CreatedAt, &alert.UpdatedAt) - if err != nil { + alert := models.AlertPolicy{ + Username: username, + Status: "true", + Message: message, + } + if err := d.GDB.Create(&alert).Error; err != nil { return models.AlertPolicy{}, err } - return alert, nil - } func (d *Database) GetAlertPolicy(id int) (models.AlertPolicy, error) { - - query := ` - SELECT id, username, status, message, created_at, updated_at - FROM alerte_policy - WHERE id = $1 - ` var alert models.AlertPolicy - err := d.QueryRow(query, id).Scan(&alert.ID, &alert.Username, &alert.Status, &alert.Message, &alert.CreatedAt, &alert.UpdatedAt) - if err != nil { + if err := d.GDB.First(&alert, id).Error; err != nil { return models.AlertPolicy{}, err } - return alert, nil - } func (d *Database) GetAllAlerts() ([]models.AlertPolicy, error) { - - query := ` - SELECT id, username, status, message, created_at, updated_at - FROM alerte_policy - ` - rows, err := d.Query(query) - if err != nil { + var alerts []models.AlertPolicy + if err := d.GDB.Find(&alerts).Error; err != nil { return nil, err } - defer rows.Close() - - var alerts []models.AlertPolicy - for rows.Next() { - var alert models.AlertPolicy - err := rows.Scan(&alert.ID, &alert.Username, &alert.Status, &alert.Message, &alert.CreatedAt, &alert.UpdatedAt) - if err != nil { - return nil, err - } - alerts = append(alerts, alert) - } - return alerts, nil - } func (d *Database) DeleteAlertPolicy(id int) error { - - query := ` - DELETE FROM alerte_policy - WHERE id = $1 - ` - _, err := d.Exec(query, id) - if err != nil { - return err - } - - return nil - + return d.GDB.Delete(&models.AlertPolicy{}, id).Error } -// EndAlert met fin à une alerte en changeant son statut à 'false' func (d *Database) EndAlert(id int) error { - - query := ` - UPDATE alerte_policy - SET status = 'false', updated_at = CURRENT_TIMESTAMP - WHERE id = $1 AND status = 'true' - ` - result, err := d.Exec(query, id) - if err != nil { - return err + result := d.GDB.Model(&models.AlertPolicy{}). + Where("id = ? AND status = 'true'", id). + Update("status", "false") + if result.Error != nil { + return result.Error } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - - if rowsAffected == 0 { + if result.RowsAffected == 0 { return fmt.Errorf("alerte non trouvée ou déjà terminée") } - return nil } -// ActivateAlert active une alerte en changeant son statut à 'true' func (d *Database) ActivateAlert(id int) error { - - query := ` - UPDATE alerte_policy - SET status = 'true', updated_at = CURRENT_TIMESTAMP - WHERE id = $1 AND status = 'false' - ` - result, err := d.Exec(query, id) - if err != nil { - return err + result := d.GDB.Model(&models.AlertPolicy{}). + Where("id = ? AND status = 'false'", id). + Update("status", "true") + if result.Error != nil { + return result.Error } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return err - } - - if rowsAffected == 0 { + if result.RowsAffected == 0 { return fmt.Errorf("alerte non trouvée ou déjà active") } - return nil } -// GetActiveAlerts récupère toutes les alertes actives (status = 'true') func (d *Database) GetActiveAlerts() ([]models.AlertPolicy, error) { - - query := ` - SELECT id, username, status, message, created_at, updated_at - FROM alerte_policy - WHERE status = 'true' - ORDER BY created_at DESC - ` - rows, err := d.Query(query) - if err != nil { + var alerts []models.AlertPolicy + if err := d.GDB.Where("status = 'true'").Order("created_at DESC").Find(&alerts).Error; err != nil { return nil, err } - defer rows.Close() - - var alerts []models.AlertPolicy - for rows.Next() { - var alert models.AlertPolicy - err := rows.Scan(&alert.ID, &alert.Username, &alert.Status, &alert.Message, &alert.CreatedAt, &alert.UpdatedAt) - if err != nil { - return nil, err - } - alerts = append(alerts, alert) - } - return alerts, nil } -// GetAlertsByUsername récupère toutes les alertes d'un livreur func (d *Database) GetAlertsByUsername(username string) ([]models.AlertPolicy, error) { - - query := ` - SELECT id, username, status, message, created_at, updated_at - FROM alerte_policy - WHERE username = $1 - ORDER BY created_at DESC - ` - rows, err := d.Query(query, username) - if err != nil { + var alerts []models.AlertPolicy + if err := d.GDB.Where("username = ?", username).Order("created_at DESC").Find(&alerts).Error; err != nil { return nil, err } - defer rows.Close() - - var alerts []models.AlertPolicy - for rows.Next() { - var alert models.AlertPolicy - err := rows.Scan(&alert.ID, &alert.Username, &alert.Status, &alert.Message, &alert.CreatedAt, &alert.UpdatedAt) - if err != nil { - return nil, err - } - alerts = append(alerts, alert) - } - return alerts, nil } diff --git a/backend/gestion/db/db_basket.go b/backend/gestion/db/db_basket.go index 45b52f5d..0fcbd33f 100644 --- a/backend/gestion/db/db_basket.go +++ b/backend/gestion/db/db_basket.go @@ -1,117 +1,100 @@ package db import ( - "database/sql" "fmt" "gestion/models" "log" "time" + + "gorm.io/gorm" ) // 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) + var productResult struct { + ID int `gorm:"column:id"` } + err := d.GDB.Raw(`SELECT id FROM products WHERE LOWER(name) = LOWER(?) AND LOWER(category) = LOWER(?)`, + nameProduct, category).Scan(&productResult).Error if err != nil { return nil, fmt.Errorf("erreur lors de la recherche du produit: %w", err) } + if productResult.ID == 0 { + return nil, fmt.Errorf("produit '%s' non trouvé dans la catégorie '%s'", nameProduct, category) + } + productID := productResult.ID - // 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) + 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) - 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, - ) + 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 if err != nil { return nil, fmt.Errorf("erreur lors de la mise à jour du panier: %w", err) } - return &basket, nil + } 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 lors de l'ajout au panier: %w", err) + } } - - // 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) +// 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 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 + var result struct { + Price float64 `gorm:"column:price"` } - // Fallback : palier inférieur le plus proche - tierQuery := ` + err := d.GDB.Raw(` 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) + 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 } - return 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 stock float64 - err := d.QueryRow(`SELECT stock FROM products WHERE id = $1`, productID).Scan(&stock) + 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 stock, nil + return result.Stock, nil } // AddProductInBasketByID ajoute un produit au panier en utilisant son ID directement @@ -121,81 +104,76 @@ func (d *Database) AddProductInBasketByID(username string, productID int, quanti 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 + 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) - 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 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 lors de l'ajout au panier: %w", err) + 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 price float64 - - query := ` + 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($1) - AND LOWER(p.category) = LOWER($2) - AND pp.quantity <= $3 + WHERE LOWER(p.name) = LOWER(?) + AND LOWER(p.category) = LOWER(?) + AND pp.quantity <= ? ORDER BY pp.quantity DESC - LIMIT 1 - ` - - err := d.QueryRow(query, name, category, quantity).Scan(&price) - if err != nil { + 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 price, nil + return result.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) + var result struct { + Stock float64 `gorm:"column:stock"` + } + err := d.GDB.Raw(`SELECT stock FROM products WHERE LOWER(name) = LOWER(?) AND LOWER(category) = LOWER(?)`, + name, category).Scan(&result).Error if err != nil { return 0, fmt.Errorf("produit non trouvé: %w", err) } - return stock, nil + return result.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) + result := d.GDB.Exec(` + UPDATE products SET stock = stock - ? + WHERE LOWER(name) = LOWER(?) AND LOWER(category) = LOWER(?) AND stock >= ?`, + quantity, name, category, quantity) + if result.Error != nil { + return fmt.Errorf("erreur mise à jour stock: %w", result.Error) } - - rows, _ := result.RowsAffected() - if rows == 0 { + if result.RowsAffected == 0 { return fmt.Errorf("stock insuffisant pour le produit") } return nil @@ -203,164 +181,105 @@ func (d *Database) DecrementProductStock(name, category string, quantity float64 // 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) + var baskets []models.Panier + err := d.GDB.Raw(` + SELECT b.id, b.username, b.product_id, b.quantity, b.price, 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) } - 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) +// DecrementProductStockByID décrémente le stock d'un produit par son ID 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) + 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) } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return fmt.Errorf("erreur lors de la vérification du stock affecté: %w", err) - } - - if rowsAffected == 0 { + if result.RowsAffected == 0 { return fmt.Errorf("stock insuffisant pour le produit %d", productID) } - return nil } // DeleteProductFromBasket supprime un produit spécifique du panier et restitue le stock. func (d *Database) DeleteProductFromBasket(basketID int) error { - tx, err := d.Begin() - if err != nil { - return fmt.Errorf("erreur transaction: %w", err) - } - defer tx.Rollback() + return d.GDB.Transaction(func(tx *gorm.DB) error { + var item struct { + ProductID int `gorm:"column:product_id"` + Quantity float64 `gorm:"column:quantity"` + } + if err := tx.Raw(`SELECT product_id, quantity FROM baskets WHERE id = ?`, basketID).Scan(&item).Error; err != nil { + return fmt.Errorf("produit non trouvé dans le panier") + } + if item.ProductID == 0 { + return fmt.Errorf("produit non trouvé dans le panier") + } - var productID int - var quantity float64 - err = tx.QueryRow( - `SELECT product_id, quantity FROM baskets WHERE id = $1`, - basketID, - ).Scan(&productID, &quantity) - if err != nil { - return fmt.Errorf("produit non trouvé dans le panier") - } + if err := tx.Exec(`UPDATE products SET stock = stock + ? WHERE id = ?`, + item.Quantity, item.ProductID).Error; err != nil { + return fmt.Errorf("erreur restitution stock: %w", err) + } - _, err = tx.Exec( - `UPDATE products SET stock = stock + $1 WHERE id = $2`, - quantity, productID, - ) - if err != nil { - return fmt.Errorf("erreur restitution stock: %w", err) - } - - result, err := tx.Exec(`DELETE FROM baskets WHERE id = $1`, basketID) - if err != nil { - return fmt.Errorf("erreur lors de la suppression du produit: %w", err) - } - rows, _ := result.RowsAffected() - if rows == 0 { - return fmt.Errorf("produit non trouvé dans le panier") - } - - return tx.Commit() + result := tx.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 et restitue les stocks. func (d *Database) ClearBasket(username string) error { - tx, err := d.Begin() - if err != nil { - return fmt.Errorf("erreur transaction: %w", err) - } - defer tx.Rollback() - - _, err = tx.Exec(` - UPDATE products p - SET stock = stock + b.quantity - FROM baskets b - WHERE b.username = $1 AND b.product_id = p.id - `, username) - if err != nil { - return fmt.Errorf("erreur restitution stock: %w", err) - } - - _, err = tx.Exec(`DELETE FROM baskets WHERE username = $1`, username) - if err != nil { - return fmt.Errorf("erreur lors du vidage du panier: %w", err) - } - - return tx.Commit() + return d.GDB.Transaction(func(tx *gorm.DB) error { + if err := tx.Exec(` + UPDATE products p + SET stock = stock + b.quantity + FROM baskets b + WHERE b.username = ? AND b.product_id = p.id`, username).Error; err != nil { + return fmt.Errorf("erreur restitution stock: %w", err) + } + if err := tx.Exec(`DELETE FROM baskets WHERE username = ?`, username).Error; 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) + var result struct { + Total float64 `gorm:"column:total"` + } + err := d.GDB.Raw(`SELECT COALESCE(SUM(price), 0) as total FROM baskets WHERE username = ?`, + username).Scan(&result).Error if err != nil { return 0, fmt.Errorf("erreur lors du calcul du total: %w", err) } - - return total, nil + return result.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) + var result struct { + Count int `gorm:"column:count"` + } + err := d.GDB.Raw(`SELECT COUNT(*) as count FROM baskets WHERE username = ?`, + username).Scan(&result).Error if err != nil { return 0, fmt.Errorf("erreur lors du comptage des items: %w", err) } - - return count, nil + return result.Count, nil } // UpdateBasketItemQuantity met à jour la quantité d'un item du panier @@ -368,70 +287,43 @@ func (d *Database) UpdateBasketItemQuantity(basketID int, quantity float64) erro 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) + result := d.GDB.Exec(`UPDATE baskets SET quantity = ?, created_at = CURRENT_TIMESTAMP WHERE id = ?`, + quantity, basketID) + if result.Error != nil { + return fmt.Errorf("erreur lors de la mise à jour de la quantité: %w", result.Error) } - - 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 { + if result.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 { + 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) } - 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 { + 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 stock < item.Quantity { - return fmt.Errorf("stock insuffisant pour le produit %d (demandé: %d, disponible: %d)", - item.ProductID, item.Quantity, stock) + if stockResult.Stock < item.Quantity { + return fmt.Errorf("stock insuffisant pour le produit %d (demandé: %g, disponible: %g)", + item.ProductID, item.Quantity, stockResult.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 { + if err := d.GDB.Exec(`UPDATE baskets SET reserved_until = ? WHERE username = ?`, + newReservation, username).Error; err != nil { return fmt.Errorf("erreur prolongation: %w", err) } @@ -442,39 +334,24 @@ func (d *Database) ExtendBasketReservations(username string) error { // 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) + 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 expiredCount > 0, nil + return result.Count > 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 { +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 FROM baskets WHERE username = ?`, + username).Scan(&items).Error; 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 } diff --git a/backend/gestion/db/db_cancel_command.go b/backend/gestion/db/db_cancel_command.go index ea7f3ad7..9c6b459c 100644 --- a/backend/gestion/db/db_cancel_command.go +++ b/backend/gestion/db/db_cancel_command.go @@ -1,164 +1,155 @@ // ============================================ // db/cancel_commands_db.go // FONCTIONS DB ATOMIQUES POUR L'ANNULATION -// VERSION 100% SÉCURISÉE - FIX ETA CHECK // ============================================ package db import ( - "database/sql" "fmt" "log" "slices" - "time" -) -// ============================================ -// ANNULATION ATOMIQUE -// ============================================ + "gorm.io/gorm" +) func (d *Database) CancelCommandAtomic(commandID int, username, reason string, force bool) (int, error) { log.Printf("🔒 [CancelAtomic] START - cmd=%d, user=%s, force=%v", commandID, username, force) - tx, err := d.Begin() - if err != nil { - return 0, fmt.Errorf("erreur transaction: %w", err) - } - defer tx.Rollback() + var penalty int - var currentStatus, cmdUsername, livreurAssign string - err = tx.QueryRow(` - SELECT status, username, COALESCE(livreur_assign, '') - FROM commandes - WHERE id = $1 - FOR UPDATE - `, commandID).Scan(¤tStatus, &cmdUsername, &livreurAssign) - - if err == sql.ErrNoRows { - return 0, fmt.Errorf("commande non trouvée") - } - if err != nil { - return 0, err - } - - log.Printf("📋 [CancelAtomic] Trouvée - status=%s, owner=%s, livreur=%s", currentStatus, cmdUsername, livreurAssign) - - if cmdUsername != username { - return 0, fmt.Errorf("commande ne vous appartient pas") - } - - nonCancellableStatuses := []string{"livre", "approved", "cancelled", "disabled"} - if slices.Contains(nonCancellableStatuses, currentStatus) { - return 0, fmt.Errorf("impossible d'annuler") - } - - isLateCancel := false - if livreurAssign != "" { - if currentStatus == "en_route" || currentStatus == "arrived" { - isLateCancel = true - log.Printf("⚠️ [CancelAtomic] Annulation TARDIVE détectée - Statut: %s", currentStatus) - } else if d.CheckCommandETAExistsAndValid(commandID) { - isLateCancel = true - log.Printf("⚠️ [CancelAtomic] Annulation TARDIVE détectée - ETA définie") - } else { - log.Printf("✅ [CancelAtomic] Annulation SANS PÉNALITÉ - Statut: %s, Pas d'ETA valide", currentStatus) + err := d.GDB.Transaction(func(tx *gorm.DB) error { + var cmdResult struct { + Status string `gorm:"column:status"` + Username string `gorm:"column:username"` + LivreurAssign string `gorm:"column:livreur_assign"` } - } else { - log.Printf("✅ [CancelAtomic] Annulation SANS PÉNALITÉ - Aucun livreur assigné") - } - - if isLateCancel && !force { - return 0, fmt.Errorf("confirmation requise") - } - - result, err := tx.Exec(` - UPDATE commandes - SET status = 'cancelled', updated_at = CURRENT_TIMESTAMP - WHERE id = $1 AND status = $2 AND username = $3 - `, commandID, currentStatus, username) - if err != nil { - return 0, err - } - if rows, _ := result.RowsAffected(); rows == 0 { - return 0, fmt.Errorf("commande déjà modifiée") - } - log.Printf("✅ [CancelAtomic] Statut mis à jour: %s → cancelled", currentStatus) - - if _, err = tx.Exec(` - UPDATE products p - SET stock = stock + ci.quantite, updated_at = CURRENT_TIMESTAMP - FROM command_items ci - WHERE ci.command_id = $1 AND ci.product_id = p.id - `, commandID); err != nil { - log.Printf("⚠️ [CancelAtomic] Erreur remboursement stock: %v", err) - } else { - log.Printf("✅ [CancelAtomic] Stock remboursé") - } - - penalty := 0 - if isLateCancel { - log.Printf("⚠️ [CancelAtomic] Annulation tardive confirmée - Application pénalité") - penalty, _ = d.CalculateCancellationPenalty(username) - if _, err = tx.Exec(` - UPDATE clients - SET amende = amende + $1, - cancellations_count = COALESCE(cancellations_count, 0) + 1, - updated_at = CURRENT_TIMESTAMP - WHERE username = $2 - `, penalty, username); err != nil { - log.Printf("❌ [CancelAtomic] Erreur pénalité: %v", err) - } else { - log.Printf("⚠️ [CancelAtomic] Pénalité: %d appliquée à %s", penalty, username) + err := tx.Raw(` + SELECT status, username, COALESCE(livreur_assign, '') as livreur_assign + FROM commandes WHERE id = ? FOR UPDATE`, commandID).Scan(&cmdResult).Error + if err != nil { + return err + } + if cmdResult.Username == "" { + return fmt.Errorf("commande non trouvée") } - } - if _, err = tx.Exec(` - INSERT INTO command_logs (command_id, status, message, author, created_at) - VALUES ($1, 'cancelled', $2, $3, CURRENT_TIMESTAMP) - `, commandID, fmt.Sprintf("Annulée par %s - Raison: %s", username, reason), username); err != nil { - log.Printf("⚠️ [CancelAtomic] Erreur log: %v", err) - } + log.Printf("📋 [CancelAtomic] Trouvée - status=%s, owner=%s, livreur=%s", + cmdResult.Status, cmdResult.Username, cmdResult.LivreurAssign) - if err := tx.Commit(); err != nil { - return 0, fmt.Errorf("erreur commit: %w", err) - } + if cmdResult.Username != username { + return fmt.Errorf("commande ne vous appartient pas") + } - if livreurAssign != "" { - go func() { - if err := d.CleanupCompletedCommandFromQueue(commandID, livreurAssign); err != nil { - log.Printf("⚠️ [CancelAtomic] Erreur cleanup queue: %v", err) + nonCancellableStatuses := []string{"livre", "approved", "cancelled", "disabled"} + if slices.Contains(nonCancellableStatuses, cmdResult.Status) { + return fmt.Errorf("impossible d'annuler") + } + + isLateCancel := false + if cmdResult.LivreurAssign != "" { + if cmdResult.Status == "en_route" || cmdResult.Status == "arrived" { + isLateCancel = true + log.Printf("⚠️ [CancelAtomic] Annulation TARDIVE détectée - Statut: %s", cmdResult.Status) + } else if d.CheckCommandETAExistsAndValid(commandID) { + isLateCancel = true + log.Printf("⚠️ [CancelAtomic] Annulation TARDIVE détectée - ETA définie") + } else { + log.Printf("✅ [CancelAtomic] Annulation SANS PÉNALITÉ - Statut: %s, Pas d'ETA valide", cmdResult.Status) } + } else { + log.Printf("✅ [CancelAtomic] Annulation SANS PÉNALITÉ - Aucun livreur assigné") + } + + if isLateCancel && !force { + return fmt.Errorf("confirmation requise") + } + + result := tx.Exec(` + UPDATE commandes + SET status = 'cancelled', updated_at = CURRENT_TIMESTAMP + WHERE id = ? AND status = ? AND username = ?`, commandID, cmdResult.Status, username) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return fmt.Errorf("commande déjà modifiée") + } + log.Printf("✅ [CancelAtomic] Statut mis à jour: %s → cancelled", cmdResult.Status) + + 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("⚠️ [CancelAtomic] Erreur remboursement stock: %v", err) + } else { + log.Printf("✅ [CancelAtomic] Stock remboursé") + } + + if isLateCancel { + log.Printf("⚠️ [CancelAtomic] Annulation tardive confirmée - Application pénalité") + penalty, _ = d.CalculateCancellationPenalty(username) + if err := tx.Exec(` + UPDATE clients + SET amende = amende + ?, + cancellations_count = COALESCE(cancellations_count, 0) + 1, + updated_at = CURRENT_TIMESTAMP + WHERE username = ?`, penalty, username).Error; err != nil { + log.Printf("❌ [CancelAtomic] Erreur pénalité: %v", err) + } else { + log.Printf("⚠️ [CancelAtomic] Pénalité: %d appliquée à %s", penalty, username) + } + } + + if err := tx.Exec(` + INSERT INTO command_logs (command_id, status, message, author, created_at) + VALUES (?, 'cancelled', ?, ?, CURRENT_TIMESTAMP)`, + commandID, + fmt.Sprintf("Annulée par %s - Raison: %s", username, reason), + username, + ).Error; err != nil { + log.Printf("⚠️ [CancelAtomic] Erreur log: %v", err) + } + + // Nettoyage async après commit + livreur := cmdResult.LivreurAssign + if livreur != "" { + go func() { + if err := d.CleanupCompletedCommandFromQueue(commandID, livreur); err != nil { + log.Printf("⚠️ [CancelAtomic] Erreur cleanup queue: %v", err) + } + }() + } + go func() { + Redis.Del(RedisCtx, + fmt.Sprintf("command:%d", commandID), + fmt.Sprintf("client:%s", username), + fmt.Sprintf("client:%s:commands", username), + ) }() + + return nil + }) + + if err != nil { + return 0, err } - go func() { - Redis.Del(RedisCtx, - fmt.Sprintf("command:%d", commandID), - fmt.Sprintf("client:%s", username), - fmt.Sprintf("client:%s:commands", username), - ) - }() log.Printf("🎉 [CancelAtomic] SUCCÈS - Commande %d annulée", commandID) return penalty, nil } -// ============================================ -// ✅ NOUVELLE FONCTION: CHECK ETA VALIDE -// ============================================ - // CheckCommandETAExistsAndValid vérifie si une ETA RÉELLE existe (> 0 minutes, non expirée) func (d *Database) CheckCommandETAExistsAndValid(commandID int) bool { etaKey := fmt.Sprintf("command:eta:%d", commandID) - // Récupérer l'ETA depuis Redis etaMinutesStr, err := Redis.Get(RedisCtx, etaKey).Result() if err != nil { log.Printf("⚠️ [CheckETA] Pas d'ETA trouvée pour cmd %d", commandID) return false } - // Parser l'ETA var etaMinutes int _, err = fmt.Sscanf(etaMinutesStr, "%d", &etaMinutes) if err != nil || etaMinutes <= 0 { @@ -166,7 +157,6 @@ func (d *Database) CheckCommandETAExistsAndValid(commandID int) bool { return false } - // Vérifier le TTL (si l'ETA existe, elle doit avoir un TTL) ttl, err := Redis.TTL(RedisCtx, etaKey).Result() if err != nil || ttl <= 0 { log.Printf("⚠️ [CheckETA] ETA expirée pour cmd %d", commandID) @@ -177,106 +167,76 @@ func (d *Database) CheckCommandETAExistsAndValid(commandID int) bool { return true } -// ============================================ -// SUPPRESSION ATOMIQUE -// ============================================ - func (d *Database) DeleteCommandAtomic(commandID int, deletedBy, role string) error { log.Printf("🔒 [DeleteAtomic] START - cmd=%d, by=%s (%s)", commandID, deletedBy, role) - // ✅ TRANSACTION - tx, err := d.Begin() - if err != nil { - return fmt.Errorf("erreur transaction: %w", err) - } - defer tx.Rollback() + return d.GDB.Transaction(func(tx *gorm.DB) error { + var cmdResult struct { + Status string `gorm:"column:status"` + Username string `gorm:"column:username"` + 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 + if err != nil { + return err + } + if cmdResult.Username == "" { + return fmt.Errorf("commande non trouvée") + } - // ✅ SELECT FOR UPDATE - var currentStatus, cmdUsername, livreurAssign string - err = tx.QueryRow(` - SELECT status, username, COALESCE(livreur_assign, '') - FROM commandes - WHERE id = $1 - FOR UPDATE - `, commandID).Scan(¤tStatus, &cmdUsername, &livreurAssign) + log.Printf("📋 [DeleteAtomic] Trouvée - status=%s, client=%s", cmdResult.Status, cmdResult.Username) - if err == sql.ErrNoRows { - return fmt.Errorf("commande non trouvée") - } - if err != nil { - return err - } + 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é") + } - log.Printf("📋 [DeleteAtomic] Trouvée - status=%s, client=%s", currentStatus, cmdUsername) + tx.Exec(` + 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) - // ✅ REMBOURSER STOCK ATOMIQUEMENT - _, err = tx.Exec(` - UPDATE products p - SET stock = stock + ci.quantite, updated_at = CURRENT_TIMESTAMP - FROM command_items ci - WHERE ci.command_id = $1 AND ci.product_id = p.id - `, commandID) + if err := tx.Exec(`DELETE FROM command_items WHERE command_id = ?`, commandID).Error; err != nil { + return err + } - if err != nil { - log.Printf("⚠️ [DeleteAtomic] Erreur remboursement: %v", err) - } else { - log.Printf("✅ [DeleteAtomic] Stock remboursé") - } + result := tx.Exec(`DELETE FROM commandes WHERE id = ?`, commandID) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return fmt.Errorf("commande non trouvée") + } - // ✅ LOG AVANT SUPPRESSION - _, err = tx.Exec(` - INSERT INTO command_logs (command_id, status, message, author, created_at) - VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP) - `, commandID, "deleted", - fmt.Sprintf("Supprimée par %s (%s) - Ancien statut: %s", deletedBy, role, currentStatus), - deletedBy) + log.Printf("✅ [DeleteAtomic] Supprimée de la DB") - // ✅ SUPPRIMER ITEMS - _, err = tx.Exec(`DELETE FROM command_items WHERE command_id = $1`, commandID) - if err != nil { - return err - } + livreur := cmdResult.LivreurAssign + cmdUsername := cmdResult.Username - // ✅ SUPPRIMER COMMANDE - result, err := tx.Exec(`DELETE FROM commandes WHERE id = $1`, commandID) - if err != nil { - return err - } + if livreur != "" { + go d.RemoveCommandFromAllQueues(commandID, livreur) + } + go func() { + Redis.Del(RedisCtx, + fmt.Sprintf("command:%d", commandID), + fmt.Sprintf("client:%s", cmdUsername), + fmt.Sprintf("client:%s:commands", cmdUsername), + ) + }() - rows, _ := result.RowsAffected() - if rows == 0 { - return fmt.Errorf("commande non trouvée") - } - - log.Printf("✅ [DeleteAtomic] Supprimée de la DB") - - // ✅ COMMIT - if err := tx.Commit(); err != nil { - return fmt.Errorf("erreur commit: %w", err) - } - - // ✅ NETTOYER QUEUES (async) - if livreurAssign != "" { - go d.RemoveCommandFromAllQueues(commandID, livreurAssign) - } - - // ✅ INVALIDER CACHES (async) - go func() { - Redis.Del(RedisCtx, - fmt.Sprintf("command:%d", commandID), - fmt.Sprintf("client:%s", cmdUsername), - fmt.Sprintf("client:%s:commands", cmdUsername), - ) - }() - - log.Printf("🎉 [DeleteAtomic] SUCCÈS - Commande %d supprimée", commandID) - return nil + return nil + }) } -// ============================================ -// FONCTIONS HELPERS (déjà sécurisées) -// ============================================ - func (d *Database) GetCommandPositionInQueue(livreurUsername string, commandID int) (int, error) { queueKey := fmt.Sprintf("queue:deliveryman:%s", livreurUsername) commandIDStr := fmt.Sprintf("%d", commandID) @@ -285,104 +245,61 @@ func (d *Database) GetCommandPositionInQueue(livreurUsername string, commandID i if err != nil { return 0, fmt.Errorf("commande non trouvée dans la queue") } - return int(rank) + 1, nil } -func (d *Database) GetCancelledCommands(username string, limit int) ([]map[string]interface{}, error) { +func (d *Database) GetCancelledCommands(username string, limit int) ([]map[string]any, error) { query := ` - SELECT id, username, status, adresse, total_prix, created_at, updated_at + SELECT id, client_order_id AS client_order_number, username, status, adresse, total_prix::float8 as total_prix, created_at, updated_at FROM commandes - WHERE status = 'cancelled' - ` + WHERE status = 'cancelled'` - args := []interface{}{} - argPos := 1 + args := []any{} if username != "" { - query += fmt.Sprintf(" AND username = $%d", argPos) + query += " AND username = ?" args = append(args, username) - argPos++ } query += " ORDER BY updated_at DESC" if limit > 0 { - query += fmt.Sprintf(" LIMIT $%d", argPos) + query += " LIMIT ?" args = append(args, limit) } - rows, err := d.Query(query, args...) - if err != nil { + var commands []map[string]any + if err := d.GDB.Raw(query, args...).Scan(&commands).Error; err != nil { return nil, fmt.Errorf("erreur récupération: %w", err) } - defer rows.Close() - - var commands []map[string]interface{} - for rows.Next() { - var id int - var username, status, adresse string - var totalPrix float64 - var createdAt, updatedAt time.Time - - err := rows.Scan(&id, &username, &status, &adresse, &totalPrix, &createdAt, &updatedAt) - if err != nil { - return nil, err - } - - commands = append(commands, map[string]interface{}{ - "id": id, - "username": username, - "status": status, - "adresse": adresse, - "total_prix": totalPrix, - "created_at": createdAt, - "updated_at": updatedAt, - }) - } - return commands, nil } -// ============================================ -// GESTION DES PÉNALITÉS -// ============================================ - // AddClientPenalty ajoute une pénalité à un client func (d *Database) AddClientPenalty(username string, points int) error { log.Printf("⚠️ [AddPenalty] Ajout pénalité: %d points pour client %s", points, username) - // ✅ Validation if points <= 0 { return fmt.Errorf("points invalides: %d", points) } - if username == "" { return fmt.Errorf("username vide") } - // ✅ UPDATE dans PostgreSQL - query := `UPDATE clients - SET amende = amende + $1, updated_at = CURRENT_TIMESTAMP - WHERE username = $2` - - result, err := d.Exec(query, points, username) - if err != nil { - log.Printf("❌ [AddPenalty] Erreur UPDATE: %v", err) - return fmt.Errorf("erreur ajout pénalité: %w", err) + result := d.GDB.Exec(` + UPDATE clients + SET amende = amende + ?, updated_at = CURRENT_TIMESTAMP + WHERE username = ?`, points, username) + if result.Error != nil { + log.Printf("❌ [AddPenalty] Erreur UPDATE: %v", result.Error) + return fmt.Errorf("erreur ajout pénalité: %w", result.Error) } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return fmt.Errorf("erreur vérification: %w", err) - } - if rowsAffected == 0 { + if result.RowsAffected == 0 { return fmt.Errorf("client non trouvé: %s", username) } log.Printf("✅ [AddPenalty] Pénalité ajoutée: +%d points pour %s", points, username) - // ✅ Invalider le cache Redis du client cacheKey := fmt.Sprintf("client:%s", username) Redis.Del(RedisCtx, cacheKey) diff --git a/backend/gestion/db/db_categories.go b/backend/gestion/db/db_categories.go index 18bf3605..3e412f58 100644 --- a/backend/gestion/db/db_categories.go +++ b/backend/gestion/db/db_categories.go @@ -3,21 +3,24 @@ package db import ( "fmt" "regexp" + "time" ) var hexColorRegex = regexp.MustCompile(`^#[0-9A-Fa-f]{6}$`) type Category struct { - ID int `json:"id"` - Name string `json:"name"` - Color string `json:"color"` - IsComingSoon bool `json:"is_coming_soon"` - CreatedAt string `json:"created_at"` + ID int `json:"id" gorm:"primaryKey;autoIncrement"` + Name string `json:"name" gorm:"column:name"` + Color string `json:"color" gorm:"column:color"` + IsComingSoon bool `json:"is_coming_soon" gorm:"column:is_coming_soon"` + CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` } +func (Category) TableName() string { return "categories" } + func ValidateCategoryColor(color string) error { if color == "" { - return nil // valeur par défaut utilisée + return nil } if !hexColorRegex.MatchString(color) { return fmt.Errorf("couleur invalide : format hexadécimal requis (ex: #7c3aed)") @@ -26,19 +29,9 @@ func ValidateCategoryColor(color string) error { } func (d *Database) GetAllCategories() ([]Category, error) { - rows, err := d.Query(`SELECT id, name, color, is_coming_soon, created_at FROM categories ORDER BY name ASC`) - if err != nil { - return nil, err - } - defer rows.Close() - var categories []Category - for rows.Next() { - var c Category - if err := rows.Scan(&c.ID, &c.Name, &c.Color, &c.IsComingSoon, &c.CreatedAt); err != nil { - return nil, err - } - categories = append(categories, c) + if err := d.GDB.Order("name ASC").Find(&categories).Error; err != nil { + return nil, err } if categories == nil { categories = []Category{} @@ -50,12 +43,8 @@ func (d *Database) CreateCategory(name, color string, isComingSoon bool) (*Categ if color == "" { color = "#7c3aed" } - var c Category - err := d.QueryRow( - `INSERT INTO categories (name, color, is_coming_soon) VALUES ($1, $2, $3) RETURNING id, name, color, is_coming_soon, created_at`, - name, color, isComingSoon, - ).Scan(&c.ID, &c.Name, &c.Color, &c.IsComingSoon, &c.CreatedAt) - if err != nil { + c := Category{Name: name, Color: color, IsComingSoon: isComingSoon} + if err := d.GDB.Create(&c).Error; err != nil { return nil, err } return &c, nil @@ -66,38 +55,35 @@ func (d *Database) UpdateCategory(id int, name, color string, isComingSoon bool) color = "#7c3aed" } var c Category - err := d.QueryRow( - `UPDATE categories SET name = $1, color = $2, is_coming_soon = $3 WHERE id = $4 RETURNING id, name, color, is_coming_soon, created_at`, - name, color, isComingSoon, id, - ).Scan(&c.ID, &c.Name, &c.Color, &c.IsComingSoon, &c.CreatedAt) - if err != nil { + if err := d.GDB.First(&c, id).Error; err != nil { + return nil, err + } + if err := d.GDB.Model(&c).Updates(Category{Name: name, Color: color, IsComingSoon: isComingSoon}).Error; err != nil { return nil, err } return &c, nil } func (d *Database) DeleteCategory(id int) error { - var count int - err := d.QueryRow(`SELECT COUNT(*) FROM products WHERE category = (SELECT name FROM categories WHERE id = $1)`, id).Scan(&count) - if err != nil { - return err - } + var count int64 + d.GDB.Table("products"). + Where("category = (SELECT name FROM categories WHERE id = ?)", id). + Count(&count) if count > 0 { return fmt.Errorf("catégorie utilisée par %d produit(s)", count) } - res, err := d.Exec(`DELETE FROM categories WHERE id = $1`, id) - if err != nil { - return err + result := d.GDB.Delete(&Category{}, id) + if result.Error != nil { + return result.Error } - n, _ := res.RowsAffected() - if n == 0 { + if result.RowsAffected == 0 { return fmt.Errorf("catégorie non trouvée") } return nil } func (d *Database) CategoryExists(name string) (bool, error) { - var count int - err := d.QueryRow(`SELECT COUNT(*) FROM categories WHERE name = $1`, name).Scan(&count) + var count int64 + err := d.GDB.Model(&Category{}).Where("name = ?", name).Count(&count).Error return count > 0, err } diff --git a/backend/gestion/db/db_clients.go b/backend/gestion/db/db_clients.go index 2fd3d80a..96da7667 100644 --- a/backend/gestion/db/db_clients.go +++ b/backend/gestion/db/db_clients.go @@ -1,26 +1,32 @@ package db import ( - "database/sql" "encoding/json" "fmt" "gestion/models" "log" "strings" + "time" + + "gorm.io/gorm" ) func (d *Database) CreateClient(client *models.Client) error { - query := `INSERT INTO clients (username, password, nom, prenom, telephone, command, amende, must_change_password, created_at) - VALUES ($1, $2, $3, $4, $5, 0, 0.0, $6, CURRENT_TIMESTAMP) - RETURNING id, created_at` - - err := d.QueryRow(query, client.Username, client.Password, client.Nom, client.Prenom, client.Telephone, client.MustChangePassword).Scan( - &client.ID, - &client.CreatedAt, - ) + var result struct { + ID int `gorm:"column:id"` + CreatedAt time.Time `gorm:"column:created_at"` + } + err := d.GDB.Raw(` + INSERT INTO clients (username, password, nom, prenom, telephone, command, amende, must_change_password, created_at) + VALUES (?, ?, ?, ?, ?, 0, 0.0, ?, CURRENT_TIMESTAMP) + RETURNING id, created_at`, + client.Username, client.Password, client.Nom, client.Prenom, client.Telephone, client.MustChangePassword, + ).Scan(&result).Error if err != nil { return fmt.Errorf("erreur lors de la création du client: %w", err) } + client.ID = result.ID + client.CreatedAt = result.CreatedAt log.Printf("✅ Client créé avec succès: %s %s (ID: %d)", client.Prenom, client.Nom, client.ID) return nil @@ -28,110 +34,109 @@ func (d *Database) CreateClient(client *models.Client) error { // GetClientByID récupère un client par son ID func (d *Database) GetClientByID(id int) (*models.Client, error) { - var client models.Client - query := `SELECT id, username, password, nom, prenom, telephone, command, amende, COALESCE(points_extra, '{}'::jsonb), created_at - FROM clients WHERE id = $1` - - var pointsExtraJSON []byte - err := d.QueryRow(query, id).Scan( - &client.ID, - &client.Username, - &client.Password, - &client.Nom, - &client.Prenom, - &client.Telephone, - &client.Command, - &client.Amende, - &pointsExtraJSON, - &client.CreatedAt, - ) - - if err == sql.ErrNoRows { - return nil, fmt.Errorf("client non trouvé") + var row struct { + ID int `gorm:"column:id"` + Username string `gorm:"column:username"` + Password string `gorm:"column:password"` + Nom string `gorm:"column:nom"` + Prenom string `gorm:"column:prenom"` + Telephone string `gorm:"column:telephone"` + Command int `gorm:"column:command"` + Amende float64 `gorm:"column:amende"` + PointsExtraJSON []byte `gorm:"column:points_extra"` + CreatedAt time.Time `gorm:"column:created_at"` } + err := d.GDB.Raw(` + SELECT id, username, password, nom, prenom, telephone, command, amende, + COALESCE(points_extra, '{}'::jsonb) as points_extra, created_at + FROM clients WHERE id = ?`, id).Scan(&row).Error if err != nil { return nil, fmt.Errorf("erreur lors de la récupération du client: %w", err) } - - if len(pointsExtraJSON) > 0 { - json.Unmarshal(pointsExtraJSON, &client.PointsExtra) + if row.ID == 0 { + return nil, fmt.Errorf("client non trouvé") } - return &client, nil + client := &models.Client{ + ID: row.ID, + Username: row.Username, + Password: row.Password, + Nom: row.Nom, + Prenom: row.Prenom, + Telephone: row.Telephone, + Command: row.Command, + Amende: row.Amende, + CreatedAt: row.CreatedAt, + } + client.PointsExtra = map[string]int{} + if len(row.PointsExtraJSON) > 0 { + json.Unmarshal(row.PointsExtraJSON, &client.PointsExtra) + } + + return client, nil } // GetAllClients récupère tous les clients func (d *Database) GetAllClients() ([]*models.Client, error) { - query := `SELECT id, username, password, nom, prenom, telephone, command, amende, referral_balance, COALESCE(points_extra, '{}'::jsonb), created_at - FROM clients ORDER BY created_at DESC` - - rows, err := d.Query(query) + var rows []struct { + ID int `gorm:"column:id"` + Username string `gorm:"column:username"` + Password string `gorm:"column:password"` + Nom string `gorm:"column:nom"` + Prenom string `gorm:"column:prenom"` + Telephone string `gorm:"column:telephone"` + Command int `gorm:"column:command"` + Amende float64 `gorm:"column:amende"` + ReferralBalance float64 `gorm:"column:referral_balance"` + PointsExtraJSON []byte `gorm:"column:points_extra"` + CreatedAt time.Time `gorm:"column:created_at"` + } + err := d.GDB.Raw(` + SELECT id, username, password, nom, prenom, telephone, command, amende, referral_balance, + COALESCE(points_extra, '{}'::jsonb) as points_extra, created_at + FROM clients ORDER BY created_at DESC`).Scan(&rows).Error if err != nil { return nil, fmt.Errorf("erreur lors de la récupération des clients: %w", err) } - defer rows.Close() - var clients []*models.Client - for rows.Next() { - client := &models.Client{} - var pointsExtraJSON []byte - err := rows.Scan( - &client.ID, - &client.Username, - &client.Password, - &client.Nom, - &client.Prenom, - &client.Telephone, - &client.Command, - &client.Amende, - &client.ReferralBalance, - &pointsExtraJSON, - &client.CreatedAt, - ) - if err != nil { - return nil, fmt.Errorf("erreur lors du scan du client: %w", err) + clients := make([]*models.Client, 0, len(rows)) + for _, row := range rows { + client := &models.Client{ + ID: row.ID, + Username: row.Username, + Password: row.Password, + Nom: row.Nom, + Prenom: row.Prenom, + Telephone: row.Telephone, + Command: row.Command, + Amende: row.Amende, + ReferralBalance: row.ReferralBalance, + CreatedAt: row.CreatedAt, } client.PointsExtra = map[string]int{} - if len(pointsExtraJSON) > 0 { - json.Unmarshal(pointsExtraJSON, &client.PointsExtra) + if len(row.PointsExtraJSON) > 0 { + json.Unmarshal(row.PointsExtraJSON, &client.PointsExtra) } clients = append(clients, client) } - if err = rows.Err(); err != nil { - return nil, fmt.Errorf("erreur lors de l'itération des résultats: %w", err) - } - return clients, nil } // UpdateClient met à jour un client existant func (d *Database) UpdateClient(client *models.Client) error { - query := `UPDATE clients - SET username = $1, password = $2, nom = $3, prenom = $4, telephone = $5, - command = $6, amende = $7 - WHERE id = $8` - - result, err := d.Exec(query, - client.Username, - client.Password, - client.Nom, - client.Prenom, - client.Telephone, - client.Command, - client.Amende, - client.ID, + result := d.GDB.Exec(` + UPDATE clients + SET username = ?, password = ?, nom = ?, prenom = ?, telephone = ?, + command = ?, amende = ? + WHERE id = ?`, + client.Username, client.Password, client.Nom, client.Prenom, client.Telephone, + client.Command, client.Amende, client.ID, ) - if err != nil { - return fmt.Errorf("erreur lors de la mise à jour du client: %w", err) + if result.Error != nil { + return fmt.Errorf("erreur lors de la mise à jour du client: %w", result.Error) } - - 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 { + if result.RowsAffected == 0 { return fmt.Errorf("client non trouvé") } @@ -142,19 +147,11 @@ func (d *Database) UpdateClient(client *models.Client) error { func (d *Database) DeleteClient(id int) error { _ = d.RevokeAllUserTokens(id, "client") - query := `DELETE FROM clients WHERE id = $1` - - result, err := d.Exec(query, id) - if err != nil { - return fmt.Errorf("erreur lors de la suppression du client: %w", err) + result := d.GDB.Exec(`DELETE FROM clients WHERE id = ?`, id) + if result.Error != nil { + return fmt.Errorf("erreur lors de la suppression du client: %w", result.Error) } - - 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 { + if result.RowsAffected == 0 { return fmt.Errorf("client non trouvé") } @@ -163,19 +160,11 @@ func (d *Database) DeleteClient(id int) error { // UpdateClientPassword met à jour le mot de passe d'un client func (d *Database) UpdateClientPassword(clientID int, hashedPassword string) error { - query := `UPDATE clients SET password = $1 WHERE id = $2` - - result, err := d.Exec(query, hashedPassword, clientID) - if err != nil { - return fmt.Errorf("erreur lors de la mise à jour du mot de passe: %w", err) + result := d.GDB.Exec(`UPDATE clients SET password = ? WHERE id = ?`, hashedPassword, clientID) + if result.Error != nil { + return fmt.Errorf("erreur lors de la mise à jour du mot de passe: %w", result.Error) } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return fmt.Errorf("erreur lors de la vérification: %w", err) - } - - if rowsAffected == 0 { + if result.RowsAffected == 0 { return fmt.Errorf("client non trouvé") } @@ -184,19 +173,13 @@ func (d *Database) UpdateClientPassword(clientID int, hashedPassword string) err // UpdateClientPasswordAndClearFlag met à jour le mot de passe et remet must_change_password à false func (d *Database) UpdateClientPasswordAndClearFlag(clientID int, hashedPassword string) error { - query := `UPDATE clients SET password = $1, must_change_password = FALSE, updated_at = CURRENT_TIMESTAMP WHERE id = $2` - - result, err := d.Exec(query, hashedPassword, clientID) - if err != nil { - return fmt.Errorf("erreur lors de la mise à jour du mot de passe: %w", err) + result := d.GDB.Exec(` + UPDATE clients SET password = ?, must_change_password = FALSE, updated_at = CURRENT_TIMESTAMP + WHERE id = ?`, hashedPassword, clientID) + if result.Error != nil { + return fmt.Errorf("erreur lors de la mise à jour du mot de passe: %w", result.Error) } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return fmt.Errorf("erreur lors de la vérification: %w", err) - } - - if rowsAffected == 0 { + if result.RowsAffected == 0 { return fmt.Errorf("client non trouvé") } @@ -210,19 +193,18 @@ func (d *Database) GetClientStats(clientID int) (map[string]interface{}, error) return nil, err } - // Compter les commandes du client - var totalCommands, pendingCommands, completedCommands int - - countQuery := `SELECT - COUNT(*) as total, - SUM(CASE WHEN status = 'pending' OR status = 'livre' THEN 1 ELSE 0 END) as pending, - SUM(CASE WHEN status = 'approved' THEN 1 ELSE 0 END) as completed - FROM commandes WHERE username = $1` - - err = d.QueryRow(countQuery, client.Username).Scan(&totalCommands, &pendingCommands, &completedCommands) - if err != nil { + var statsResult struct { + Total int `gorm:"column:total"` + Pending int `gorm:"column:pending"` + Completed int `gorm:"column:completed"` + } + if err := d.GDB.Raw(` + SELECT + COUNT(*) as total, + COALESCE(SUM(CASE WHEN status = 'pending' OR status = 'livre' THEN 1 ELSE 0 END), 0) as pending, + COALESCE(SUM(CASE WHEN status = 'approved' THEN 1 ELSE 0 END), 0) as completed + FROM commandes WHERE username = ?`, client.Username).Scan(&statsResult).Error; err != nil { log.Printf("⚠️ Erreur calcul stats: %v", err) - totalCommands, pendingCommands, completedCommands = 0, 0, 0 } stats := map[string]interface{}{ @@ -231,9 +213,9 @@ func (d *Database) GetClientStats(clientID int) (map[string]interface{}, error) "nom": client.Nom, "prenom": client.Prenom, "telephone": client.Telephone, - "total_commands": totalCommands, - "pending_commands": pendingCommands, - "completed_commands": completedCommands, + "total_commands": statsResult.Total, + "pending_commands": statsResult.Pending, + "completed_commands": statsResult.Completed, "points_extra": client.PointsExtra, "amende": client.Amende, "member_since": client.CreatedAt, @@ -243,23 +225,22 @@ func (d *Database) GetClientStats(clientID int) (map[string]interface{}, error) } func (d *Database) GetClientAmende(username string) (float64, error) { - var amende float64 - query := `SELECT COALESCE(amende, 0) FROM clients WHERE username = $1` - - err := d.QueryRow(query, username).Scan(&amende) + var result struct { + Amende float64 `gorm:"column:amende"` + } + err := d.GDB.Raw(`SELECT COALESCE(amende, 0) as amende FROM clients WHERE username = ?`, username).Scan(&result).Error if err != nil { log.Printf("❌ [GetClientAmende] Erreur pour %s: %v", username, err) return 0, fmt.Errorf("erreur récupération pénalités: %w", err) } - log.Printf("💰 [GetClientAmende] Client %s: %.2f points", username, amende) - return amende, nil + log.Printf("💰 [GetClientAmende] Client %s: %.2f points", username, result.Amende) + return result.Amende, nil } func (d *Database) PayClientPenalties(username string, amountPaid float64) error { log.Printf("💳 [PayClientPenalties] Paiement de %.2f points pour %s", amountPaid, username) - // Vérifier le montant actuel currentAmount, err := d.GetClientAmende(username) if err != nil { return err @@ -273,21 +254,14 @@ func (d *Database) PayClientPenalties(username string, amountPaid float64) error return fmt.Errorf("montant insuffisant: %.2f payé, %.2f requis", amountPaid, currentAmount) } - query := `UPDATE clients - SET amende = 0, updated_at = CURRENT_TIMESTAMP - WHERE username = $1` - - result, err := d.Exec(query, username) - if err != nil { - log.Printf("❌ [PayClientPenalties] Erreur UPDATE: %v", err) - return fmt.Errorf("erreur paiement pénalités: %w", err) + result := d.GDB.Exec(` + UPDATE clients SET amende = 0, updated_at = CURRENT_TIMESTAMP + WHERE username = ?`, username) + if result.Error != nil { + log.Printf("❌ [PayClientPenalties] Erreur UPDATE: %v", result.Error) + return fmt.Errorf("erreur paiement pénalités: %w", result.Error) } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return fmt.Errorf("erreur vérification: %w", err) - } - if rowsAffected == 0 { + if result.RowsAffected == 0 { return fmt.Errorf("client non trouvé") } @@ -299,20 +273,11 @@ func (d *Database) PayClientPenalties(username string, amountPaid float64) error // IncrementClientCommandCount incrémente le compteur de commandes du client func (d *Database) IncrementClientCommandCount(username string) error { - query := `UPDATE clients - SET command = command + 1 - WHERE username = $1` - - result, err := d.Exec(query, username) - if err != nil { - return fmt.Errorf("erreur lors de l'incrémentation du compteur: %w", err) + result := d.GDB.Exec(`UPDATE clients SET command = command + 1 WHERE username = ?`, username) + if result.Error != nil { + return fmt.Errorf("erreur lors de l'incrémentation du compteur: %w", result.Error) } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return fmt.Errorf("erreur lors de la vérification: %w", err) - } - if rowsAffected == 0 { + if result.RowsAffected == 0 { return fmt.Errorf("client non trouvé") } @@ -323,23 +288,19 @@ func (d *Database) AddClientPointsByCategory(username string, points int, poolKe if poolKey == "" { poolKey = "pool_0" } - result, err := d.Exec(` + result := d.GDB.Exec(` UPDATE clients SET points_extra = jsonb_set( COALESCE(points_extra, '{}'::jsonb), - ARRAY[$2], - to_jsonb(COALESCE((points_extra->>$2)::int, 0) + $3) + ARRAY[?], + to_jsonb(COALESCE((points_extra->>?)::int, 0) + ?) ), updated_at = CURRENT_TIMESTAMP - WHERE username = $1 - `, username, poolKey, points) - if err != nil { - return fmt.Errorf("erreur lors de l'ajout de points: %w", err) + WHERE username = ?`, + poolKey, poolKey, points, username) + if result.Error != nil { + return fmt.Errorf("erreur lors de l'ajout de points: %w", result.Error) } - rowsAffected, err := result.RowsAffected() - if err != nil { - return fmt.Errorf("erreur lors de la vérification: %w", err) - } - if rowsAffected == 0 { + if result.RowsAffected == 0 { return fmt.Errorf("client non trouvé") } log.Printf("✅ %d points (key=%s) ajoutés au client %s", points, poolKey, username) @@ -347,101 +308,114 @@ func (d *Database) AddClientPointsByCategory(username string, points int, poolKe } func (d *Database) CalculateAndAddPointsForCommand(commandID int, username string) (int, error) { - tx, err := d.Begin() - if err != nil { - return 0, fmt.Errorf("erreur transaction: %w", err) - } - defer tx.Rollback() - - points, _, err := d.CalculateAndAddPointsForCommandTx(tx, commandID, username) + var totalPoints int + err := d.GDB.Transaction(func(tx *gorm.DB) error { + points, _, err := d.CalculateAndAddPointsForCommandTx(tx, commandID, username) + if err != nil { + return err + } + totalPoints = points + return nil + }) if err != nil { return 0, err } - - if err := tx.Commit(); err != nil { - return 0, fmt.Errorf("erreur commit: %w", err) - } - return points, nil + return totalPoints, nil } func (d *Database) GetClientByTelephone(telephone string) (*models.Client, error) { - client := &models.Client{} - query := `SELECT id, username, password, nom, prenom, telephone, command, amende, created_at - FROM clients WHERE telephone = $1` - - err := d.QueryRow(query, telephone).Scan( - &client.ID, - &client.Username, - &client.Password, - &client.Nom, - &client.Prenom, - &client.Telephone, - &client.Command, - &client.Amende, - &client.CreatedAt, - ) - - if err == sql.ErrNoRows { - return nil, nil + var row struct { + ID int `gorm:"column:id"` + Username string `gorm:"column:username"` + Password string `gorm:"column:password"` + Nom string `gorm:"column:nom"` + Prenom string `gorm:"column:prenom"` + Telephone string `gorm:"column:telephone"` + Command int `gorm:"column:command"` + Amende float64 `gorm:"column:amende"` + CreatedAt time.Time `gorm:"column:created_at"` } + err := d.GDB.Raw(` + SELECT id, username, password, nom, prenom, telephone, command, amende, created_at + FROM clients WHERE telephone = ?`, telephone).Scan(&row).Error if err != nil { return nil, fmt.Errorf("erreur lors de la récupération du client: %w", err) } + if row.ID == 0 { + return nil, nil + } - return client, nil + return &models.Client{ + ID: row.ID, + Username: row.Username, + Password: row.Password, + Nom: row.Nom, + Prenom: row.Prenom, + Telephone: row.Telephone, + Command: row.Command, + Amende: row.Amende, + CreatedAt: row.CreatedAt, + }, nil } // GetClientByUsername récupère un client par son username func (d *Database) GetClientByUsername(username string) (*models.Client, error) { - client := &models.Client{} - var pointsExtraJSON []byte - query := `SELECT id, username, password, nom, prenom, telephone, command, amende, must_change_password, COALESCE(points_extra, '{}'::jsonb), created_at - FROM clients WHERE username = $1` - - err := d.QueryRow(query, username).Scan( - &client.ID, - &client.Username, - &client.Password, - &client.Nom, - &client.Prenom, - &client.Telephone, - &client.Command, - &client.Amende, - &client.MustChangePassword, - &pointsExtraJSON, - &client.CreatedAt, - ) - - if err == sql.ErrNoRows { - return nil, nil + var row struct { + ID int `gorm:"column:id"` + Username string `gorm:"column:username"` + Password string `gorm:"column:password"` + Nom string `gorm:"column:nom"` + Prenom string `gorm:"column:prenom"` + Telephone string `gorm:"column:telephone"` + Command int `gorm:"column:command"` + Amende float64 `gorm:"column:amende"` + MustChangePassword bool `gorm:"column:must_change_password"` + PointsExtraJSON []byte `gorm:"column:points_extra"` + CreatedAt time.Time `gorm:"column:created_at"` } + err := d.GDB.Raw(` + SELECT id, username, password, nom, prenom, telephone, command, amende, + must_change_password, COALESCE(points_extra, '{}'::jsonb) as points_extra, created_at + FROM clients WHERE username = ?`, username).Scan(&row).Error if err != nil { return nil, fmt.Errorf("erreur lors de la récupération du client: %w", err) } + if row.ID == 0 { + return nil, nil + } + client := &models.Client{ + ID: row.ID, + Username: row.Username, + Password: row.Password, + Nom: row.Nom, + Prenom: row.Prenom, + Telephone: row.Telephone, + Command: row.Command, + Amende: row.Amende, + MustChangePassword: row.MustChangePassword, + CreatedAt: row.CreatedAt, + } client.PointsExtra = map[string]int{} - if len(pointsExtraJSON) > 0 { - json.Unmarshal(pointsExtraJSON, &client.PointsExtra) + if len(row.PointsExtraJSON) > 0 { + json.Unmarshal(row.PointsExtraJSON, &client.PointsExtra) } return client, nil } func (d *Database) GetClientPenaltiesInfo(username string) (map[string]interface{}, error) { - // Récupérer le montant des pénalités amende, err := d.GetClientAmende(username) if err != nil { return nil, err } - // Récupérer le nombre d'annulations cancellationsCount, err := d.GetClientCancellationsCount(username) if err != nil { log.Printf("⚠️ [GetClientPenaltiesInfo] Erreur récup annulations: %v", err) cancellationsCount = 0 } - // Récupérer l'historique d'annulations cancellationHistory, err := d.GetClientCancellationHistory(username) if err != nil { log.Printf("⚠️ [GetClientPenaltiesInfo] Erreur récup historique: %v", err) @@ -481,13 +455,10 @@ func (d *Database) CheckClientCanOrder(username string) (bool, float64, error) { // extraPoolKey != "" → reset points_extra[extraPoolKey] uniquement // extraPoolKey == "" (poolIdx=-1) → reset total points_extra func (d *Database) ResetClientPoint(username string, poolIdx int, extraPoolKey string) error { - var query string - switch { - case extraPoolKey != "": - _, err := d.Exec( - `UPDATE clients SET points_extra = points_extra - $2, updated_at = CURRENT_TIMESTAMP WHERE username = $1`, - username, extraPoolKey, - ) + if extraPoolKey != "" { + err := d.GDB.Exec(` + UPDATE clients SET points_extra = points_extra - ?, updated_at = CURRENT_TIMESTAMP + WHERE username = ?`, extraPoolKey, username).Error if err != nil { log.Printf("❌ [ResetClientPointAdmin] Erreur UPDATE extra: %v", err) } else { @@ -495,25 +466,20 @@ func (d *Database) ResetClientPoint(username string, poolIdx int, extraPoolKey s Redis.Del(RedisCtx, cacheKey) } return err - default: // -1 ou poolIdx sans clé → reset total - query = `UPDATE clients SET points_extra = '{}'::jsonb, updated_at = CURRENT_TIMESTAMP WHERE username = $1` } - result, err := d.Exec(query, username) - if err != nil { - log.Printf("❌ [ResetClientPointAdmin] Erreur UPDATE: %v", err) - return fmt.Errorf("erreur reset points: %w", err) + // -1 ou poolIdx sans clé → reset total + result := d.GDB.Exec(` + UPDATE clients SET points_extra = '{}'::jsonb, updated_at = CURRENT_TIMESTAMP + WHERE username = ?`, username) + if result.Error != nil { + log.Printf("❌ [ResetClientPointAdmin] Erreur UPDATE: %v", result.Error) + return fmt.Errorf("erreur reset points: %w", result.Error) } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return fmt.Errorf("erreur vérification: %w", err) - } - if rowsAffected == 0 { + if result.RowsAffected == 0 { return fmt.Errorf("client non trouvé") } - // Invalider le cache Redis du client cacheKey := fmt.Sprintf("client:%s", username) Redis.Del(RedisCtx, cacheKey) @@ -525,33 +491,20 @@ func (d *Database) ResetClientPenalties(username string, resetCancellationsCount var query string if resetCancellationsCount { - query = `UPDATE clients - SET amende = 0, - cancellations_count = 0, - updated_at = CURRENT_TIMESTAMP - WHERE username = $1` + query = `UPDATE clients SET amende = 0, cancellations_count = 0, updated_at = CURRENT_TIMESTAMP WHERE username = ?` } else { - query = `UPDATE clients - SET amende = 0, - updated_at = CURRENT_TIMESTAMP - WHERE username = $1` + query = `UPDATE clients SET amende = 0, updated_at = CURRENT_TIMESTAMP WHERE username = ?` } - result, err := d.Exec(query, username) - if err != nil { - log.Printf("❌ [ResetClientPenalties] Erreur UPDATE: %v", err) - return fmt.Errorf("erreur reset pénalités: %w", err) + result := d.GDB.Exec(query, username) + if result.Error != nil { + log.Printf("❌ [ResetClientPenalties] Erreur UPDATE: %v", result.Error) + return fmt.Errorf("erreur reset pénalités: %w", result.Error) } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return fmt.Errorf("erreur vérification: %w", err) - } - if rowsAffected == 0 { + if result.RowsAffected == 0 { return fmt.Errorf("client non trouvé") } - // Invalider le cache Redis du client cacheKey := fmt.Sprintf("client:%s", username) Redis.Del(RedisCtx, cacheKey) @@ -559,38 +512,29 @@ func (d *Database) ResetClientPenalties(username string, resetCancellationsCount } func (d *Database) GetAllClientsWithPenalties() ([]map[string]interface{}, error) { - query := ` + var rows []struct { + Username string `gorm:"column:username"` + Amende float64 `gorm:"column:amende"` + CancellationsCount int `gorm:"column:cancellations_count"` + UpdatedAt interface{} `gorm:"column:updated_at"` + } + err := d.GDB.Raw(` SELECT username, amende, COALESCE(cancellations_count, 0) as cancellations_count, updated_at FROM clients WHERE amende > 0 - ORDER BY amende DESC - ` - - rows, err := d.Query(query) + ORDER BY amende DESC`).Scan(&rows).Error if err != nil { log.Printf("❌ [GetAllClientsWithPenalties] Erreur query: %v", err) return nil, fmt.Errorf("erreur récupération clients: %w", err) } - defer rows.Close() - - var clients []map[string]interface{} - for rows.Next() { - var username string - var amende float64 - var cancellationsCount int - var updatedAt interface{} - - err := rows.Scan(&username, &amende, &cancellationsCount, &updatedAt) - if err != nil { - log.Printf("⚠️ [GetAllClientsWithPenalties] Erreur scan: %v", err) - continue - } + clients := make([]map[string]interface{}, 0, len(rows)) + for _, row := range rows { clients = append(clients, map[string]interface{}{ - "username": username, - "total_penalty": amende, - "cancellations_count": cancellationsCount, - "last_updated": updatedAt, + "username": row.Username, + "total_penalty": row.Amende, + "cancellations_count": row.CancellationsCount, + "last_updated": row.UpdatedAt, }) } @@ -600,51 +544,42 @@ func (d *Database) GetAllClientsWithPenalties() ([]map[string]interface{}, error } func (d *Database) GetClientPenaltiesStats() (map[string]interface{}, error) { - query := ` + var result struct { + ClientsWithPenalties int `gorm:"column:clients_with_penalties"` + TotalPenalties float64 `gorm:"column:total_penalties"` + AvgPenalty float64 `gorm:"column:avg_penalty"` + MaxPenalty float64 `gorm:"column:max_penalty"` + TotalClients int `gorm:"column:total_clients"` + } + + err := d.GDB.Raw(` SELECT COUNT(CASE WHEN amende > 0 THEN 1 END) as clients_with_penalties, COALESCE(SUM(amende), 0) as total_penalties, COALESCE(AVG(amende), 0) as avg_penalty, COALESCE(MAX(amende), 0) as max_penalty, COUNT(*) as total_clients - FROM clients - ` - - var stats struct { - ClientsWithPenalties int - TotalPenalties float64 - AvgPenalty float64 - MaxPenalty float64 - TotalClients int - } - - err := d.QueryRow(query).Scan( - &stats.ClientsWithPenalties, - &stats.TotalPenalties, - &stats.AvgPenalty, - &stats.MaxPenalty, - &stats.TotalClients, - ) + FROM clients`).Scan(&result).Error if err != nil { log.Printf("❌ [GetClientPenaltiesStats] Erreur: %v", err) return nil, fmt.Errorf("erreur récupération stats: %w", err) } - result := map[string]interface{}{ - "clients_with_penalties": stats.ClientsWithPenalties, - "total_penalties": stats.TotalPenalties, - "average_penalty": stats.AvgPenalty, - "max_penalty": stats.MaxPenalty, - "total_clients": stats.TotalClients, + stats := map[string]interface{}{ + "clients_with_penalties": result.ClientsWithPenalties, + "total_penalties": result.TotalPenalties, + "average_penalty": result.AvgPenalty, + "max_penalty": result.MaxPenalty, + "total_clients": result.TotalClients, } log.Printf("📊 [GetClientPenaltiesStats] Stats: %d/%d clients avec pénalités", - stats.ClientsWithPenalties, stats.TotalClients) + result.ClientsWithPenalties, result.TotalClients) - return result, nil + return stats, nil } -func (d *Database) CalculateAndAddPointsForCommandTx(tx *sql.Tx, commandID int, username string) (int, string, error) { +func (d *Database) CalculateAndAddPointsForCommandTx(tx *gorm.DB, commandID int, username string) (int, string, error) { log.Printf("💰 [CalcPointsTx] START - cmd=%d, user=%s", commandID, username) // Charger les paramètres globaux @@ -674,45 +609,34 @@ func (d *Database) CalculateAndAddPointsForCommandTx(tx *sql.Tx, commandID int, } // ✅ ÉTAPE 1: Récupérer tous les items de la commande avec leurs catégories - rows, err := tx.Query(` + var items []struct { + Quantite float64 `gorm:"column:quantite"` + Prix float64 `gorm:"column:prix"` + Category string `gorm:"column:category"` + } + if err := tx.Raw(` SELECT ci.quantite, ci.prix, COALESCE(p.category, '') as category FROM command_items ci LEFT JOIN products p ON ci.product_id = p.id - WHERE ci.command_id = $1 - `, commandID) - if err != nil { + WHERE ci.command_id = ? + `, commandID).Scan(&items).Error; err != nil { log.Printf("❌ [CalcPointsTx] Erreur query items: %v", err) return 0, "", fmt.Errorf("erreur récupération items: %w", err) } - defer rows.Close() - var itemCount int - poolTotals := make([]float64, len(pools)) - - for rows.Next() { - var quantite, prix float64 - var category string - if err := rows.Scan(&quantite, &prix, &category); err != nil { - log.Printf("❌ [CalcPointsTx] Erreur scan: %v", err) - return 0, "", fmt.Errorf("erreur lecture item: %w", err) - } - itemCount++ - catLower := strings.ToLower(category) - if poolIdx, ok := catToPool[catLower]; ok { - poolTotals[poolIdx] += prix - } - } - - if err = rows.Err(); err != nil { - log.Printf("❌ [CalcPointsTx] Erreur rows: %v", err) - return 0, "", fmt.Errorf("erreur itération items: %w", err) - } - - if itemCount == 0 { + if len(items) == 0 { log.Printf("⚠️ [CalcPointsTx] Aucun item trouvé pour cmd %d", commandID) return 0, "", nil } + poolTotals := make([]float64, len(pools)) + for _, item := range items { + catLower := strings.ToLower(item.Category) + if poolIdx, ok := catToPool[catLower]; ok { + poolTotals[poolIdx] += item.Prix + } + } + for i, t := range poolTotals { log.Printf("📊 [CalcPointsTx] Pool[%d] (%s): %.2f€", i, pools[i].Name, t) } @@ -747,16 +671,15 @@ func (d *Database) CalculateAndAddPointsForCommandTx(tx *sql.Tx, commandID int, if poolPts[i] == 0 { continue } - _, err = tx.Exec(` + if err := tx.Exec(` UPDATE clients SET points_extra = jsonb_set( COALESCE(points_extra, '{}'::jsonb), - ARRAY[$2], - to_jsonb(COALESCE((points_extra->>$2)::int, 0) + $3) + ARRAY[?], + to_jsonb(COALESCE((points_extra->>?)::int, 0) + ?) ), updated_at = CURRENT_TIMESTAMP - WHERE username = $1 - `, username, pool.Key, poolPts[i]) - if err != nil { + WHERE username = ? + `, pool.Key, pool.Key, poolPts[i], username).Error; err != nil { log.Printf("❌ [CalcPointsTx] Erreur UPDATE points_extra pool[%d] (%s): %v", i, pool.Key, err) return 0, "", fmt.Errorf("erreur mise à jour points pool[%d]: %w", i, err) } @@ -783,22 +706,22 @@ func (d *Database) CanUserAccessCommand( // 🚚 Livreur : seulement commandes assignées if role == "livreur" { - err := d.QueryRow(` + err := d.GDB.Raw(` SELECT EXISTS( SELECT 1 FROM commandes - WHERE id = $1 AND livreur_assign = $2 + WHERE id = ? AND livreur_assign = ? ) - `, commandID, username).Scan(&exists) + `, commandID, username).Scan(&exists).Error return exists, err } // 👤 User : seulement SES commandes - err := d.QueryRow(` + err := d.GDB.Raw(` SELECT EXISTS( SELECT 1 FROM commandes - WHERE id = $1 AND username = $2 + WHERE id = ? AND username = ? ) - `, commandID, username).Scan(&exists) + `, commandID, username).Scan(&exists).Error return exists, err } diff --git a/backend/gestion/db/db_command_items.go b/backend/gestion/db/db_command_items.go index e762a3bb..8052636f 100644 --- a/backend/gestion/db/db_command_items.go +++ b/backend/gestion/db/db_command_items.go @@ -1,7 +1,6 @@ package db import ( - "database/sql" "fmt" "log" "strings" @@ -151,9 +150,7 @@ func (d *Database) InsertCommandItemWithClientInfo( // ✅ VÉRIFIER QUE LA COMMANDE EXISTE var exists bool - checkQuery := `SELECT EXISTS(SELECT 1 FROM commandes WHERE id = $1)` - err := d.QueryRow(checkQuery, commandID).Scan(&exists) - if err != nil { + if err := d.GDB.Raw(`SELECT EXISTS(SELECT 1 FROM commandes WHERE id = ?)`, commandID).Scan(&exists).Error; err != nil { log.Printf("❌ Erreur vérification commande: %v", err) return fmt.Errorf("erreur vérification commande: %w", err) } @@ -162,16 +159,15 @@ func (d *Database) InsertCommandItemWithClientInfo( } // ✅ INSERT - query := `INSERT INTO command_items ( - command_id, produit, product_id, quantite, prix, - client_username, client_nom, client_prenom, client_telephone, delivery_address, - status, created_at, updated_at - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, 'pending', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)` - - _, err = d.Exec(query, + err := d.GDB.Exec(` + INSERT INTO command_items ( + command_id, produit, product_id, quantite, prix, + client_username, client_nom, client_prenom, client_telephone, delivery_address, + status, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`, commandID, produit, productID, quantite, prix, clientUsername, clientNom, clientPrenom, clientTelephone, deliveryAddress, - ) + ).Error if err != nil { log.Printf("❌ Erreur INSERT command_items: %v", err) return fmt.Errorf("erreur insertion item: %w", err) @@ -194,7 +190,31 @@ func (d *Database) GetCommandItems(commandID int) ([]map[string]interface{}, err return nil, err } - query := ` + var rows []struct { + ID int `gorm:"column:id"` + CommandID int `gorm:"column:command_id"` + Produit string `gorm:"column:produit"` + ProductID *int64 `gorm:"column:product_id"` + Quantite float64 `gorm:"column:quantite"` + Prix float64 `gorm:"column:prix"` + ClientUsername string `gorm:"column:client_username"` + ClientNom string `gorm:"column:client_nom"` + ClientPrenom string `gorm:"column:client_prenom"` + ClientTelephone string `gorm:"column:client_telephone"` + DeliveryAddress *string `gorm:"column:delivery_address"` + Status *string `gorm:"column:status"` + CreatedAt time.Time `gorm:"column:created_at"` + UpdatedAt time.Time `gorm:"column:updated_at"` + CommandStatus *string `gorm:"column:command_status"` + CommandAddress *string `gorm:"column:command_address"` + TotalPrix float64 `gorm:"column:total_prix"` + ReferralUsed float64 `gorm:"column:referral_used"` + LivreurAssign *string `gorm:"column:livreur_assign"` + CommandCreatedAt *time.Time `gorm:"column:command_created_at"` + Category string `gorm:"column:category"` + } + + err := d.GDB.Raw(` SELECT ci.id, ci.command_id, @@ -220,94 +240,93 @@ func (d *Database) GetCommandItems(commandID int) ([]map[string]interface{}, err FROM command_items ci LEFT JOIN commandes c ON ci.command_id = c.id LEFT JOIN products p ON ci.product_id = p.id - WHERE ci.command_id = $1 - ORDER BY ci.id ASC` - - rows, err := d.Query(query, commandID) + WHERE ci.command_id = ? + ORDER BY ci.id ASC`, commandID).Scan(&rows).Error if err != nil { log.Printf("❌ Erreur query: %v", err) return nil, fmt.Errorf("erreur récupération items: %w", err) } - defer rows.Close() - var items []map[string]interface{} - - for rows.Next() { - var id, commandID int - var quantite float64 - var productID sql.NullInt64 // ✅ FIX: Utiliser NullInt64 pour gérer NULL - var produit, clientUsername, clientNom, clientPrenom, clientTelephone string - var deliveryAddress, status sql.NullString - var prix, totalPrix float64 - var createdAt, updatedAt, commandCreatedAt time.Time - var commandStatus, commandAddress, livreurAssign sql.NullString - var category string - var referralUsed float64 - - // ✅ FIX: Utiliser &productID (sql.NullInt64) - err := rows.Scan( - &id, &commandID, &produit, &productID, &quantite, &prix, - &clientUsername, &clientNom, &clientPrenom, &clientTelephone, &deliveryAddress, &status, - &createdAt, &updatedAt, - &commandStatus, &commandAddress, &totalPrix, &referralUsed, &livreurAssign, &commandCreatedAt, - &category, - ) - - if err != nil { - log.Printf("❌ Erreur scan: %v", err) - return nil, fmt.Errorf("erreur scan: %w", err) + items := make([]map[string]interface{}, 0, len(rows)) + for _, row := range rows { + productIDValue := 0 + if row.ProductID != nil { + productIDValue = int(*row.ProductID) } - // ✅ CONVERTIR sql.NullInt64 en int (0 si NULL) - productIDValue := 0 - if productID.Valid { - productIDValue = int(productID.Int64) + var commandCreatedAt interface{} + if row.CommandCreatedAt != nil { + commandCreatedAt = *row.CommandCreatedAt } item := map[string]interface{}{ - "id": id, - "command_id": commandID, - "produit": produit, - "product_id": productIDValue, // ✅ FIX: Utiliser la valeur convertie - "quantite": quantite, - "prix": prix, - "client_username": clientUsername, - "client_nom": clientNom, - "client_prenom": clientPrenom, - "client_telephone": clientTelephone, - "delivery_address": deliveryAddress.String, - "status": status.String, - "created_at": createdAt, - "updated_at": updatedAt, + "id": row.ID, + "command_id": row.CommandID, + "produit": row.Produit, + "product_id": productIDValue, + "quantite": row.Quantite, + "prix": row.Prix, + "client_username": row.ClientUsername, + "client_nom": row.ClientNom, + "client_prenom": row.ClientPrenom, + "client_telephone": row.ClientTelephone, + "delivery_address": ptrStr(row.DeliveryAddress), + "status": ptrStr(row.Status), + "created_at": row.CreatedAt, + "updated_at": row.UpdatedAt, // Infos commande - "command_status": commandStatus.String, - "command_address": commandAddress.String, - "total_prix": totalPrix, - "referral_used": referralUsed, - "livreur_assign": livreurAssign.String, + "command_status": ptrStr(row.CommandStatus), + "command_address": ptrStr(row.CommandAddress), + "total_prix": row.TotalPrix, + "referral_used": row.ReferralUsed, + "livreur_assign": ptrStr(row.LivreurAssign), "command_created_at": commandCreatedAt, - "category": category, + "category": row.Category, } - items = append(items, item) } - if err = rows.Err(); err != nil { - log.Printf("❌ Erreur itération: %v", err) - return nil, fmt.Errorf("erreur itération: %w", err) - } - log.Printf("✅ %d items récupérés avec infos client et catégories", len(items)) return items, nil } +// ptrStr retourne la valeur d'un *string ou "" si nil +func ptrStr(s *string) string { + if s == nil { + return "" + } + return *s +} + func (d *Database) GetCommandItemsByUsername(username string) ([]map[string]interface{}, error) { if err := validateUsername(username); err != nil { log.Printf("❌ [GetCommandItemsByUsername] %v", err) return nil, err } - query := ` + var rows []struct { + ID int `gorm:"column:id"` + CommandID int `gorm:"column:command_id"` + Produit string `gorm:"column:produit"` + ProductID *int64 `gorm:"column:product_id"` + Quantite float64 `gorm:"column:quantite"` + Prix float64 `gorm:"column:prix"` + ClientUsername string `gorm:"column:client_username"` + ClientNom string `gorm:"column:client_nom"` + ClientPrenom string `gorm:"column:client_prenom"` + ClientTelephone string `gorm:"column:client_telephone"` + DeliveryAddress *string `gorm:"column:delivery_address"` + Status *string `gorm:"column:status"` + CreatedAt time.Time `gorm:"column:created_at"` + UpdatedAt time.Time `gorm:"column:updated_at"` + CommandStatus *string `gorm:"column:command_status"` + CommandAddress *string `gorm:"column:command_address"` + TotalPrix float64 `gorm:"column:total_prix"` + LivreurAssign *string `gorm:"column:livreur_assign"` + CommandCreatedAt *time.Time `gorm:"column:command_created_at"` + } + + err := d.GDB.Raw(` SELECT ci.id, ci.command_id, @@ -330,73 +349,49 @@ func (d *Database) GetCommandItemsByUsername(username string) ([]map[string]inte c.created_at as command_created_at FROM command_items ci LEFT JOIN commandes c ON ci.command_id = c.id - WHERE ci.client_username = $1 - ORDER BY ci.command_id DESC, ci.id ASC` - - rows, err := d.Query(query, username) + WHERE ci.client_username = ? + ORDER BY ci.command_id DESC, ci.id ASC`, username).Scan(&rows).Error if err != nil { log.Printf("❌ Erreur query: %v", err) return nil, fmt.Errorf("erreur récupération items: %w", err) } - defer rows.Close() - var items []map[string]interface{} - - for rows.Next() { - var id, commandID int - var quantite float64 - var productID sql.NullInt64 - var produit, clientUsername, clientNom, clientPrenom, clientTelephone string - var deliveryAddress, status sql.NullString - var prix, totalPrix float64 - var createdAt, updatedAt, commandCreatedAt time.Time - var commandStatus, commandAddress, livreurAssign sql.NullString - - err := rows.Scan( - &id, &commandID, &produit, &productID, &quantite, &prix, - &clientUsername, &clientNom, &clientPrenom, &clientTelephone, &deliveryAddress, &status, - &createdAt, &updatedAt, - &commandStatus, &commandAddress, &totalPrix, &livreurAssign, &commandCreatedAt, - ) - if err != nil { - log.Printf("❌ Erreur scan: %v", err) - return nil, fmt.Errorf("erreur scan: %w", err) + items := make([]map[string]interface{}, 0, len(rows)) + for _, row := range rows { + productIDValue := 0 + if row.ProductID != nil { + productIDValue = int(*row.ProductID) } - productIDValue := 0 - if productID.Valid { - productIDValue = int(productID.Int64) + var commandCreatedAt interface{} + if row.CommandCreatedAt != nil { + commandCreatedAt = *row.CommandCreatedAt } item := map[string]interface{}{ - "id": id, - "command_id": commandID, - "produit": produit, + "id": row.ID, + "command_id": row.CommandID, + "produit": row.Produit, "product_id": productIDValue, - "quantite": quantite, - "prix": prix, - "client_username": clientUsername, - "client_nom": clientNom, - "client_prenom": clientPrenom, - "client_telephone": clientTelephone, - "delivery_address": deliveryAddress.String, - "status": status.String, - "created_at": createdAt, - "updated_at": updatedAt, - "command_status": commandStatus.String, - "command_address": commandAddress.String, - "total_prix": totalPrix, - "livreur_assign": livreurAssign.String, + "quantite": row.Quantite, + "prix": row.Prix, + "client_username": row.ClientUsername, + "client_nom": row.ClientNom, + "client_prenom": row.ClientPrenom, + "client_telephone": row.ClientTelephone, + "delivery_address": ptrStr(row.DeliveryAddress), + "status": ptrStr(row.Status), + "created_at": row.CreatedAt, + "updated_at": row.UpdatedAt, + "command_status": ptrStr(row.CommandStatus), + "command_address": ptrStr(row.CommandAddress), + "total_prix": row.TotalPrix, + "livreur_assign": ptrStr(row.LivreurAssign), "command_created_at": commandCreatedAt, } items = append(items, item) } - if err = rows.Err(); err != nil { - log.Printf("❌ Erreur itération: %v", err) - return nil, fmt.Errorf("erreur itération: %w", err) - } - log.Printf("✅ %d items récupérés pour l'utilisateur %s", len(items), username) return items, nil } @@ -412,29 +407,28 @@ func (d *Database) DeleteCommandItem(commandID, itemID int) error { } // Récupérer le prix et la quantité avant suppression pour mettre à jour le total - var prix, quantite float64 - checkQuery := `SELECT prix, quantite FROM command_items WHERE id = $1 AND command_id = $2` - err := d.QueryRow(checkQuery, itemID, commandID).Scan(&prix, &quantite) - if err == sql.ErrNoRows { - return fmt.Errorf("item %d non trouvé dans la commande %d", itemID, commandID) + var result struct { + Prix float64 `gorm:"column:prix"` + Quantite float64 `gorm:"column:quantite"` } - if err != nil { + if err := d.GDB.Raw(`SELECT prix, quantite FROM command_items WHERE id = ? AND command_id = ?`, itemID, commandID).Scan(&result).Error; err != nil { return fmt.Errorf("erreur vérification item: %w", err) } + if result.Prix == 0 && result.Quantite == 0 { + return fmt.Errorf("item %d non trouvé dans la commande %d", itemID, commandID) + } // Supprimer l'item - _, err = d.Exec(`DELETE FROM command_items WHERE id = $1`, itemID) - if 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) return fmt.Errorf("erreur suppression item: %w", err) } // Recalculer le total de la commande - _, err = d.Exec( - `UPDATE commandes SET total_prix = GREATEST(0, total_prix - $1) WHERE id = $2`, - prix*quantite, commandID, - ) - if err != nil { + if err := d.GDB.Exec( + `UPDATE commandes SET total_prix = GREATEST(0, total_prix - ?) WHERE id = ?`, + result.Prix*result.Quantite, commandID, + ).Error; err != nil { log.Printf("⚠️ [DeleteCommandItem] Erreur maj total commande: %v", err) } @@ -453,9 +447,7 @@ func (d *Database) UpdateCommandItemStatus(itemID int, status string) error { } var exists bool - checkQuery := `SELECT EXISTS(SELECT 1 FROM command_items WHERE id = $1)` - err := d.QueryRow(checkQuery, itemID).Scan(&exists) - if err != nil { + if err := d.GDB.Raw(`SELECT EXISTS(SELECT 1 FROM command_items WHERE id = ?)`, itemID).Scan(&exists).Error; err != nil { log.Printf("❌ Erreur vérification: %v", err) return fmt.Errorf("erreur vérification item: %w", err) } @@ -464,22 +456,15 @@ func (d *Database) UpdateCommandItemStatus(itemID int, status string) error { return fmt.Errorf("item %d non trouvé", itemID) } - query := `UPDATE command_items - SET status = $1, updated_at = CURRENT_TIMESTAMP - WHERE id = $2` - - result, err := d.Exec(query, status, itemID) - if err != nil { - log.Printf("❌ Erreur UPDATE: %v", err) - return fmt.Errorf("erreur mise à jour statut: %w", err) + result := d.GDB.Exec(` + UPDATE command_items + SET status = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ?`, status, itemID) + if result.Error != nil { + log.Printf("❌ Erreur UPDATE: %v", result.Error) + return fmt.Errorf("erreur mise à jour statut: %w", result.Error) } - - rowsAffected, err := result.RowsAffected() - if err != nil { - log.Printf("❌ Erreur RowsAffected: %v", err) - return fmt.Errorf("erreur vérification: %w", err) - } - if rowsAffected == 0 { + if result.RowsAffected == 0 { log.Printf("❌ Item %d non trouvé", itemID) return fmt.Errorf("item non trouvé") } diff --git a/backend/gestion/db/db_command_priority.go b/backend/gestion/db/db_command_priority.go index 597b730e..2092b5a2 100644 --- a/backend/gestion/db/db_command_priority.go +++ b/backend/gestion/db/db_command_priority.go @@ -6,228 +6,149 @@ package db import ( - "database/sql" "fmt" "gestion/models" "log" "slices" - "time" ) // GetAllCommandsOldestFirst récupère les commandes triées par ancienneté (plus anciennes en premier) -func (d *Database) GetAllCommandsOldestFirst(status, username string) ([]map[string]interface{}, error) { +func (d *Database) GetAllCommandsOldestFirst(status, username string) ([]map[string]any, error) { query := `SELECT c.id, c.username, c.status, c.adresse, c.total_prix, c.livreur_assign, c.created_at, c.updated_at FROM commandes c WHERE 1=1` - args := []interface{}{} - argPosition := 1 + args := []any{} - // Filtrer par status if status != "" { validStatuses := []string{"pending", "assigned", "en_route", "livre", "approved", "cancelled", "disabled"} - - isValid := slices.Contains(validStatuses, status) - if !isValid { + if !slices.Contains(validStatuses, status) { return nil, fmt.Errorf("statut invalide: %s", status) } - - query += fmt.Sprintf(" AND c.status = $%d", argPosition) + query += " AND c.status = ?" args = append(args, status) - argPosition++ } - // Filtrer par username si fourni if username != "" { - query += fmt.Sprintf(" AND c.username = $%d", argPosition) + query += " AND c.username = ?" args = append(args, username) - argPosition++ } query += " ORDER BY c.created_at ASC" - rows, err := d.Query(query, args...) - if err != nil { + var commands []map[string]any + if err := d.GDB.Raw(query, args...).Scan(&commands).Error; err != nil { return nil, fmt.Errorf("erreur récupération commandes prioritaires: %w", err) } - defer rows.Close() - - var commands []map[string]interface{} - for rows.Next() { - var id int - var username, status, adresse string - var livreurAssign sql.NullString - var totalPrix float64 - var createdAt, updatedAt time.Time - - err := rows.Scan(&id, &username, &status, &adresse, &totalPrix, &livreurAssign, &createdAt, &updatedAt) - if err != nil { - return nil, fmt.Errorf("erreur scan commande: %w", err) - } - - command := map[string]interface{}{ - "id": id, - "username": username, - "status": status, - "adresse": adresse, - "total_prix": totalPrix, - "created_at": createdAt, - "updated_at": updatedAt, - } - - if livreurAssign.Valid { - command["livreur_assign"] = livreurAssign.String - } else { - command["livreur_assign"] = nil - } - - commands = append(commands, command) - } - - if err = rows.Err(); err != nil { - return nil, fmt.Errorf("erreur itération résultats: %w", err) - } - return commands, nil } // GetOldestPendingCommand récupère la commande pending la plus ancienne -func (d *Database) GetOldestPendingCommand() (map[string]interface{}, error) { - query := `SELECT c.id, c.username, c.status, c.adresse, c.total_prix, - c.livreur_assign, c.created_at, c.updated_at - FROM commandes c - WHERE c.status = 'pending' - ORDER BY c.created_at ASC - LIMIT 1` - - var id int - var username, status, adresse string - var livreurAssign sql.NullString - var totalPrix float64 - var createdAt, updatedAt time.Time - - err := d.QueryRow(query).Scan( - &id, &username, &status, &adresse, &totalPrix, - &livreurAssign, &createdAt, &updatedAt, - ) - - if err == sql.ErrNoRows { - return nil, nil // Aucune commande pending - } +func (d *Database) GetOldestPendingCommand() (map[string]any, error) { + var commands []map[string]any + err := d.GDB.Raw(` + SELECT c.id, c.username, c.status, c.adresse, c.total_prix, + c.livreur_assign, c.created_at, c.updated_at + FROM commandes c + WHERE c.status = 'pending' + ORDER BY c.created_at ASC + LIMIT 1`).Scan(&commands).Error if err != nil { return nil, fmt.Errorf("erreur récupération commande la plus ancienne: %w", err) } - - command := map[string]interface{}{ - "id": id, - "username": username, - "status": status, - "adresse": adresse, - "total_prix": totalPrix, - "created_at": createdAt, - "updated_at": updatedAt, + if len(commands) == 0 { + return nil, nil } - - if livreurAssign.Valid { - command["livreur_assign"] = livreurAssign.String - } else { - command["livreur_assign"] = nil - } - - return command, nil + return commands[0], nil } // GetPendingCommandsWithPriority récupère les commandes pending avec calcul de priorité func (d *Database) GetPendingCommandsWithPriority() ([]*models.CommandPriority, error) { - query := `SELECT c.id, c.username, c.status, c.adresse, c.total_prix, - c.created_at, c.updated_at, - EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - c.created_at)) as waiting_seconds - FROM commandes c - WHERE c.status = 'pending' - ORDER BY c.created_at ASC` + var rows []struct { + ID int `gorm:"column:id"` + Username string `gorm:"column:username"` + Status string `gorm:"column:status"` + Adresse string `gorm:"column:adresse"` + TotalPrix float64 `gorm:"column:total_prix"` + CreatedAt string `gorm:"column:created_at"` + UpdatedAt string `gorm:"column:updated_at"` + WaitingSeconds float64 `gorm:"column:waiting_seconds"` + } - rows, err := d.Query(query) + err := d.GDB.Raw(` + SELECT c.id, c.username, c.status, c.adresse, c.total_prix, + c.created_at, c.updated_at, + EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - c.created_at)) as waiting_seconds + FROM commandes c + WHERE c.status = 'pending' + ORDER BY c.created_at ASC`).Scan(&rows).Error if err != nil { return nil, fmt.Errorf("erreur récupération commandes avec priorité: %w", err) } - defer rows.Close() - - var commands []*models.CommandPriority - for rows.Next() { - var id int - var username, status, adresse string - var totalPrix float64 - var createdAt, updatedAt time.Time - var waitingSeconds float64 - - err := rows.Scan(&id, &username, &status, &adresse, &totalPrix, - &createdAt, &updatedAt, &waitingSeconds) - if err != nil { - return nil, fmt.Errorf("erreur scan commande priorité: %w", err) - } + commands := make([]*models.CommandPriority, 0, len(rows)) + for _, row := range rows { cmd := &models.CommandPriority{ - ID: id, - Username: username, - Status: status, - Address: adresse, - TotalPrice: totalPrix, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - WaitingSeconds: int(waitingSeconds), - WaitingMinutes: int(waitingSeconds / 60), + ID: row.ID, + Username: row.Username, + Status: row.Status, + Address: row.Adresse, + TotalPrice: row.TotalPrix, + WaitingSeconds: int(row.WaitingSeconds), + WaitingMinutes: int(row.WaitingSeconds / 60), } - commands = append(commands, cmd) } - if err = rows.Err(); err != nil { - return nil, fmt.Errorf("erreur itération résultats priorité: %w", err) - } - return commands, nil } // GetCommandWaitingTime récupère le temps d'attente d'une commande func (d *Database) GetCommandWaitingTime(commandID int) (int, error) { - query := `SELECT EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - created_at))::INTEGER as waiting_seconds - FROM commandes - WHERE id = $1` - - var waitingSeconds int - err := d.QueryRow(query, commandID).Scan(&waitingSeconds) - if err == sql.ErrNoRows { - return 0, fmt.Errorf("commande non trouvée") + var result struct { + WaitingSeconds int `gorm:"column:waiting_seconds"` } + err := d.GDB.Raw(` + SELECT EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - created_at))::INTEGER as waiting_seconds + FROM commandes WHERE id = ?`, commandID).Scan(&result).Error if err != nil { return 0, fmt.Errorf("erreur récupération temps d'attente: %w", err) } - - return waitingSeconds, nil + if result.WaitingSeconds == 0 { + // Vérifie si la commande existe vraiment + var exists bool + d.GDB.Raw(`SELECT EXISTS(SELECT 1 FROM commandes WHERE id = ?)`, commandID).Scan(&exists) + if !exists { + return 0, fmt.Errorf("commande non trouvée") + } + } + return result.WaitingSeconds, nil } // GetPendingCommandsStats récupère des statistiques sur les commandes en attente -func (d *Database) GetPendingCommandsStats() (map[string]interface{}, error) { - query := `SELECT - COUNT(*) as total_pending, - AVG(EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - created_at))) as avg_waiting_seconds, - MIN(created_at) as oldest_command_date, - MAX(created_at) as newest_command_date - FROM commandes - WHERE status = 'pending'` +func (d *Database) GetPendingCommandsStats() (map[string]any, error) { + var result struct { + TotalPending int `gorm:"column:total_pending"` + AvgWaitingSeconds *float64 `gorm:"column:avg_waiting_seconds"` + OldestCommandDate *string `gorm:"column:oldest_command_date"` + NewestCommandDate *string `gorm:"column:newest_command_date"` + } - var totalPending int - var avgWaitingSeconds sql.NullFloat64 - var oldestDate, newestDate sql.NullTime - - err := d.QueryRow(query).Scan(&totalPending, &avgWaitingSeconds, &oldestDate, &newestDate) + err := d.GDB.Raw(` + SELECT + COUNT(*) as total_pending, + AVG(EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - created_at))) as avg_waiting_seconds, + MIN(created_at) as oldest_command_date, + MAX(created_at) as newest_command_date + FROM commandes + WHERE status = 'pending'`).Scan(&result).Error if err != nil { return nil, fmt.Errorf("erreur récupération stats: %w", err) } - stats := map[string]interface{}{ - "total_pending": totalPending, + stats := map[string]any{ + "total_pending": result.TotalPending, "avg_waiting_seconds": 0, "avg_waiting_minutes": 0, "oldest_command_date": nil, @@ -235,25 +156,21 @@ func (d *Database) GetPendingCommandsStats() (map[string]interface{}, error) { "oldest_waiting_minutes": 0, } - if avgWaitingSeconds.Valid { - stats["avg_waiting_seconds"] = int(avgWaitingSeconds.Float64) - stats["avg_waiting_minutes"] = int(avgWaitingSeconds.Float64 / 60) + if result.AvgWaitingSeconds != nil { + stats["avg_waiting_seconds"] = int(*result.AvgWaitingSeconds) + stats["avg_waiting_minutes"] = int(*result.AvgWaitingSeconds / 60) } - if oldestDate.Valid { - stats["oldest_command_date"] = oldestDate.Time - waitingTime := time.Since(oldestDate.Time) - stats["oldest_waiting_minutes"] = int(waitingTime.Minutes()) + if result.OldestCommandDate != nil { + stats["oldest_command_date"] = *result.OldestCommandDate } - if newestDate.Valid { - stats["newest_command_date"] = newestDate.Time + if result.NewestCommandDate != nil { + stats["newest_command_date"] = *result.NewestCommandDate } - log.Printf("📊 [STATS] Commandes pending: %d | Attente moyenne: %d min | Plus ancienne: %d min", - totalPending, - stats["avg_waiting_minutes"], - stats["oldest_waiting_minutes"]) + log.Printf("📊 [STATS] Commandes pending: %d | Attente moyenne: %d min", + result.TotalPending, stats["avg_waiting_minutes"]) return stats, nil } diff --git a/backend/gestion/db/db_commands.go b/backend/gestion/db/db_commands.go index d7a4b63c..4ad81f39 100644 --- a/backend/gestion/db/db_commands.go +++ b/backend/gestion/db/db_commands.go @@ -1,13 +1,14 @@ package db import ( - "database/sql" "fmt" "gestion/models" "log" "slices" "strings" "time" + + "gorm.io/gorm" ) func sanitizeString(s string) string { @@ -44,31 +45,21 @@ func validateAddress(address string) error { } type basketItem struct { - ProductID int - Quantity float64 - Price float64 + ProductID int `gorm:"column:product_id"` + Quantity float64 `gorm:"column:quantity"` + Price float64 `gorm:"column:price"` } func (d *Database) fetchBasketItems(username string) ([]basketItem, float64, error) { - rows, err := d.Query(`SELECT product_id, quantity, price FROM baskets WHERE username = $1`, username) - if err != nil { + var items []basketItem + if err := d.GDB.Raw(`SELECT product_id, quantity, price FROM baskets WHERE username = ?`, username).Scan(&items).Error; err != nil { return nil, 0, fmt.Errorf("erreur récupération panier: %w", err) } - defer rows.Close() - var items []basketItem total := 0.0 - for rows.Next() { - var item basketItem - if err := rows.Scan(&item.ProductID, &item.Quantity, &item.Price); err != nil { - return nil, 0, fmt.Errorf("erreur scan panier: %w", err) - } - items = append(items, item) + for _, item := range items { total += item.Price } - if err := rows.Err(); err != nil { - return nil, 0, err - } return items, total, nil } @@ -93,11 +84,12 @@ func validateCommandStatus(status string) error { } func (d *Database) CreateCommand(username string) (*models.Command, error) { - var adresse string - clientQuery := `SELECT username FROM clients WHERE username = $1` - err := d.QueryRow(clientQuery, username).Scan(&adresse) - if err != nil { - adresse = "Adresse non spécifiée" + var addrResult struct { + Username string `gorm:"column:username"` + } + adresse := "Adresse non spécifiée" + if err := d.GDB.Raw(`SELECT username FROM clients WHERE username = ?`, username).Scan(&addrResult).Error; err == nil && addrResult.Username != "" { + adresse = addrResult.Username } basketItems, totalPrix, err := d.fetchBasketItems(username) @@ -109,38 +101,37 @@ func (d *Database) CreateCommand(username string) (*models.Command, error) { return nil, fmt.Errorf("le panier est vide") } - commandQuery := `INSERT INTO commandes (username, status, adresse, total_prix, created_at, updated_at) - VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) - RETURNING id, created_at, updated_at` - - var commandID int - var createdAt, updatedAt time.Time - err = d.QueryRow(commandQuery, username, "pending", adresse, totalPrix).Scan( - &commandID, - &createdAt, - &updatedAt, - ) + var cmdResult struct { + ID int `gorm:"column:id"` + CreatedAt time.Time `gorm:"column:created_at"` + UpdatedAt time.Time `gorm:"column:updated_at"` + } + err = d.GDB.Raw(` + INSERT INTO commandes (username, status, adresse, total_prix, client_order_id, created_at, updated_at) + VALUES (?, ?, ?, ?, (SELECT COALESCE(MAX(client_order_id), 0) + 1 FROM commandes WHERE username = ?), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + RETURNING id, created_at, updated_at`, + username, "pending", adresse, totalPrix, username).Scan(&cmdResult).Error if err != nil { return nil, fmt.Errorf("erreur lors de la création de la commande: %w", err) } + commandID := cmdResult.ID + for _, item := range basketItems { productName, err := d.GetProductNameByID(item.ProductID) if err != nil { productName = "Produit inconnu" } - itemQuery := `INSERT INTO command_items (command_id, produit, product_id, quantite, prix) - VALUES ($1, $2, $3, $4, $5)` - _, err = d.Exec(itemQuery, commandID, productName, item.ProductID, item.Quantity, item.Price) - if err != nil { + if err := d.GDB.Exec(` + INSERT INTO command_items (command_id, produit, product_id, quantite, prix) + VALUES (?, ?, ?, ?, ?)`, + commandID, productName, item.ProductID, item.Quantity, item.Price).Error; err != nil { return nil, fmt.Errorf("erreur lors de l'insertion des items: %w", err) } } - clearBasketQuery := `DELETE FROM baskets WHERE username = $1` - _, err = d.Exec(clearBasketQuery, username) - if err != nil { + if err := d.GDB.Exec(`DELETE FROM baskets WHERE username = ?`, username).Error; err != nil { return nil, fmt.Errorf("erreur lors du vidage du panier: %w", err) } @@ -196,20 +187,22 @@ func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (* return nil, fmt.Errorf("montant de commande invalide: %.2f€", totalPrix) } - commandQuery := `INSERT INTO commandes (username, status, adresse, total_prix, created_at, updated_at) - VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) - RETURNING id, created_at, updated_at` - - var commandID int - var createdAt, updatedAt time.Time - - err = d.QueryRow(commandQuery, username, "pending", deliveryAddress, totalPrix).Scan( - &commandID, &createdAt, &updatedAt, - ) + var cmdResult struct { + ID int `gorm:"column:id"` + CreatedAt time.Time `gorm:"column:created_at"` + UpdatedAt time.Time `gorm:"column:updated_at"` + } + err = d.GDB.Raw(` + INSERT INTO commandes (username, status, adresse, total_prix, client_order_id, created_at, updated_at) + VALUES (?, ?, ?, ?, (SELECT COALESCE(MAX(client_order_id), 0) + 1 FROM commandes WHERE username = ?), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + RETURNING id, created_at, updated_at`, + username, "pending", deliveryAddress, totalPrix, username).Scan(&cmdResult).Error if err != nil { return nil, fmt.Errorf("erreur création commande: %w", err) } + commandID := cmdResult.ID + for _, item := range basketItems { productName, err := d.GetProductNameByID(item.ProductID) if err != nil || productName == "" { @@ -233,22 +226,17 @@ func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (* return nil, fmt.Errorf("erreur insertion items: %w", err) } - stockQuery := `UPDATE products SET stock = stock - $1 WHERE id = $2 AND stock >= $1` - result, err := d.Exec(stockQuery, item.Quantity, item.ProductID) - if err != nil { - log.Printf("⚠️ Erreur décrémentation stock produit %d: %v", item.ProductID, err) - return nil, fmt.Errorf("erreur mise à jour stock: %w", err) + result := d.GDB.Exec(`UPDATE products SET stock = stock - ? WHERE id = ? AND stock >= ?`, item.Quantity, item.ProductID, item.Quantity) + if result.Error != nil { + log.Printf("⚠️ Erreur décrémentation stock produit %d: %v", item.ProductID, result.Error) + return nil, fmt.Errorf("erreur mise à jour stock: %w", result.Error) } - - rowsAffected, _ := result.RowsAffected() - if rowsAffected == 0 { + if result.RowsAffected == 0 { log.Printf("⚠️ [CHECKOUT] Stock déjà réservé pour produit %d (double réservation panier/checkout)", item.ProductID) } } - clearBasketQuery := `DELETE FROM baskets WHERE username = $1` - _, err = d.Exec(clearBasketQuery, username) - if err != nil { + if err := d.GDB.Exec(`DELETE FROM baskets WHERE username = ?`, username).Error; err != nil { log.Printf("⚠️ Erreur vidage panier: %v", err) } @@ -264,8 +252,8 @@ func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (* Status: "pending", Total: totalPrix, DeliveryAddress: deliveryAddress, - CreatedAt: createdAt, - UpdatedAt: updatedAt, + CreatedAt: cmdResult.CreatedAt, + UpdatedAt: cmdResult.UpdatedAt, } return command, nil @@ -287,73 +275,66 @@ func (d *Database) GetAllCommands(status, username string) ([]map[string]any, er query := `SELECT c.id, c.username, c.status, c.adresse, c.total_prix, c.livreur_assign, c.created_at, c.updated_at, c.proposed_address, c.address_proposal_status, - ROW_NUMBER() OVER (PARTITION BY c.username ORDER BY c.id) AS client_order_number + c.client_order_id AS client_order_number FROM commandes c WHERE 1=1` args := []interface{}{} - argPosition := 1 if status == "" { query += ` AND c.status IN ('pending', 'assigned', 'en_route', 'arrived', 'livre')` } else { - query += fmt.Sprintf(" AND c.status = $%d", argPosition) + query += ` AND c.status = ?` args = append(args, status) - argPosition++ } if username != "" { - query += fmt.Sprintf(" AND c.username = $%d", argPosition) + query += ` AND c.username = ?` args = append(args, username) - argPosition++ } query += " ORDER BY c.created_at DESC LIMIT 1000" - rows, err := d.Query(query, args...) - if err != nil { + var rows []struct { + ID int `gorm:"column:id"` + Username string `gorm:"column:username"` + Status string `gorm:"column:status"` + Adresse string `gorm:"column:adresse"` + TotalPrix float64 `gorm:"column:total_prix"` + LivreurAssign *string `gorm:"column:livreur_assign"` + CreatedAt time.Time `gorm:"column:created_at"` + UpdatedAt time.Time `gorm:"column:updated_at"` + ProposedAddress *string `gorm:"column:proposed_address"` + AddressProposalStatus string `gorm:"column:address_proposal_status"` + ClientOrderNumber int `gorm:"column:client_order_number"` + } + + if err := d.GDB.Raw(query, args...).Scan(&rows).Error; err != nil { return nil, fmt.Errorf("erreur lors de la récupération des commandes: %w", err) } - defer rows.Close() - - var commands []map[string]any - for rows.Next() { - var id int - var username, status, adresse string - var livreurAssign sql.NullString - var proposedAddress sql.NullString - var addressProposalStatus string - var totalPrix float64 - var createdAt, updatedAt time.Time - var clientOrderNumber int - - err := rows.Scan(&id, &username, &status, &adresse, &totalPrix, &livreurAssign, &createdAt, &updatedAt, &proposedAddress, &addressProposalStatus, &clientOrderNumber) - if err != nil { - return nil, fmt.Errorf("erreur lors du scan de la commande: %w", err) - } - - adresse = sanitizeString(adresse) + commands := make([]map[string]any, 0, len(rows)) + for _, row := range rows { command := map[string]any{ - "id": id, - "username": username, - "status": status, - "adresse": adresse, - "total_prix": totalPrix, - "created_at": createdAt, - "updated_at": updatedAt, - "address_proposal_status": addressProposalStatus, - "client_order_number": clientOrderNumber, + "id": row.ID, + "username": row.Username, + "status": row.Status, + "adresse": sanitizeString(row.Adresse), + "total_prix": row.TotalPrix, + "created_at": row.CreatedAt, + "updated_at": row.UpdatedAt, + "address_proposal_status": row.AddressProposalStatus, + "client_order_number": row.ClientOrderNumber, } - if livreurAssign.Valid { - command["livreur_assign"] = livreurAssign.String + if row.LivreurAssign != nil { + command["livreur_assign"] = *row.LivreurAssign } else { command["livreur_assign"] = nil } - if proposedAddress.Valid { - command["proposed_address"] = proposedAddress.String + if row.ProposedAddress != nil { + command["proposed_address"] = *row.ProposedAddress } else { command["proposed_address"] = nil } @@ -361,85 +342,73 @@ func (d *Database) GetAllCommands(status, username string) ([]map[string]any, er commands = append(commands, command) } - if err = rows.Err(); err != nil { - return nil, fmt.Errorf("erreur lors de l'itération des résultats: %w", err) - } - return commands, nil } func (d *Database) GetCommandCount() (int, error) { - var count int - err := d.QueryRow("SELECT COUNT(*) FROM commandes").Scan(&count) + var result struct { + Count int `gorm:"column:count"` + } + err := d.GDB.Raw("SELECT COUNT(*) as count FROM commandes").Scan(&result).Error if err != nil { return 0, fmt.Errorf("erreur récupération count commandes: %w", err) } - return count, nil + return result.Count, nil } func (d *Database) SetCommandReferralUsed(commandID int, amount float64) error { - _, err := d.Exec(`UPDATE commandes SET referral_used = $1 WHERE id = $2`, amount, commandID) - return err + return d.GDB.Exec(`UPDATE commandes SET referral_used = ? WHERE id = ?`, amount, commandID).Error } func (d *Database) GetCommandByID(id int) (map[string]any, error) { - query := `SELECT c.id, c.username, c.status, c.adresse, c.total_prix, c.livreur_assign, c.created_at, c.updated_at, - c.proposed_address, c.address_proposal_status, c.referral_used, - (SELECT COUNT(*) FROM commandes c2 WHERE c2.username = c.username AND c2.id <= c.id) AS client_order_number - FROM commandes c WHERE c.id = $1` - - var commandID int - var username, status, adresse string - var livreurAssign sql.NullString - var proposedAddress sql.NullString - var addressProposalStatus string - var totalPrix, referralUsed float64 - var createdAt, updatedAt time.Time - var clientOrderNumber int - - err := d.QueryRow(query, id).Scan( - &commandID, - &username, - &status, - &adresse, - &totalPrix, - &livreurAssign, - &createdAt, - &updatedAt, - &proposedAddress, - &addressProposalStatus, - &referralUsed, - &clientOrderNumber, - ) - - if err == sql.ErrNoRows { - return nil, fmt.Errorf("commande non trouvée") + var row struct { + ID int `gorm:"column:id"` + Username string `gorm:"column:username"` + Status string `gorm:"column:status"` + Adresse string `gorm:"column:adresse"` + TotalPrix float64 `gorm:"column:total_prix"` + LivreurAssign *string `gorm:"column:livreur_assign"` + CreatedAt time.Time `gorm:"column:created_at"` + UpdatedAt time.Time `gorm:"column:updated_at"` + ProposedAddress *string `gorm:"column:proposed_address"` + AddressProposalStatus string `gorm:"column:address_proposal_status"` + ReferralUsed float64 `gorm:"column:referral_used"` + ClientOrderNumber int `gorm:"column:client_order_number"` } + + err := d.GDB.Raw(` + SELECT c.id, c.username, c.status, c.adresse, c.total_prix, c.livreur_assign, c.created_at, c.updated_at, + c.proposed_address, c.address_proposal_status, c.referral_used, + c.client_order_id AS client_order_number + FROM commandes c WHERE c.id = ?`, id).Scan(&row).Error if err != nil { return nil, fmt.Errorf("erreur lors de la récupération de la commande: %w", err) } - - command := map[string]any{ - "id": commandID, - "username": username, - "status": status, - "adresse": adresse, - "total_prix": totalPrix, - "created_at": createdAt, - "updated_at": updatedAt, - "address_proposal_status": addressProposalStatus, - "referral_used": referralUsed, - "client_order_number": clientOrderNumber, + if row.ID == 0 { + return nil, fmt.Errorf("commande non trouvée") } - if livreurAssign.Valid { - command["livreur_assign"] = livreurAssign.String + command := map[string]any{ + "id": row.ID, + "username": row.Username, + "status": row.Status, + "adresse": row.Adresse, + "total_prix": row.TotalPrix, + "created_at": row.CreatedAt, + "updated_at": row.UpdatedAt, + "address_proposal_status": row.AddressProposalStatus, + "referral_used": row.ReferralUsed, + "client_order_number": row.ClientOrderNumber, + } + + if row.LivreurAssign != nil { + command["livreur_assign"] = *row.LivreurAssign } else { command["livreur_assign"] = nil } - if proposedAddress.Valid { - command["proposed_address"] = proposedAddress.String + if row.ProposedAddress != nil { + command["proposed_address"] = *row.ProposedAddress } else { command["proposed_address"] = nil } @@ -448,18 +417,18 @@ func (d *Database) GetCommandByID(id int) (map[string]any, error) { } func (d *Database) GetCommandAddress(commandID int) (string, error) { - var address string - query := `SELECT adresse FROM commandes WHERE id = $1` - - err := d.QueryRow(query, commandID).Scan(&address) - if err == sql.ErrNoRows { - return "", fmt.Errorf("commande non trouvée") + var result struct { + Adresse string `gorm:"column:adresse"` } + err := d.GDB.Raw(`SELECT adresse FROM commandes WHERE id = ?`, commandID).Scan(&result).Error if err != nil { return "", fmt.Errorf("erreur lors de la récupération de l'adresse: %w", err) } + if result.Adresse == "" { + return "", fmt.Errorf("commande non trouvée") + } - return address, nil + return result.Adresse, nil } func (d *Database) UpdateCommandAddress(commandID int, deliveryAddress string) error { @@ -470,20 +439,13 @@ func (d *Database) UpdateCommandAddress(commandID int, deliveryAddress string) e return fmt.Errorf("adresse vide non autorisée") } - query := `UPDATE commandes - SET adresse = $1, updated_at = CURRENT_TIMESTAMP - WHERE id = $2` - - result, err := d.Exec(query, deliveryAddress, commandID) - if err != nil { - return fmt.Errorf("erreur lors de la mise à jour de l'adresse: %w", err) + result := d.GDB.Exec(` + UPDATE commandes SET adresse = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ?`, deliveryAddress, commandID) + if result.Error != nil { + return fmt.Errorf("erreur lors de la mise à jour de l'adresse: %w", result.Error) } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return fmt.Errorf("erreur lors de la vérification: %w", err) - } - if rowsAffected == 0 { + if result.RowsAffected == 0 { return fmt.Errorf("commande non trouvée") } @@ -497,16 +459,14 @@ func (d *Database) ProposeAddressChange(commandID int, proposedAddress, proposed return err } - query := `UPDATE commandes - SET proposed_address = $1, address_proposal_status = 'pending', updated_at = CURRENT_TIMESTAMP - WHERE id = $2` - - result, err := d.Exec(query, proposedAddress, commandID) - if err != nil { - return fmt.Errorf("erreur proposition adresse: %w", err) + result := d.GDB.Exec(` + UPDATE commandes + SET proposed_address = ?, address_proposal_status = 'pending', updated_at = CURRENT_TIMESTAMP + WHERE id = ?`, proposedAddress, commandID) + if result.Error != nil { + return fmt.Errorf("erreur proposition adresse: %w", result.Error) } - rowsAffected, _ := result.RowsAffected() - if rowsAffected == 0 { + if result.RowsAffected == 0 { return fmt.Errorf("commande non trouvée") } @@ -525,20 +485,19 @@ func (d *Database) RespondToAddressProposal(commandID int, clientUsername string query = `UPDATE commandes SET adresse = proposed_address, proposed_address = NULL, address_proposal_status = 'accepted', updated_at = CURRENT_TIMESTAMP - WHERE id = $1 AND username = $2 AND address_proposal_status = 'pending'` + WHERE id = ? AND username = ? AND address_proposal_status = 'pending'` } else { query = `UPDATE commandes SET proposed_address = NULL, address_proposal_status = 'rejected', updated_at = CURRENT_TIMESTAMP - WHERE id = $1 AND username = $2 AND address_proposal_status = 'pending'` + WHERE id = ? AND username = ? AND address_proposal_status = 'pending'` } - result, err := d.Exec(query, commandID, clientUsername) - if err != nil { - return fmt.Errorf("erreur réponse proposition adresse: %w", err) + result := d.GDB.Exec(query, commandID, clientUsername) + if result.Error != nil { + return fmt.Errorf("erreur réponse proposition adresse: %w", result.Error) } - rowsAffected, _ := result.RowsAffected() - if rowsAffected == 0 { + if result.RowsAffected == 0 { return fmt.Errorf("aucune proposition en attente pour cette commande") } @@ -560,19 +519,11 @@ func (d *Database) UpdateCommandStatus(commandID int, status string) error { return err } - query := `UPDATE commandes SET status = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2` - - result, err := d.Exec(query, status, commandID) - if err != nil { - return fmt.Errorf("erreur lors de la mise à jour du statut: %w", err) + result := d.GDB.Exec(`UPDATE commandes SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, status, commandID) + if result.Error != nil { + return fmt.Errorf("erreur lors de la mise à jour du statut: %w", result.Error) } - - 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 { + if result.RowsAffected == 0 { return fmt.Errorf("commande non trouvée") } @@ -583,11 +534,10 @@ func (d *Database) AddCommandLog(commandID int, status, message, author string) sanitizedMessage := sanitizeLogMessage(message) sanitizedAuthor := sanitizeLogMessage(author) - query := `INSERT INTO command_logs (command_id, status, message, author, created_at) - VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP)` - - _, err := d.Exec(query, commandID, status, sanitizedMessage, sanitizedAuthor) - if err != nil { + if err := d.GDB.Exec(` + INSERT INTO command_logs (command_id, status, message, author, created_at) + VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)`, + commandID, status, sanitizedMessage, sanitizedAuthor).Error; err != nil { log.Printf("⚠️ Avertissement: impossible d'ajouter le log (table command_logs peut-être manquante): %v", err) return nil } @@ -597,43 +547,36 @@ func (d *Database) AddCommandLog(commandID int, status, message, author string) // GetCommandLogs récupère tous les logs d'une commande func (d *Database) GetCommandLogs(commandID int) ([]map[string]any, error) { - query := `SELECT id, command_id, status, message, author, created_at - FROM command_logs - WHERE command_id = $1 - ORDER BY created_at ASC` + var rows []struct { + ID int `gorm:"column:id"` + CommandID int `gorm:"column:command_id"` + Status string `gorm:"column:status"` + Message string `gorm:"column:message"` + Author string `gorm:"column:author"` + CreatedAt time.Time `gorm:"column:created_at"` + } - rows, err := d.Query(query, commandID) + err := d.GDB.Raw(` + SELECT id, command_id, status, message, author, created_at + FROM command_logs + WHERE command_id = ? + ORDER BY created_at ASC`, commandID).Scan(&rows).Error if err != nil { // Si la table n'existe pas, retourner un tableau vide au lieu d'une erreur log.Printf("⚠️ Avertissement: impossible de récupérer les logs: %v", err) return []map[string]any{}, nil } - defer rows.Close() - var logs []map[string]any - for rows.Next() { - var id, commandID int - var status, message, author string - var createdAt time.Time - - err := rows.Scan(&id, &commandID, &status, &message, &author, &createdAt) - if err != nil { - return nil, fmt.Errorf("erreur lors du scan du log: %w", err) - } - - logEntry := map[string]any{ - "id": id, - "command_id": commandID, - "status": status, - "message": message, - "author": author, - "created_at": createdAt, - } - logs = append(logs, logEntry) - } - - if err = rows.Err(); err != nil { - return nil, fmt.Errorf("erreur lors de l'itération des résultats: %w", err) + logs := make([]map[string]any, 0, len(rows)) + for _, row := range rows { + logs = append(logs, map[string]any{ + "id": row.ID, + "command_id": row.CommandID, + "status": row.Status, + "message": row.Message, + "author": row.Author, + "created_at": row.CreatedAt, + }) } return logs, nil @@ -655,113 +598,100 @@ func sanitizeLogMessage(message string) string { } func (d *Database) ValidateDeliveryAtomic(commandID int, adminUsername string) (int, error) { - tx, err := d.Begin() - if err != nil { - log.Printf("❌ [ValidateAtomic] Erreur début transaction: %v", err) - return 0, fmt.Errorf("erreur transaction: %w", err) - } - defer tx.Rollback() + var totalPoints int + var cmdUsernameOut string + var livreurAssignOut string - var currentStatus, cmdUsername, livreurAssign string - var totalPrix float64 - err = tx.QueryRow(` - SELECT status, username, COALESCE(livreur_assign, ''), total_prix - FROM commandes - WHERE id = $1 - FOR UPDATE - `, commandID).Scan(¤tStatus, &cmdUsername, &livreurAssign, &totalPrix) - - if err == sql.ErrNoRows { - log.Printf("❌ [ValidateAtomic] Commande %d non trouvée", commandID) - return 0, fmt.Errorf("commande non trouvée") - } - if err != nil { - log.Printf("❌ [ValidateAtomic] Erreur SELECT: %v", err) - return 0, fmt.Errorf("erreur lecture commande: %w", err) - } - - log.Printf("📋 [ValidateAtomic] Commande trouvée - status=%s, client=%s, livreur=%s", - currentStatus, cmdUsername, livreurAssign) - - validStatuses := []string{"assigned", "en_route", "pending", "livre"} - isValid := slices.Contains(validStatuses, currentStatus) - - if !isValid { - log.Printf("❌ [ValidateAtomic] Statut invalide pour validation: %s", currentStatus) - return 0, fmt.Errorf("statut invalide pour validation: %s", currentStatus) - } - - if currentStatus == "approved" { - log.Printf("⚠️ [ValidateAtomic] Commande %d déjà approuvée", commandID) - return 0, fmt.Errorf("commande déjà approuvée") - } - - result, err := tx.Exec(` - UPDATE commandes - SET status = 'approved', updated_at = CURRENT_TIMESTAMP - WHERE id = $1 AND status = $2 - `, commandID, currentStatus) - - if err != nil { - log.Printf("❌ [ValidateAtomic] Erreur UPDATE: %v", err) - return 0, fmt.Errorf("erreur mise à jour statut: %w", err) - } - - rows, _ := result.RowsAffected() - if rows == 0 { - log.Printf("❌ [ValidateAtomic] Commande %d déjà modifiée (race condition évitée)", commandID) - return 0, fmt.Errorf("commande déjà modifiée par une autre requête") - } - - totalPoints := 0 - if cmdUsername != "" { - log.Printf("🔍 [ValidateAtomic] Calcul points pour client: %s", cmdUsername) - - points, _, err := d.CalculateAndAddPointsForCommandTx(tx, commandID, cmdUsername) - if err != nil { - log.Printf("❌ [ValidateAtomic] Erreur calcul/ajout points: %v", err) - return 0, fmt.Errorf("erreur attribution points: %w", err) + err := d.GDB.Transaction(func(tx *gorm.DB) error { + var cmd struct { + Status string `gorm:"column:status"` + Username string `gorm:"column:username"` + LivreurAssign string `gorm:"column:livreur_assign"` + TotalPrix float64 `gorm:"column:total_prix"` } - totalPoints = points - - log.Printf("✅ [ValidateAtomic] %d points attribués à %s", totalPoints, cmdUsername) - - _, err = tx.Exec(` - UPDATE clients - SET command = command + 1, updated_at = CURRENT_TIMESTAMP - WHERE username = $1 - `, cmdUsername) - if err != nil { - log.Printf("⚠️ [ValidateAtomic] Erreur incrémentation compteur: %v", err) - } else { - log.Printf("✅ [ValidateAtomic] Compteur commandes incrémenté pour %s", cmdUsername) + if err := tx.Raw(` + SELECT status, username, COALESCE(livreur_assign, '') as livreur_assign, total_prix + FROM commandes WHERE id = ? FOR UPDATE`, commandID).Scan(&cmd).Error; err != nil { + log.Printf("❌ [ValidateAtomic] Erreur SELECT: %v", err) + return fmt.Errorf("erreur lecture commande: %w", err) + } + if cmd.Username == "" { + log.Printf("❌ [ValidateAtomic] Commande %d non trouvée", commandID) + return fmt.Errorf("commande non trouvée") } - } - _, err = tx.Exec(` - INSERT INTO command_logs (command_id, status, message, author, created_at) - VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP) - `, commandID, "approved", - fmt.Sprintf("Livraison validée par admin %s - %d points attribués", adminUsername, totalPoints), - adminUsername) + log.Printf("📋 [ValidateAtomic] Commande trouvée - status=%s, client=%s, livreur=%s", + cmd.Status, cmd.Username, cmd.LivreurAssign) + + validStatuses := []string{"assigned", "en_route", "pending", "livre"} + if !slices.Contains(validStatuses, cmd.Status) { + log.Printf("❌ [ValidateAtomic] Statut invalide pour validation: %s", cmd.Status) + return fmt.Errorf("statut invalide pour validation: %s", cmd.Status) + } + + if cmd.Status == "approved" { + log.Printf("⚠️ [ValidateAtomic] Commande %d déjà approuvée", commandID) + return fmt.Errorf("commande déjà approuvée") + } + + result := tx.Exec(` + UPDATE commandes SET status = 'approved', updated_at = CURRENT_TIMESTAMP + WHERE id = ? AND status = ?`, commandID, cmd.Status) + if result.Error != nil { + log.Printf("❌ [ValidateAtomic] Erreur UPDATE: %v", result.Error) + return fmt.Errorf("erreur mise à jour statut: %w", result.Error) + } + if result.RowsAffected == 0 { + log.Printf("❌ [ValidateAtomic] Commande %d déjà modifiée (race condition évitée)", commandID) + return fmt.Errorf("commande déjà modifiée par une autre requête") + } + + if cmd.Username != "" { + log.Printf("🔍 [ValidateAtomic] Calcul points pour client: %s", cmd.Username) + + points, _, err := d.CalculateAndAddPointsForCommandTx(tx, commandID, cmd.Username) + if err != nil { + log.Printf("❌ [ValidateAtomic] Erreur calcul/ajout points: %v", err) + return fmt.Errorf("erreur attribution points: %w", err) + } + totalPoints = points + + log.Printf("✅ [ValidateAtomic] %d points attribués à %s", totalPoints, cmd.Username) + + if err := tx.Exec(` + UPDATE clients SET command = command + 1, updated_at = CURRENT_TIMESTAMP + WHERE username = ?`, cmd.Username).Error; err != nil { + log.Printf("⚠️ [ValidateAtomic] Erreur incrémentation compteur: %v", err) + } else { + log.Printf("✅ [ValidateAtomic] Compteur commandes incrémenté pour %s", cmd.Username) + } + } + + if err := tx.Exec(` + INSERT INTO command_logs (command_id, status, message, author, created_at) + VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)`, + commandID, "approved", + fmt.Sprintf("Livraison validée par admin %s - %d points attribués", adminUsername, totalPoints), + adminUsername).Error; err != nil { + log.Printf("⚠️ [ValidateAtomic] Erreur ajout log: %v", err) + } + + cmdUsernameOut = cmd.Username + livreurAssignOut = cmd.LivreurAssign + return nil + }) if err != nil { - log.Printf("⚠️ [ValidateAtomic] Erreur ajout log: %v", err) - } - - if err := tx.Commit(); err != nil { - log.Printf("❌ [ValidateAtomic] Erreur COMMIT: %v", err) - return 0, fmt.Errorf("erreur commit transaction: %w", err) + return 0, err } log.Printf("🎉 [ValidateAtomic] SUCCÈS - Commande %d validée, %d points attribués", commandID, totalPoints) - if livreurAssign != "" { - log.Printf("📦 [ValidateAtomic] Optimisation queue pour livreur: %s", livreurAssign) + if livreurAssignOut != "" { + log.Printf("📦 [ValidateAtomic] Optimisation queue pour livreur: %s", livreurAssignOut) go func() { - err := d.CompleteDeliveryAndProcessNext(livreurAssign, commandID) - if err != nil { + if err := d.CompleteDeliveryAndProcessNext(livreurAssignOut, commandID); err != nil { log.Printf("⚠️ [ValidateAtomic] Erreur optimisation queue: %v", err) } }() @@ -771,11 +701,11 @@ func (d *Database) ValidateDeliveryAtomic(commandID int, adminUsername string) ( commandCacheKey := fmt.Sprintf("command:%d", commandID) Redis.Del(RedisCtx, commandCacheKey) - if cmdUsername != "" { - clientCacheKey := fmt.Sprintf("client:%s", cmdUsername) + if cmdUsernameOut != "" { + clientCacheKey := fmt.Sprintf("client:%s", cmdUsernameOut) Redis.Del(RedisCtx, clientCacheKey) - clientCommandsCacheKey := fmt.Sprintf("client:%s:commands", cmdUsername) + clientCommandsCacheKey := fmt.Sprintf("client:%s:commands", cmdUsernameOut) Redis.Del(RedisCtx, clientCommandsCacheKey) } @@ -789,120 +719,99 @@ func (d *Database) ValidateDeliveryAtomic(commandID int, adminUsername string) ( func (d *Database) ApproveDeliveryAtomic(commandID int, username string) (int, string, error) { log.Printf("🔒 [ApproveAtomic] START - cmd=%d, client=%s", commandID, username) - // ✅ ÉTAPE 1: Démarrer une transaction - tx, err := d.Begin() - if err != nil { - log.Printf("❌ [ApproveAtomic] Erreur début transaction: %v", err) - return 0, "", fmt.Errorf("erreur transaction: %w", err) - } - defer tx.Rollback() + var totalPoints int + var pointCategory string + var livreurAssignOut string - // ✅ ÉTAPE 2: SELECT FOR UPDATE pour verrouiller la commande - var currentStatus, cmdUsername, livreurAssign string - err = tx.QueryRow(` - SELECT status, username, COALESCE(livreur_assign, '') - FROM commandes - WHERE id = $1 - FOR UPDATE - `, commandID).Scan(¤tStatus, &cmdUsername, &livreurAssign) + err := d.GDB.Transaction(func(tx *gorm.DB) error { + var cmd struct { + Status string `gorm:"column:status"` + Username string `gorm:"column:username"` + LivreurAssign string `gorm:"column:livreur_assign"` + } + if err := tx.Raw(` + SELECT status, username, COALESCE(livreur_assign, '') as livreur_assign + FROM commandes WHERE id = ? FOR UPDATE`, commandID).Scan(&cmd).Error; err != nil { + log.Printf("❌ [ApproveAtomic] Erreur SELECT: %v", err) + return fmt.Errorf("erreur lecture commande: %w", err) + } + if cmd.Username == "" { + log.Printf("❌ [ApproveAtomic] Commande %d non trouvée", commandID) + return fmt.Errorf("commande non trouvée") + } - if err == sql.ErrNoRows { - log.Printf("❌ [ApproveAtomic] Commande %d non trouvée", commandID) - return 0, "", fmt.Errorf("commande non trouvée") - } - if err != nil { - log.Printf("❌ [ApproveAtomic] Erreur SELECT: %v", err) - return 0, "", fmt.Errorf("erreur lecture commande: %w", err) - } + log.Printf("📋 [ApproveAtomic] Commande trouvée - status=%s, owner=%s", cmd.Status, cmd.Username) - log.Printf("📋 [ApproveAtomic] Commande trouvée - status=%s, owner=%s", currentStatus, cmdUsername) + if cmd.Username != username { + log.Printf("❌ [ApproveAtomic] Commande n'appartient pas à %s (propriétaire: %s)", + username, cmd.Username) + return fmt.Errorf("cette commande ne vous appartient pas") + } - // ✅ ÉTAPE 3: Vérifier propriété - if cmdUsername != username { - log.Printf("❌ [ApproveAtomic] Commande n'appartient pas à %s (propriétaire: %s)", - username, cmdUsername) - return 0, "", fmt.Errorf("cette commande ne vous appartient pas") - } + if cmd.Status != "livre" { + log.Printf("❌ [ApproveAtomic] Statut invalide: %s (attendu: livre)", cmd.Status) + return fmt.Errorf("commande doit être en statut 'livre' (statut actuel: %s)", cmd.Status) + } - // ✅ ÉTAPE 4: Vérifier le statut - if currentStatus != "livre" { - log.Printf("❌ [ApproveAtomic] Statut invalide: %s (attendu: livre)", currentStatus) - return 0, "", fmt.Errorf("commande doit être en statut 'livre' (statut actuel: %s)", currentStatus) - } + result := tx.Exec(` + UPDATE commandes SET status = 'approved', updated_at = CURRENT_TIMESTAMP + WHERE id = ? AND status = 'livre' AND username = ?`, commandID, username) + if result.Error != nil { + log.Printf("❌ [ApproveAtomic] Erreur UPDATE: %v", result.Error) + return fmt.Errorf("erreur mise à jour statut: %w", result.Error) + } + if result.RowsAffected == 0 { + log.Printf("❌ [ApproveAtomic] Commande %d déjà modifiée (race condition évitée)", commandID) + return fmt.Errorf("commande déjà approuvée ou modifiée") + } - // ✅ ÉTAPE 5: UPDATE avec vérification du statut - result, err := tx.Exec(` - UPDATE commandes - SET status = 'approved', updated_at = CURRENT_TIMESTAMP - WHERE id = $1 AND status = 'livre' AND username = $2 - `, commandID, username) + log.Printf("✅ [ApproveAtomic] Statut mis à jour: livre → approved") + + pts, cat, err := d.CalculateAndAddPointsForCommandTx(tx, commandID, username) + if err != nil { + log.Printf("❌ [ApproveAtomic] Erreur calcul points: %v", err) + return fmt.Errorf("erreur attribution points: %w", err) + } + totalPoints = pts + pointCategory = cat + + log.Printf("✅ [ApproveAtomic] %d points [%s] attribués à %s", totalPoints, pointCategory, username) + + if err := tx.Exec(` + UPDATE clients SET command = command + 1, updated_at = CURRENT_TIMESTAMP + WHERE username = ?`, username).Error; err != nil { + log.Printf("⚠️ [ApproveAtomic] Erreur incrémentation compteur: %v", err) + } + + if err := tx.Exec(` + INSERT INTO command_logs (command_id, status, message, author, created_at) + VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)`, + commandID, "approved", + fmt.Sprintf("Livraison confirmée par le client %s - %d points [%s] attribués", username, totalPoints, pointCategory), + username).Error; err != nil { + log.Printf("⚠️ [ApproveAtomic] Erreur ajout log: %v", err) + } + + livreurAssignOut = cmd.LivreurAssign + return nil + }) if err != nil { - log.Printf("❌ [ApproveAtomic] Erreur UPDATE: %v", err) - return 0, "", fmt.Errorf("erreur mise à jour statut: %w", err) - } - - rows, _ := result.RowsAffected() - if rows == 0 { - log.Printf("❌ [ApproveAtomic] Commande %d déjà modifiée (race condition évitée)", commandID) - return 0, "", fmt.Errorf("commande déjà approuvée ou modifiée") - } - - log.Printf("✅ [ApproveAtomic] Statut mis à jour: livre → approved") - - // ✅ ÉTAPE 6: Calculer et ajouter les points - totalPoints, pointCategory, err := d.CalculateAndAddPointsForCommandTx(tx, commandID, username) - if err != nil { - log.Printf("❌ [ApproveAtomic] Erreur calcul points: %v", err) - return 0, "", fmt.Errorf("erreur attribution points: %w", err) - } - - log.Printf("✅ [ApproveAtomic] %d points [%s] attribués à %s", totalPoints, pointCategory, username) - - // ✅ ÉTAPE 7: Incrémenter le compteur de commandes - _, err = tx.Exec(` - UPDATE clients - SET command = command + 1, updated_at = CURRENT_TIMESTAMP - WHERE username = $1 - `, username) - if err != nil { - log.Printf("⚠️ [ApproveAtomic] Erreur incrémentation compteur: %v", err) - // On ne bloque pas pour ça - } - - // ✅ ÉTAPE 8: Ajouter un log - _, err = tx.Exec(` - INSERT INTO command_logs (command_id, status, message, author, created_at) - VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP) - `, commandID, "approved", - fmt.Sprintf("Livraison confirmée par le client %s - %d points [%s] attribués", username, totalPoints, pointCategory), - username) - - if err != nil { - log.Printf("⚠️ [ApproveAtomic] Erreur ajout log: %v", err) - } - - // ✅ ÉTAPE 9: Commit - if err := tx.Commit(); err != nil { - log.Printf("❌ [ApproveAtomic] Erreur COMMIT: %v", err) - return 0, "", fmt.Errorf("erreur commit transaction: %w", err) + return 0, "", err } log.Printf("🎉 [ApproveAtomic] SUCCÈS - Commande %d approuvée, %d points [%s] attribués", commandID, totalPoints, pointCategory) - // ✅ ÉTAPE 10: Optimiser queue livreur (async, après commit) - if livreurAssign != "" { - log.Printf("📦 [ApproveAtomic] Optimisation queue pour livreur: %s", livreurAssign) + if livreurAssignOut != "" { + log.Printf("📦 [ApproveAtomic] Optimisation queue pour livreur: %s", livreurAssignOut) go func() { - err := d.CompleteDeliveryAndProcessNext(livreurAssign, commandID) - if err != nil { + if err := d.CompleteDeliveryAndProcessNext(livreurAssignOut, commandID); err != nil { log.Printf("⚠️ [ApproveAtomic] Erreur optimisation queue: %v", err) } }() } - // ✅ ÉTAPE 11: Invalider caches (async) go func() { commandCacheKey := fmt.Sprintf("command:%d", commandID) Redis.Del(RedisCtx, commandCacheKey) @@ -923,79 +832,77 @@ func (d *Database) ApproveDeliveryAtomic(commandID int, username string) (int, s func (d *Database) ApproveDeliveryAtomicByStaff(commandID int, staffUsername string) (int, string, string, error) { log.Printf("🔒 [ApproveAtomicStaff] START - cmd=%d, staff=%s", commandID, staffUsername) - tx, err := d.Begin() + var totalPoints int + var pointCategory string + var clientUsernameOut string + var livreurAssignOut string + + err := d.GDB.Transaction(func(tx *gorm.DB) error { + var cmd struct { + Status string `gorm:"column:status"` + Username string `gorm:"column:username"` + LivreurAssign string `gorm:"column:livreur_assign"` + } + if err := tx.Raw(` + SELECT status, username, COALESCE(livreur_assign, '') as livreur_assign + FROM commandes WHERE id = ? FOR UPDATE`, commandID).Scan(&cmd).Error; err != nil { + return fmt.Errorf("erreur lecture commande: %w", err) + } + if cmd.Username == "" { + return fmt.Errorf("commande non trouvée") + } + + if cmd.Status != "livre" { + return fmt.Errorf("commande doit être en statut 'livre' (statut actuel: %s)", cmd.Status) + } + + result := tx.Exec(` + UPDATE commandes SET status = 'approved', updated_at = CURRENT_TIMESTAMP + WHERE id = ? AND status = 'livre'`, commandID) + if result.Error != nil { + return fmt.Errorf("erreur mise à jour statut: %w", result.Error) + } + if result.RowsAffected == 0 { + return fmt.Errorf("commande déjà approuvée ou modifiée") + } + + pts, cat, err := d.CalculateAndAddPointsForCommandTx(tx, commandID, cmd.Username) + if err != nil { + return fmt.Errorf("erreur attribution points: %w", err) + } + totalPoints = pts + pointCategory = cat + + if err := tx.Exec(` + UPDATE clients SET command = command + 1, updated_at = CURRENT_TIMESTAMP + WHERE username = ?`, cmd.Username).Error; err != nil { + log.Printf("⚠️ [ApproveAtomicStaff] Erreur incrémentation compteur: %v", err) + } + + if err := tx.Exec(` + INSERT INTO command_logs (command_id, status, message, author, created_at) + VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)`, + commandID, "approved", + fmt.Sprintf("Réception confirmée par %s au nom du client %s - %d points attribués", staffUsername, cmd.Username, totalPoints), + staffUsername).Error; err != nil { + log.Printf("⚠️ [ApproveAtomicStaff] Erreur log: %v", err) + } + + clientUsernameOut = cmd.Username + livreurAssignOut = cmd.LivreurAssign + return nil + }) + if err != nil { - return 0, "", "", fmt.Errorf("erreur transaction: %w", err) - } - defer tx.Rollback() - - var currentStatus, clientUsername, livreurAssign string - err = tx.QueryRow(` - SELECT status, username, COALESCE(livreur_assign, '') - FROM commandes - WHERE id = $1 - FOR UPDATE - `, commandID).Scan(¤tStatus, &clientUsername, &livreurAssign) - - if err == sql.ErrNoRows { - return 0, "", "", fmt.Errorf("commande non trouvée") - } - if err != nil { - return 0, "", "", fmt.Errorf("erreur lecture commande: %w", err) - } - - if currentStatus != "livre" { - return 0, "", "", fmt.Errorf("commande doit être en statut 'livre' (statut actuel: %s)", currentStatus) - } - - result, err := tx.Exec(` - UPDATE commandes - SET status = 'approved', updated_at = CURRENT_TIMESTAMP - WHERE id = $1 AND status = 'livre' - `, commandID) - if err != nil { - return 0, "", "", fmt.Errorf("erreur mise à jour statut: %w", err) - } - - rows, _ := result.RowsAffected() - if rows == 0 { - return 0, "", "", fmt.Errorf("commande déjà approuvée ou modifiée") - } - - totalPoints, pointCategory, err := d.CalculateAndAddPointsForCommandTx(tx, commandID, clientUsername) - if err != nil { - return 0, "", "", fmt.Errorf("erreur attribution points: %w", err) - } - - _, err = tx.Exec(` - UPDATE clients - SET command = command + 1, updated_at = CURRENT_TIMESTAMP - WHERE username = $1 - `, clientUsername) - if err != nil { - log.Printf("⚠️ [ApproveAtomicStaff] Erreur incrémentation compteur: %v", err) - } - - _, err = tx.Exec(` - INSERT INTO command_logs (command_id, status, message, author, created_at) - VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP) - `, commandID, "approved", - fmt.Sprintf("Réception confirmée par %s au nom du client %s - %d points attribués", staffUsername, clientUsername, totalPoints), - staffUsername) - if err != nil { - log.Printf("⚠️ [ApproveAtomicStaff] Erreur log: %v", err) - } - - if err := tx.Commit(); err != nil { - return 0, "", "", fmt.Errorf("erreur commit: %w", err) + return 0, "", "", err } log.Printf("🎉 [ApproveAtomicStaff] SUCCÈS - cmd=%d approuvée par %s, %d points → client %s", - commandID, staffUsername, totalPoints, clientUsername) + commandID, staffUsername, totalPoints, clientUsernameOut) - if livreurAssign != "" { + if livreurAssignOut != "" { go func() { - if err := d.CompleteDeliveryAndProcessNext(livreurAssign, commandID); err != nil { + if err := d.CompleteDeliveryAndProcessNext(livreurAssignOut, commandID); err != nil { log.Printf("⚠️ [ApproveAtomicStaff] Erreur queue: %v", err) } }() @@ -1003,9 +910,9 @@ func (d *Database) ApproveDeliveryAtomicByStaff(commandID int, staffUsername str go func() { Redis.Del(RedisCtx, fmt.Sprintf("command:%d", commandID)) - Redis.Del(RedisCtx, fmt.Sprintf("client:%s", clientUsername)) - Redis.Del(RedisCtx, fmt.Sprintf("client:%s:commands", clientUsername)) + Redis.Del(RedisCtx, fmt.Sprintf("client:%s", clientUsernameOut)) + Redis.Del(RedisCtx, fmt.Sprintf("client:%s:commands", clientUsernameOut)) }() - return totalPoints, pointCategory, clientUsername, nil + return totalPoints, pointCategory, clientUsernameOut, nil } diff --git a/backend/gestion/db/db_delivery.go b/backend/gestion/db/db_delivery.go index 698d5905..e364d6cf 100644 --- a/backend/gestion/db/db_delivery.go +++ b/backend/gestion/db/db_delivery.go @@ -1,46 +1,24 @@ package db import ( - "database/sql" "fmt" "log" "slices" - "time" + + "gorm.io/gorm" ) // GetAvailableDeliveryPersons récupère tous les livreurs disponibles func (d *Database) GetAvailableDeliveryPersons() ([]map[string]any, error) { - query := `SELECT id, username, total, livraison - FROM users - WHERE role = 'livreur' - ORDER BY username` - - rows, err := d.Query(query) + var livreurs []map[string]any + err := d.GDB.Raw(` + SELECT id, username, total, livraison + FROM users + WHERE role = 'livreur' + ORDER BY username`).Scan(&livreurs).Error if err != nil { return nil, fmt.Errorf("erreur lors de la récupération des livreurs: %w", err) } - defer rows.Close() - - var livreurs []map[string]any - for rows.Next() { - var id int - var username string - var total, livraison float64 - - err := rows.Scan(&id, &username, &total, &livraison) - if err != nil { - return nil, fmt.Errorf("erreur lors du scan du livreur: %w", err) - } - - livreur := map[string]any{ - "id": id, - "username": username, - "total": total, - "livraison": livraison, - } - livreurs = append(livreurs, livreur) - } - return livreurs, nil } @@ -48,255 +26,172 @@ func (d *Database) GetAvailableDeliveryPersons() ([]map[string]any, error) { func (d *Database) AssignDeliveryPerson(commandID int, livreurUsername string) error { log.Printf("📦 [AssignDeliveryPerson] START - commandID=%d, livreur=%s", commandID, livreurUsername) - tx, err := d.Begin() - if err != nil { - log.Printf("❌ Erreur démarrage transaction: %v", err) - return fmt.Errorf("erreur démarrage transaction: %w", err) - } - defer tx.Rollback() + return d.GDB.Transaction(func(tx *gorm.DB) error { + var roleResult struct { + Role string `gorm:"column:role"` + } + err := tx.Raw(`SELECT role FROM users WHERE username = ? FOR UPDATE`, livreurUsername).Scan(&roleResult).Error + if err != nil { + log.Printf("❌ Erreur vérification livreur: %v", err) + return fmt.Errorf("erreur lors de la vérification du livreur: %w", err) + } + if roleResult.Role == "" { + log.Printf("❌ Livreur '%s' non trouvé", livreurUsername) + return fmt.Errorf("livreur non trouvé") + } + if roleResult.Role != "livreur" { + log.Printf("❌ L'utilisateur '%s' n'est pas un livreur (role=%s)", livreurUsername, roleResult.Role) + return fmt.Errorf("l'utilisateur n'est pas un livreur") + } - var role string - checkQuery := `SELECT role FROM users WHERE username = $1 FOR UPDATE` - err = tx.QueryRow(checkQuery, livreurUsername).Scan(&role) - if err == sql.ErrNoRows { - log.Printf("❌ Livreur '%s' non trouvé", livreurUsername) - return fmt.Errorf("livreur non trouvé") - } - if err != nil { - log.Printf("❌ Erreur vérification livreur: %v", err) - return fmt.Errorf("erreur lors de la vérification du livreur: %w", err) - } - if role != "livreur" { - log.Printf("❌ L'utilisateur '%s' n'est pas un livreur (role=%s)", livreurUsername, role) - return fmt.Errorf("l'utilisateur n'est pas un livreur") - } + var cmdResult struct { + Status string `gorm:"column:status"` + LivreurAssign *string `gorm:"column:livreur_assign"` + } + err = tx.Raw(`SELECT status, livreur_assign FROM commandes WHERE id = ? FOR UPDATE`, commandID).Scan(&cmdResult).Error + if err != nil { + log.Printf("❌ Erreur vérification commande: %v", err) + return fmt.Errorf("erreur lors de la vérification de la commande: %w", err) + } + if cmdResult.Status == "" { + log.Printf("❌ Commande %d non trouvée", commandID) + return fmt.Errorf("commande non trouvée") + } - var currentStatus string - var currentLivreur sql.NullString - statusQuery := `SELECT status, livreur_assign - FROM commandes - WHERE id = $1 - FOR UPDATE` + validStatusesForAssignment := []string{"pending", "assigned"} + if !slices.Contains(validStatusesForAssignment, cmdResult.Status) { + log.Printf("❌ Statut invalide pour assignation: %s", cmdResult.Status) + return fmt.Errorf("commande en statut '%s', impossible d'assigner un livreur", cmdResult.Status) + } - err = tx.QueryRow(statusQuery, commandID).Scan(¤tStatus, ¤tLivreur) - if err == sql.ErrNoRows { - log.Printf("❌ Commande %d non trouvée", commandID) - return fmt.Errorf("commande non trouvée") - } - if err != nil { - log.Printf("❌ Erreur vérification commande: %v", err) - return fmt.Errorf("erreur lors de la vérification de la commande: %w", err) - } + result := tx.Exec(` + UPDATE commandes + SET livreur_assign = ?, + status = 'assigned', + updated_at = CURRENT_TIMESTAMP + WHERE id = ? + AND status IN ('pending', 'assigned')`, livreurUsername, commandID) + if result.Error != nil { + log.Printf("❌ Erreur UPDATE: %v", result.Error) + return fmt.Errorf("erreur lors de l'assignation du livreur: %w", result.Error) + } + if result.RowsAffected == 0 { + log.Printf("❌ Impossible d'assigner: conditions non remplies") + return fmt.Errorf("impossible d'assigner la commande (déjà assignée ou statut changé)") + } - validStatusesForAssignment := []string{"pending", "assigned"} - isValidStatus := slices.Contains(validStatusesForAssignment, currentStatus) + if err := tx.Exec(` + INSERT INTO command_logs (command_id, status, message, author, created_at) + VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)`, + commandID, "assigned", + fmt.Sprintf("Livraison assignée au livreur %s", livreurUsername), + "admin", + ).Error; err != nil { + log.Printf("⚠️ Erreur ajout log: %v", err) + // Non bloquant + } - if !isValidStatus { - log.Printf("❌ Statut invalide pour assignation: %s", currentStatus) - return fmt.Errorf("commande en statut '%s', impossible d'assigner un livreur", currentStatus) - } - - updateQuery := `UPDATE commandes - SET livreur_assign = $1, - status = 'assigned', - updated_at = CURRENT_TIMESTAMP - WHERE id = $2 - AND status IN ('pending', 'assigned')` - - result, err := tx.Exec(updateQuery, livreurUsername, commandID) - if err != nil { - log.Printf("❌ Erreur UPDATE: %v", err) - return fmt.Errorf("erreur lors de l'assignation du livreur: %w", err) - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - log.Printf("❌ Erreur RowsAffected: %v", err) - return fmt.Errorf("erreur lors de la vérification: %w", err) - } - if rowsAffected == 0 { - log.Printf("❌ Impossible d'assigner: conditions non remplies") - return fmt.Errorf("impossible d'assigner la commande (déjà assignée ou statut changé)") - } - - logQuery := `INSERT INTO command_logs (command_id, status, message, author, created_at) - VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP)` - - _, err = tx.Exec(logQuery, commandID, "assigned", - fmt.Sprintf("Livraison assignée au livreur %s", livreurUsername), - "admin") - if err != nil { - log.Printf("⚠️ Erreur ajout log: %v", err) - // Non bloquant - } - - err = tx.Commit() - if err != nil { - log.Printf("❌ Erreur COMMIT: %v", err) - return fmt.Errorf("erreur commit transaction: %w", err) - } - - return nil + return nil + }) } // GetDeliveryPersonCommands récupère les commandes assignées à un livreur func (d *Database) GetDeliveryPersonCommands(livreurUsername string, status string) ([]map[string]any, error) { - query := `SELECT id, username, status, adresse, total_prix, livreur_assign, created_at, updated_at + query := `SELECT id, username, status, adresse, total_prix::float8 as total_prix, livreur_assign, created_at, updated_at FROM commandes - WHERE livreur_assign = $1` + WHERE livreur_assign = ?` args := []any{livreurUsername} if status != "" { - query += " AND status = $2" + query += " AND status = ?" args = append(args, status) } query += " ORDER BY created_at DESC" - rows, err := d.Query(query, args...) - if err != nil { + var commands []map[string]any + if err := d.GDB.Raw(query, args...).Scan(&commands).Error; err != nil { return nil, fmt.Errorf("erreur lors de la récupération des commandes: %w", err) } - defer rows.Close() - - var commands []map[string]any - for rows.Next() { - var id int - var username, status, adresse string - var livreurAssign sql.NullString - var totalPrix float64 - var createdAt, updatedAt time.Time - - err := rows.Scan(&id, &username, &status, &adresse, &totalPrix, &livreurAssign, &createdAt, &updatedAt) - if err != nil { - return nil, fmt.Errorf("erreur lors du scan: %w", err) - } - - command := map[string]any{ - "id": id, - "username": username, - "status": status, - "adresse": adresse, - "total_prix": totalPrix, - "livreur_assign": livreurAssign.String, - "created_at": createdAt, - "updated_at": updatedAt, - } - commands = append(commands, command) - } - return commands, nil } func (d *Database) IncrementLivreurDeliveryCount(livreurUsername string) error { - query := `UPDATE users - SET livraison = livraison + 1, - total = total + 1, - updated_at = CURRENT_TIMESTAMP - WHERE username = $1 AND role = 'livreur'` - - result, err := d.Exec(query, livreurUsername) - if err != nil { - return fmt.Errorf("erreur lors de l'incrémentation des livraisons: %w", err) + result := d.GDB.Exec(` + UPDATE users + SET livraison = livraison + 1, + total = total + 1, + updated_at = CURRENT_TIMESTAMP + WHERE username = ? AND role = 'livreur'`, livreurUsername) + if result.Error != nil { + return fmt.Errorf("erreur lors de l'incrémentation des livraisons: %w", result.Error) } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return fmt.Errorf("erreur lors de la vérification: %w", err) - } - if rowsAffected == 0 { + if result.RowsAffected == 0 { return fmt.Errorf("livreur non trouvé") } - return nil } func (d *Database) ApproveDelivery(commandID int, clientUsername string) error { - tx, err := d.Begin() - if err != nil { - log.Printf("❌ Erreur démarrage transaction: %v", err) - return fmt.Errorf("erreur démarrage transaction: %w", err) - } - defer tx.Rollback() - - var commandUsername, currentStatus string - var livreurAssign sql.NullString - - checkQuery := `SELECT username, status, livreur_assign - FROM commandes - WHERE id = $1 - FOR UPDATE` - - err = tx.QueryRow(checkQuery, commandID).Scan(&commandUsername, ¤tStatus, &livreurAssign) - if err == sql.ErrNoRows { - return fmt.Errorf("commande non trouvée") - } - if err != nil { - return fmt.Errorf("erreur lors de la vérification de la commande: %w", err) - } - - if commandUsername != clientUsername { - return fmt.Errorf("cette commande ne vous appartient pas") - } - - if currentStatus != "livre" { - return fmt.Errorf("cette commande n'est pas encore livrée (statut actuel: %s)", currentStatus) - } - - updateQuery := `UPDATE commandes - SET status = 'approved', - updated_at = CURRENT_TIMESTAMP - WHERE id = $1 - AND status = 'livre' - AND username = $2` - - result, err := tx.Exec(updateQuery, commandID, clientUsername) - if err != nil { - return fmt.Errorf("erreur lors de l'approbation de la livraison: %w", err) - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return fmt.Errorf("erreur lors de la vérification: %w", err) - } - if rowsAffected == 0 { - return fmt.Errorf("impossible d'approuver: statut changé ou commande introuvable") - } - - if livreurAssign.Valid && livreurAssign.String != "" { - incrementQuery := `UPDATE users - SET livraison = livraison + 1, - total = total + 1, - updated_at = CURRENT_TIMESTAMP - WHERE username = $1 AND role = 'livreur'` - - result, err := tx.Exec(incrementQuery, livreurAssign.String) + return d.GDB.Transaction(func(tx *gorm.DB) error { + var cmdResult struct { + Username string `gorm:"column:username"` + Status string `gorm:"column:status"` + LivreurAssign *string `gorm:"column:livreur_assign"` + } + err := tx.Raw(` + SELECT username, status, livreur_assign + FROM commandes WHERE id = ? FOR UPDATE`, commandID).Scan(&cmdResult).Error if err != nil { - log.Printf("⚠️ Erreur incrémentation livreur: %v", err) - } else { - rows, _ := result.RowsAffected() - if rows > 0 { - log.Printf(" ✅ Compteur livreur incrémenté: %s", livreurAssign.String) + return fmt.Errorf("erreur lors de la vérification de la commande: %w", err) + } + if cmdResult.Username == "" { + return fmt.Errorf("commande non trouvée") + } + + if cmdResult.Username != clientUsername { + return fmt.Errorf("cette commande ne vous appartient pas") + } + if cmdResult.Status != "livre" { + return fmt.Errorf("cette commande n'est pas encore livrée (statut actuel: %s)", cmdResult.Status) + } + + result := tx.Exec(` + UPDATE commandes + SET status = 'approved', updated_at = CURRENT_TIMESTAMP + WHERE id = ? AND status = 'livre' AND username = ?`, commandID, clientUsername) + if result.Error != nil { + return fmt.Errorf("erreur lors de l'approbation de la livraison: %w", result.Error) + } + if result.RowsAffected == 0 { + return fmt.Errorf("impossible d'approuver: statut changé ou commande introuvable") + } + + if cmdResult.LivreurAssign != nil && *cmdResult.LivreurAssign != "" { + res := tx.Exec(` + UPDATE users + SET livraison = livraison + 1, + total = total + 1, + updated_at = CURRENT_TIMESTAMP + WHERE username = ? AND role = 'livreur'`, *cmdResult.LivreurAssign) + if res.Error != nil { + log.Printf("⚠️ Erreur incrémentation livreur: %v", res.Error) + } else if res.RowsAffected > 0 { + log.Printf(" ✅ Compteur livreur incrémenté: %s", *cmdResult.LivreurAssign) } } - } - logQuery := `INSERT INTO command_logs (command_id, status, message, author, created_at) - VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP)` + if err := tx.Exec(` + INSERT INTO command_logs (command_id, status, message, author, created_at) + VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)`, + commandID, "approved", + fmt.Sprintf("Livraison approuvée par le client %s", clientUsername), + clientUsername, + ).Error; err != nil { + log.Printf("⚠️ Erreur ajout log: %v", err) + } - _, err = tx.Exec(logQuery, commandID, "approved", - fmt.Sprintf("Livraison approuvée par le client %s", clientUsername), - clientUsername) - if err != nil { - log.Printf("⚠️ Erreur ajout log: %v", err) - } - - err = tx.Commit() - if err != nil { - log.Printf("❌ Erreur COMMIT: %v", err) - return fmt.Errorf("erreur commit transaction: %w", err) - } - - return nil + return nil + }) } diff --git a/backend/gestion/db/db_delivery_issues.go b/backend/gestion/db/db_delivery_issues.go index 4922718c..ce245b05 100644 --- a/backend/gestion/db/db_delivery_issues.go +++ b/backend/gestion/db/db_delivery_issues.go @@ -7,96 +7,40 @@ import ( ) func (d *Database) GetDeliveryIssues(status string) ([]models.DeliveryIssue, error) { - query := ` - SELECT id, command_id, issue_type, description, status, - reported_by, COALESCE(resolved_by, ''), COALESCE(resolution, ''), - created_at, updated_at - FROM delivery_issues - ` - - var args []any + q := d.GDB.Order("created_at DESC") if status != "" { - query += " WHERE status = $1" - args = append(args, status) + q = q.Where("status = ?", status) } - - query += " ORDER BY created_at DESC" - - rows, err := d.Query(query, args...) - if err != nil { + var issues []models.DeliveryIssue + if err := q.Find(&issues).Error; err != nil { return nil, fmt.Errorf("erreur récupération problèmes: %w", err) } - defer rows.Close() - - var issues []models.DeliveryIssue - for rows.Next() { - var issue models.DeliveryIssue - err := rows.Scan( - &issue.ID, - &issue.CommandID, - &issue.IssueType, - &issue.Description, - &issue.Status, - &issue.ReportedBy, - &issue.ResolvedBy, - &issue.Resolution, - &issue.CreatedAt, - &issue.UpdatedAt, - ) - if err != nil { - return nil, fmt.Errorf("erreur scan problème: %w", err) - } - issues = append(issues, issue) - } - return issues, nil } -// CreateDeliveryIssue crée un nouveau problème func (d *Database) CreateDeliveryIssue(commandID int, issueType, description, reportedBy string) (*models.DeliveryIssue, error) { - query := ` - INSERT INTO delivery_issues (command_id, issue_type, description, status, reported_by, created_at, updated_at) - VALUES ($1, $2, $3, 'open', $4, NOW(), NOW()) - RETURNING id, command_id, issue_type, description, status, reported_by, created_at, updated_at - ` - - var issue models.DeliveryIssue - err := d.QueryRow(query, commandID, issueType, description, reportedBy).Scan( - &issue.ID, - &issue.CommandID, - &issue.IssueType, - &issue.Description, - &issue.Status, - &issue.ReportedBy, - &issue.CreatedAt, - &issue.UpdatedAt, - ) - - if err != nil { + issue := models.DeliveryIssue{ + CommandID: commandID, + IssueType: issueType, + Description: description, + Status: "open", + ReportedBy: reportedBy, + } + if err := d.GDB.Create(&issue).Error; err != nil { return nil, fmt.Errorf("erreur création problème: %w", err) } - return &issue, nil } -// UpdateDeliveryIssue met à jour un problème func (d *Database) UpdateDeliveryIssue(issueID int, status, resolution, resolvedBy string) error { - query := ` - UPDATE delivery_issues - SET status = $1, resolution = $2, resolved_by = $3, updated_at = NOW() - WHERE id = $4 - ` - - result, err := d.Exec(query, status, resolution, resolvedBy, issueID) - if err != nil { - return fmt.Errorf("erreur mise à jour problème: %w", err) + result := d.GDB.Model(&models.DeliveryIssue{}).Where("id = ?", issueID). + Updates(map[string]any{"status": status, "resolution": resolution, "resolved_by": resolvedBy}) + if result.Error != nil { + return fmt.Errorf("erreur mise à jour problème: %w", result.Error) } - - rowsAffected, _ := result.RowsAffected() - if rowsAffected == 0 { + if result.RowsAffected == 0 { return fmt.Errorf("problème non trouvé") } - log.Printf("✅ Problème %d mis à jour: status=%s", issueID, status) return nil } diff --git a/backend/gestion/db/db_delivery_manage.go b/backend/gestion/db/db_delivery_manage.go index ff09a12b..57947a1e 100644 --- a/backend/gestion/db/db_delivery_manage.go +++ b/backend/gestion/db/db_delivery_manage.go @@ -6,7 +6,6 @@ package db import ( - "database/sql" "fmt" "log" "strings" @@ -22,52 +21,38 @@ var allowedStatuses = map[string]bool{ // CountDeliveriesByStatus compte les livraisons d'un livreur par statut func (d *Database) CountDeliveriesByStatus(livreurUsername string, statuses string) (int, error) { if statuses == "" { - // Cas simple: toutes les livraisons - query := `SELECT COUNT(*) - FROM commandes - WHERE livreur_assign = $1` - - var count int - err := d.QueryRow(query, livreurUsername).Scan(&count) + var result struct { + Count int `gorm:"column:count"` + } + err := d.GDB.Raw(`SELECT COUNT(*) as count FROM commandes WHERE livreur_assign = ?`, livreurUsername).Scan(&result).Error if err != nil { log.Printf("❌ [CountDeliveries] Erreur: %v", err) return 0, fmt.Errorf("erreur comptage livraisons: %w", err) } - return count, nil + return result.Count, nil } - // ✅ Valider les statuts cleanStatuses, err := ValidateStatuses(statuses) if err != nil { log.Printf("❌ [CountDeliveries] Validation échouée: %v", err) return 0, err } - // ✅ Construire la requête avec IN et placeholders - placeholders := make([]string, len(cleanStatuses)) - args := []interface{}{livreurUsername} - - for i, status := range cleanStatuses { - placeholders[i] = fmt.Sprintf("$%d", i+2) - args = append(args, status) + var result struct { + Count int `gorm:"column:count"` } - - query := fmt.Sprintf(`SELECT COUNT(*) - FROM commandes - WHERE livreur_assign = $1 - AND status IN (%s)`, strings.Join(placeholders, ",")) - - var count int - err = d.QueryRow(query, args...).Scan(&count) + err = d.GDB.Raw( + `SELECT COUNT(*) as count FROM commandes WHERE livreur_assign = ? AND status IN ?`, + livreurUsername, cleanStatuses, + ).Scan(&result).Error if err != nil { log.Printf("❌ [CountDeliveries] Erreur: %v", err) return 0, fmt.Errorf("erreur comptage livraisons: %w", err) } log.Printf("✅ [CountDeliveries] %d livraisons pour %s avec statuts %v", - count, livreurUsername, cleanStatuses) - - return count, nil + result.Count, livreurUsername, cleanStatuses) + return result.Count, nil } func ValidateStatuses(statuses string) ([]string, error) { @@ -77,7 +62,6 @@ func ValidateStatuses(statuses string) ([]string, error) { statusList := strings.Split(statuses, ",") - // Liste blanche complète validStatusMap := map[string]bool{ "pending": true, "assigned": true, @@ -93,12 +77,9 @@ func ValidateStatuses(statuses string) ([]string, error) { for _, status := range statusList { status = strings.TrimSpace(status) - - // Ignorer les chaînes vides if status == "" { continue } - if validStatusMap[status] { cleanStatuses = append(cleanStatuses, status) } else { @@ -106,7 +87,6 @@ func ValidateStatuses(statuses string) ([]string, error) { } } - // Logger les statuts invalides if len(invalidStatuses) > 0 { log.Printf("⚠️ [ValidateStatuses] Statuts invalides ignorés: %v", invalidStatuses) } @@ -120,32 +100,25 @@ func ValidateStatuses(statuses string) ([]string, error) { // GetLastDeliveryDate récupère la date de la dernière livraison d'un livreur func (d *Database) GetLastDeliveryDate(livreurUsername string) (*time.Time, error) { - query := `SELECT MAX(updated_at) - FROM commandes - WHERE livreur_assign = $1 - AND status = 'approved'` - - var lastDate sql.NullTime - err := d.QueryRow(query, livreurUsername).Scan(&lastDate) + var result struct { + LastDate *time.Time `gorm:"column:last_date"` + } + err := d.GDB.Raw(` + SELECT MAX(updated_at) as last_date + FROM commandes + WHERE livreur_assign = ? AND status = 'approved'`, livreurUsername).Scan(&result).Error if err != nil { return nil, fmt.Errorf("erreur récupération dernière livraison: %w", err) } - - if !lastDate.Valid { - return nil, nil // Aucune livraison - } - - t := lastDate.Time - return &t, nil + return result.LastDate, nil } // GetCurrentCommand récupère l'ID de la commande en cours d'un livreur func (d *Database) GetCurrentCommand(livreurUsername string) (int, error) { - // Récupérer depuis Redis currentKey := fmt.Sprintf("delivery:current:%s", livreurUsername) currentIDStr, err := Redis.Get(RedisCtx, currentKey).Result() if err != nil { - return 0, nil // Pas de commande en cours + return 0, nil } var currentID int @@ -157,93 +130,40 @@ func (d *Database) GetCurrentCommand(livreurUsername string) (int, error) { return currentID, nil } -// ============================================ -// 📜 HISTORIQUE DES LIVRAISONS -// ============================================ - // GetDeliveryPersonHistory récupère l'historique paginé des livraisons d'un livreur -func (d *Database) GetDeliveryPersonHistory(livreurUsername string, limit, offset int) ([]map[string]interface{}, error) { - query := ` +func (d *Database) GetDeliveryPersonHistory(livreurUsername string, limit, offset int) ([]map[string]any, error) { + var history []map[string]any + err := d.GDB.Raw(` SELECT c.id as command_id, c.username as client, c.status, c.adresse, - c.total_prix, + c.total_prix::float8 as total_prix, c.created_at as assigned_at, c.updated_at as completed_at FROM commandes c - WHERE c.livreur_assign = $1 + WHERE c.livreur_assign = ? ORDER BY c.created_at DESC - LIMIT $2 OFFSET $3 - ` - - rows, err := d.Query(query, livreurUsername, limit, offset) + LIMIT ? OFFSET ?`, livreurUsername, limit, offset).Scan(&history).Error if err != nil { return nil, fmt.Errorf("erreur récupération historique: %w", err) } - defer rows.Close() - - var history []map[string]interface{} - for rows.Next() { - var commandID int - var client, status, adresse string - var totalPrix float64 - var assignedAt, completedAt time.Time - - err := rows.Scan( - &commandID, - &client, - &status, - &adresse, - &totalPrix, - &assignedAt, - &completedAt, - ) - if err != nil { - return nil, fmt.Errorf("erreur scan historique: %w", err) - } - - // Calculer la durée de livraison si complétée - var deliveryTime float64 - if status == "approved" || status == "livre" { - deliveryTime = completedAt.Sub(assignedAt).Minutes() - } - - entry := map[string]interface{}{ - "command_id": commandID, - "client": client, - "status": status, - "adresse": adresse, - "total_prix": totalPrix, - "assigned_at": assignedAt.Format("2006-01-02 15:04:05"), - "completed_at": completedAt.Format("2006-01-02 15:04:05"), - "delivery_time": deliveryTime, - } - - history = append(history, entry) - } - return history, nil } // GetDeliveryPersonStatus récupère le statut d'un livreur func (d *Database) GetDeliveryPersonStatus(livreurUsername string) (string, error) { statusKey := fmt.Sprintf("delivery:status:%s", livreurUsername) - status, err := Redis.Get(RedisCtx, statusKey).Result() if err != nil { - // Statut par défaut si non trouvé return "offline", nil } - return status, nil } // UpdateDeliveryPersonStatus met à jour le statut d'un livreur - func (d *Database) UpdateDeliveryPersonStatus(livreurUsername string, status string) error { - // 🔹 Valider le statut if !allowedStatuses[status] { return fmt.Errorf("statut invalide: %s", status) } @@ -251,9 +171,7 @@ func (d *Database) UpdateDeliveryPersonStatus(livreurUsername string, status str log.Printf("🔄 [UpdateStatus] Mise à jour: %s → %s", livreurUsername, status) statusKey := fmt.Sprintf("delivery:status:%s", livreurUsername) - - err := Redis.Set(RedisCtx, statusKey, status, 0).Err() - if err != nil { + if err := Redis.Set(RedisCtx, statusKey, status, 0).Err(); err != nil { log.Printf("❌ [UpdateStatus] Erreur Redis: %v", err) return fmt.Errorf("erreur mise à jour statut: %w", err) } @@ -265,19 +183,16 @@ func (d *Database) UpdateDeliveryPersonStatus(livreurUsername string, status str // GetDeliverymanQueueSize récupère la taille de la queue d'un livreur func (d *Database) GetDeliverymanQueueSize(livreurUsername string) (int, error) { queueKey := fmt.Sprintf("delivery:queue:%s", livreurUsername) - size, err := Redis.LLen(RedisCtx, queueKey).Result() if err != nil { return 0, fmt.Errorf("erreur récupération taille queue: %w", err) } - return int(size), nil } // GetDeliverymanQueue récupère la queue complète d'un livreur func (d *Database) GetDeliverymanQueue(livreurUsername string) ([]int, error) { queueKey := fmt.Sprintf("delivery:queue:%s", livreurUsername) - commands, err := Redis.LRange(RedisCtx, queueKey, 0, -1).Result() if err != nil { return nil, fmt.Errorf("erreur récupération queue: %w", err) @@ -289,84 +204,66 @@ func (d *Database) GetDeliverymanQueue(livreurUsername string) ([]int, error) { fmt.Sscanf(cmdStr, "%d", &cmdID) queue = append(queue, cmdID) } - return queue, nil } // UpdateCommandLivreur met à jour le livreur assigné à une commande func (d *Database) UpdateCommandLivreur(commandID int, livreurUsername string) error { - query := `UPDATE commandes - SET livreur_assign = $1, updated_at = CURRENT_TIMESTAMP - WHERE id = $2` - - result, err := d.Exec(query, livreurUsername, commandID) - if err != nil { - return fmt.Errorf("erreur mise à jour livreur: %w", err) + result := d.GDB.Exec(` + UPDATE commandes + SET livreur_assign = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ?`, livreurUsername, commandID) + if result.Error != nil { + return fmt.Errorf("erreur mise à jour livreur: %w", result.Error) } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return fmt.Errorf("erreur vérification: %w", err) - } - if rowsAffected == 0 { + if result.RowsAffected == 0 { return fmt.Errorf("commande non trouvée") } - return nil } // GetAllDeliveryPersonsStats récupère les stats de tous les livreurs func (d *Database) GetAllDeliveryPersonsStats() ([]map[string]any, error) { - // Récupérer tous les livreurs livreurs, err := d.GetAvailableDeliveryPersons() if err != nil { return nil, fmt.Errorf("erreur récupération livreurs: %w", err) } var stats []map[string]any - for _, livreur := range livreurs { username := livreur["username"].(string) - // Compter les livraisons totalDeliveries, _ := d.CountDeliveriesByStatus(username, "") completedDeliveries, _ := d.CountDeliveriesByStatus(username, "approved") queueSize, _ := d.GetDeliverymanQueueSize(username) status, _ := d.GetDeliveryPersonStatus(username) - statEntry := map[string]any{ + stats = append(stats, map[string]any{ "username": username, "total_deliveries": totalDeliveries, "completed_deliveries": completedDeliveries, "queue_size": queueSize, "status": status, - } - - stats = append(stats, statEntry) + }) } - return stats, nil } // GetDeliveryPersonsByStatus récupère les livreurs par statut func (d *Database) GetDeliveryPersonsByStatus(status string) ([]string, error) { - // Récupérer tous les livreurs livreurs, err := d.GetAvailableDeliveryPersons() if err != nil { return nil, fmt.Errorf("erreur récupération livreurs: %w", err) } var filteredLivreurs []string - for _, livreur := range livreurs { username := livreur["username"].(string) currentStatus, _ := d.GetDeliveryPersonStatus(username) - if currentStatus == status { filteredLivreurs = append(filteredLivreurs, username) } } - return filteredLivreurs, nil } @@ -376,7 +273,6 @@ func (d *Database) GetAvailableDeliveryPersonsCount() (int, error) { if err != nil { return 0, err } - return len(availableLivreurs), nil } @@ -384,7 +280,6 @@ func (d *Database) GetAvailableDeliveryPersonsCount() (int, error) { func (d *Database) ClearDeliveryPersonData(livreurUsername string) error { log.Printf("🗑️ [ClearDeliveryData] Nettoyage données pour: %s", livreurUsername) - // Supprimer de Redis keys := []string{ fmt.Sprintf("delivery:status:%s", livreurUsername), fmt.Sprintf("delivery:location:%s", livreurUsername), @@ -394,8 +289,7 @@ func (d *Database) ClearDeliveryPersonData(livreurUsername string) error { } for _, key := range keys { - err := Redis.Del(RedisCtx, key).Err() - if err != nil { + if err := Redis.Del(RedisCtx, key).Err(); err != nil { log.Printf("⚠️ [ClearDeliveryData] Erreur suppression clé %s: %v", key, err) } } diff --git a/backend/gestion/db/db_delivery_mode.go b/backend/gestion/db/db_delivery_mode.go index d880f369..87c23763 100644 --- a/backend/gestion/db/db_delivery_mode.go +++ b/backend/gestion/db/db_delivery_mode.go @@ -5,34 +5,21 @@ import ( "log" ) -// GetCommandCategories retourne les catégories distinctes des produits d'une commande func (d *Database) GetCommandCategories(commandID int) ([]string, error) { - query := ` + var categories []string + err := d.GDB.Raw(` SELECT DISTINCT p.category FROM command_items ci JOIN products p ON p.id = ci.product_id - WHERE ci.command_id = $1 AND p.category IS NOT NULL AND p.category != ''` - - rows, err := d.Query(query, commandID) + WHERE ci.command_id = ? AND p.category IS NOT NULL AND p.category != ''`, + commandID, + ).Scan(&categories).Error if err != nil { return nil, fmt.Errorf("erreur lecture catégories commande %d: %w", commandID, err) } - defer rows.Close() - - var categories []string - for rows.Next() { - var cat string - if err := rows.Scan(&cat); err == nil { - categories = append(categories, cat) - } - } - return categories, rows.Err() + return categories, nil } -// GetEligibleDeliverymenForCommand retourne les usernames des livreurs éligibles pour une commande. -// En mode "single" → tous les livreurs actifs. -// En mode "category_based" → le(s) livreur(s) assigné(s) aux catégories de la commande. -// Si aucune correspondance → fallback sur tous les livreurs actifs. func (d *Database) GetEligibleDeliverymenForCommand(commandID int) ([]string, error) { settings, err := d.GetSettings() if err != nil { @@ -55,20 +42,17 @@ func (d *Database) GetEligibleDeliverymenForCommand(commandID int) ([]string, er return allActive() } - // Mode category_based : récupérer les catégories de la commande categories, err := d.GetCommandCategories(commandID) if err != nil || len(categories) == 0 { log.Printf("⚠️ [DELIVERY_MODE] Cmd %d: catégories non trouvées — fallback single", commandID) return allActive() } - // Construire un set des catégories de la commande catSet := make(map[string]bool, len(categories)) for _, c := range categories { catSet[c] = true } - // Trouver les livreurs dont la route intersecte les catégories eligible := make(map[string]bool) for _, route := range settings.DeliveryMode.CategoryRoutes { for _, routeCat := range route.Categories { diff --git a/backend/gestion/db/db_history.go b/backend/gestion/db/db_history.go index 6c77dfcd..70d8c437 100644 --- a/backend/gestion/db/db_history.go +++ b/backend/gestion/db/db_history.go @@ -6,11 +6,10 @@ package db import ( - "database/sql" "fmt" "log" "maps" - "time" + "strconv" ) // GetCompletedCommandsByUsername récupère toutes les commandes terminées (approved) d'un utilisateur @@ -18,73 +17,24 @@ func (d *Database) GetCompletedCommandsByUsername(username string) ([]map[string query := ` SELECT id, + client_order_id AS client_order_number, username, status, adresse, - total_prix, + total_prix::float8 as total_prix, livreur_assign, created_at, updated_at FROM commandes - WHERE username = $1 AND status = 'approved' + WHERE username = ? AND status = 'approved' ORDER BY created_at DESC ` - rows, err := d.Query(query, username) - if err != nil { + var commands []map[string]any + if err := d.GDB.Raw(query, username).Scan(&commands).Error; err != nil { log.Printf("❌ [GetCompletedCommands] Erreur query: %v", err) return nil, fmt.Errorf("erreur lors de la récupération des commandes terminées: %w", err) } - defer rows.Close() - - var commands []map[string]any - - for rows.Next() { - var id int - var username, status, adresse string - var livreurAssign sql.NullString - var totalPrix float64 - var createdAt, updatedAt time.Time - - err := rows.Scan( - &id, - &username, - &status, - &adresse, - &totalPrix, - &livreurAssign, - &createdAt, - &updatedAt, - ) - if err != nil { - log.Printf("❌ [GetCompletedCommands] Erreur scan: %v", err) - return nil, fmt.Errorf("erreur lors du scan de la commande: %w", err) - } - - command := map[string]any{ - "id": id, - "username": username, - "status": status, - "adresse": adresse, - "total_prix": totalPrix, - "created_at": createdAt, - "updated_at": updatedAt, - } - - if livreurAssign.Valid { - command["livreur_assign"] = livreurAssign.String - } else { - command["livreur_assign"] = nil - } - - commands = append(commands, command) - } - - if err = rows.Err(); err != nil { - log.Printf("❌ [GetCompletedCommands] Erreur itération: %v", err) - return nil, fmt.Errorf("erreur lors de l'itération des résultats: %w", err) - } - return commands, nil } @@ -98,12 +48,11 @@ func (d *Database) GetCompletedCommandsWithItems(username string) ([]map[string] var enrichedCommands []map[string]any for _, command := range commands { - commandID, ok := command["id"].(int) - if !ok { + commandID, _ := strconv.Atoi(fmt.Sprintf("%v", command["id"])) + if commandID == 0 { continue } - // Récupérer les items items, err := d.GetCommandItems(commandID) if err != nil { log.Printf("⚠️ [GetCompletedWithItems] Erreur items pour cmd %d: %v", commandID, err) @@ -134,39 +83,16 @@ func (d *Database) GetCommandsStatsByUsername(username string) (map[string]any, COUNT(*) as total_count, COALESCE(SUM(total_prix) FILTER (WHERE status = 'approved'), 0) as total_spent FROM commandes - WHERE username = $1 + WHERE username = ? ` - var approvedCount, pendingCount, assignedCount, enRouteCount, livreCount, cancelledCount, totalCount int - var totalSpent float64 - - err := d.QueryRow(query, username).Scan( - &approvedCount, - &pendingCount, - &assignedCount, - &enRouteCount, - &livreCount, - &cancelledCount, - &totalCount, - &totalSpent, - ) - if err != nil { + var result map[string]any + if err := d.GDB.Raw(query, username).Scan(&result).Error; err != nil { log.Printf("❌ [GetCommandsStats] Erreur: %v", err) return nil, fmt.Errorf("erreur lors de la récupération des statistiques: %w", err) } - stats := map[string]any{ - "approved_count": approvedCount, - "pending_count": pendingCount, - "assigned_count": assignedCount, - "en_route_count": enRouteCount, - "livre_count": livreCount, - "cancelled_count": cancelledCount, - "total_count": totalCount, - "total_spent": totalSpent, - } - - return stats, nil + return result, nil } // GetCommandsByStatus récupère les commandes d'un utilisateur par statut @@ -176,145 +102,49 @@ func (d *Database) GetCommandsByStatus(username, status string) ([]map[string]an query := ` SELECT id, + client_order_id AS client_order_number, username, status, adresse, - total_prix, + total_prix::float8 as total_prix, livreur_assign, created_at, updated_at FROM commandes - WHERE username = $1 AND status = $2 + WHERE username = ? AND status = ? ORDER BY created_at DESC ` - rows, err := d.Query(query, username, status) - if err != nil { - return nil, fmt.Errorf("erreur lors de la récupération des commandes: %w", err) - } - defer rows.Close() - var commands []map[string]any - - for rows.Next() { - var id int - var username, status, adresse string - var livreurAssign sql.NullString - var totalPrix float64 - var createdAt, updatedAt time.Time - - err := rows.Scan( - &id, - &username, - &status, - &adresse, - &totalPrix, - &livreurAssign, - &createdAt, - &updatedAt, - ) - if err != nil { - log.Printf("❌ [GetCommandsByStatus] Erreur scan: %v", err) - return nil, fmt.Errorf("erreur lors du scan: %w", err) - } - - command := map[string]any{ - "id": id, - "username": username, - "status": status, - "adresse": adresse, - "total_prix": totalPrix, - "created_at": createdAt, - "updated_at": updatedAt, - } - - if livreurAssign.Valid { - command["livreur_assign"] = livreurAssign.String - } else { - command["livreur_assign"] = nil - } - - commands = append(commands, command) - } - - if err = rows.Err(); err != nil { - return nil, fmt.Errorf("erreur lors de l'itération: %w", err) + if err := d.GDB.Raw(query, username, status).Scan(&commands).Error; err != nil { + return nil, fmt.Errorf("erreur lors de la récupération des commandes: %w", err) } return commands, nil } // GetRecentCompletedOrders récupère les N dernières commandes terminées d'un utilisateur func (d *Database) GetRecentCompletedOrders(username string, limit int) ([]map[string]any, error) { - query := ` SELECT id, + client_order_id AS client_order_number, username, status, adresse, - total_prix, + total_prix::float8 as total_prix, livreur_assign, created_at, updated_at FROM commandes - WHERE username = $1 AND status = 'approved' + WHERE username = ? AND status = 'approved' ORDER BY created_at DESC - LIMIT $2 + LIMIT ? ` - rows, err := d.Query(query, username, limit) - if err != nil { + var commands []map[string]any + if err := d.GDB.Raw(query, username, limit).Scan(&commands).Error; err != nil { log.Printf("❌ [GetRecentCompleted] Erreur query: %v", err) return nil, fmt.Errorf("erreur lors de la récupération: %w", err) } - defer rows.Close() - - var commands []map[string]any - - for rows.Next() { - var id int - var username, status, adresse string - var livreurAssign sql.NullString - var totalPrix float64 - var createdAt, updatedAt time.Time - - err := rows.Scan( - &id, - &username, - &status, - &adresse, - &totalPrix, - &livreurAssign, - &createdAt, - &updatedAt, - ) - if err != nil { - log.Printf("❌ [GetRecentCompleted] Erreur scan: %v", err) - return nil, fmt.Errorf("erreur lors du scan: %w", err) - } - - command := map[string]any{ - "id": id, - "username": username, - "status": status, - "adresse": adresse, - "total_prix": totalPrix, - "created_at": createdAt, - "updated_at": updatedAt, - } - - if livreurAssign.Valid { - command["livreur_assign"] = livreurAssign.String - } else { - command["livreur_assign"] = nil - } - - commands = append(commands, command) - } - - if err = rows.Err(); err != nil { - return nil, fmt.Errorf("erreur lors de l'itération: %w", err) - } - return commands, nil } diff --git a/backend/gestion/db/db_init.go b/backend/gestion/db/db_init.go index 7b021b5e..9468c5c6 100644 --- a/backend/gestion/db/db_init.go +++ b/backend/gestion/db/db_init.go @@ -8,6 +8,9 @@ import ( "time" _ "github.com/lib/pq" + "gorm.io/driver/postgres" + "gorm.io/gorm" + "gorm.io/gorm/logger" ) // ============================================ @@ -17,6 +20,7 @@ import ( // Database encapsule la connexion à la base de données type Database struct { *sql.DB + GDB *gorm.DB } // DB est l'instance globale de la base de données @@ -58,8 +62,18 @@ func InitDB() *Database { log.Println("✅ Connexion à PostgreSQL établie avec succès") + // Initialiser GORM en réutilisant la connexion sql.DB existante + gormDB, err := gorm.Open(postgres.New(postgres.Config{ + Conn: db, + }), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Silent), + }) + if err != nil { + log.Fatalf("❌ Erreur initialisation GORM: %v", err) + } + // Créer l'instance Database - database := &Database{db} + database := &Database{db, gormDB} // Assigner à la variable globale DB = database @@ -76,16 +90,6 @@ func InitDB() *Database { log.Fatalf("❌ Erreur migration must_change_password: %v", err) } - // Migration: ajouter colonne push_token pour les notifications push (clients) - if _, err = database.Exec(`ALTER TABLE clients ADD COLUMN IF NOT EXISTS push_token TEXT`); err != nil { - log.Fatalf("❌ Erreur migration push_token: %v", err) - } - - // Migration: ajouter colonne push_token pour les notifications push (livreurs/users) - if _, err = database.Exec(`ALTER TABLE users ADD COLUMN IF NOT EXISTS push_token TEXT`); err != nil { - log.Fatalf("❌ Erreur migration push_token users: %v", err) - } - // Migration: proposition de modification d'adresse par admin/cabine if _, err = database.Exec(`ALTER TABLE commandes ADD COLUMN IF NOT EXISTS proposed_address TEXT`); err != nil { log.Fatalf("❌ Erreur migration proposed_address: %v", err) @@ -166,6 +170,23 @@ func InitDB() *Database { log.Fatalf("❌ Erreur migration commandes.payment_method: %v", err) } + // Migration: identifiant de commande par client (numérotation indépendante par client, commence à 1) + if _, err = database.Exec(`ALTER TABLE commandes ADD COLUMN IF NOT EXISTS client_order_id INTEGER NOT NULL DEFAULT 0`); err != nil { + log.Fatalf("❌ Erreur migration commandes.client_order_id: %v", err) + } + // Backfill: numéroter les commandes existantes par client dans l'ordre d'insertion + if _, err = database.Exec(` + UPDATE commandes c + SET client_order_id = sub.rn + FROM ( + SELECT id, ROW_NUMBER() OVER (PARTITION BY username ORDER BY id) AS rn + FROM commandes + ) sub + WHERE c.id = sub.id AND c.client_order_id = 0 + `); err != nil { + log.Fatalf("❌ Erreur backfill commandes.client_order_id: %v", err) + } + // Migration: table de suivi des paiements crypto if _, err = database.Exec(` CREATE TABLE IF NOT EXISTS crypto_payments ( diff --git a/backend/gestion/db/db_jwt_tokens.go b/backend/gestion/db/db_jwt_tokens.go index 640de53a..30bcc746 100644 --- a/backend/gestion/db/db_jwt_tokens.go +++ b/backend/gestion/db/db_jwt_tokens.go @@ -1,167 +1,127 @@ package db import ( - "database/sql" "fmt" "log" "time" + + "gorm.io/gorm" ) -func (d *Database) SaveToken(userID int, userType string, token string, expiresAt time.Time) error { - // Valider le user_type - validTypes := map[string]bool{ - "client": true, - "admin": true, - "cabine": true, - "livreur": true, - } +type jwtToken struct { + ID int `gorm:"primaryKey;autoIncrement"` + UserID int `gorm:"column:user_id;index"` + UserType string `gorm:"column:user_type"` + Token string `gorm:"column:token;uniqueIndex"` + DateSave time.Time `gorm:"column:date_save;autoCreateTime"` + DateFin time.Time `gorm:"column:date_fin"` +} - if !validTypes[userType] { +func (jwtToken) TableName() string { return "jwt_tokens" } + +var validTokenTypes = map[string]bool{ + "client": true, "admin": true, "cabine": true, "livreur": true, +} + +func (d *Database) SaveToken(userID int, userType string, token string, expiresAt time.Time) error { + if !validTokenTypes[userType] { return fmt.Errorf("type d'utilisateur invalide: %s", userType) } - - query := `INSERT INTO jwt_tokens (user_id, user_type, token, date_save, date_fin) - VALUES ($1, $2, $3, CURRENT_TIMESTAMP, $4)` - - _, err := d.Exec(query, userID, userType, token, expiresAt) - if err != nil { + t := jwtToken{UserID: userID, UserType: userType, Token: token, DateFin: expiresAt} + if err := d.GDB.Create(&t).Error; err != nil { return fmt.Errorf("erreur lors de l'enregistrement du token: %w", err) } - log.Printf("✅ Token enregistré pour %s ID: %d", userType, userID) return nil } -// IsTokenValid vérifie si un token existe et n'est pas expiré func (d *Database) IsTokenValid(token string) (bool, error) { - query := `SELECT COUNT(*) FROM jwt_tokens - WHERE token = $1 AND date_fin > $2` - - var count int - err := d.QueryRow(query, token, time.Now()).Scan(&count) - if err != nil { - return false, fmt.Errorf("erreur lors de la vérification du token: %w", err) - } - - return count > 0, nil + var count int64 + err := d.GDB.Model(&jwtToken{}). + Where("token = ? AND date_fin > ?", token, time.Now()). + Count(&count).Error + return count > 0, err } -// RevokeToken révoque un token (le supprime de la base) func (d *Database) RevokeToken(token string) error { - query := `DELETE FROM jwt_tokens WHERE token = $1` - - result, err := d.Exec(query, token) - if err != nil { - return fmt.Errorf("erreur lors de la révocation du token: %w", err) + result := d.GDB.Where("token = ?", token).Delete(&jwtToken{}) + if result.Error != nil { + return fmt.Errorf("erreur lors de la révocation du token: %w", result.Error) } - - rowsAffected, _ := result.RowsAffected() - if rowsAffected > 0 { + if result.RowsAffected > 0 { log.Printf("✅ Token révoqué avec succès") } - return nil } -// RevokeAllUserTokens révoque tous les tokens d'un utilisateur func (d *Database) RevokeAllUserTokens(userID int, userType string) error { - query := `DELETE FROM jwt_tokens WHERE user_id = $1 AND user_type = $2` - - result, err := d.Exec(query, userID, userType) - if err != nil { - return fmt.Errorf("erreur lors de la révocation des tokens: %w", err) + result := d.GDB.Where("user_id = ? AND user_type = ?", userID, userType).Delete(&jwtToken{}) + if result.Error != nil { + return fmt.Errorf("erreur lors de la révocation des tokens: %w", result.Error) } - - rowsAffected, _ := result.RowsAffected() - log.Printf("✅ %d token(s) révoqué(s) pour %s ID: %d", rowsAffected, userType, userID) - + log.Printf("✅ %d token(s) révoqué(s) pour %s ID: %d", result.RowsAffected, userType, userID) return nil } -// GetUserActiveTokens récupère tous les tokens actifs d'un utilisateur func (d *Database) GetUserActiveTokens(userID int, userType string) ([]map[string]any, error) { - query := `SELECT id, token, date_save, date_fin - FROM jwt_tokens - WHERE user_id = $1 AND user_type = $2 AND date_fin > $3 - ORDER BY date_save DESC` - - rows, err := d.Query(query, userID, userType, time.Now()) + var tokens []jwtToken + err := d.GDB.Where("user_id = ? AND user_type = ? AND date_fin > ?", userID, userType, time.Now()). + Order("date_save DESC").Find(&tokens).Error if err != nil { return nil, fmt.Errorf("erreur lors de la récupération des tokens: %w", err) } - defer rows.Close() - var tokens []map[string]any - for rows.Next() { - var id int - var token string - var dateSave, dateFin time.Time - - err := rows.Scan(&id, &token, &dateSave, &dateFin) - if err != nil { - return nil, fmt.Errorf("erreur lors du scan: %w", err) + result := make([]map[string]any, len(tokens)) + for i, t := range tokens { + truncated := t.Token + if len(truncated) > 20 { + truncated = truncated[:20] + "..." + } + result[i] = map[string]any{ + "id": t.ID, + "token": truncated, + "date_save": t.DateSave, + "date_fin": t.DateFin, + "user_type": t.UserType, } - - tokens = append(tokens, map[string]any{ - "id": id, - "token": token[:20] + "...", // Tronquer pour la sécurité - "date_save": dateSave, - "date_fin": dateFin, - "user_type": userType, - }) } - - return tokens, nil + return result, nil } func (d *Database) GetTokenInfo(token string) (map[string]any, error) { - query := `SELECT user_id, user_type, date_save, date_fin - FROM jwt_tokens - WHERE token = $1 AND date_fin > $2` - - var userID int - var userType string - var dateSave, dateFin time.Time - - err := d.QueryRow(query, token, time.Now()).Scan(&userID, &userType, &dateSave, &dateFin) - if err == sql.ErrNoRows { - return nil, fmt.Errorf("token non trouvé ou expiré") - } + var t jwtToken + err := d.GDB.Where("token = ? AND date_fin > ?", token, time.Now()).First(&t).Error if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("token non trouvé ou expiré") + } return nil, fmt.Errorf("erreur lors de la récupération des infos du token: %w", err) } - - tokenInfo := map[string]any{ - "user_id": userID, - "user_type": userType, - "date_save": dateSave, - "date_fin": dateFin, - } - - return tokenInfo, nil + return map[string]any{ + "user_id": t.UserID, + "user_type": t.UserType, + "date_save": t.DateSave, + "date_fin": t.DateFin, + }, nil } func (d *Database) CountActiveTokensByType() (map[string]int, error) { - query := `SELECT user_type, COUNT(*) as count - FROM jwt_tokens - WHERE date_fin > $1 - GROUP BY user_type` - - rows, err := d.Query(query, time.Now()) + type row struct { + UserType string + Count int + } + var rows []row + err := d.GDB.Model(&jwtToken{}). + Select("user_type, COUNT(*) as count"). + Where("date_fin > ?", time.Now()). + Group("user_type"). + Scan(&rows).Error if err != nil { return nil, fmt.Errorf("erreur lors du comptage des tokens: %w", err) } - defer rows.Close() - - counts := make(map[string]int) - for rows.Next() { - var userType string - var count int - if err := rows.Scan(&userType, &count); err != nil { - return nil, err - } - counts[userType] = count + counts := make(map[string]int, len(rows)) + for _, r := range rows { + counts[r.UserType] = r.Count } - return counts, nil } diff --git a/backend/gestion/db/db_media.go b/backend/gestion/db/db_media.go index 54140cf0..60548f18 100644 --- a/backend/gestion/db/db_media.go +++ b/backend/gestion/db/db_media.go @@ -1,7 +1,6 @@ package db import ( - "database/sql" "fmt" "gestion/models" "log" @@ -42,7 +41,6 @@ func validateProductID(productID int) error { // validateMediaType vérifie le type de média func validateMediaType(mediaType string) error { validTypes := []string{"image", "video"} - mediaType = strings.ToLower(strings.TrimSpace(mediaType)) if slices.Contains(validTypes, mediaType) { return nil @@ -55,46 +53,29 @@ func validateMediaURL(url string) error { if len(url) == 0 { return fmt.Errorf("URL vide") } - if len(url) > 500 { return fmt.Errorf("URL trop longue (max 500 caractères)") } - - // ✅ PROTECTION PATH TRAVERSAL if strings.Contains(url, "..") || strings.Contains(url, "...") || strings.Contains(url, "..//") { return fmt.Errorf("path traversal détecté dans l'URL") } - - // ✅ VÉRIFIER QUE L'URL COMMENCE PAR /uploads/ if !strings.HasPrefix(url, "/uploads/") { return fmt.Errorf("URL doit commencer par /uploads/") } - - // ✅ VÉRIFIER QU'IL N'Y A PAS DE CARACTÈRES DANGEREUX - dangerousChars := []string{ - "<", ">", "\"", "'", ";", "|", "&", "$", "`", "\\", - } - + dangerousChars := []string{"<", ">", "\"", "'", ";", "|", "&", "$", "`", "\\"} for _, char := range dangerousChars { if strings.Contains(url, char) { return fmt.Errorf("caractères interdits dans l'URL") } } - return nil } -// ============================================ -// CREATE MEDIA - VERSION SÉCURISÉE -// ============================================ - -func (db *Database) CreateMedia(media any) error { +func (d *Database) CreateMedia(media any) error { log.Printf("🔒 [CreateMedia] START - Type: %T", media) - // ✅ TYPE ASSERTION SÉCURISÉE m, ok := media.(MediaInterface) if !ok { - // Vérifier si c'est un pointeur vers models.Media if mediaPtr, isPtr := media.(*models.Media); isPtr { m = mediaPtr ok = true @@ -116,29 +97,25 @@ func (db *Database) CreateMedia(media any) error { log.Printf("❌ [CreateMedia] %v", err) return err } - if err := validateMediaType(mediaType); err != nil { log.Printf("❌ [CreateMedia] %v", err) return err } - if err := validateMediaURL(mediaURL); err != nil { log.Printf("❌ [CreateMedia] %v", err) return err } - if err := db.InsertMedia(m, productID, mediaURL, mediaType); err != nil { + if err := d.InsertMedia(m, productID, mediaURL, mediaType); err != nil { log.Printf("❌ [InsertMedia] %v", err) return err } return nil } -func (db *Database) InsertMedia(m any, productID int, mediaURL any, mediaType string) error { +func (d *Database) InsertMedia(m any, productID int, mediaURL any, mediaType string) error { var exists bool - checkQuery := `SELECT EXISTS(SELECT 1 FROM products WHERE id = $1)` - err := db.QueryRow(checkQuery, productID).Scan(&exists) - if err != nil { + if err := d.GDB.Raw(`SELECT EXISTS(SELECT 1 FROM products WHERE id = ?)`, productID).Scan(&exists).Error; err != nil { log.Printf("❌ [CreateMedia] Erreur vérification produit: %v", err) return fmt.Errorf("erreur vérification produit: %w", err) } @@ -147,178 +124,111 @@ func (db *Database) InsertMedia(m any, productID int, mediaURL any, mediaType st return fmt.Errorf("produit %d n'existe pas", productID) } - query := `INSERT INTO media (product_id, url, type, created_at) - VALUES ($1, $2, $3, $4) RETURNING id` - - var mediaID int - now := time.Now() - - err = db.QueryRow(query, productID, mediaURL, mediaType, now).Scan(&mediaID) + var result struct { + ID int `gorm:"column:id"` + } + err := d.GDB.Raw(` + INSERT INTO media (product_id, url, type, created_at) + VALUES (?, ?, ?, ?) RETURNING id`, + productID, mediaURL, mediaType, time.Now(), + ).Scan(&result).Error if err != nil { log.Printf("❌ [CreateMedia] Erreur INSERT: %v", err) return fmt.Errorf("erreur création média: %w", err) } + if mi, ok := m.(MediaInterface); ok { - mi.SetID(mediaID) + mi.SetID(result.ID) } - log.Printf("✅ [CreateMedia] Média créé: ID=%d, Type=%s", mediaID, mediaType) + log.Printf("✅ [CreateMedia] Média créé: ID=%d, Type=%s", result.ID, mediaType) return nil } -// ============================================ -// GET MEDIA BY ID - VERSION SÉCURISÉE -// ============================================ - -func (db *Database) GetMediaByID(mediaID int) (*models.Media, error) { +func (d *Database) GetMediaByID(mediaID int) (*models.Media, error) { log.Printf("🔍 [GetMediaByID] START - ID=%d", mediaID) - // ✅ VALIDATION if err := validateMediaID(mediaID); err != nil { log.Printf("❌ [GetMediaByID] %v", err) return nil, err } var media models.Media - - query := `SELECT id, product_id, url, type, created_at - FROM media - WHERE id = $1` - - err := db.QueryRow(query, mediaID).Scan( - &media.ID, - &media.ProductID, - &media.URL, - &media.Type, - &media.CreatedAt, - ) - - if err == sql.ErrNoRows { - log.Printf("❌ [GetMediaByID] Média %d non trouvé", mediaID) - return nil, fmt.Errorf("média non trouvé") - } - + err := d.GDB.Raw(` + SELECT id, product_id, url, type, created_at + FROM media WHERE id = ?`, mediaID).Scan(&media).Error if err != nil { log.Printf("❌ [GetMediaByID] Erreur query: %v", err) return nil, fmt.Errorf("erreur récupération média: %w", err) } + if media.ID == 0 { + log.Printf("❌ [GetMediaByID] Média %d non trouvé", mediaID) + return nil, fmt.Errorf("média non trouvé") + } log.Printf("✅ [GetMediaByID] Média trouvé: Type=%s", media.Type) - return &media, nil } -// ============================================ -// GET MEDIA BY PRODUCT ID - VERSION SÉCURISÉE -// ============================================ - -func (db *Database) GetMediaByProductID(productID int) ([]models.Media, error) { +func (d *Database) GetMediaByProductID(productID int) ([]models.Media, error) { log.Printf("🖼️ [GetMediaByProductID] START - ProductID=%d", productID) - // ✅ VALIDATION if err := validateProductID(productID); err != nil { log.Printf("❌ [GetMediaByProductID] %v", err) return nil, err } - query := `SELECT id, product_id, url, type, created_at - FROM media - WHERE product_id = $1 - ORDER BY id ASC` - - rows, err := db.Query(query, productID) + var mediaList []models.Media + err := d.GDB.Raw(` + SELECT id, product_id, url, type, created_at + FROM media WHERE product_id = ? + ORDER BY id ASC`, productID).Scan(&mediaList).Error if err != nil { log.Printf("❌ [GetMediaByProductID] Erreur query: %v", err) return nil, fmt.Errorf("erreur récupération médias: %w", err) } - defer rows.Close() - - var mediaList []models.Media - - for rows.Next() { - var media models.Media - - if err := rows.Scan( - &media.ID, - &media.ProductID, - &media.URL, - &media.Type, - &media.CreatedAt, - ); err != nil { - log.Printf("❌ [GetMediaByProductID] Erreur scan: %v", err) - return nil, fmt.Errorf("erreur scan média: %w", err) - } - - log.Printf(" 🖼️ Media: ID=%d, Type=%s", media.ID, media.Type) - mediaList = append(mediaList, media) - } - - if err = rows.Err(); err != nil { - log.Printf("❌ [GetMediaByProductID] Erreur iteration: %v", err) - return nil, fmt.Errorf("erreur itération médias: %w", err) - } log.Printf("✅ [GetMediaByProductID] %d médias trouvés", len(mediaList)) - return mediaList, nil } -// ============================================ -// UPDATE MEDIA - VERSION SÉCURISÉE -// ============================================ - -func (db *Database) UpdateMedia(media *models.Media) error { +func (d *Database) UpdateMedia(media *models.Media) error { if media == nil { return fmt.Errorf("média nil") } - if err := validateMediaID(media.ID); err != nil { log.Printf("❌ [UpdateMedia] %v", err) return err } - if err := validateMediaType(media.Type); err != nil { log.Printf("❌ [UpdateMedia] %v", err) return err } - if err := validateMediaURL(media.URL); err != nil { log.Printf("❌ [UpdateMedia] %v", err) return err } - - // ✅ VÉRIFIER QUE LE MÉDIA EXISTE - if err := db.CheckMediaExists(media); err != nil { + if err := d.CheckMediaExists(media); err != nil { log.Printf("❌ [UpdateMedia] %v", err) return err } - // ✅ UPDATE - query := `UPDATE media - SET url = $1, type = $2 - WHERE id = $3` - - result, err := db.Exec(query, media.URL, media.Type, media.ID) - if err != nil { - log.Printf("❌ [UpdateMedia] Erreur UPDATE: %v", err) - return fmt.Errorf("erreur mise à jour média: %w", err) + result := d.GDB.Exec(`UPDATE media SET url = ?, type = ? WHERE id = ?`, media.URL, media.Type, media.ID) + if result.Error != nil { + log.Printf("❌ [UpdateMedia] Erreur UPDATE: %v", result.Error) + return fmt.Errorf("erreur mise à jour média: %w", result.Error) } - - rowsAffected, _ := result.RowsAffected() - if rowsAffected == 0 { + if result.RowsAffected == 0 { log.Printf("❌ [UpdateMedia] Aucune ligne affectée") return fmt.Errorf("média non trouvé") } log.Printf("✅ [UpdateMedia] Média %d mis à jour", media.ID) - return nil } -func (db *Database) CheckMediaExists(media *models.Media) error { +func (d *Database) CheckMediaExists(media *models.Media) error { var exists bool - checkQuery := `SELECT EXISTS(SELECT 1 FROM media WHERE id = $1)` - err := db.QueryRow(checkQuery, media.ID).Scan(&exists) - if err != nil { + if err := d.GDB.Raw(`SELECT EXISTS(SELECT 1 FROM media WHERE id = ?)`, media.ID).Scan(&exists).Error; err != nil { log.Printf("❌ [UpdateMedia] Erreur vérification: %v", err) return fmt.Errorf("erreur vérification média: %w", err) } @@ -329,38 +239,33 @@ func (db *Database) CheckMediaExists(media *models.Media) error { return nil } -func (db *Database) DeleteMedia(mediaID int) error { +func (d *Database) DeleteMedia(mediaID int) error { if err := validateMediaID(mediaID); err != nil { log.Printf("❌ [DeleteMedia] %v", err) return err } media := models.Media{ID: mediaID} - if err := db.CheckMediaExists(&media); err != nil { + if err := d.CheckMediaExists(&media); err != nil { log.Printf("❌ [DeleteMedia] %v", err) return err } - query := `DELETE FROM media WHERE id = $1` - - result, err := db.Exec(query, mediaID) - if err != nil { - log.Printf("❌ [DeleteMedia] Erreur DELETE: %v", err) - return fmt.Errorf("erreur suppression média: %w", err) + result := d.GDB.Exec(`DELETE FROM media WHERE id = ?`, mediaID) + if result.Error != nil { + log.Printf("❌ [DeleteMedia] Erreur DELETE: %v", result.Error) + return fmt.Errorf("erreur suppression média: %w", result.Error) } - - rowsAffected, _ := result.RowsAffected() - if rowsAffected == 0 { + if result.RowsAffected == 0 { log.Printf("❌ [DeleteMedia] Aucune ligne affectée") return fmt.Errorf("média non trouvé") } log.Printf("✅ [DeleteMedia] Média %d supprimé", mediaID) - return nil } -func (db *Database) DeleteMediaByProductID(productID int) error { +func (d *Database) DeleteMediaByProductID(productID int) error { log.Printf("🗑️ [DeleteMediaByProductID] START - ProductID=%d", productID) if err := validateProductID(productID); err != nil { @@ -369,9 +274,7 @@ func (db *Database) DeleteMediaByProductID(productID int) error { } var exists bool - checkQuery := `SELECT EXISTS(SELECT 1 FROM products WHERE id = $1)` - err := db.QueryRow(checkQuery, productID).Scan(&exists) - if err != nil { + if err := d.GDB.Raw(`SELECT EXISTS(SELECT 1 FROM products WHERE id = ?)`, productID).Scan(&exists).Error; err != nil { log.Printf("❌ [DeleteMediaByProductID] Erreur vérification: %v", err) return fmt.Errorf("erreur vérification produit: %w", err) } @@ -380,17 +283,13 @@ func (db *Database) DeleteMediaByProductID(productID int) error { return fmt.Errorf("produit %d non trouvé", productID) } - query := `DELETE FROM media WHERE product_id = $1` - - result, err := db.Exec(query, productID) - if err != nil { - log.Printf("❌ [DeleteMediaByProductID] Erreur DELETE: %v", err) - return fmt.Errorf("erreur suppression médias: %w", err) + result := d.GDB.Exec(`DELETE FROM media WHERE product_id = ?`, productID) + if result.Error != nil { + log.Printf("❌ [DeleteMediaByProductID] Erreur DELETE: %v", result.Error) + return fmt.Errorf("erreur suppression médias: %w", result.Error) } - rowsAffected, _ := result.RowsAffected() log.Printf("✅ [DeleteMediaByProductID] %d média(s) supprimé(s) pour produit %d", - rowsAffected, productID) - + result.RowsAffected, productID) return nil } diff --git a/backend/gestion/db/db_notifications.go b/backend/gestion/db/db_notifications.go index 31d77058..1b282b93 100644 --- a/backend/gestion/db/db_notifications.go +++ b/backend/gestion/db/db_notifications.go @@ -9,7 +9,6 @@ import ( ) func (d *Database) NotifyClient(username string, commandID int, notifType, message string) error { - // Sauvegarder la notification dans Redis notifKey := fmt.Sprintf("notifications:%s", username) notification := map[string]any{ @@ -24,7 +23,6 @@ func (d *Database) NotifyClient(username string, commandID int, notifType, messa Redis.LPush(RedisCtx, notifKey, notifJSON) Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour) - // Diffusion Telegram si le client a lié son compte if services.TelegramBot != nil && services.TelegramBot.IsConfigured() { if chatID, ok, err := d.GetClientTelegramChatID(username); err == nil && ok { go services.TelegramBot.SendMessage(chatID, fmt.Sprintf("🔔 Notification\n\n%s", message)) @@ -51,7 +49,6 @@ func (d *Database) NotifyLivreur(username string, commandID int, notifType, mess Redis.LPush(RedisCtx, notifKey, notifJSON) Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour) - // Diffusion Telegram si le livreur a lié son compte if services.TelegramBot != nil && services.TelegramBot.IsConfigured() { if chatID, ok, err := d.GetUserTelegramChatID(username); err == nil && ok { go services.TelegramBot.SendMessage(chatID, fmt.Sprintf("🔔 Notification\n\n%s", message)) @@ -63,16 +60,14 @@ func (d *Database) NotifyLivreur(username string, commandID int, notifType, mess } // NotifyAllAdminCabine stocke une notification Redis pour tous les admins/cabines -// et envoie un push aux ceux qui ont un token. Appelé dès qu'une nouvelle commande est créée. func (d *Database) NotifyAllAdminCabine(commandID int, clientUsername, deliveryAddr string) { - rows, err := d.Query( - `SELECT username FROM users WHERE role IN ('admin','cabine')`, - ) - if err != nil { + var users []struct { + Username string `gorm:"column:username"` + } + if err := d.GDB.Raw(`SELECT username FROM users WHERE role IN ('admin','cabine')`).Scan(&users).Error; err != nil { log.Printf("❌ [ADMIN_NOTIF] Erreur lecture users admin/cabine: %v", err) return } - defer rows.Close() msg := fmt.Sprintf("Nouvelle commande #%d de %s — %s", commandID, clientUsername, deliveryAddr) @@ -86,18 +81,13 @@ func (d *Database) NotifyAllAdminCabine(commandID int, clientUsername, deliveryA notifJSON, _ := json.Marshal(notification) count := 0 - for rows.Next() { - var username string - if err := rows.Scan(&username); err != nil { - continue - } - notifKey := fmt.Sprintf("notifications:%s", username) + for _, u := range users { + notifKey := fmt.Sprintf("notifications:%s", u.Username) Redis.LPush(RedisCtx, notifKey, notifJSON) Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour) - // Diffusion Telegram individuelle if services.TelegramBot != nil && services.TelegramBot.IsConfigured() { - if chatID, ok, err := d.GetUserTelegramChatID(username); err == nil && ok { + if chatID, ok, err := d.GetUserTelegramChatID(u.Username); err == nil && ok { capturedChatID := chatID capturedMsg := msg go services.TelegramBot.SendMessage(capturedChatID, fmt.Sprintf("🔔 Nouvelle commande\n\n%s", capturedMsg)) @@ -108,17 +98,15 @@ func (d *Database) NotifyAllAdminCabine(commandID int, clientUsername, deliveryA log.Printf("📬 [ADMIN_NOTIF] Notif Redis (%d users) pour commande #%d", count, commandID) } -// NotifyAllAdminCabineAlert envoie une notification Redis + push à tous les admins/cabines -// lors du déclenchement d'une alerte par un livreur. +// NotifyAllAdminCabineAlert envoie une notification Redis à tous les admins/cabines lors d'une alerte func (d *Database) NotifyAllAdminCabineAlert(alertID int, livreurUsername, alertMessage string) { - rows, err := d.Query( - `SELECT username FROM users WHERE role IN ('admin','cabine')`, - ) - if err != nil { + var users []struct { + Username string `gorm:"column:username"` + } + if err := d.GDB.Raw(`SELECT username FROM users WHERE role IN ('admin','cabine')`).Scan(&users).Error; err != nil { log.Printf("❌ [ALERT_NOTIF] Erreur lecture users admin/cabine: %v", err) return } - defer rows.Close() body := fmt.Sprintf("%s — livreur : %s", alertMessage, livreurUsername) @@ -132,18 +120,13 @@ func (d *Database) NotifyAllAdminCabineAlert(alertID int, livreurUsername, alert notifJSON, _ := json.Marshal(notification) count := 0 - for rows.Next() { - var username string - if err := rows.Scan(&username); err != nil { - continue - } - notifKey := fmt.Sprintf("notifications:%s", username) + for _, u := range users { + notifKey := fmt.Sprintf("notifications:%s", u.Username) Redis.LPush(RedisCtx, notifKey, notifJSON) Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour) - // Diffusion Telegram individuelle if services.TelegramBot != nil && services.TelegramBot.IsConfigured() { - if chatID, ok, err := d.GetUserTelegramChatID(username); err == nil && ok { + if chatID, ok, err := d.GetUserTelegramChatID(u.Username); err == nil && ok { capturedChatID := chatID capturedBody := body go services.TelegramBot.SendMessage(capturedChatID, fmt.Sprintf("🚨 Alerte livreur\n\n%s", capturedBody)) @@ -156,11 +139,10 @@ func (d *Database) NotifyAllAdminCabineAlert(alertID int, livreurUsername, alert // AddDeliveryRating ajoute une note pour un livreur func (d *Database) AddDeliveryRating(livreurUsername string, commandID, rating int, comment string) error { - query := ` + err := d.GDB.Exec(` INSERT INTO delivery_ratings (livreur_username, command_id, rating, comment, created_at) - VALUES (?, ?, ?, ?, NOW()) - ` - _, err := d.Exec(query, livreurUsername, commandID, rating, comment) + VALUES (?, ?, ?, ?, NOW())`, + livreurUsername, commandID, rating, comment).Error if err != nil { log.Printf("⚠️ Erreur sauvegarde note livreur: %v", err) return err diff --git a/backend/gestion/db/db_payments.go b/backend/gestion/db/db_payments.go index 8325e07b..83f94ba1 100644 --- a/backend/gestion/db/db_payments.go +++ b/backend/gestion/db/db_payments.go @@ -1,21 +1,25 @@ package db import ( - "database/sql" "fmt" "gestion/models" "log" + + "gorm.io/gorm" ) func (d *Database) CreateCryptoPayment(commandID int, nowPaymentID, status, priceCurrency, payCurrency, payAddress string, priceAmount, payAmount float64) (*models.CryptoPayment, error) { - var p models.CryptoPayment - err := d.DB.QueryRow(` - INSERT INTO crypto_payments (command_id, nowpayment_id, status, price_amount, price_currency, pay_currency, pay_address, pay_amount) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8) - RETURNING id, command_id, nowpayment_id, status, price_amount, price_currency, pay_currency, pay_address, pay_amount, created_at, updated_at`, - commandID, nowPaymentID, status, priceAmount, priceCurrency, payCurrency, payAddress, payAmount, - ).Scan(&p.ID, &p.CommandID, &p.NowPaymentID, &p.Status, &p.PriceAmount, &p.PriceCurrency, &p.PayCurrency, &p.PayAddress, &p.PayAmount, &p.CreatedAt, &p.UpdatedAt) - if err != nil { + p := models.CryptoPayment{ + CommandID: commandID, + NowPaymentID: nowPaymentID, + Status: status, + PriceAmount: priceAmount, + PriceCurrency: priceCurrency, + PayCurrency: payCurrency, + PayAddress: payAddress, + PayAmount: payAmount, + } + if err := d.GDB.Create(&p).Error; err != nil { return nil, fmt.Errorf("CreateCryptoPayment: %w", err) } return &p, nil @@ -23,15 +27,11 @@ func (d *Database) CreateCryptoPayment(commandID int, nowPaymentID, status, pric func (d *Database) GetCryptoPaymentByCommandID(commandID int) (*models.CryptoPayment, error) { var p models.CryptoPayment - err := d.DB.QueryRow(` - SELECT id, command_id, nowpayment_id, status, price_amount, price_currency, pay_currency, pay_address, pay_amount, created_at, updated_at - FROM crypto_payments WHERE command_id = $1 - ORDER BY created_at DESC LIMIT 1`, commandID, - ).Scan(&p.ID, &p.CommandID, &p.NowPaymentID, &p.Status, &p.PriceAmount, &p.PriceCurrency, &p.PayCurrency, &p.PayAddress, &p.PayAmount, &p.CreatedAt, &p.UpdatedAt) - if err == sql.ErrNoRows { - return nil, nil - } + err := d.GDB.Where("command_id = ?", commandID).Order("created_at DESC").First(&p).Error if err != nil { + if isNotFound(err) { + return nil, nil + } return nil, err } return &p, nil @@ -39,84 +39,47 @@ func (d *Database) GetCryptoPaymentByCommandID(commandID int) (*models.CryptoPay func (d *Database) GetCryptoPaymentByNowPaymentID(nowPaymentID string) (*models.CryptoPayment, error) { var p models.CryptoPayment - err := d.DB.QueryRow(` - SELECT id, command_id, nowpayment_id, status, price_amount, price_currency, pay_currency, pay_address, pay_amount, created_at, updated_at - FROM crypto_payments WHERE nowpayment_id = $1`, nowPaymentID, - ).Scan(&p.ID, &p.CommandID, &p.NowPaymentID, &p.Status, &p.PriceAmount, &p.PriceCurrency, &p.PayCurrency, &p.PayAddress, &p.PayAmount, &p.CreatedAt, &p.UpdatedAt) - if err == sql.ErrNoRows { - return nil, nil - } + err := d.GDB.Where("nowpayment_id = ?", nowPaymentID).First(&p).Error if err != nil { + if isNotFound(err) { + return nil, nil + } return nil, err } return &p, nil } func (d *Database) GetPendingCryptoPayments() ([]models.CryptoPayment, error) { - rows, err := d.DB.Query(` - SELECT id, command_id, nowpayment_id, status, price_amount, price_currency, pay_currency, pay_address, pay_amount, created_at, updated_at - FROM crypto_payments - WHERE status NOT IN ('finished', 'failed', 'expired', 'refunded') - ORDER BY created_at ASC`) - if err != nil { - return nil, err - } - defer rows.Close() - var payments []models.CryptoPayment - for rows.Next() { - var p models.CryptoPayment - if err := rows.Scan(&p.ID, &p.CommandID, &p.NowPaymentID, &p.Status, &p.PriceAmount, &p.PriceCurrency, &p.PayCurrency, &p.PayAddress, &p.PayAmount, &p.CreatedAt, &p.UpdatedAt); err != nil { - continue - } - payments = append(payments, p) - } - return payments, nil + err := d.GDB.Where("status NOT IN ?", []string{"finished", "failed", "expired", "refunded"}). + Order("created_at ASC").Find(&payments).Error + return payments, err } func (d *Database) UpdateCryptoPaymentStatus(id int, status string, payAmount float64) error { - _, err := d.DB.Exec(` - UPDATE crypto_payments SET status = $1, pay_amount = $2, updated_at = NOW() - WHERE id = $3`, status, payAmount, id) - return err + return d.GDB.Model(&models.CryptoPayment{}).Where("id = ?", id). + Updates(map[string]any{"status": status, "pay_amount": payAmount}).Error } -// ActivateCryptoCommand passe la commande de 'pending_payment' → 'pending' une fois le paiement confirmé func (d *Database) ActivateCryptoCommand(commandID int) error { - _, err := d.DB.Exec(` - UPDATE commandes SET status = 'pending', updated_at = NOW() - WHERE id = $1 AND status = 'pending_payment'`, commandID) - return err + return d.GDB.Exec(`UPDATE commandes SET status = 'pending', updated_at = NOW() WHERE id = ? AND status = 'pending_payment'`, commandID).Error } -// CancelCryptoCommand annule une commande en attente de paiement et restaure le stock func (d *Database) CancelCryptoCommand(commandID int) error { - tx, err := d.DB.Begin() - if err != nil { - return err - } - defer tx.Rollback() - - rows, err := tx.Query(`SELECT product_id, quantite FROM command_items WHERE command_id = $1`, commandID) - if err != nil { - return err - } - defer rows.Close() - - for rows.Next() { - var productID int - var qty float64 - if err := rows.Scan(&productID, &qty); err != nil { - continue + return d.GDB.Transaction(func(tx *gorm.DB) error { + type item struct { + ProductID int + Quantite float64 } - if _, err := tx.Exec(`UPDATE products SET stock = stock + $1 WHERE id = $2`, qty, productID); err != nil { - log.Printf("[CANCEL CRYPTO] erreur restauration stock produit %d: %v", productID, err) + var items []item + if err := tx.Raw(`SELECT product_id, quantite FROM command_items WHERE command_id = ?`, commandID).Scan(&items).Error; err != nil { + return err } - } - - if _, err := tx.Exec(`UPDATE commandes SET status = 'cancelled', updated_at = NOW() WHERE id = $1 AND status = 'pending_payment'`, commandID); err != nil { - return err - } - - return tx.Commit() + for _, it := range items { + 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 tx.Exec(`UPDATE commandes SET status = 'cancelled', updated_at = NOW() WHERE id = ? AND status = 'pending_payment'`, commandID).Error + }) } diff --git a/backend/gestion/db/db_product.go b/backend/gestion/db/db_product.go index 09c19c1f..acea1534 100644 --- a/backend/gestion/db/db_product.go +++ b/backend/gestion/db/db_product.go @@ -1,7 +1,6 @@ package db import ( - "database/sql" "fmt" "gestion/models" "log" @@ -9,7 +8,7 @@ import ( ) // CreateProduct crée un nouveau produit avec ses prix -func (db *Database) CreateProduct(product any) error { +func (d *Database) CreateProduct(product any) error { log.Printf("🔍 [DB CreateProduct] Type reçu: %T", product) log.Printf("🔍 [DB CreateProduct] Valeur: %+v", product) @@ -29,7 +28,7 @@ func (db *Database) CreateProduct(product any) error { if !ok { if prodPtr, isPtr := product.(*models.Product); isPtr { log.Printf("✅ [DB CreateProduct] C'est un *models.Product, utilisons-le directement") - p = prodPtr // ça fonctionne maintenant car *models.Product implémente ProductInterface + p = prodPtr ok = true } else { return fmt.Errorf("type de produit invalide: reçu %T, attendu ProductInterface", product) @@ -46,43 +45,40 @@ func (db *Database) CreateProduct(product any) error { log.Printf("📦 [DB CreateProduct] Nombre de prix: %d", len(p.GetPrices())) } - // Insérer le produit avec le stock et l'unité - query := `INSERT INTO products (name, category, description, stock, unit, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, created_at, updated_at` - now := time.Now() - var productID int - var createdAt, updatedAt time.Time + var result struct { + ID int `gorm:"column:id"` + CreatedAt time.Time `gorm:"column:created_at"` + UpdatedAt time.Time `gorm:"column:updated_at"` + } - err := db.QueryRow(query, p.GetName(), p.GetCategory(), p.GetDescription(), p.GetStock(), p.GetUnit(), now, now). - Scan(&productID, &createdAt, &updatedAt) + err := d.GDB.Raw(` + INSERT INTO products (name, category, description, stock, unit, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING id, created_at, updated_at`, + p.GetName(), p.GetCategory(), p.GetDescription(), p.GetStock(), p.GetUnit(), now, now, + ).Scan(&result).Error if err != nil { log.Printf("❌ [DB CreateProduct] Erreur INSERT: %v", err) return fmt.Errorf("erreur création produit: %v", err) } - log.Printf("✅ [DB CreateProduct] Produit inséré avec ID: %d", productID) + log.Printf("✅ [DB CreateProduct] Produit inséré avec ID: %d", result.ID) - // Mettre à jour le produit avec l'ID et les dates - p.SetID(productID) - p.SetCreatedAt(createdAt) - p.SetUpdatedAt(updatedAt) + p.SetID(result.ID) + p.SetCreatedAt(result.CreatedAt) + p.SetUpdatedAt(result.UpdatedAt) - // Insérer les prix - prices := p.GetPrices() - for i, price := range prices { - priceQuery := `INSERT INTO product_prices (product_id, quantity, price) - VALUES ($1, $2, $3)` - _, err := db.Exec(priceQuery, productID, price.Quantity, price.Price) + for i, price := range p.GetPrices() { + err := d.GDB.Exec(`INSERT INTO product_prices (product_id, quantity, price) VALUES (?, ?, ?)`, + result.ID, price.Quantity, price.Price).Error if err != nil { log.Printf("❌ [DB CreateProduct] Erreur insertion prix[%d]: %v", i, err) return fmt.Errorf("erreur insertion prix: %v", err) } - log.Printf("✅ [DB CreateProduct] Prix[%d] inséré: quantity=%g, price=%.2f", - i, price.Quantity, price.Price) + log.Printf("✅ [DB CreateProduct] Prix[%d] inséré: quantity=%g, price=%.2f", i, price.Quantity, price.Price) } - log.Printf("🎉 [DB CreateProduct] Produit créé avec succès! ID=%d", productID) + log.Printf("🎉 [DB CreateProduct] Produit créé avec succès! ID=%d", result.ID) return nil } @@ -91,27 +87,21 @@ func (d *Database) GetProductByID(id int) (models.Product, error) { log.Printf("📦 [GetProductByID] START - ID=%d", id) var p models.Product - - // ✅ AJOUTER stock dans le SELECT - err := d.QueryRow(` + err := d.GDB.Raw(` SELECT id, name, category, description, stock, unit, created_at, updated_at FROM products - WHERE id=$1 - `, id).Scan(&p.ID, &p.Name, &p.Category, &p.Description, &p.Stock, &p.Unit, &p.CreatedAt, &p.UpdatedAt) - + WHERE id = ?`, id).Scan(&p).Error if err != nil { log.Printf("❌ [GetProductByID] Erreur query: %v", err) return p, err } - log.Printf("📊 [GetProductByID] Product scanned: ID=%d, Name=%s, Stock=%.2f", - p.ID, p.Name, p.Stock) + log.Printf("📊 [GetProductByID] Product scanned: ID=%d, Name=%s, Stock=%.2f", p.ID, p.Name, p.Stock) - // ✅ CHARGER LES PRICES prices, err := d.GetProductPrices(p.ID) if err != nil { log.Printf("⚠️ [GetProductByID] Erreur loading prices: %v", err) - p.Prices = []models.ProductPrice{} // Tableau vide au lieu de nil + p.Prices = []models.ProductPrice{} } else { p.Prices = prices log.Printf("✅ [GetProductByID] Loaded %d prices for product %d", len(prices), p.ID) @@ -119,51 +109,38 @@ func (d *Database) GetProductByID(id int) (models.Product, error) { return p, nil } + func (d *Database) GetAllProducts() ([]models.Product, error) { log.Println("📦 [GetAllProducts] START") - rows, err := d.Query(` + var products []models.Product + err := d.GDB.Raw(` SELECT id, name, category, description, stock, unit, created_at, updated_at FROM products - ORDER BY id ASC - `) + ORDER BY id ASC`).Scan(&products).Error if err != nil { log.Printf("❌ [GetAllProducts] Erreur query: %v", err) return nil, err } - defer rows.Close() - var products []models.Product - for rows.Next() { - var p models.Product - if err := rows.Scan(&p.ID, &p.Name, &p.Category, &p.Description, &p.Stock, &p.Unit, &p.CreatedAt, &p.UpdatedAt); err != nil { - log.Printf("❌ [GetAllProducts] Erreur scan: %v", err) - return nil, err - } - - log.Printf("📊 [GetAllProducts] Product scanned: ID=%d, Name=%s, Stock=%.2f", p.ID, p.Name, p.Stock) - - // ✅ CHARGER LES PRICES - prices, err := d.GetProductPrices(p.ID) + for i := range products { + prices, err := d.GetProductPrices(products[i].ID) if err != nil { - log.Printf("⚠️ [GetAllProducts] Erreur loading prices for product %d: %v", p.ID, err) - p.Prices = []models.ProductPrice{} + log.Printf("⚠️ [GetAllProducts] Erreur loading prices for product %d: %v", products[i].ID, err) + products[i].Prices = []models.ProductPrice{} } else { - p.Prices = prices - log.Printf("✅ [GetAllProducts] Loaded %d prices for product %d", len(prices), p.ID) + products[i].Prices = prices + log.Printf("✅ [GetAllProducts] Loaded %d prices for product %d", len(prices), products[i].ID) } - // ✅ CHARGER LES MÉDIAS (MANQUANT AVANT!) - media, err := d.GetMediaByProductID(p.ID) + media, err := d.GetMediaByProductID(products[i].ID) if err != nil { - log.Printf("⚠️ [GetAllProducts] Erreur loading media for product %d: %v", p.ID, err) - p.Media = []models.Media{} + log.Printf("⚠️ [GetAllProducts] Erreur loading media for product %d: %v", products[i].ID, err) + products[i].Media = []models.Media{} } else { - p.Media = media - log.Printf("✅ [GetAllProducts] Loaded %d media for product %d", len(media), p.ID) + products[i].Media = media + log.Printf("✅ [GetAllProducts] Loaded %d media for product %d", len(media), products[i].ID) } - - products = append(products, p) } log.Printf("✅ [GetAllProducts] Total products loaded: %d", len(products)) @@ -171,73 +148,50 @@ func (d *Database) GetAllProducts() ([]models.Product, error) { } // GetProductsByCategory récupère les produits par catégorie avec prices ET stock -func (db *Database) GetProductsByCategory(category string) ([]models.Product, error) { +func (d *Database) GetProductsByCategory(category string) ([]models.Product, error) { log.Printf("📦 [GetProductsByCategory] START - Category=%s", category) - rows, err := db.Query(` + var products []models.Product + err := d.GDB.Raw(` SELECT id, name, category, description, stock, unit, created_at, updated_at FROM products - WHERE category = $1 - ORDER BY created_at DESC - `, category) - + WHERE category = ? + ORDER BY created_at DESC`, category).Scan(&products).Error if err != nil { log.Printf("❌ [GetProductsByCategory] Erreur query: %v", err) return nil, fmt.Errorf("erreur lors de la récupération des produits: %w", err) } - defer rows.Close() - var products []models.Product - for rows.Next() { - var p models.Product - if err := rows.Scan(&p.ID, &p.Name, &p.Category, &p.Description, &p.Stock, &p.Unit, &p.CreatedAt, &p.UpdatedAt); err != nil { - log.Printf("❌ [GetProductsByCategory] Erreur scan: %v", err) - return nil, fmt.Errorf("erreur lors du scan d'un produit: %w", err) - } - - log.Printf("📊 [GetProductsByCategory] Product scanned: ID=%d, Name=%s, Stock=%.2f", - p.ID, p.Name, p.Stock) - - // ✅ CHARGER LES PRICES pour chaque produit - prices, err := db.GetProductPrices(p.ID) + for i := range products { + prices, err := d.GetProductPrices(products[i].ID) if err != nil { - log.Printf("⚠️ [GetProductsByCategory] Erreur loading prices for product %d: %v", p.ID, err) - p.Prices = []models.ProductPrice{} // Tableau vide au lieu de nil + log.Printf("⚠️ [GetProductsByCategory] Erreur loading prices for product %d: %v", products[i].ID, err) + products[i].Prices = []models.ProductPrice{} } else { - p.Prices = prices - log.Printf("✅ [GetProductsByCategory] Loaded %d prices for product %d", len(prices), p.ID) + products[i].Prices = prices + log.Printf("✅ [GetProductsByCategory] Loaded %d prices for product %d", len(prices), products[i].ID) } - - products = append(products, p) - } - - if err := rows.Err(); err != nil { - log.Printf("❌ [GetProductsByCategory] Erreur iteration: %v", err) - return nil, fmt.Errorf("erreur lors de l'itération des produits: %w", err) } log.Printf("✅ [GetProductsByCategory] Total products loaded: %d", len(products)) return products, nil } -func (db *Database) UpdateProduct(productID int, name, category, description, unit string, stock float64, prices []models.ProductPrice) error { - _, err := db.Exec(` - UPDATE products - SET name = $1, category = $2, description = $3, stock = $4, unit = $5, updated_at = $6 - WHERE id = $7 - `, name, category, description, stock, unit, time.Now(), productID) +func (d *Database) UpdateProduct(productID int, name, category, description, unit string, stock float64, prices []models.ProductPrice) error { + err := d.GDB.Exec(` + UPDATE products + SET name = ?, category = ?, description = ?, stock = ?, unit = ?, updated_at = ? + WHERE id = ?`, + name, category, description, stock, unit, time.Now(), productID).Error if err != nil { return fmt.Errorf("erreur mise à jour produit: %w", err) } - db.Exec(`DELETE FROM product_prices WHERE product_id = $1`, productID) + d.GDB.Exec(`DELETE FROM product_prices WHERE product_id = ?`, productID) for _, price := range prices { - _, err := db.Exec(` - INSERT INTO product_prices (product_id, quantity, price) - VALUES ($1, $2, $3) - `, productID, price.Quantity, price.Price) - if err != nil { + if err := d.GDB.Exec(`INSERT INTO product_prices (product_id, quantity, price) VALUES (?, ?, ?)`, + productID, price.Quantity, price.Price).Error; err != nil { log.Printf("❌ [UpdateProduct] Erreur prix: %v", err) } } @@ -246,30 +200,27 @@ func (db *Database) UpdateProduct(productID int, name, category, description, un } // DeleteProduct supprime un produit -func (db *Database) DeleteProduct(productID int) error { - query := `DELETE FROM products WHERE id = $1` - result, err := db.Exec(query, productID) - if err != nil { - return fmt.Errorf("erreur suppression produit: %v", err) +func (d *Database) DeleteProduct(productID int) error { + result := d.GDB.Exec(`DELETE FROM products WHERE id = ?`, productID) + if result.Error != nil { + return fmt.Errorf("erreur suppression produit: %v", result.Error) } - - rowsAffected, _ := result.RowsAffected() - if rowsAffected == 0 { + if result.RowsAffected == 0 { return fmt.Errorf("produit introuvable") } - return nil } func (d *Database) GetProductNameByID(productID int) (string, error) { - var name string - query := `SELECT name FROM products WHERE id = $1` - err := d.QueryRow(query, productID).Scan(&name) + var result struct { + Name string `gorm:"column:name"` + } + err := d.GDB.Raw(`SELECT name FROM products WHERE id = ?`, productID).Scan(&result).Error if err != nil { - if err == sql.ErrNoRows { - return "", fmt.Errorf("produit non trouvé") - } return "", err } - return name, nil + if result.Name == "" { + return "", fmt.Errorf("produit non trouvé") + } + return result.Name, nil } diff --git a/backend/gestion/db/db_product_price.go b/backend/gestion/db/db_product_price.go index 4b2500af..f9cd7949 100644 --- a/backend/gestion/db/db_product_price.go +++ b/backend/gestion/db/db_product_price.go @@ -3,79 +3,43 @@ package db import ( "fmt" "gestion/models" - "log" ) -// GetProductPrices récupère tous les prix d'un produit -func (db *Database) GetProductPrices(productID int) ([]models.ProductPrice, error) { - log.Printf("💰 [GetProductPrices] Loading prices for product %d", productID) - - query := `SELECT id, product_id, quantity, price, created_at - FROM product_prices - WHERE product_id = $1 - ORDER BY quantity ASC` - - rows, err := db.Query(query, productID) - if err != nil { - log.Printf("❌ [GetProductPrices] Erreur query: %v", err) - return nil, fmt.Errorf("erreur récupération prix: %v", err) - } - defer rows.Close() - +func (d *Database) GetProductPrices(productID int) ([]models.ProductPrice, error) { var prices []models.ProductPrice - for rows.Next() { - var price models.ProductPrice - if err := rows.Scan(&price.ID, &price.ProductID, &price.Quantity, &price.Price, &price.CreatedAt); err != nil { - log.Printf("❌ [GetProductPrices] Erreur scan: %v", err) - return nil, err - } - log.Printf(" 💰 Price: qty=%.0f, price=%.2f", price.Quantity, price.Price) - prices = append(prices, price) + if err := d.GDB.Where("product_id = ?", productID).Order("quantity ASC").Find(&prices).Error; err != nil { + return nil, fmt.Errorf("erreur récupération prix: %w", err) } - - log.Printf("✅ [GetProductPrices] Found %d prices for product %d", len(prices), productID) return prices, nil } -// CreateProductPrice ajoute un nouveau prix pour un produit -func (db *Database) CreateProductPrice(productID int, quantity float64, price float64) error { - query := `INSERT INTO product_prices (product_id, quantity, price) - VALUES ($1, $2, $3)` - _, err := db.Exec(query, productID, quantity, price) - if err != nil { - return fmt.Errorf("erreur création prix: %v", err) +func (d *Database) CreateProductPrice(productID int, quantity float64, price float64) error { + p := models.ProductPrice{ProductID: productID, Quantity: quantity, Price: price} + if err := d.GDB.Create(&p).Error; err != nil { + return fmt.Errorf("erreur création prix: %w", err) } return nil } -// UpdateProductPrice met à jour un prix -func (db *Database) UpdateProductPrice(priceID int, quantity float64, price float64) error { - query := `UPDATE product_prices SET quantity = $1, price = $2 WHERE id = $3` - result, err := db.Exec(query, quantity, price, priceID) - if err != nil { - return fmt.Errorf("erreur mise à jour prix: %v", err) +func (d *Database) UpdateProductPrice(priceID int, quantity float64, price float64) error { + result := d.GDB.Model(&models.ProductPrice{}).Where("id = ?", priceID). + Updates(map[string]any{"quantity": quantity, "price": price}) + if result.Error != nil { + return fmt.Errorf("erreur mise à jour prix: %w", result.Error) } - - rowsAffected, _ := result.RowsAffected() - if rowsAffected == 0 { + if result.RowsAffected == 0 { return fmt.Errorf("prix introuvable") } - return nil } -// DeleteProductPrice supprime un prix -func (db *Database) DeleteProductPrice(priceID int) error { - query := `DELETE FROM product_prices WHERE id = $1` - result, err := db.Exec(query, priceID) - if err != nil { - return fmt.Errorf("erreur suppression prix: %v", err) +func (d *Database) DeleteProductPrice(priceID int) error { + result := d.GDB.Delete(&models.ProductPrice{}, priceID) + if result.Error != nil { + return fmt.Errorf("erreur suppression prix: %w", result.Error) } - - rowsAffected, _ := result.RowsAffected() - if rowsAffected == 0 { + if result.RowsAffected == 0 { return fmt.Errorf("prix introuvable") } - return nil } diff --git a/backend/gestion/db/db_referral.go b/backend/gestion/db/db_referral.go index ecea58ab..a17c8eb7 100644 --- a/backend/gestion/db/db_referral.go +++ b/backend/gestion/db/db_referral.go @@ -1,94 +1,60 @@ package db import ( - "database/sql" "fmt" + + "gorm.io/gorm" ) -// GetClientReferralBalance retourne le solde parrainage d'un client. func (d *Database) GetClientReferralBalance(username string) (float64, error) { var balance float64 - err := d.QueryRow( - `SELECT referral_balance FROM clients WHERE username = $1`, - username, - ).Scan(&balance) - if err == sql.ErrNoRows { - return 0, fmt.Errorf("client non trouvé") + err := d.GDB.Table("clients").Select("referral_balance").Where("username = ?", username).Scan(&balance).Error + if err != nil { + return 0, err } - return balance, err + return balance, nil } -// CreditClientReferral ajoute un montant au solde parrainage d'un client. func (d *Database) CreditClientReferral(username string, amount float64) error { if amount <= 0 { return fmt.Errorf("le montant doit être positif") } - res, err := d.Exec( - `UPDATE clients SET referral_balance = referral_balance + $1 WHERE username = $2`, - amount, username, - ) - if err != nil { - return err + result := d.GDB.Exec(`UPDATE clients SET referral_balance = referral_balance + ? WHERE username = ?`, amount, username) + if result.Error != nil { + return result.Error } - n, _ := res.RowsAffected() - if n == 0 { + if result.RowsAffected == 0 { return fmt.Errorf("client non trouvé") } return nil } -// DebitReferralBalance déduit atomiquement le solde parrainage avant la création de commande. -// Gère sa propre transaction avec FOR UPDATE pour éviter le double-spend concurrent. -// Retourne une erreur si le solde est insuffisant. func (d *Database) DebitReferralBalance(username string, amount float64) error { if amount <= 0 { return nil } - tx, err := d.Begin() - if err != nil { - return fmt.Errorf("erreur transaction: %w", err) - } - var balance float64 - if err := tx.QueryRow( - `SELECT referral_balance FROM clients WHERE username = $1 FOR UPDATE`, - username, - ).Scan(&balance); err != nil { - tx.Rollback() - return fmt.Errorf("client non trouvé") - } - if balance < amount { - tx.Rollback() - return fmt.Errorf("solde parrainage insuffisant (disponible: %.2f€)", balance) - } - if _, err := tx.Exec( - `UPDATE clients SET referral_balance = referral_balance - $1 WHERE username = $2`, - amount, username, - ); err != nil { - tx.Rollback() - return err - } - return tx.Commit() + return d.GDB.Transaction(func(tx *gorm.DB) error { + var balance float64 + if err := tx.Raw(`SELECT referral_balance FROM clients WHERE username = ? FOR UPDATE`, username).Scan(&balance).Error; err != nil { + return fmt.Errorf("client non trouvé") + } + if balance < amount { + return fmt.Errorf("solde parrainage insuffisant (disponible: %.2f€)", balance) + } + return tx.Exec(`UPDATE clients SET referral_balance = referral_balance - ? WHERE username = ?`, amount, username).Error + }) } -// UseClientReferralBalance déduit un montant du solde parrainage dans une transaction. -func (d *Database) UseClientReferralBalance(tx *sql.Tx, username string, amount float64) error { +func (d *Database) UseClientReferralBalance(tx *gorm.DB, username string, amount float64) error { if amount <= 0 { return nil } var balance float64 - err := tx.QueryRow( - `SELECT referral_balance FROM clients WHERE username = $1 FOR UPDATE`, - username, - ).Scan(&balance) - if err != nil { + if err := tx.Raw(`SELECT referral_balance FROM clients WHERE username = ? FOR UPDATE`, username).Scan(&balance).Error; err != nil { return fmt.Errorf("client non trouvé") } if balance < amount { return fmt.Errorf("solde parrainage insuffisant (disponible: %.2f€)", balance) } - _, err = tx.Exec( - `UPDATE clients SET referral_balance = referral_balance - $1 WHERE username = $2`, - amount, username, - ) - return err + return tx.Exec(`UPDATE clients SET referral_balance = referral_balance - ? WHERE username = ?`, amount, username).Error } diff --git a/backend/gestion/db/db_sanction_client.go b/backend/gestion/db/db_sanction_client.go index 53935b9f..cf307deb 100644 --- a/backend/gestion/db/db_sanction_client.go +++ b/backend/gestion/db/db_sanction_client.go @@ -14,36 +14,29 @@ import ( // GetClientCancellationsCount récupère le nombre d'annulations tardives d'un client func (d *Database) GetClientCancellationsCount(username string) (int, error) { - var count int - query := `SELECT COALESCE(cancellations_count, 0) FROM clients WHERE username = $1` - - err := d.QueryRow(query, username).Scan(&count) + var result struct { + Count int `gorm:"column:count"` + } + err := d.GDB.Raw(`SELECT COALESCE(cancellations_count, 0) as count FROM clients WHERE username = ?`, username).Scan(&result).Error if err != nil { log.Printf("❌ [GetCancellationsCount] Erreur: %v", err) return 0, fmt.Errorf("erreur récupération compteur: %w", err) } - - return count, nil + return result.Count, nil } // IncrementClientCancellationsCount incrémente le compteur d'annulations func (d *Database) IncrementClientCancellationsCount(username string) error { - query := `UPDATE clients - SET cancellations_count = COALESCE(cancellations_count, 0) + 1, - updated_at = CURRENT_TIMESTAMP - WHERE username = $1` - - result, err := d.Exec(query, username) - if err != nil { - log.Printf("❌ [IncrementCancellations] Erreur: %v", err) - return fmt.Errorf("erreur incrémentation: %w", err) + result := d.GDB.Exec(` + UPDATE clients + SET cancellations_count = COALESCE(cancellations_count, 0) + 1, + updated_at = CURRENT_TIMESTAMP + WHERE username = ?`, username) + if result.Error != nil { + log.Printf("❌ [IncrementCancellations] Erreur: %v", result.Error) + return fmt.Errorf("erreur incrémentation: %w", result.Error) } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return fmt.Errorf("erreur vérification: %w", err) - } - if rowsAffected == 0 { + if result.RowsAffected == 0 { return fmt.Errorf("client non trouvé") } @@ -105,22 +98,16 @@ func (d *Database) ApplyCancellationPenalty(username string) (int, error) { return 0, err } - query := `UPDATE clients - SET amende = $1, - updated_at = CURRENT_TIMESTAMP - WHERE username = $2` - - result, err := d.Exec(query, float64(penalty), username) - if err != nil { - log.Printf("❌ [ApplyCancellationPenalty] Erreur UPDATE: %v", err) - return 0, fmt.Errorf("erreur application pénalité: %w", err) + result := d.GDB.Exec(` + UPDATE clients + SET amende = ?, + updated_at = CURRENT_TIMESTAMP + WHERE username = ?`, float64(penalty), username) + if result.Error != nil { + log.Printf("❌ [ApplyCancellationPenalty] Erreur UPDATE: %v", result.Error) + return 0, fmt.Errorf("erreur application pénalité: %w", result.Error) } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return 0, fmt.Errorf("erreur vérification: %w", err) - } - if rowsAffected == 0 { + if result.RowsAffected == 0 { return 0, fmt.Errorf("client non trouvé") } diff --git a/backend/gestion/db/db_settings.go b/backend/gestion/db/db_settings.go index 97f46ae0..ed0507b3 100644 --- a/backend/gestion/db/db_settings.go +++ b/backend/gestion/db/db_settings.go @@ -4,6 +4,8 @@ import ( "encoding/json" "fmt" "gestion/models" + + "gorm.io/gorm" ) // DefaultDeliverySchedule retourne un planning de livraison par défaut (tous les jours, 9h-20h) @@ -84,66 +86,64 @@ func DefaultSettings() models.AppSettings { func (d *Database) GetSettings() (models.AppSettings, error) { settings := DefaultSettings() - rows, err := d.Query(`SELECT key, value FROM app_settings`) - if err != nil { + var rows []struct { + Key string `gorm:"column:key"` + Value string `gorm:"column:value"` + } + if err := d.GDB.Raw(`SELECT key, value FROM app_settings`).Scan(&rows).Error; err != nil { return settings, fmt.Errorf("erreur lecture settings: %w", err) } - defer rows.Close() - for rows.Next() { - var key, value string - if err := rows.Scan(&key, &value); err != nil { - continue - } - switch key { + for _, row := range rows { + switch row.Key { case "penalties_enabled": - settings.PenaltiesEnabled = value == "true" + settings.PenaltiesEnabled = row.Value == "true" case "show_amende_score": - settings.ShowAmendeScore = value == "true" + settings.ShowAmendeScore = row.Value == "true" case "points_enabled": - settings.PointsEnabled = value == "true" + settings.PointsEnabled = row.Value == "true" case "points_pools": var pools []models.PointsPool - if err := json.Unmarshal([]byte(value), &pools); err == nil { + if err := json.Unmarshal([]byte(row.Value), &pools); err == nil { settings.PointsPools = pools } case "referral_enabled": - settings.ReferralEnabled = value == "true" + settings.ReferralEnabled = row.Value == "true" case "crypto_payment_enabled": - settings.CryptoPaymentEnabled = value == "true" + settings.CryptoPaymentEnabled = row.Value == "true" case "crypto_only": - settings.CryptoOnly = value == "true" + settings.CryptoOnly = row.Value == "true" case "nowpayments_api_key": - settings.NowPaymentsAPIKey = value + settings.NowPaymentsAPIKey = row.Value case "nowpayments_ipn_secret": - settings.NowPaymentsIPNSecret = value + settings.NowPaymentsIPNSecret = row.Value case "nowpayments_currencies": var currencies []string - if err := json.Unmarshal([]byte(value), ¤cies); err == nil { + if err := json.Unmarshal([]byte(row.Value), ¤cies); err == nil { settings.NowPaymentsCurrencies = currencies } case "delivery_schedule": var sched models.DeliverySchedule - if err := json.Unmarshal([]byte(value), &sched); err == nil { + if err := json.Unmarshal([]byte(row.Value), &sched); err == nil { settings.DeliverySchedule = sched } case "postal_zones": var zones []models.PostalZone - if err := json.Unmarshal([]byte(value), &zones); err == nil { + if err := json.Unmarshal([]byte(row.Value), &zones); err == nil { settings.PostalZones = zones } case "telegram_bot_token": - settings.TelegramBotToken = value + settings.TelegramBotToken = row.Value case "telegram_bot_username": - settings.TelegramBotUsername = value + settings.TelegramBotUsername = row.Value case "delivery_mode": var mode models.DeliveryModeConfig - if err := json.Unmarshal([]byte(value), &mode); err == nil { + if err := json.Unmarshal([]byte(row.Value), &mode); err == nil { settings.DeliveryMode = mode } } } - return settings, rows.Err() + return settings, nil } // UpdateSettings sauvegarde les paramètres dans la DB @@ -158,7 +158,6 @@ func (d *Database) UpdateSettings(s models.AppSettings) error { if s.PointsPools == nil { s.PointsPools = []models.PointsPool{} } - // S'assurer que chaque pool a des slices non-nil for i := range s.PointsPools { if s.PointsPools[i].Categories == nil { s.PointsPools[i].Categories = []string{} @@ -173,15 +172,6 @@ func (d *Database) UpdateSettings(s models.AppSettings) error { return fmt.Errorf("erreur sérialisation pools: %w", err) } - tx, err := d.Begin() - if err != nil { - return err - } - defer tx.Rollback() - - upsert := `INSERT INTO app_settings (key, value) VALUES ($1, $2) - ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value` - if s.NowPaymentsCurrencies == nil { s.NowPaymentsCurrencies = []string{} } @@ -190,6 +180,27 @@ func (d *Database) UpdateSettings(s models.AppSettings) error { return fmt.Errorf("erreur sérialisation nowpayments_currencies: %w", err) } + schedJSON, err := json.Marshal(s.DeliverySchedule) + if err != nil { + return fmt.Errorf("erreur sérialisation delivery_schedule: %w", err) + } + + if s.PostalZones == nil { + s.PostalZones = []models.PostalZone{} + } + zonesJSON, err := json.Marshal(s.PostalZones) + if err != nil { + return fmt.Errorf("erreur sérialisation postal_zones: %w", err) + } + + if s.DeliveryMode.CategoryRoutes == nil { + s.DeliveryMode.CategoryRoutes = []models.CategoryRoute{} + } + deliveryModeJSON, err := json.Marshal(s.DeliveryMode) + if err != nil { + return fmt.Errorf("erreur sérialisation delivery_mode: %w", err) + } + pairs := [][2]string{ {"penalties_enabled", boolStr(s.PenaltiesEnabled)}, {"show_amende_score", boolStr(s.ShowAmendeScore)}, @@ -201,39 +212,22 @@ func (d *Database) UpdateSettings(s models.AppSettings) error { {"nowpayments_api_key", s.NowPaymentsAPIKey}, {"nowpayments_ipn_secret", s.NowPaymentsIPNSecret}, {"nowpayments_currencies", string(currenciesJSON)}, + {"delivery_schedule", string(schedJSON)}, + {"postal_zones", string(zonesJSON)}, + {"telegram_bot_token", s.TelegramBotToken}, + {"telegram_bot_username", s.TelegramBotUsername}, + {"delivery_mode", string(deliveryModeJSON)}, } - schedJSON, err := json.Marshal(s.DeliverySchedule) - if err != nil { - return fmt.Errorf("erreur sérialisation delivery_schedule: %w", err) - } - pairs = append(pairs, [2]string{"delivery_schedule", string(schedJSON)}) + upsert := `INSERT INTO app_settings (key, value) VALUES (?, ?) + ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value` - if s.PostalZones == nil { - s.PostalZones = []models.PostalZone{} - } - zonesJSON, err := json.Marshal(s.PostalZones) - if err != nil { - return fmt.Errorf("erreur sérialisation postal_zones: %w", err) - } - pairs = append(pairs, [2]string{"postal_zones", string(zonesJSON)}) - pairs = append(pairs, [2]string{"telegram_bot_token", s.TelegramBotToken}) - pairs = append(pairs, [2]string{"telegram_bot_username", s.TelegramBotUsername}) - - if s.DeliveryMode.CategoryRoutes == nil { - s.DeliveryMode.CategoryRoutes = []models.CategoryRoute{} - } - deliveryModeJSON, err := json.Marshal(s.DeliveryMode) - if err != nil { - return fmt.Errorf("erreur sérialisation delivery_mode: %w", err) - } - pairs = append(pairs, [2]string{"delivery_mode", string(deliveryModeJSON)}) - - for _, p := range pairs { - if _, err = tx.Exec(upsert, p[0], p[1]); err != nil { - return fmt.Errorf("erreur upsert %s: %w", p[0], err) + return d.GDB.Transaction(func(tx *gorm.DB) error { + for _, p := range pairs { + if err := tx.Exec(upsert, p[0], p[1]).Error; err != nil { + return fmt.Errorf("erreur upsert %s: %w", p[0], err) + } } - } - - return tx.Commit() + return nil + }) } diff --git a/backend/gestion/db/db_telegram.go b/backend/gestion/db/db_telegram.go index 44ed1210..b4fc9206 100644 --- a/backend/gestion/db/db_telegram.go +++ b/backend/gestion/db/db_telegram.go @@ -2,7 +2,6 @@ package db import ( "crypto/rand" - "database/sql" "encoding/hex" "encoding/json" "fmt" @@ -18,7 +17,7 @@ func (d *Database) MigrateAddTelegramColumns() { `ALTER TABLE users ADD COLUMN IF NOT EXISTS telegram_chat_id BIGINT`, } for _, q := range migrations { - if _, err := d.Exec(q); err != nil { + if err := d.GDB.Exec(q).Error; err != nil { log.Printf("⚠️ [TELEGRAM_MIGRATION] %v", err) } } @@ -68,53 +67,64 @@ func ValidateAndConsumeLinkToken(token string) (username, role string, err error } func (d *Database) SaveClientTelegramChatID(username string, chatID int64) error { - _, err := d.Exec(`UPDATE clients SET telegram_chat_id = $1 WHERE username = $2`, chatID, username) - return err + return d.GDB.Exec(`UPDATE clients SET telegram_chat_id = ? WHERE username = ?`, chatID, username).Error } func (d *Database) GetClientTelegramChatID(username string) (int64, bool, error) { - var chatID sql.NullInt64 - err := d.QueryRow(`SELECT telegram_chat_id FROM clients WHERE username = $1`, username).Scan(&chatID) + var result struct { + ChatID *int64 `gorm:"column:telegram_chat_id"` + } + err := d.GDB.Raw(`SELECT telegram_chat_id FROM clients WHERE username = ?`, username).Scan(&result).Error if err != nil { return 0, false, err } - return chatID.Int64, chatID.Valid, nil + if result.ChatID == nil { + return 0, false, nil + } + return *result.ChatID, true, nil } func (d *Database) DeleteClientTelegramChatID(username string) error { - _, err := d.Exec(`UPDATE clients SET telegram_chat_id = NULL WHERE username = $1`, username) - return err + return d.GDB.Exec(`UPDATE clients SET telegram_chat_id = NULL WHERE username = ?`, username).Error } func (d *Database) SaveUserTelegramChatID(username string, chatID int64) error { - _, err := d.Exec(`UPDATE users SET telegram_chat_id = $1 WHERE username = $2`, chatID, username) - return err + return d.GDB.Exec(`UPDATE users SET telegram_chat_id = ? WHERE username = ?`, chatID, username).Error } func (d *Database) GetUserTelegramChatID(username string) (int64, bool, error) { - var chatID sql.NullInt64 - err := d.QueryRow(`SELECT telegram_chat_id FROM users WHERE username = $1`, username).Scan(&chatID) + var result struct { + ChatID *int64 `gorm:"column:telegram_chat_id"` + } + err := d.GDB.Raw(`SELECT telegram_chat_id FROM users WHERE username = ?`, username).Scan(&result).Error if err != nil { return 0, false, err } - return chatID.Int64, chatID.Valid, nil + if result.ChatID == nil { + return 0, false, nil + } + return *result.ChatID, true, nil } func (d *Database) DeleteUserTelegramChatID(username string) error { - _, err := d.Exec(`UPDATE users SET telegram_chat_id = NULL WHERE username = $1`, username) - return err + return d.GDB.Exec(`UPDATE users SET telegram_chat_id = NULL WHERE username = ?`, username).Error } // GetUserByTelegramChatID retrouve un utilisateur (clients + users) par chat_id func (d *Database) GetUserByTelegramChatID(chatID int64) (username, role string, err error) { - err = d.QueryRow(`SELECT username FROM clients WHERE telegram_chat_id = $1`, chatID).Scan(&username) - if err == nil { - return username, "client", nil + var clientResult struct { + Username string `gorm:"column:username"` + } + if err = d.GDB.Raw(`SELECT username FROM clients WHERE telegram_chat_id = ?`, chatID).Scan(&clientResult).Error; err == nil && clientResult.Username != "" { + return clientResult.Username, "client", nil } - err = d.QueryRow(`SELECT username, role FROM users WHERE telegram_chat_id = $1`, chatID).Scan(&username, &role) - if err == nil { - return username, role, nil + var userResult struct { + Username string `gorm:"column:username"` + Role string `gorm:"column:role"` + } + if err = d.GDB.Raw(`SELECT username, role FROM users WHERE telegram_chat_id = ?`, chatID).Scan(&userResult).Error; err == nil && userResult.Username != "" { + return userResult.Username, userResult.Role, nil } return "", "", fmt.Errorf("aucun compte lié à ce chat_id") diff --git a/backend/gestion/db/db_users.go b/backend/gestion/db/db_users.go index 639dc0ac..3675bad8 100644 --- a/backend/gestion/db/db_users.go +++ b/backend/gestion/db/db_users.go @@ -1,205 +1,90 @@ package db import ( - "database/sql" "fmt" "gestion/models" "log" - "time" ) func (d *Database) CreateUser(user *models.User) error { - query := `INSERT INTO users (username, password, role, created_at, updated_at) - VALUES ($1, $2, $3, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) - RETURNING id, created_at, updated_at` - - var createdAt, updatedAt time.Time - err := d.QueryRow(query, user.Username, user.Password, user.Role).Scan( - &user.ID, - &createdAt, - &updatedAt, - ) - if err != nil { + if err := d.GDB.Create(user).Error; err != nil { return fmt.Errorf("erreur lors de la création de l'utilisateur: %w", err) } - return nil } -// GetAllUsers récupère tous les utilisateurs func (d *Database) GetAllUsers() ([]*models.User, error) { - query := `SELECT id, username, password, role, created_at, updated_at - FROM users ORDER BY created_at DESC` - - rows, err := d.Query(query) - if err != nil { + var users []*models.User + if err := d.GDB.Order("created_at DESC").Find(&users).Error; err != nil { return nil, fmt.Errorf("erreur lors de la récupération des utilisateurs: %w", err) } - defer rows.Close() - - var users []*models.User - for rows.Next() { - user := &models.User{} - var createdAt, updatedAt time.Time - err := rows.Scan( - &user.ID, - &user.Username, - &user.Password, - &user.Role, - &createdAt, - &updatedAt, - ) - if err != nil { - return nil, fmt.Errorf("erreur lors du scan de l'utilisateur: %w", err) - } - users = append(users, user) - } - - if err = rows.Err(); err != nil { - return nil, fmt.Errorf("erreur lors de l'itération des résultats: %w", err) - } - return users, nil } func (d *Database) GetAllDeliveryMen() ([]*models.User, error) { - query := ` - SELECT id, username, password, role - FROM users - WHERE role = 'livreur' - ` - - rows, err := d.Query(query) - if err != nil { - return nil, fmt.Errorf("erreur lors de la récupération des utilisateurs: %w", err) - } - defer rows.Close() - var users []*models.User - - for rows.Next() { - user := &models.User{} - err := rows.Scan( - &user.ID, - &user.Username, - &user.Password, - &user.Role, - ) - if err != nil { - return nil, fmt.Errorf("erreur lors du scan de l'utilisateur: %w", err) - } - users = append(users, user) + if err := d.GDB.Where("role = ?", "livreur").Find(&users).Error; err != nil { + return nil, fmt.Errorf("erreur lors de la récupération des livreurs: %w", err) } - - if err = rows.Err(); err != nil { - return nil, fmt.Errorf("erreur lors de l'itération des résultats: %w", err) - } - return users, nil } -// UpdateUser met à jour un utilisateur existant func (d *Database) UpdateUser(user *models.User) error { - query := `UPDATE users - SET username = $1, password = $2, role = $3, updated_at = CURRENT_TIMESTAMP - WHERE id = $4` - - result, err := d.Exec(query, user.Username, user.Password, user.Role, user.ID) - if err != nil { - return fmt.Errorf("erreur lors de la mise à jour de l'utilisateur: %w", err) + result := d.GDB.Model(user).Updates(models.User{ + Username: user.Username, + Password: user.Password, + Role: user.Role, + }) + if result.Error != nil { + return fmt.Errorf("erreur lors de la mise à jour de l'utilisateur: %w", result.Error) } - - 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 { + if result.RowsAffected == 0 { return fmt.Errorf("utilisateur non trouvé") } - return nil } -// DeleteUser supprime un utilisateur func (d *Database) DeleteUser(id int) error { - var role string - err := d.QueryRow(`SELECT role FROM users WHERE id = $1`, id).Scan(&role) - if err != nil { - if err == sql.ErrNoRows { + var user models.User + if err := d.GDB.First(&user, id).Error; err != nil { + if isNotFound(err) { return fmt.Errorf("utilisateur non trouvé") } return fmt.Errorf("erreur lors de la récupération du rôle: %w", err) } - _ = d.RevokeAllUserTokens(id, role) + _ = d.RevokeAllUserTokens(id, user.Role) - query := `DELETE FROM users WHERE id = $1` - - result, err := d.Exec(query, id) - if err != nil { - return fmt.Errorf("erreur lors de la suppression de l'utilisateur: %w", err) + result := d.GDB.Delete(&models.User{}, id) + if result.Error != nil { + return fmt.Errorf("erreur lors de la suppression de l'utilisateur: %w", result.Error) } - - 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 { + if result.RowsAffected == 0 { return fmt.Errorf("utilisateur non trouvé") } - log.Printf("✅ Utilisateur supprimé (ID: %d, Role: %s)", id, role) + log.Printf("✅ Utilisateur supprimé (ID: %d, Role: %s)", id, user.Role) return nil } -// GetUserByID récupère un utilisateur par son ID func (d *Database) GetUserByID(id int) (*models.User, error) { - user := &models.User{} - query := `SELECT id, username, password, role, created_at, updated_at - FROM users WHERE id = $1` - - var createdAt, updatedAt time.Time - err := d.QueryRow(query, id).Scan( - &user.ID, - &user.Username, - &user.Password, - &user.Role, - &createdAt, - &updatedAt, - ) - - if err == sql.ErrNoRows { - return nil, fmt.Errorf("utilisateur non trouvé") - } - if err != nil { + var user models.User + if err := d.GDB.First(&user, id).Error; err != nil { + if isNotFound(err) { + return nil, fmt.Errorf("utilisateur non trouvé") + } return nil, fmt.Errorf("erreur lors de la récupération de l'utilisateur: %w", err) } - - return user, nil + return &user, nil } func (d *Database) GetUserByUsername(username string) (*models.User, error) { - user := &models.User{} - query := `SELECT id, username, password, role - FROM users WHERE username = $1` - - err := d.QueryRow(query, username).Scan( - &user.ID, - &user.Username, - &user.Password, - &user.Role, - ) - - if err == sql.ErrNoRows { - return nil, fmt.Errorf("utilisateur non trouvé") - } - - // Autres erreurs - if err != nil { + var user models.User + if err := d.GDB.Where("username = ?", username).First(&user).Error; err != nil { + if isNotFound(err) { + return nil, fmt.Errorf("utilisateur non trouvé") + } return nil, fmt.Errorf("erreur lors de la récupération de l'utilisateur: %w", err) } - - return user, nil + return &user, nil } diff --git a/backend/gestion/db/helpers.go b/backend/gestion/db/helpers.go new file mode 100644 index 00000000..e4f6ae69 --- /dev/null +++ b/backend/gestion/db/helpers.go @@ -0,0 +1,8 @@ +package db + +import "gorm.io/gorm" + +// isNotFound retourne true si l'erreur GORM est un "record not found" +func isNotFound(err error) bool { + return err == gorm.ErrRecordNotFound +} diff --git a/backend/gestion/go.mod b/backend/gestion/go.mod index 27fa329a..d16f4379 100644 --- a/backend/gestion/go.mod +++ b/backend/gestion/go.mod @@ -30,6 +30,12 @@ require ( github.com/gorilla/context v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/sessions v1.4.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.6.0 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -50,4 +56,6 @@ require ( golang.org/x/text v0.27.0 // indirect golang.org/x/tools v0.34.0 // indirect google.golang.org/protobuf v1.36.9 // indirect + gorm.io/driver/postgres v1.6.0 // indirect + gorm.io/gorm v1.31.1 // indirect ) diff --git a/backend/gestion/go.sum b/backend/gestion/go.sum index 9b9bdff2..5c211e60 100644 --- a/backend/gestion/go.sum +++ b/backend/gestion/go.sum @@ -52,6 +52,18 @@ github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kX github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -83,6 +95,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -117,3 +130,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= +gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= +gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= +gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= diff --git a/backend/gestion/handlers/auth.go b/backend/gestion/handlers/auth.go index f563bfc2..0a0e5aa4 100644 --- a/backend/gestion/handlers/auth.go +++ b/backend/gestion/handlers/auth.go @@ -176,11 +176,13 @@ func RegisterClient(c *gin.Context) { func AdminCreateClient(c *gin.Context) { var req models.RegisterClientRequest if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"}) + log.Printf("❌ [ADMIN_CREATE_CLIENT] Binding error: %v | body: username=%q nom=%q prenom=%q tel=%q", err, req.Username, req.Nom, req.Prenom, req.Telephone) + c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides", "detail": err.Error()}) return } if !utils.ValidatePhoneNumber(req.Telephone) { + log.Printf("❌ [ADMIN_CREATE_CLIENT] Téléphone invalide: %q", req.Telephone) c.JSON(http.StatusBadRequest, gin.H{"error": "Numéro de téléphone invalide"}) return } diff --git a/backend/gestion/handlers/cancel_command.go b/backend/gestion/handlers/cancel_command.go index e98d3fec..7cdccda0 100644 --- a/backend/gestion/handlers/cancel_command.go +++ b/backend/gestion/handlers/cancel_command.go @@ -325,7 +325,7 @@ func GetAllCancelledOrders(c *gin.Context) { // ✅ ENRICHIR les données (sans exposer d'infos sensibles inutiles) var enrichedOrders []map[string]any for _, order := range cancelledOrders { - orderID, _ := order["id"].(int) + orderID, _ := strconv.Atoi(fmt.Sprintf("%v", order["id"])) items, _ := database.GetCommandItems(orderID) logs, _ := database.GetCommandLogs(orderID) diff --git a/backend/gestion/handlers/deleviry.go b/backend/gestion/handlers/deleviry.go index 91959b19..fa2f48c9 100644 --- a/backend/gestion/handlers/deleviry.go +++ b/backend/gestion/handlers/deleviry.go @@ -41,7 +41,7 @@ func GetMyDeliveries(c *gin.Context) { filteredCommands := make([]gin.H, len(commands)) for i, cmd := range commands { - commandID, _ := cmd["id"].(int) + commandID, _ := strconv.Atoi(fmt.Sprintf("%v", cmd["id"])) items, _ := database.GetCommandItems(commandID) // Client info SANS téléphone diff --git a/backend/gestion/handlers/history.go b/backend/gestion/handlers/history.go index 902e8ae5..9cf09ed0 100644 --- a/backend/gestion/handlers/history.go +++ b/backend/gestion/handlers/history.go @@ -10,6 +10,7 @@ import ( "gestion/db" "log" "net/http" + "strconv" "github.com/gin-gonic/gin" ) @@ -128,8 +129,8 @@ func GetMyCompletedOrdersWithItems(c *gin.Context) { var enrichedCommands []map[string]interface{} for _, command := range commands { - commandID, ok := command["id"].(int) - if !ok { + commandID, _ := strconv.Atoi(fmt.Sprintf("%v", command["id"])) + if commandID == 0 { continue } diff --git a/backend/gestion/main.go b/backend/gestion/main.go index e0419225..346f0704 100644 --- a/backend/gestion/main.go +++ b/backend/gestion/main.go @@ -22,32 +22,26 @@ import ( ) func main() { - // Forcer la timezone Europe/Paris (UTC+1/+2) if loc, err := time.LoadLocation("Europe/Paris"); err == nil { time.Local = loc } else { log.Printf("⚠️ Impossible de charger la timezone Europe/Paris: %v", err) } - // Chargement des variables d'environnement if err := godotenv.Load(); err != nil { log.Println("⚠️ Aucun fichier .env trouvé, utilisation des valeurs par défaut.") } - // Initialisation de la base de données database := db.InitDB() defer database.Close() log.Printf("✅ Database initialisée: %+v", database) - // Initialisation de Redis db.InitRedis() defer db.Redis.Close() log.Println("✅ Redis initialisé avec succès") - // Initialisation du service de géolocalisation geoService := services.NewGeoService(db.Redis, db.RedisCtx) log.Println("✅ Service de géolocalisation initialisé") - // Initialisation du service Telegram telegramService := services.NewTelegramService() if telegramService.IsConfigured() { log.Println("✅ Service Telegram initialisé") @@ -60,15 +54,12 @@ func main() { log.Println("ℹ️ Service Telegram désactivé (TELEGRAM_BOT_TOKEN non défini)") } - // Migration: ajout des colonnes telegram_chat_id database.MigrateAddTelegramColumns() - // Priorité DB > .env pour la config Telegram if dbSettings, err := database.GetSettings(); err == nil { telegramService.Reload(dbSettings.TelegramBotToken, dbSettings.TelegramBotUsername) if dbSettings.TelegramBotToken != "" { log.Printf("✅ [TELEGRAM] Config chargée depuis la DB (username: %s)", dbSettings.TelegramBotUsername) - // Enregistrer le webhook si l'URL est configurée (et pas déjà fait depuis l'env) if webhookURL := os.Getenv("TELEGRAM_WEBHOOK_URL"); webhookURL != "" { if err := telegramService.SetWebhook(webhookURL); err != nil { log.Printf("⚠️ [TELEGRAM] Erreur enregistrement webhook (DB reload): %v", err) @@ -77,9 +68,6 @@ func main() { } } - // ============================================ - // 🧹 NETTOYAGE INITIAL DES COMMANDES INVALIDES - // ============================================ log.Println("") log.Println("🧹 Démarrage du nettoyage des commandes invalides...") removed, err := database.CleanupInvalidQueueCommands() @@ -93,32 +81,21 @@ func main() { } } - // ============================================ - // 🔄 DÉMARRAGE DU SCHEDULER DE NETTOYAGE AUTO - // ============================================ go database.StartQueueCleanupScheduler() log.Println("✅ Scheduler de nettoyage démarré (toutes les 5 min)") - // ============================================ - // 🔄 SYNCHRONISATION DES STATUTS LIVREURS - // ============================================ if err := database.SyncAllDeliverymanStatuses(); err != nil { log.Printf("⚠️ Erreur synchronisation statuts: %v", err) } else { log.Println("✅ Synchronisation des statuts livreurs terminée") } - // ============================================ - // ⭐ DÉMARRAGE DU CRON JOB AUTO-ASSIGNATION - // ============================================ go workers.StartAutoAssignmentCron(database, geoService) log.Println("✅ Cron job auto-assignation démarré (5 min)") - // Démarrage des workers Redis en arrière-plan go workers.StartRedisWorkers(database) log.Println("✅ Workers Redis démarrés") - // Worker de vérification des paiements crypto (recharge les clés dynamiquement) workers.StartDynamicPaymentChecker(database, func() *services.NowPaymentsClient { s, err := database.GetSettings() if err != nil || !s.CryptoPaymentEnabled || s.NowPaymentsAPIKey == "" { @@ -130,7 +107,6 @@ func main() { gin.SetMode(gin.ReleaseMode) r := gin.Default() - // Configuration des sessions store := cookie.NewStore([]byte(os.Getenv("SESSION_SECRET"))) store.Options(sessions.Options{ Path: "/", @@ -142,7 +118,6 @@ func main() { }) r.Use(sessions.Sessions("mysession", store)) - // Configuration CORS r.Use(cors.New(cors.Config{ AllowOrigins: []string{"https://uber-stup.club", "https://5.181.0.112.nip.io", "https://5.181.0.112.nip.io:8080", "https://5.181.0.112.nip.io:8443", "https://mln-uber.club", "http://localhost:5173", "http://5.181.0.112"}, AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"}, @@ -151,7 +126,6 @@ func main() { AllowCredentials: true, })) - // Middleware pour injecter la base de données et geoService r.Use(func(c *gin.Context) { c.Set("database", database) c.Set("geoService", geoService) @@ -167,12 +141,8 @@ func main() { c.Next() }) - // Fichiers statiques r.Static("/uploads", "./uploads") - // ============================================ - // ENREGISTRER TOUTES LES ROUTES - // ============================================ routes.SetupRoutes(r, database, geoService) if err := r.Run(":8080"); err != nil { diff --git a/backend/gestion/models/address.go b/backend/gestion/models/address.go index 65cd58ea..c20117b2 100644 --- a/backend/gestion/models/address.go +++ b/backend/gestion/models/address.go @@ -3,9 +3,11 @@ package models import "time" type Address struct { - ID int64 `db:"id"` - InvalidAddress string `db:"invalid_address"` - CorrectAddress string `db:"correct_address"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` + ID int64 `db:"id" gorm:"primaryKey;autoIncrement"` + InvalidAddress string `db:"invalid_address" gorm:"column:invalid_address"` + CorrectAddress string `db:"correct_address" gorm:"column:correct_address"` + CreatedAt time.Time `db:"created_at" gorm:"autoCreateTime"` + UpdatedAt time.Time `db:"updated_at" gorm:"autoUpdateTime"` } + +func (Address) TableName() string { return "adresse_correction" } diff --git a/backend/gestion/models/alert.go b/backend/gestion/models/alert.go index a56e5f0f..e7eb0410 100644 --- a/backend/gestion/models/alert.go +++ b/backend/gestion/models/alert.go @@ -1,10 +1,14 @@ package models +import "time" + type AlertPolicy struct { - ID int `json:"id"` - Username string `json:"username"` - Status string `json:"status"` - Message string `json:"message"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` + ID int `json:"id" gorm:"primaryKey;autoIncrement"` + Username string `json:"username" gorm:"column:username"` + Status string `json:"status" gorm:"column:status"` + Message string `json:"message" gorm:"column:message"` + CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` } + +func (AlertPolicy) TableName() string { return "alerte_policy" } diff --git a/backend/gestion/models/client.go b/backend/gestion/models/client.go index 8e6e410a..485e9715 100644 --- a/backend/gestion/models/client.go +++ b/backend/gestion/models/client.go @@ -4,19 +4,20 @@ package models import "time" type Client struct { - ID int `json:"id"` - Username string `json:"username"` - Password string `json:"-"` // Ne jamais exposer le password dans le JSON - Nom string `json:"nom"` - Prenom string `json:"prenom"` - Telephone string `json:"telephone"` - Command int `json:"command"` - PointsExtra map[string]int `json:"points_extra"` - Amende float64 `json:"amende"` - CancellationsCount int `json:"cancellations_count"` - LastPenaltyReason string `json:"last_penalty_reason"` - MustChangePassword bool `json:"must_change_password"` - PushToken string `json:"-"` - ReferralBalance float64 `json:"referral_balance"` - CreatedAt time.Time `json:"created_at"` + ID int `gorm:"primaryKey;autoIncrement" json:"id"` + Username string `gorm:"column:username" json:"username"` + Password string `gorm:"column:password" json:"-"` + Nom string `gorm:"column:nom" json:"nom"` + Prenom string `gorm:"column:prenom" json:"prenom"` + Telephone string `gorm:"column:telephone" json:"telephone"` + Command int `gorm:"column:command" json:"command"` + PointsExtra map[string]int `gorm:"-" json:"points_extra"` + Amende float64 `gorm:"column:amende" json:"amende"` + CancellationsCount int `gorm:"column:cancellations_count" json:"cancellations_count"` + LastPenaltyReason string `gorm:"column:last_penalty_reason" json:"last_penalty_reason"` + MustChangePassword bool `gorm:"column:must_change_password" json:"must_change_password"` + ReferralBalance float64 `gorm:"column:referral_balance" json:"referral_balance"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` } + +func (Client) TableName() string { return "clients" } diff --git a/backend/gestion/models/livreur.go b/backend/gestion/models/livreur.go index 83e0682e..2c9e0d68 100644 --- a/backend/gestion/models/livreur.go +++ b/backend/gestion/models/livreur.go @@ -10,14 +10,16 @@ type LivreurPosition struct { } type DeliveryIssue struct { - ID int `json:"id"` - CommandID int `json:"command_id"` - IssueType string `json:"issue_type"` - Description string `json:"description"` - Status string `json:"status"` // "open", "in_progress", "resolved" - ReportedBy string `json:"reported_by"` - ResolvedBy string `json:"resolved_by,omitempty"` - Resolution string `json:"resolution,omitempty"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID int `json:"id" gorm:"primaryKey;autoIncrement"` + CommandID int `json:"command_id" gorm:"column:command_id;index"` + IssueType string `json:"issue_type" gorm:"column:issue_type"` + Description string `json:"description" gorm:"column:description"` + Status string `json:"status" gorm:"column:status"` + ReportedBy string `json:"reported_by" gorm:"column:reported_by"` + ResolvedBy string `json:"resolved_by,omitempty" gorm:"column:resolved_by"` + Resolution string `json:"resolution,omitempty" gorm:"column:resolution"` + CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` } + +func (DeliveryIssue) TableName() string { return "delivery_issues" } diff --git a/backend/gestion/models/media.go b/backend/gestion/models/media.go index 5c642fbe..9dfc41dd 100644 --- a/backend/gestion/models/media.go +++ b/backend/gestion/models/media.go @@ -3,13 +3,15 @@ package models import "time" type Media struct { - ID int `json:"id"` - ProductID int `json:"product_id"` - Type string `json:"type"` - URL string `json:"url"` - CreatedAt time.Time `json:"created_at"` // ✅ Ajouté + ID int `gorm:"primaryKey;autoIncrement" json:"id"` + ProductID int `gorm:"column:product_id" json:"product_id"` + Type string `gorm:"column:type" json:"type"` + URL string `gorm:"column:url" json:"url"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` } +func (Media) TableName() string { return "media" } + // Méthodes pour l'interface Database func (m *Media) GetProductID() int { return m.ProductID } func (m *Media) GetType() string { return m.Type } diff --git a/backend/gestion/models/payment.go b/backend/gestion/models/payment.go index 4693002b..1b759117 100644 --- a/backend/gestion/models/payment.go +++ b/backend/gestion/models/payment.go @@ -4,15 +4,17 @@ import "time" // CryptoPayment suit un paiement crypto NowPayments lié à une commande type CryptoPayment struct { - ID int `json:"id"` - CommandID int `json:"command_id"` - NowPaymentID string `json:"nowpayment_id"` - Status string `json:"status"` // waiting, confirming, confirmed, sending, partially_paid, finished, failed, refunded, expired - PriceAmount float64 `json:"price_amount"` - PriceCurrency string `json:"price_currency"` - PayCurrency string `json:"pay_currency"` - PayAddress string `json:"pay_address"` - PayAmount float64 `json:"pay_amount"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID int `json:"id" gorm:"primaryKey;autoIncrement"` + CommandID int `json:"command_id" gorm:"column:command_id;index"` + NowPaymentID string `json:"nowpayment_id" gorm:"column:nowpayment_id;uniqueIndex"` + Status string `json:"status" gorm:"column:status"` + PriceAmount float64 `json:"price_amount" gorm:"column:price_amount"` + PriceCurrency string `json:"price_currency" gorm:"column:price_currency"` + PayCurrency string `json:"pay_currency" gorm:"column:pay_currency"` + PayAddress string `json:"pay_address" gorm:"column:pay_address"` + PayAmount float64 `json:"pay_amount" gorm:"column:pay_amount"` + CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` } + +func (CryptoPayment) TableName() string { return "crypto_payments" } diff --git a/backend/gestion/models/product.go b/backend/gestion/models/product.go index defc823a..b75ccdad 100644 --- a/backend/gestion/models/product.go +++ b/backend/gestion/models/product.go @@ -3,26 +3,30 @@ package models import "time" type Product struct { - ID int `json:"id"` - Name string `json:"name" binding:"required"` - Category string `json:"category" binding:"required"` - Description string `json:"description"` - Stock float64 `json:"stock"` // ← ajouter le stock ici - Unit string `json:"unit"` // kg | g | bag | l | cl | pcs | u - Prices []ProductPrice `json:"prices"` - Media []Media `json:"media,omitempty"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID int `json:"id" gorm:"primaryKey;autoIncrement"` + Name string `json:"name" gorm:"column:name" binding:"required"` + Category string `json:"category" gorm:"column:category" binding:"required"` + Description string `json:"description" gorm:"column:description"` + Stock float64 `json:"stock" gorm:"column:stock"` + Unit string `json:"unit" gorm:"column:unit"` + Prices []ProductPrice `json:"prices" gorm:"foreignKey:ProductID"` + Media []Media `json:"media,omitempty" gorm:"foreignKey:ProductID"` + CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` } +func (Product) TableName() string { return "products" } + type ProductPrice struct { - ID int `json:"id"` - ProductID int `json:"product_id"` - Quantity float64 `json:"quantity" binding:"required"` - Price float64 `json:"price" binding:"required"` - CreatedAt time.Time `json:"created_at"` + ID int `json:"id" gorm:"primaryKey;autoIncrement"` + ProductID int `json:"product_id" gorm:"column:product_id;index"` + Quantity float64 `json:"quantity" gorm:"column:quantity" binding:"required"` + Price float64 `json:"price" gorm:"column:price" binding:"required"` + CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` } +func (ProductPrice) TableName() string { return "product_prices" } + type StockInfo struct { ProductID int `json:"product_id"` ProductName string `json:"product_name"` diff --git a/backend/gestion/models/user.go b/backend/gestion/models/user.go index 2f085297..e5481fe7 100644 --- a/backend/gestion/models/user.go +++ b/backend/gestion/models/user.go @@ -1,8 +1,14 @@ package models +import "time" + type User struct { - ID int `json:"id"` - Username string `json:"username"` - Password string `json:"password,omitempty"` // omitempty pour ne pas l'exposer dans les réponses JSON - Role string `json:"role"` // "user" ou "admin" + ID int `json:"id" gorm:"primaryKey;autoIncrement"` + Username string `json:"username" gorm:"column:username;uniqueIndex"` + Password string `json:"password,omitempty" gorm:"column:password"` + Role string `json:"role" gorm:"column:role"` + CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` } + +func (User) TableName() string { return "users" } diff --git a/backend/gestion/workers/cron_auto_assign.go b/backend/gestion/workers/cron_auto_assign.go index d0c0d46d..0f79bcb5 100644 --- a/backend/gestion/workers/cron_auto_assign.go +++ b/backend/gestion/workers/cron_auto_assign.go @@ -5,6 +5,7 @@ import ( "gestion/db" "gestion/services" "log" + "strconv" "time" ) @@ -57,14 +58,10 @@ func processAutoAssignmentWithPriority(database *db.Database, geoService *servic skippedCount := 0 for i, cmd := range commands { - commandID, ok := cmd["id"].(int) - if !ok { - if idFloat, ok := cmd["id"].(float64); ok { - commandID = int(idFloat) - } else { - skippedCount++ - continue - } + commandID, _ := strconv.Atoi(fmt.Sprintf("%v", cmd["id"])) + if commandID == 0 { + skippedCount++ + continue } // Log la position dans la queue de priorité diff --git a/mobile/src/api/api_types.ts b/mobile/src/api/api_types.ts index 4290010a..b971b63d 100644 --- a/mobile/src/api/api_types.ts +++ b/mobile/src/api/api_types.ts @@ -226,6 +226,7 @@ export interface OrderItem { */ export interface OrderDetail { id: number; + client_order_number?: number; username: string; status: string; delivery_address?: string; diff --git a/mobile/src/screens/client/OrderDetailsScreen.tsx b/mobile/src/screens/client/OrderDetailsScreen.tsx index 56fdd40f..342e53e2 100644 --- a/mobile/src/screens/client/OrderDetailsScreen.tsx +++ b/mobile/src/screens/client/OrderDetailsScreen.tsx @@ -327,7 +327,7 @@ export default function OrderDetailsScreen() { > - Commande #{order.id} + Commande #{order.client_order_number ?? order.id} diff --git a/mobile/src/screens/client/OrderHistoryScreen.tsx b/mobile/src/screens/client/OrderHistoryScreen.tsx index 1100f9ac..5562c563 100644 --- a/mobile/src/screens/client/OrderHistoryScreen.tsx +++ b/mobile/src/screens/client/OrderHistoryScreen.tsx @@ -370,7 +370,7 @@ export default function OrderHistoryScreen() { > - Commande #{order.id} + Commande #{order.client_order_number ?? order.id} diff --git a/mobile/src/screens/client/OrderTrackingScreen.tsx b/mobile/src/screens/client/OrderTrackingScreen.tsx index dc6db74b..4d4ffac2 100644 --- a/mobile/src/screens/client/OrderTrackingScreen.tsx +++ b/mobile/src/screens/client/OrderTrackingScreen.tsx @@ -406,7 +406,7 @@ export default function OrderTrackingScreen() { > - Commande #{order.id} + Commande #{order.client_order_number ?? order.id} diff --git a/mobile/src/screens/client/ProfileScreen.tsx b/mobile/src/screens/client/ProfileScreen.tsx index ba30d340..d4775391 100644 --- a/mobile/src/screens/client/ProfileScreen.tsx +++ b/mobile/src/screens/client/ProfileScreen.tsx @@ -6,6 +6,7 @@ import { StyleSheet, TouchableOpacity, Alert, + Modal, KeyboardAvoidingView, Platform, Linking, @@ -45,6 +46,9 @@ export default function ProfileScreen() { const [telegramEnabled, setTelegramEnabled] = useState(false); const [telegramLoading, setTelegramLoading] = useState(false); + // Modal confirmation infos par défaut + const [showSaveModal, setShowSaveModal] = useState(false); + const loadData = useCallback(async () => { setLoadingProfile(true); const [savedAddress, savedPhone, savedSignal, profileRes, tgStatus] = await Promise.all([ @@ -85,6 +89,7 @@ export default function ProfileScreen() { AsyncStorage.setItem(STORAGE_PHONE, defaultPhone.trim()), AsyncStorage.setItem(STORAGE_SIGNAL, signalPseudo.trim()), ]); + setShowSaveModal(false); Alert.alert("Enregistré", "Informations par défaut sauvegardées"); }; @@ -201,6 +206,74 @@ export default function ProfileScreen() { }, saveBtnText: { color: "#fff", fontSize: fontSize.sm, fontWeight: fontWeight.semibold }, saveBtnTextSecondary: { color: colors.accent }, + // Modal + modalOverlay: { + flex: 1, + backgroundColor: "rgba(0,0,0,0.6)", + justifyContent: "center", + alignItems: "center", + padding: spacing.l, + }, + modalBox: { + backgroundColor: colors.bgCard, + borderRadius: borderRadius.md, + borderWidth: 1, + borderColor: colors.borderLight, + padding: spacing.xl, + width: "100%", + maxWidth: 360, + alignItems: "center", + }, + modalIconWrap: { + width: 52, + height: 52, + borderRadius: 26, + backgroundColor: colors.accent + "22", + borderWidth: 1, + borderColor: colors.accent + "44", + justifyContent: "center", + alignItems: "center", + marginBottom: spacing.m, + }, + modalTitle: { + color: colors.textPrimary, + fontSize: fontSize.md, + fontWeight: fontWeight.bold, + textAlign: "center", + marginBottom: spacing.s, + }, + modalBody: { + color: colors.textMuted, + fontSize: fontSize.sm, + textAlign: "center", + lineHeight: 20, + marginBottom: spacing.l, + }, + modalActions: { + flexDirection: "row", + gap: spacing.m, + width: "100%", + }, + modalBtnCancel: { + flex: 1, + paddingVertical: spacing.m, + borderRadius: borderRadius.sm, + backgroundColor: "transparent", + borderWidth: 1, + borderColor: colors.borderLight, + alignItems: "center", + }, + modalBtnConfirm: { + flex: 1, + paddingVertical: spacing.m, + borderRadius: borderRadius.sm, + backgroundColor: "transparent", + borderWidth: 1, + borderColor: colors.accent + "66", + alignItems: "center", + }, + modalBtnCancelText: { color: colors.textMuted, fontSize: fontSize.sm, fontWeight: fontWeight.semibold }, + modalBtnConfirmText: { color: colors.accent, fontSize: fontSize.sm, fontWeight: fontWeight.semibold }, }), [colors]); if (loadingProfile) { @@ -316,7 +389,7 @@ export default function ProfileScreen() { setShowSaveModal(true)} > @@ -365,6 +438,46 @@ export default function ProfileScreen() { )} + + setShowSaveModal(false)} + > + setShowSaveModal(false)} + > + {}}> + + + + + Enregistrer les infos par défaut ? + + Adresse, téléphone de livraison et pseudo Signal seront sauvegardés et pré-remplis lors de vos prochaines commandes. + + + setShowSaveModal(false)} + > + Annuler + + + Confirmer + + + + + + + ); }