chore: update id order

This commit is contained in:
2026-03-28 17:00:00 +01:00
parent 5380abe8ed
commit 3bf3f5e605
48 changed files with 2347 additions and 3693 deletions
+20 -40
View File
@@ -1,67 +1,47 @@
package db package db
import ( import (
"database/sql"
"errors"
"fmt" "fmt"
"gestion/models" "gestion/models"
) )
func (d *Database) CheckAddress(addressByUser *models.Command) error { func (d *Database) CheckAddress(addressByUser *models.Command) error {
var correction models.Address var correction models.Address
err := d.DB.QueryRow( result := d.GDB.Where("invalid_address = ?", addressByUser.DeliveryAddress).First(&correction)
`SELECT invalid_address, correct_address FROM adresse_correction WHERE invalid_address = $1`, if result.Error != nil {
addressByUser.DeliveryAddress, if isNotFound(result.Error) {
).Scan(&correction.InvalidAddress, &correction.CorrectAddress) return nil
if errors.Is(err, sql.ErrNoRows) { }
return nil return fmt.Errorf("checkAddress: %w", result.Error)
} }
if err != nil { addressByUser.DeliveryAddress = correction.CorrectAddress
return fmt.Errorf("checkAddress: %w", err) return fmt.Errorf("Adresse invalide %s", correction.CorrectAddress)
}
var correctedAddress = correction.CorrectAddress
addressByUser.DeliveryAddress = correctedAddress
return fmt.Errorf("Adresse invalide %s", correctedAddress)
} }
func (d *Database) AddAddress(CorrectAddressByAdmin string, InvalidAddressByAdmin string) error { func (d *Database) AddAddress(CorrectAddressByAdmin string, InvalidAddressByAdmin string) error {
_, err := d.DB.Exec( address := models.Address{
`INSERT INTO adresse_correction InvalidAddress: InvalidAddressByAdmin,
(invalid_address, correct_address) CorrectAddress: CorrectAddressByAdmin,
VALUES ($1, $2)`, }
InvalidAddressByAdmin, CorrectAddressByAdmin, if err := d.GDB.Create(&address).Error; err != nil {
)
if err != nil {
return fmt.Errorf("addAddress: %w", err) return fmt.Errorf("addAddress: %w", err)
} }
return nil return nil
} }
func (d *Database) DeleteAddress(InvalidAddressByAdmin string, CorrectAddressByAdmin string) error { func (d *Database) DeleteAddress(InvalidAddressByAdmin string, CorrectAddressByAdmin string) error {
_, err := d.DB.Exec( result := d.GDB.Where("invalid_address = ? AND correct_address = ?", InvalidAddressByAdmin, CorrectAddressByAdmin).
`DELETE FROM adresse_correction WHERE invalid_address = $1 AND correct_address = $2`, Delete(&models.Address{})
InvalidAddressByAdmin, CorrectAddressByAdmin, if result.Error != nil {
) return fmt.Errorf("deleteAddress: %w", result.Error)
if err != nil {
return fmt.Errorf("deleteAddress: %w", err)
} }
return nil return nil
} }
func (d *Database) AllAddress() ([]models.Address, error) { func (d *Database) AllAddress() ([]models.Address, error) {
rows, err := d.DB.Query("SELECT invalid_address, correct_address FROM adresse_correction") var addresses []models.Address
if err != nil { if err := d.GDB.Find(&addresses).Error; err != nil {
return nil, fmt.Errorf("getAllAddress: %w", err) return nil, fmt.Errorf("getAllAddress: %w", err)
} }
defer rows.Close() return addresses, nil
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()
} }
+26 -133
View File
@@ -6,182 +6,75 @@ import (
) )
func (d *Database) CreateAlert(username string, message string) (models.AlertPolicy, error) { func (d *Database) CreateAlert(username string, message string) (models.AlertPolicy, error) {
alert := models.AlertPolicy{
query := ` Username: username,
INSERT INTO alerte_policy (username, status, message) Status: "true",
VALUES ($1, 'true', $2) Message: message,
RETURNING id, username, status, message, created_at, updated_at }
` if err := d.GDB.Create(&alert).Error; err != nil {
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 {
return models.AlertPolicy{}, err return models.AlertPolicy{}, err
} }
return alert, nil return alert, nil
} }
func (d *Database) GetAlertPolicy(id int) (models.AlertPolicy, error) { 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 var alert models.AlertPolicy
err := d.QueryRow(query, id).Scan(&alert.ID, &alert.Username, &alert.Status, &alert.Message, &alert.CreatedAt, &alert.UpdatedAt) if err := d.GDB.First(&alert, id).Error; err != nil {
if err != nil {
return models.AlertPolicy{}, err return models.AlertPolicy{}, err
} }
return alert, nil return alert, nil
} }
func (d *Database) GetAllAlerts() ([]models.AlertPolicy, error) { func (d *Database) GetAllAlerts() ([]models.AlertPolicy, error) {
var alerts []models.AlertPolicy
query := ` if err := d.GDB.Find(&alerts).Error; err != nil {
SELECT id, username, status, message, created_at, updated_at
FROM alerte_policy
`
rows, err := d.Query(query)
if err != nil {
return nil, err 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 return alerts, nil
} }
func (d *Database) DeleteAlertPolicy(id int) error { func (d *Database) DeleteAlertPolicy(id int) error {
return d.GDB.Delete(&models.AlertPolicy{}, id).Error
query := `
DELETE FROM alerte_policy
WHERE id = $1
`
_, err := d.Exec(query, id)
if err != nil {
return err
}
return nil
} }
// EndAlert met fin à une alerte en changeant son statut à 'false'
func (d *Database) EndAlert(id int) error { func (d *Database) EndAlert(id int) error {
result := d.GDB.Model(&models.AlertPolicy{}).
query := ` Where("id = ? AND status = 'true'", id).
UPDATE alerte_policy Update("status", "false")
SET status = 'false', updated_at = CURRENT_TIMESTAMP if result.Error != nil {
WHERE id = $1 AND status = 'true' return result.Error
`
result, err := d.Exec(query, id)
if err != nil {
return err
} }
if result.RowsAffected == 0 {
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
return fmt.Errorf("alerte non trouvée ou déjà terminée") return fmt.Errorf("alerte non trouvée ou déjà terminée")
} }
return nil return nil
} }
// ActivateAlert active une alerte en changeant son statut à 'true'
func (d *Database) ActivateAlert(id int) error { func (d *Database) ActivateAlert(id int) error {
result := d.GDB.Model(&models.AlertPolicy{}).
query := ` Where("id = ? AND status = 'false'", id).
UPDATE alerte_policy Update("status", "true")
SET status = 'true', updated_at = CURRENT_TIMESTAMP if result.Error != nil {
WHERE id = $1 AND status = 'false' return result.Error
`
result, err := d.Exec(query, id)
if err != nil {
return err
} }
if result.RowsAffected == 0 {
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
return fmt.Errorf("alerte non trouvée ou déjà active") return fmt.Errorf("alerte non trouvée ou déjà active")
} }
return nil return nil
} }
// GetActiveAlerts récupère toutes les alertes actives (status = 'true')
func (d *Database) GetActiveAlerts() ([]models.AlertPolicy, error) { func (d *Database) GetActiveAlerts() ([]models.AlertPolicy, error) {
var alerts []models.AlertPolicy
query := ` if err := d.GDB.Where("status = 'true'").Order("created_at DESC").Find(&alerts).Error; err != nil {
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 {
return nil, err 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 return alerts, nil
} }
// GetAlertsByUsername récupère toutes les alertes d'un livreur
func (d *Database) GetAlertsByUsername(username string) ([]models.AlertPolicy, error) { func (d *Database) GetAlertsByUsername(username string) ([]models.AlertPolicy, error) {
var alerts []models.AlertPolicy
query := ` if err := d.GDB.Where("username = ?", username).Order("created_at DESC").Find(&alerts).Error; err != nil {
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 {
return nil, err 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 return alerts, nil
} }
+195 -318
View File
@@ -1,117 +1,100 @@
package db package db
import ( import (
"database/sql"
"fmt" "fmt"
"gestion/models" "gestion/models"
"log" "log"
"time" "time"
"gorm.io/gorm"
) )
// AddProductInBasket ajoute un produit au panier de l'utilisateur // AddProductInBasket ajoute un produit au panier de l'utilisateur
func (d *Database) AddProductInBasket(username, nameProduct string, quantity float64, category string) (*models.Panier, error) { 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 productResult struct {
var productID int ID int `gorm:"column:id"`
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)
} }
err := d.GDB.Raw(`SELECT id FROM products WHERE LOWER(name) = LOWER(?) AND LOWER(category) = LOWER(?)`,
nameProduct, category).Scan(&productResult).Error
if err != nil { if err != nil {
return nil, fmt.Errorf("erreur lors de la recherche du produit: %w", err) 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) price, err := d.GetProductPrice(nameProduct, category, quantity)
if err != nil { if err != nil {
return nil, fmt.Errorf("erreur récupération prix: %w", err) return nil, fmt.Errorf("erreur récupération prix: %w", err)
} }
// Vérifier si le produit existe déjà dans le panier var existing struct {
var existingID int ID int `gorm:"column:id"`
var existingQuantity, existingPrice float64 Quantity float64 `gorm:"column:quantity"`
checkQuery := `SELECT id, quantity, price FROM baskets WHERE username = $1 AND product_id = $2` Price float64 `gorm:"column:price"`
err = d.QueryRow(checkQuery, username, productID).Scan(&existingID, &existingQuantity, &existingPrice) }
d.GDB.Raw(`SELECT id, quantity, price FROM baskets WHERE username = ? AND product_id = ?`,
username, productID).Scan(&existing)
if err == nil { var basket models.Panier
// Produit déjà dans le panier : cumuler quantité et prix total de la ligne if existing.ID != 0 {
newQuantity := existingQuantity + quantity newQuantity := existing.Quantity + quantity
newPrice := existingPrice + price newPrice := existing.Price + price
updateQuery := `UPDATE baskets SET quantity = $1, price = $2, created_at = CURRENT_TIMESTAMP err = d.GDB.Raw(`
WHERE id = $3 RETURNING id, username, product_id, quantity, price, created_at` UPDATE baskets SET quantity = ?, price = ?, created_at = CURRENT_TIMESTAMP
WHERE id = ? RETURNING id, username, product_id, quantity, price, created_at`,
var basket models.Panier newQuantity, newPrice, existing.ID).Scan(&basket).Error
err = d.QueryRow(updateQuery, newQuantity, newPrice, existingID).Scan(
&basket.ID,
&basket.Username,
&basket.ProductID,
&basket.Quantity,
&basket.Price,
&basket.CreatedAt,
)
if err != nil { if err != nil {
return nil, fmt.Errorf("erreur lors de la mise à jour du panier: %w", err) 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 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) { func (d *Database) GetProductPriceByID(productID int, quantity float64) (float64, error) {
var price float64 var result struct {
Price float64 `gorm:"column:price"`
// Comparaison NUMERIC précise : évite les problèmes float64 vs NUMERIC(10,3)
exactQuery := `
SELECT price FROM product_prices
WHERE product_id = $1 AND quantity = ROUND($2::NUMERIC, 3)
LIMIT 1
`
err := d.QueryRow(exactQuery, productID, quantity).Scan(&price)
if err == nil {
return price, nil
} }
// Fallback : palier inférieur le plus proche err := d.GDB.Raw(`
tierQuery := `
SELECT price FROM product_prices SELECT price FROM product_prices
WHERE product_id = $1 AND quantity <= ROUND($2::NUMERIC, 3) WHERE product_id = ? AND quantity = ROUND(?::NUMERIC, 3)
ORDER BY quantity DESC LIMIT 1 LIMIT 1`, productID, quantity).Scan(&result).Error
` if err == nil && result.Price > 0 {
err = d.QueryRow(tierQuery, productID, quantity).Scan(&price) return result.Price, nil
if err != nil {
return 0, fmt.Errorf("aucun prix trouvé pour product_id=%d qty=%.3f: %w", productID, quantity, err)
} }
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 // GetProductStockByID récupère le stock d'un produit par son ID
func (d *Database) GetProductStockByID(productID int) (float64, error) { func (d *Database) GetProductStockByID(productID int) (float64, error) {
var stock float64 var result struct {
err := d.QueryRow(`SELECT stock FROM products WHERE id = $1`, productID).Scan(&stock) Stock float64 `gorm:"column:stock"`
}
err := d.GDB.Raw(`SELECT stock FROM products WHERE id = ?`, productID).Scan(&result).Error
if err != nil { if err != nil {
return 0, fmt.Errorf("produit %d non trouvé: %w", productID, err) 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 // 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) return nil, fmt.Errorf("erreur récupération prix: %w", err)
} }
var existingID int var existing struct {
var existingQuantity, existingPrice float64 ID int `gorm:"column:id"`
checkQuery := `SELECT id, quantity, price FROM baskets WHERE username = $1 AND product_id = $2` Quantity float64 `gorm:"column:quantity"`
err = d.QueryRow(checkQuery, username, productID).Scan(&existingID, &existingQuantity, &existingPrice) Price float64 `gorm:"column:price"`
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
} }
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 var basket models.Panier
err = d.QueryRow(insertQuery, username, productID, quantity, price).Scan( if existing.ID != 0 {
&basket.ID, &basket.Username, &basket.ProductID, &basket.Quantity, &basket.Price, &basket.CreatedAt, 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 { 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 return &basket, nil
} }
// GetProductPrice récupère le prix réel d'un produit pour une quantité donnée (legacy) // 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) { func (d *Database) GetProductPrice(name, category string, quantity float64) (float64, error) {
var price float64 var result struct {
Price float64 `gorm:"column:price"`
query := ` }
err := d.GDB.Raw(`
SELECT price SELECT price
FROM product_prices pp FROM product_prices pp
INNER JOIN products p ON pp.product_id = p.id INNER JOIN products p ON pp.product_id = p.id
WHERE LOWER(p.name) = LOWER($1) WHERE LOWER(p.name) = LOWER(?)
AND LOWER(p.category) = LOWER($2) AND LOWER(p.category) = LOWER(?)
AND pp.quantity <= $3 AND pp.quantity <= ?
ORDER BY pp.quantity DESC ORDER BY pp.quantity DESC
LIMIT 1 LIMIT 1`, name, category, quantity).Scan(&result).Error
` if err != nil || result.Price == 0 {
err := d.QueryRow(query, name, category, quantity).Scan(&price)
if err != nil {
return 0, fmt.Errorf("prix produit introuvable pour %f %s: %w", quantity, name, err) return 0, fmt.Errorf("prix produit introuvable pour %f %s: %w", quantity, name, err)
} }
return result.Price, nil
return price, nil
} }
func (d *Database) GetProductStock(name, category string) (float64, error) { func (d *Database) GetProductStock(name, category string) (float64, error) {
var stock float64 var result struct {
query := `SELECT stock FROM products WHERE LOWER(name) = LOWER($1) AND LOWER(category) = LOWER($2)` Stock float64 `gorm:"column:stock"`
err := d.QueryRow(query, name, category).Scan(&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 { if err != nil {
return 0, fmt.Errorf("produit non trouvé: %w", err) 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 { 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 := d.GDB.Exec(`
result, err := d.Exec(query, quantity, name, category) UPDATE products SET stock = stock - ?
if err != nil { WHERE LOWER(name) = LOWER(?) AND LOWER(category) = LOWER(?) AND stock >= ?`,
return fmt.Errorf("erreur mise à jour stock: %w", err) quantity, name, category, quantity)
if result.Error != nil {
return fmt.Errorf("erreur mise à jour stock: %w", result.Error)
} }
if result.RowsAffected == 0 {
rows, _ := result.RowsAffected()
if rows == 0 {
return fmt.Errorf("stock insuffisant pour le produit") return fmt.Errorf("stock insuffisant pour le produit")
} }
return nil 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 // GetAllProductsInBasket récupère tous les produits du panier d'un utilisateur
func (d *Database) GetAllProductsInBasket(username string) ([]models.Panier, error) { 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, var baskets []models.Panier
p.name, p.category, p.description err := d.GDB.Raw(`
FROM baskets b SELECT b.id, b.username, b.product_id, b.quantity, b.price, b.created_at,
INNER JOIN products p ON b.product_id = p.id p.name as product_name, p.category, p.description
WHERE b.username = $1 FROM baskets b
ORDER BY b.created_at DESC` INNER JOIN products p ON b.product_id = p.id
WHERE b.username = ?
rows, err := d.Query(query, username) ORDER BY b.created_at DESC`, username).Scan(&baskets).Error
if err != nil { if err != nil {
return nil, fmt.Errorf("erreur lors de la récupération du panier: %w", err) 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 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 { func (d *Database) DecrementProductStockByID(productID int, quantity float64) error {
query := ` result := d.GDB.Exec(`
UPDATE products UPDATE products SET stock = stock - ?
SET stock = stock - $1 WHERE id = ? AND stock >= ?`, quantity, productID, quantity)
WHERE id = $2 if result.Error != nil {
AND stock >= $1 return fmt.Errorf("erreur lors de la mise à jour du stock: %w", result.Error)
`
result, err := d.Exec(query, quantity, productID)
if err != nil {
return fmt.Errorf("erreur lors de la mise à jour du stock: %w", err)
} }
if result.RowsAffected == 0 {
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("erreur lors de la vérification du stock affecté: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("stock insuffisant pour le produit %d", productID) return fmt.Errorf("stock insuffisant pour le produit %d", productID)
} }
return nil return nil
} }
// DeleteProductFromBasket supprime un produit spécifique du panier et restitue le stock. // DeleteProductFromBasket supprime un produit spécifique du panier et restitue le stock.
func (d *Database) DeleteProductFromBasket(basketID int) error { func (d *Database) DeleteProductFromBasket(basketID int) error {
tx, err := d.Begin() return d.GDB.Transaction(func(tx *gorm.DB) error {
if err != nil { var item struct {
return fmt.Errorf("erreur transaction: %w", err) ProductID int `gorm:"column:product_id"`
} Quantity float64 `gorm:"column:quantity"`
defer tx.Rollback() }
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 if err := tx.Exec(`UPDATE products SET stock = stock + ? WHERE id = ?`,
var quantity float64 item.Quantity, item.ProductID).Error; err != nil {
err = tx.QueryRow( return fmt.Errorf("erreur restitution stock: %w", err)
`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")
}
_, err = tx.Exec( result := tx.Exec(`DELETE FROM baskets WHERE id = ?`, basketID)
`UPDATE products SET stock = stock + $1 WHERE id = $2`, if result.Error != nil {
quantity, productID, return fmt.Errorf("erreur lors de la suppression du produit: %w", result.Error)
) }
if err != nil { if result.RowsAffected == 0 {
return fmt.Errorf("erreur restitution stock: %w", err) return fmt.Errorf("produit non trouvé dans le panier")
} }
return nil
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()
} }
// ClearBasket vide complètement le panier d'un utilisateur et restitue les stocks. // ClearBasket vide complètement le panier d'un utilisateur et restitue les stocks.
func (d *Database) ClearBasket(username string) error { func (d *Database) ClearBasket(username string) error {
tx, err := d.Begin() return d.GDB.Transaction(func(tx *gorm.DB) error {
if err != nil { if err := tx.Exec(`
return fmt.Errorf("erreur transaction: %w", err) UPDATE products p
} SET stock = stock + b.quantity
defer tx.Rollback() FROM baskets b
WHERE b.username = ? AND b.product_id = p.id`, username).Error; err != nil {
_, err = tx.Exec(` return fmt.Errorf("erreur restitution stock: %w", err)
UPDATE products p }
SET stock = stock + b.quantity if err := tx.Exec(`DELETE FROM baskets WHERE username = ?`, username).Error; err != nil {
FROM baskets b return fmt.Errorf("erreur lors du vidage du panier: %w", err)
WHERE b.username = $1 AND b.product_id = p.id }
`, username) return nil
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()
} }
// GetBasketTotal calcule le montant total du panier d'un utilisateur // 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) { func (d *Database) GetBasketTotal(username string) (float64, error) {
query := `SELECT COALESCE(SUM(price), 0) as total var result struct {
FROM baskets Total float64 `gorm:"column:total"`
WHERE username = $1` }
err := d.GDB.Raw(`SELECT COALESCE(SUM(price), 0) as total FROM baskets WHERE username = ?`,
var total float64 username).Scan(&result).Error
err := d.QueryRow(query, username).Scan(&total)
if err != nil { if err != nil {
return 0, fmt.Errorf("erreur lors du calcul du total: %w", err) return 0, fmt.Errorf("erreur lors du calcul du total: %w", err)
} }
return result.Total, nil
return total, nil
} }
// GetBasketItemCount compte le nombre d'items dans le panier // GetBasketItemCount compte le nombre d'items dans le panier
func (d *Database) GetBasketItemCount(username string) (int, error) { func (d *Database) GetBasketItemCount(username string) (int, error) {
query := `SELECT COUNT(*) FROM baskets WHERE username = $1` var result struct {
Count int `gorm:"column:count"`
var count int }
err := d.QueryRow(query, username).Scan(&count) err := d.GDB.Raw(`SELECT COUNT(*) as count FROM baskets WHERE username = ?`,
username).Scan(&result).Error
if err != nil { if err != nil {
return 0, fmt.Errorf("erreur lors du comptage des items: %w", err) return 0, fmt.Errorf("erreur lors du comptage des items: %w", err)
} }
return result.Count, nil
return count, nil
} }
// UpdateBasketItemQuantity met à jour la quantité d'un item du panier // 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 { if quantity <= 0 {
return fmt.Errorf("la quantité doit être supérieure à 0") return fmt.Errorf("la quantité doit être supérieure à 0")
} }
result := d.GDB.Exec(`UPDATE baskets SET quantity = ?, created_at = CURRENT_TIMESTAMP WHERE id = ?`,
query := `UPDATE baskets SET quantity = $1, created_at = CURRENT_TIMESTAMP WHERE id = $2` quantity, basketID)
result, err := d.Exec(query, quantity, basketID) if result.Error != nil {
if err != nil { return fmt.Errorf("erreur lors de la mise à jour de la quantité: %w", result.Error)
return fmt.Errorf("erreur lors de la mise à jour de la quantité: %w", err)
} }
if result.RowsAffected == 0 {
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("erreur lors de la vérification des lignes affectées: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("produit non trouvé dans le panier") return fmt.Errorf("produit non trouvé dans le panier")
} }
return nil return nil
} }
// ExtendBasketReservations prolonge les réservations // ExtendBasketReservations prolonge les réservations
func (d *Database) ExtendBasketReservations(username string) error { func (d *Database) ExtendBasketReservations(username string) error {
query := `SELECT product_id, quantity FROM baskets WHERE username = $1` var items []struct {
rows, err := d.Query(query, username) ProductID int `gorm:"column:product_id"`
if err != nil { 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) 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 { for _, item := range items {
var stock int var stockResult struct {
err := d.QueryRow(`SELECT stock FROM products WHERE id = $1`, item.ProductID).Scan(&stock) Stock float64 `gorm:"column:stock"`
if err != nil { }
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) return fmt.Errorf("produit %d non trouvé: %w", item.ProductID, err)
} }
if stockResult.Stock < item.Quantity {
if stock < item.Quantity { return fmt.Errorf("stock insuffisant pour le produit %d (demandé: %g, disponible: %g)",
return fmt.Errorf("stock insuffisant pour le produit %d (demandé: %d, disponible: %d)", item.ProductID, item.Quantity, stockResult.Stock)
item.ProductID, item.Quantity, stock)
} }
} }
// Prolonger les réservations de 15 minutes
newReservation := time.Now().Add(15 * time.Minute) newReservation := time.Now().Add(15 * time.Minute)
updateQuery := `UPDATE baskets if err := d.GDB.Exec(`UPDATE baskets SET reserved_until = ? WHERE username = ?`,
SET reserved_until = $1 newReservation, username).Error; err != nil {
WHERE username = $2`
_, err = d.Exec(updateQuery, newReservation, username)
if err != nil {
return fmt.Errorf("erreur prolongation: %w", err) 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 // CheckBasketReservations vérifie si les réservations sont expirées
func (d *Database) CheckBasketReservations(username string) (bool, error) { func (d *Database) CheckBasketReservations(username string) (bool, error) {
query := `SELECT COUNT(*) FROM baskets var result struct {
WHERE username = $1 Count int `gorm:"column:count"`
AND (reserved_until IS NULL OR reserved_until < CURRENT_TIMESTAMP)` }
err := d.GDB.Raw(`
var expiredCount int SELECT COUNT(*) as count FROM baskets
err := d.QueryRow(query, username).Scan(&expiredCount) WHERE username = ? AND (reserved_until IS NULL OR reserved_until < CURRENT_TIMESTAMP)`,
username).Scan(&result).Error
if err != nil { if err != nil {
return false, err return false, err
} }
return result.Count > 0, nil
return expiredCount > 0, nil
} }
func (d *Database) GetBasketItems(username string) ([]map[string]interface{}, error) { func (d *Database) GetBasketItems(username string) ([]map[string]any, error) {
query := `SELECT product_id, quantity, price FROM baskets WHERE username = $1` var items []map[string]any
rows, err := d.Query(query, username) if err := d.GDB.Raw(`SELECT product_id, quantity::float8 as quantity, price::float8 as price FROM baskets WHERE username = ?`,
if err != nil { username).Scan(&items).Error; err != nil {
return nil, err 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 return items, nil
} }
+184 -267
View File
@@ -1,164 +1,155 @@
// ============================================ // ============================================
// db/cancel_commands_db.go // db/cancel_commands_db.go
// FONCTIONS DB ATOMIQUES POUR L'ANNULATION // FONCTIONS DB ATOMIQUES POUR L'ANNULATION
// VERSION 100% SÉCURISÉE - FIX ETA CHECK
// ============================================ // ============================================
package db package db
import ( import (
"database/sql"
"fmt" "fmt"
"log" "log"
"slices" "slices"
"time"
)
// ============================================ "gorm.io/gorm"
// ANNULATION ATOMIQUE )
// ============================================
func (d *Database) CancelCommandAtomic(commandID int, username, reason string, force bool) (int, error) { 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) log.Printf("🔒 [CancelAtomic] START - cmd=%d, user=%s, force=%v", commandID, username, force)
tx, err := d.Begin() var penalty int
if err != nil {
return 0, fmt.Errorf("erreur transaction: %w", err)
}
defer tx.Rollback()
var currentStatus, cmdUsername, livreurAssign string err := d.GDB.Transaction(func(tx *gorm.DB) error {
err = tx.QueryRow(` var cmdResult struct {
SELECT status, username, COALESCE(livreur_assign, '') Status string `gorm:"column:status"`
FROM commandes Username string `gorm:"column:username"`
WHERE id = $1 LivreurAssign string `gorm:"column:livreur_assign"`
FOR UPDATE
`, commandID).Scan(&currentStatus, &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)
} }
} else { err := tx.Raw(`
log.Printf("✅ [CancelAtomic] Annulation SANS PÉNALITÉ - Aucun livreur assigné") SELECT status, username, COALESCE(livreur_assign, '') as livreur_assign
} FROM commandes WHERE id = ? FOR UPDATE`, commandID).Scan(&cmdResult).Error
if err != nil {
if isLateCancel && !force { return err
return 0, fmt.Errorf("confirmation requise") }
} if cmdResult.Username == "" {
return fmt.Errorf("commande non trouvée")
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)
} }
}
if _, err = tx.Exec(` log.Printf("📋 [CancelAtomic] Trouvée - status=%s, owner=%s, livreur=%s",
INSERT INTO command_logs (command_id, status, message, author, created_at) cmdResult.Status, cmdResult.Username, cmdResult.LivreurAssign)
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)
}
if err := tx.Commit(); err != nil { if cmdResult.Username != username {
return 0, fmt.Errorf("erreur commit: %w", err) return fmt.Errorf("commande ne vous appartient pas")
} }
if livreurAssign != "" { nonCancellableStatuses := []string{"livre", "approved", "cancelled", "disabled"}
go func() { if slices.Contains(nonCancellableStatuses, cmdResult.Status) {
if err := d.CleanupCompletedCommandFromQueue(commandID, livreurAssign); err != nil { return fmt.Errorf("impossible d'annuler")
log.Printf("⚠️ [CancelAtomic] Erreur cleanup queue: %v", err) }
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) log.Printf("🎉 [CancelAtomic] SUCCÈS - Commande %d annulée", commandID)
return penalty, nil return penalty, nil
} }
// ============================================
// ✅ NOUVELLE FONCTION: CHECK ETA VALIDE
// ============================================
// CheckCommandETAExistsAndValid vérifie si une ETA RÉELLE existe (> 0 minutes, non expirée) // CheckCommandETAExistsAndValid vérifie si une ETA RÉELLE existe (> 0 minutes, non expirée)
func (d *Database) CheckCommandETAExistsAndValid(commandID int) bool { func (d *Database) CheckCommandETAExistsAndValid(commandID int) bool {
etaKey := fmt.Sprintf("command:eta:%d", commandID) etaKey := fmt.Sprintf("command:eta:%d", commandID)
// Récupérer l'ETA depuis Redis
etaMinutesStr, err := Redis.Get(RedisCtx, etaKey).Result() etaMinutesStr, err := Redis.Get(RedisCtx, etaKey).Result()
if err != nil { if err != nil {
log.Printf("⚠️ [CheckETA] Pas d'ETA trouvée pour cmd %d", commandID) log.Printf("⚠️ [CheckETA] Pas d'ETA trouvée pour cmd %d", commandID)
return false return false
} }
// Parser l'ETA
var etaMinutes int var etaMinutes int
_, err = fmt.Sscanf(etaMinutesStr, "%d", &etaMinutes) _, err = fmt.Sscanf(etaMinutesStr, "%d", &etaMinutes)
if err != nil || etaMinutes <= 0 { if err != nil || etaMinutes <= 0 {
@@ -166,7 +157,6 @@ func (d *Database) CheckCommandETAExistsAndValid(commandID int) bool {
return false return false
} }
// Vérifier le TTL (si l'ETA existe, elle doit avoir un TTL)
ttl, err := Redis.TTL(RedisCtx, etaKey).Result() ttl, err := Redis.TTL(RedisCtx, etaKey).Result()
if err != nil || ttl <= 0 { if err != nil || ttl <= 0 {
log.Printf("⚠️ [CheckETA] ETA expirée pour cmd %d", commandID) log.Printf("⚠️ [CheckETA] ETA expirée pour cmd %d", commandID)
@@ -177,106 +167,76 @@ func (d *Database) CheckCommandETAExistsAndValid(commandID int) bool {
return true return true
} }
// ============================================
// SUPPRESSION ATOMIQUE
// ============================================
func (d *Database) DeleteCommandAtomic(commandID int, deletedBy, role string) error { func (d *Database) DeleteCommandAtomic(commandID int, deletedBy, role string) error {
log.Printf("🔒 [DeleteAtomic] START - cmd=%d, by=%s (%s)", commandID, deletedBy, role) log.Printf("🔒 [DeleteAtomic] START - cmd=%d, by=%s (%s)", commandID, deletedBy, role)
// ✅ TRANSACTION return d.GDB.Transaction(func(tx *gorm.DB) error {
tx, err := d.Begin() var cmdResult struct {
if err != nil { Status string `gorm:"column:status"`
return fmt.Errorf("erreur transaction: %w", err) Username string `gorm:"column:username"`
} LivreurAssign string `gorm:"column:livreur_assign"`
defer tx.Rollback() }
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 log.Printf("📋 [DeleteAtomic] Trouvée - status=%s, client=%s", cmdResult.Status, cmdResult.Username)
var currentStatus, cmdUsername, livreurAssign string
err = tx.QueryRow(`
SELECT status, username, COALESCE(livreur_assign, '')
FROM commandes
WHERE id = $1
FOR UPDATE
`, commandID).Scan(&currentStatus, &cmdUsername, &livreurAssign)
if err == sql.ErrNoRows { if err := tx.Exec(`
return fmt.Errorf("commande non trouvée") UPDATE products p
} SET stock = stock + ci.quantite, updated_at = CURRENT_TIMESTAMP
if err != nil { FROM command_items ci
return err 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 if err := tx.Exec(`DELETE FROM command_items WHERE command_id = ?`, commandID).Error; err != nil {
_, err = tx.Exec(` return err
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 != nil { result := tx.Exec(`DELETE FROM commandes WHERE id = ?`, commandID)
log.Printf("⚠️ [DeleteAtomic] Erreur remboursement: %v", err) if result.Error != nil {
} else { return result.Error
log.Printf("✅ [DeleteAtomic] Stock remboursé") }
} if result.RowsAffected == 0 {
return fmt.Errorf("commande non trouvée")
}
// ✅ LOG AVANT SUPPRESSION log.Printf("✅ [DeleteAtomic] Supprimée de la DB")
_, 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)
// ✅ SUPPRIMER ITEMS livreur := cmdResult.LivreurAssign
_, err = tx.Exec(`DELETE FROM command_items WHERE command_id = $1`, commandID) cmdUsername := cmdResult.Username
if err != nil {
return err
}
// ✅ SUPPRIMER COMMANDE if livreur != "" {
result, err := tx.Exec(`DELETE FROM commandes WHERE id = $1`, commandID) go d.RemoveCommandFromAllQueues(commandID, livreur)
if err != nil { }
return err go func() {
} Redis.Del(RedisCtx,
fmt.Sprintf("command:%d", commandID),
fmt.Sprintf("client:%s", cmdUsername),
fmt.Sprintf("client:%s:commands", cmdUsername),
)
}()
rows, _ := result.RowsAffected() return nil
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
} }
// ============================================
// FONCTIONS HELPERS (déjà sécurisées)
// ============================================
func (d *Database) GetCommandPositionInQueue(livreurUsername string, commandID int) (int, error) { func (d *Database) GetCommandPositionInQueue(livreurUsername string, commandID int) (int, error) {
queueKey := fmt.Sprintf("queue:deliveryman:%s", livreurUsername) queueKey := fmt.Sprintf("queue:deliveryman:%s", livreurUsername)
commandIDStr := fmt.Sprintf("%d", commandID) commandIDStr := fmt.Sprintf("%d", commandID)
@@ -285,104 +245,61 @@ func (d *Database) GetCommandPositionInQueue(livreurUsername string, commandID i
if err != nil { if err != nil {
return 0, fmt.Errorf("commande non trouvée dans la queue") return 0, fmt.Errorf("commande non trouvée dans la queue")
} }
return int(rank) + 1, nil 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 := ` 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 FROM commandes
WHERE status = 'cancelled' WHERE status = 'cancelled'`
`
args := []interface{}{} args := []any{}
argPos := 1
if username != "" { if username != "" {
query += fmt.Sprintf(" AND username = $%d", argPos) query += " AND username = ?"
args = append(args, username) args = append(args, username)
argPos++
} }
query += " ORDER BY updated_at DESC" query += " ORDER BY updated_at DESC"
if limit > 0 { if limit > 0 {
query += fmt.Sprintf(" LIMIT $%d", argPos) query += " LIMIT ?"
args = append(args, limit) args = append(args, limit)
} }
rows, err := d.Query(query, args...) var commands []map[string]any
if err != nil { if err := d.GDB.Raw(query, args...).Scan(&commands).Error; err != nil {
return nil, fmt.Errorf("erreur récupération: %w", err) 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 return commands, nil
} }
// ============================================
// GESTION DES PÉNALITÉS
// ============================================
// AddClientPenalty ajoute une pénalité à un client // AddClientPenalty ajoute une pénalité à un client
func (d *Database) AddClientPenalty(username string, points int) error { func (d *Database) AddClientPenalty(username string, points int) error {
log.Printf("⚠️ [AddPenalty] Ajout pénalité: %d points pour client %s", points, username) log.Printf("⚠️ [AddPenalty] Ajout pénalité: %d points pour client %s", points, username)
// ✅ Validation
if points <= 0 { if points <= 0 {
return fmt.Errorf("points invalides: %d", points) return fmt.Errorf("points invalides: %d", points)
} }
if username == "" { if username == "" {
return fmt.Errorf("username vide") return fmt.Errorf("username vide")
} }
// ✅ UPDATE dans PostgreSQL result := d.GDB.Exec(`
query := `UPDATE clients UPDATE clients
SET amende = amende + $1, updated_at = CURRENT_TIMESTAMP SET amende = amende + ?, updated_at = CURRENT_TIMESTAMP
WHERE username = $2` WHERE username = ?`, points, username)
if result.Error != nil {
result, err := d.Exec(query, points, username) log.Printf("❌ [AddPenalty] Erreur UPDATE: %v", result.Error)
if err != nil { return fmt.Errorf("erreur ajout pénalité: %w", result.Error)
log.Printf("❌ [AddPenalty] Erreur UPDATE: %v", err)
return fmt.Errorf("erreur ajout pénalité: %w", err)
} }
if result.RowsAffected == 0 {
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("erreur vérification: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("client non trouvé: %s", username) return fmt.Errorf("client non trouvé: %s", username)
} }
log.Printf("✅ [AddPenalty] Pénalité ajoutée: +%d points pour %s", points, 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) cacheKey := fmt.Sprintf("client:%s", username)
Redis.Del(RedisCtx, cacheKey) Redis.Del(RedisCtx, cacheKey)
+27 -41
View File
@@ -3,21 +3,24 @@ package db
import ( import (
"fmt" "fmt"
"regexp" "regexp"
"time"
) )
var hexColorRegex = regexp.MustCompile(`^#[0-9A-Fa-f]{6}$`) var hexColorRegex = regexp.MustCompile(`^#[0-9A-Fa-f]{6}$`)
type Category struct { type Category struct {
ID int `json:"id"` ID int `json:"id" gorm:"primaryKey;autoIncrement"`
Name string `json:"name"` Name string `json:"name" gorm:"column:name"`
Color string `json:"color"` Color string `json:"color" gorm:"column:color"`
IsComingSoon bool `json:"is_coming_soon"` IsComingSoon bool `json:"is_coming_soon" gorm:"column:is_coming_soon"`
CreatedAt string `json:"created_at"` CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
} }
func (Category) TableName() string { return "categories" }
func ValidateCategoryColor(color string) error { func ValidateCategoryColor(color string) error {
if color == "" { if color == "" {
return nil // valeur par défaut utilisée return nil
} }
if !hexColorRegex.MatchString(color) { if !hexColorRegex.MatchString(color) {
return fmt.Errorf("couleur invalide : format hexadécimal requis (ex: #7c3aed)") 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) { 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 var categories []Category
for rows.Next() { if err := d.GDB.Order("name ASC").Find(&categories).Error; err != nil {
var c Category return nil, err
if err := rows.Scan(&c.ID, &c.Name, &c.Color, &c.IsComingSoon, &c.CreatedAt); err != nil {
return nil, err
}
categories = append(categories, c)
} }
if categories == nil { if categories == nil {
categories = []Category{} categories = []Category{}
@@ -50,12 +43,8 @@ func (d *Database) CreateCategory(name, color string, isComingSoon bool) (*Categ
if color == "" { if color == "" {
color = "#7c3aed" color = "#7c3aed"
} }
var c Category c := Category{Name: name, Color: color, IsComingSoon: isComingSoon}
err := d.QueryRow( if err := d.GDB.Create(&c).Error; err != nil {
`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 {
return nil, err return nil, err
} }
return &c, nil return &c, nil
@@ -66,38 +55,35 @@ func (d *Database) UpdateCategory(id int, name, color string, isComingSoon bool)
color = "#7c3aed" color = "#7c3aed"
} }
var c Category var c Category
err := d.QueryRow( if err := d.GDB.First(&c, id).Error; err != nil {
`UPDATE categories SET name = $1, color = $2, is_coming_soon = $3 WHERE id = $4 RETURNING id, name, color, is_coming_soon, created_at`, return nil, err
name, color, isComingSoon, id, }
).Scan(&c.ID, &c.Name, &c.Color, &c.IsComingSoon, &c.CreatedAt) if err := d.GDB.Model(&c).Updates(Category{Name: name, Color: color, IsComingSoon: isComingSoon}).Error; err != nil {
if err != nil {
return nil, err return nil, err
} }
return &c, nil return &c, nil
} }
func (d *Database) DeleteCategory(id int) error { func (d *Database) DeleteCategory(id int) error {
var count int var count int64
err := d.QueryRow(`SELECT COUNT(*) FROM products WHERE category = (SELECT name FROM categories WHERE id = $1)`, id).Scan(&count) d.GDB.Table("products").
if err != nil { Where("category = (SELECT name FROM categories WHERE id = ?)", id).
return err Count(&count)
}
if count > 0 { if count > 0 {
return fmt.Errorf("catégorie utilisée par %d produit(s)", count) return fmt.Errorf("catégorie utilisée par %d produit(s)", count)
} }
res, err := d.Exec(`DELETE FROM categories WHERE id = $1`, id) result := d.GDB.Delete(&Category{}, id)
if err != nil { if result.Error != nil {
return err return result.Error
} }
n, _ := res.RowsAffected() if result.RowsAffected == 0 {
if n == 0 {
return fmt.Errorf("catégorie non trouvée") return fmt.Errorf("catégorie non trouvée")
} }
return nil return nil
} }
func (d *Database) CategoryExists(name string) (bool, error) { func (d *Database) CategoryExists(name string) (bool, error) {
var count int var count int64
err := d.QueryRow(`SELECT COUNT(*) FROM categories WHERE name = $1`, name).Scan(&count) err := d.GDB.Model(&Category{}).Where("name = ?", name).Count(&count).Error
return count > 0, err return count > 0, err
} }
+294 -371
View File
@@ -1,26 +1,32 @@
package db package db
import ( import (
"database/sql"
"encoding/json" "encoding/json"
"fmt" "fmt"
"gestion/models" "gestion/models"
"log" "log"
"strings" "strings"
"time"
"gorm.io/gorm"
) )
func (d *Database) CreateClient(client *models.Client) error { func (d *Database) CreateClient(client *models.Client) error {
query := `INSERT INTO clients (username, password, nom, prenom, telephone, command, amende, must_change_password, created_at) var result struct {
VALUES ($1, $2, $3, $4, $5, 0, 0.0, $6, CURRENT_TIMESTAMP) ID int `gorm:"column:id"`
RETURNING id, created_at` CreatedAt time.Time `gorm:"column:created_at"`
}
err := d.QueryRow(query, client.Username, client.Password, client.Nom, client.Prenom, client.Telephone, client.MustChangePassword).Scan( err := d.GDB.Raw(`
&client.ID, INSERT INTO clients (username, password, nom, prenom, telephone, command, amende, must_change_password, created_at)
&client.CreatedAt, 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 { if err != nil {
return fmt.Errorf("erreur lors de la création du client: %w", err) 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) log.Printf("✅ Client créé avec succès: %s %s (ID: %d)", client.Prenom, client.Nom, client.ID)
return nil return nil
@@ -28,110 +34,109 @@ func (d *Database) CreateClient(client *models.Client) error {
// GetClientByID récupère un client par son ID // GetClientByID récupère un client par son ID
func (d *Database) GetClientByID(id int) (*models.Client, error) { func (d *Database) GetClientByID(id int) (*models.Client, error) {
var client models.Client var row struct {
query := `SELECT id, username, password, nom, prenom, telephone, command, amende, COALESCE(points_extra, '{}'::jsonb), created_at ID int `gorm:"column:id"`
FROM clients WHERE id = $1` Username string `gorm:"column:username"`
Password string `gorm:"column:password"`
var pointsExtraJSON []byte Nom string `gorm:"column:nom"`
err := d.QueryRow(query, id).Scan( Prenom string `gorm:"column:prenom"`
&client.ID, Telephone string `gorm:"column:telephone"`
&client.Username, Command int `gorm:"column:command"`
&client.Password, Amende float64 `gorm:"column:amende"`
&client.Nom, PointsExtraJSON []byte `gorm:"column:points_extra"`
&client.Prenom, CreatedAt time.Time `gorm:"column:created_at"`
&client.Telephone,
&client.Command,
&client.Amende,
&pointsExtraJSON,
&client.CreatedAt,
)
if err == sql.ErrNoRows {
return nil, fmt.Errorf("client non trouvé")
} }
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 { if err != nil {
return nil, fmt.Errorf("erreur lors de la récupération du client: %w", err) return nil, fmt.Errorf("erreur lors de la récupération du client: %w", err)
} }
if row.ID == 0 {
if len(pointsExtraJSON) > 0 { return nil, fmt.Errorf("client non trouvé")
json.Unmarshal(pointsExtraJSON, &client.PointsExtra)
} }
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 // GetAllClients récupère tous les clients
func (d *Database) GetAllClients() ([]*models.Client, error) { 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 var rows []struct {
FROM clients ORDER BY created_at DESC` ID int `gorm:"column:id"`
Username string `gorm:"column:username"`
rows, err := d.Query(query) 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 { if err != nil {
return nil, fmt.Errorf("erreur lors de la récupération des clients: %w", err) return nil, fmt.Errorf("erreur lors de la récupération des clients: %w", err)
} }
defer rows.Close()
var clients []*models.Client clients := make([]*models.Client, 0, len(rows))
for rows.Next() { for _, row := range rows {
client := &models.Client{} client := &models.Client{
var pointsExtraJSON []byte ID: row.ID,
err := rows.Scan( Username: row.Username,
&client.ID, Password: row.Password,
&client.Username, Nom: row.Nom,
&client.Password, Prenom: row.Prenom,
&client.Nom, Telephone: row.Telephone,
&client.Prenom, Command: row.Command,
&client.Telephone, Amende: row.Amende,
&client.Command, ReferralBalance: row.ReferralBalance,
&client.Amende, CreatedAt: row.CreatedAt,
&client.ReferralBalance,
&pointsExtraJSON,
&client.CreatedAt,
)
if err != nil {
return nil, fmt.Errorf("erreur lors du scan du client: %w", err)
} }
client.PointsExtra = map[string]int{} client.PointsExtra = map[string]int{}
if len(pointsExtraJSON) > 0 { if len(row.PointsExtraJSON) > 0 {
json.Unmarshal(pointsExtraJSON, &client.PointsExtra) json.Unmarshal(row.PointsExtraJSON, &client.PointsExtra)
} }
clients = append(clients, client) 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 return clients, nil
} }
// UpdateClient met à jour un client existant // UpdateClient met à jour un client existant
func (d *Database) UpdateClient(client *models.Client) error { func (d *Database) UpdateClient(client *models.Client) error {
query := `UPDATE clients result := d.GDB.Exec(`
SET username = $1, password = $2, nom = $3, prenom = $4, telephone = $5, UPDATE clients
command = $6, amende = $7 SET username = ?, password = ?, nom = ?, prenom = ?, telephone = ?,
WHERE id = $8` command = ?, amende = ?
WHERE id = ?`,
result, err := d.Exec(query, client.Username, client.Password, client.Nom, client.Prenom, client.Telephone,
client.Username, client.Command, client.Amende, client.ID,
client.Password,
client.Nom,
client.Prenom,
client.Telephone,
client.Command,
client.Amende,
client.ID,
) )
if err != nil { if result.Error != nil {
return fmt.Errorf("erreur lors de la mise à jour du client: %w", err) return fmt.Errorf("erreur lors de la mise à jour du client: %w", result.Error)
} }
if result.RowsAffected == 0 {
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("erreur lors de la vérification des lignes affectées: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("client non trouvé") 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 { func (d *Database) DeleteClient(id int) error {
_ = d.RevokeAllUserTokens(id, "client") _ = d.RevokeAllUserTokens(id, "client")
query := `DELETE FROM clients WHERE id = $1` result := d.GDB.Exec(`DELETE FROM clients WHERE id = ?`, id)
if result.Error != nil {
result, err := d.Exec(query, id) return fmt.Errorf("erreur lors de la suppression du client: %w", result.Error)
if err != nil {
return fmt.Errorf("erreur lors de la suppression du client: %w", err)
} }
if result.RowsAffected == 0 {
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("erreur lors de la vérification des lignes affectées: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("client non trouvé") 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 // UpdateClientPassword met à jour le mot de passe d'un client
func (d *Database) UpdateClientPassword(clientID int, hashedPassword string) error { func (d *Database) UpdateClientPassword(clientID int, hashedPassword string) error {
query := `UPDATE clients SET password = $1 WHERE id = $2` result := d.GDB.Exec(`UPDATE clients SET password = ? WHERE id = ?`, hashedPassword, clientID)
if result.Error != nil {
result, err := d.Exec(query, hashedPassword, clientID) return fmt.Errorf("erreur lors de la mise à jour du mot de passe: %w", result.Error)
if err != nil {
return fmt.Errorf("erreur lors de la mise à jour du mot de passe: %w", err)
} }
if result.RowsAffected == 0 {
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("erreur lors de la vérification: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("client non trouvé") 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 // UpdateClientPasswordAndClearFlag met à jour le mot de passe et remet must_change_password à false
func (d *Database) UpdateClientPasswordAndClearFlag(clientID int, hashedPassword string) error { 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 := d.GDB.Exec(`
UPDATE clients SET password = ?, must_change_password = FALSE, updated_at = CURRENT_TIMESTAMP
result, err := d.Exec(query, hashedPassword, clientID) WHERE id = ?`, hashedPassword, clientID)
if err != nil { if result.Error != nil {
return fmt.Errorf("erreur lors de la mise à jour du mot de passe: %w", err) return fmt.Errorf("erreur lors de la mise à jour du mot de passe: %w", result.Error)
} }
if result.RowsAffected == 0 {
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("erreur lors de la vérification: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("client non trouvé") return fmt.Errorf("client non trouvé")
} }
@@ -210,19 +193,18 @@ func (d *Database) GetClientStats(clientID int) (map[string]interface{}, error)
return nil, err return nil, err
} }
// Compter les commandes du client var statsResult struct {
var totalCommands, pendingCommands, completedCommands int Total int `gorm:"column:total"`
Pending int `gorm:"column:pending"`
countQuery := `SELECT Completed int `gorm:"column:completed"`
COUNT(*) as total, }
SUM(CASE WHEN status = 'pending' OR status = 'livre' THEN 1 ELSE 0 END) as pending, if err := d.GDB.Raw(`
SUM(CASE WHEN status = 'approved' THEN 1 ELSE 0 END) as completed SELECT
FROM commandes WHERE username = $1` COUNT(*) as total,
COALESCE(SUM(CASE WHEN status = 'pending' OR status = 'livre' THEN 1 ELSE 0 END), 0) as pending,
err = d.QueryRow(countQuery, client.Username).Scan(&totalCommands, &pendingCommands, &completedCommands) COALESCE(SUM(CASE WHEN status = 'approved' THEN 1 ELSE 0 END), 0) as completed
if err != nil { FROM commandes WHERE username = ?`, client.Username).Scan(&statsResult).Error; err != nil {
log.Printf("⚠️ Erreur calcul stats: %v", err) log.Printf("⚠️ Erreur calcul stats: %v", err)
totalCommands, pendingCommands, completedCommands = 0, 0, 0
} }
stats := map[string]interface{}{ stats := map[string]interface{}{
@@ -231,9 +213,9 @@ func (d *Database) GetClientStats(clientID int) (map[string]interface{}, error)
"nom": client.Nom, "nom": client.Nom,
"prenom": client.Prenom, "prenom": client.Prenom,
"telephone": client.Telephone, "telephone": client.Telephone,
"total_commands": totalCommands, "total_commands": statsResult.Total,
"pending_commands": pendingCommands, "pending_commands": statsResult.Pending,
"completed_commands": completedCommands, "completed_commands": statsResult.Completed,
"points_extra": client.PointsExtra, "points_extra": client.PointsExtra,
"amende": client.Amende, "amende": client.Amende,
"member_since": client.CreatedAt, "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) { func (d *Database) GetClientAmende(username string) (float64, error) {
var amende float64 var result struct {
query := `SELECT COALESCE(amende, 0) FROM clients WHERE username = $1` Amende float64 `gorm:"column:amende"`
}
err := d.QueryRow(query, username).Scan(&amende) err := d.GDB.Raw(`SELECT COALESCE(amende, 0) as amende FROM clients WHERE username = ?`, username).Scan(&result).Error
if err != nil { if err != nil {
log.Printf("❌ [GetClientAmende] Erreur pour %s: %v", username, err) log.Printf("❌ [GetClientAmende] Erreur pour %s: %v", username, err)
return 0, fmt.Errorf("erreur récupération pénalités: %w", err) return 0, fmt.Errorf("erreur récupération pénalités: %w", err)
} }
log.Printf("💰 [GetClientAmende] Client %s: %.2f points", username, amende) log.Printf("💰 [GetClientAmende] Client %s: %.2f points", username, result.Amende)
return amende, nil return result.Amende, nil
} }
func (d *Database) PayClientPenalties(username string, amountPaid float64) error { func (d *Database) PayClientPenalties(username string, amountPaid float64) error {
log.Printf("💳 [PayClientPenalties] Paiement de %.2f points pour %s", amountPaid, username) log.Printf("💳 [PayClientPenalties] Paiement de %.2f points pour %s", amountPaid, username)
// Vérifier le montant actuel
currentAmount, err := d.GetClientAmende(username) currentAmount, err := d.GetClientAmende(username)
if err != nil { if err != nil {
return err 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) return fmt.Errorf("montant insuffisant: %.2f payé, %.2f requis", amountPaid, currentAmount)
} }
query := `UPDATE clients result := d.GDB.Exec(`
SET amende = 0, updated_at = CURRENT_TIMESTAMP UPDATE clients SET amende = 0, updated_at = CURRENT_TIMESTAMP
WHERE username = $1` WHERE username = ?`, username)
if result.Error != nil {
result, err := d.Exec(query, username) log.Printf("❌ [PayClientPenalties] Erreur UPDATE: %v", result.Error)
if err != nil { return fmt.Errorf("erreur paiement pénalités: %w", result.Error)
log.Printf("❌ [PayClientPenalties] Erreur UPDATE: %v", err)
return fmt.Errorf("erreur paiement pénalités: %w", err)
} }
if result.RowsAffected == 0 {
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("erreur vérification: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("client non trouvé") 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 // IncrementClientCommandCount incrémente le compteur de commandes du client
func (d *Database) IncrementClientCommandCount(username string) error { func (d *Database) IncrementClientCommandCount(username string) error {
query := `UPDATE clients result := d.GDB.Exec(`UPDATE clients SET command = command + 1 WHERE username = ?`, username)
SET command = command + 1 if result.Error != nil {
WHERE username = $1` return fmt.Errorf("erreur lors de l'incrémentation du compteur: %w", result.Error)
result, err := d.Exec(query, username)
if err != nil {
return fmt.Errorf("erreur lors de l'incrémentation du compteur: %w", err)
} }
if result.RowsAffected == 0 {
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("erreur lors de la vérification: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("client non trouvé") return fmt.Errorf("client non trouvé")
} }
@@ -323,23 +288,19 @@ func (d *Database) AddClientPointsByCategory(username string, points int, poolKe
if poolKey == "" { if poolKey == "" {
poolKey = "pool_0" poolKey = "pool_0"
} }
result, err := d.Exec(` result := d.GDB.Exec(`
UPDATE clients UPDATE clients
SET points_extra = jsonb_set( SET points_extra = jsonb_set(
COALESCE(points_extra, '{}'::jsonb), COALESCE(points_extra, '{}'::jsonb),
ARRAY[$2], ARRAY[?],
to_jsonb(COALESCE((points_extra->>$2)::int, 0) + $3) to_jsonb(COALESCE((points_extra->>?)::int, 0) + ?)
), updated_at = CURRENT_TIMESTAMP ), updated_at = CURRENT_TIMESTAMP
WHERE username = $1 WHERE username = ?`,
`, username, poolKey, points) poolKey, poolKey, points, username)
if err != nil { if result.Error != nil {
return fmt.Errorf("erreur lors de l'ajout de points: %w", err) return fmt.Errorf("erreur lors de l'ajout de points: %w", result.Error)
} }
rowsAffected, err := result.RowsAffected() if result.RowsAffected == 0 {
if err != nil {
return fmt.Errorf("erreur lors de la vérification: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("client non trouvé") return fmt.Errorf("client non trouvé")
} }
log.Printf("✅ %d points (key=%s) ajoutés au client %s", points, poolKey, username) 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) { func (d *Database) CalculateAndAddPointsForCommand(commandID int, username string) (int, error) {
tx, err := d.Begin() var totalPoints int
if err != nil { err := d.GDB.Transaction(func(tx *gorm.DB) error {
return 0, fmt.Errorf("erreur transaction: %w", err) points, _, err := d.CalculateAndAddPointsForCommandTx(tx, commandID, username)
} if err != nil {
defer tx.Rollback() return err
}
points, _, err := d.CalculateAndAddPointsForCommandTx(tx, commandID, username) totalPoints = points
return nil
})
if err != nil { if err != nil {
return 0, err return 0, err
} }
return totalPoints, nil
if err := tx.Commit(); err != nil {
return 0, fmt.Errorf("erreur commit: %w", err)
}
return points, nil
} }
func (d *Database) GetClientByTelephone(telephone string) (*models.Client, error) { func (d *Database) GetClientByTelephone(telephone string) (*models.Client, error) {
client := &models.Client{} var row struct {
query := `SELECT id, username, password, nom, prenom, telephone, command, amende, created_at ID int `gorm:"column:id"`
FROM clients WHERE telephone = $1` Username string `gorm:"column:username"`
Password string `gorm:"column:password"`
err := d.QueryRow(query, telephone).Scan( Nom string `gorm:"column:nom"`
&client.ID, Prenom string `gorm:"column:prenom"`
&client.Username, Telephone string `gorm:"column:telephone"`
&client.Password, Command int `gorm:"column:command"`
&client.Nom, Amende float64 `gorm:"column:amende"`
&client.Prenom, CreatedAt time.Time `gorm:"column:created_at"`
&client.Telephone,
&client.Command,
&client.Amende,
&client.CreatedAt,
)
if err == sql.ErrNoRows {
return nil, nil
} }
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 { if err != nil {
return nil, fmt.Errorf("erreur lors de la récupération du client: %w", err) 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 // GetClientByUsername récupère un client par son username
func (d *Database) GetClientByUsername(username string) (*models.Client, error) { func (d *Database) GetClientByUsername(username string) (*models.Client, error) {
client := &models.Client{} var row struct {
var pointsExtraJSON []byte ID int `gorm:"column:id"`
query := `SELECT id, username, password, nom, prenom, telephone, command, amende, must_change_password, COALESCE(points_extra, '{}'::jsonb), created_at Username string `gorm:"column:username"`
FROM clients WHERE username = $1` Password string `gorm:"column:password"`
Nom string `gorm:"column:nom"`
err := d.QueryRow(query, username).Scan( Prenom string `gorm:"column:prenom"`
&client.ID, Telephone string `gorm:"column:telephone"`
&client.Username, Command int `gorm:"column:command"`
&client.Password, Amende float64 `gorm:"column:amende"`
&client.Nom, MustChangePassword bool `gorm:"column:must_change_password"`
&client.Prenom, PointsExtraJSON []byte `gorm:"column:points_extra"`
&client.Telephone, CreatedAt time.Time `gorm:"column:created_at"`
&client.Command,
&client.Amende,
&client.MustChangePassword,
&pointsExtraJSON,
&client.CreatedAt,
)
if err == sql.ErrNoRows {
return nil, nil
} }
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 { if err != nil {
return nil, fmt.Errorf("erreur lors de la récupération du client: %w", err) 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{} client.PointsExtra = map[string]int{}
if len(pointsExtraJSON) > 0 { if len(row.PointsExtraJSON) > 0 {
json.Unmarshal(pointsExtraJSON, &client.PointsExtra) json.Unmarshal(row.PointsExtraJSON, &client.PointsExtra)
} }
return client, nil return client, nil
} }
func (d *Database) GetClientPenaltiesInfo(username string) (map[string]interface{}, error) { func (d *Database) GetClientPenaltiesInfo(username string) (map[string]interface{}, error) {
// Récupérer le montant des pénalités
amende, err := d.GetClientAmende(username) amende, err := d.GetClientAmende(username)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Récupérer le nombre d'annulations
cancellationsCount, err := d.GetClientCancellationsCount(username) cancellationsCount, err := d.GetClientCancellationsCount(username)
if err != nil { if err != nil {
log.Printf("⚠️ [GetClientPenaltiesInfo] Erreur récup annulations: %v", err) log.Printf("⚠️ [GetClientPenaltiesInfo] Erreur récup annulations: %v", err)
cancellationsCount = 0 cancellationsCount = 0
} }
// Récupérer l'historique d'annulations
cancellationHistory, err := d.GetClientCancellationHistory(username) cancellationHistory, err := d.GetClientCancellationHistory(username)
if err != nil { if err != nil {
log.Printf("⚠️ [GetClientPenaltiesInfo] Erreur récup historique: %v", err) 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 != "" → reset points_extra[extraPoolKey] uniquement
// extraPoolKey == "" (poolIdx=-1) → reset total points_extra // extraPoolKey == "" (poolIdx=-1) → reset total points_extra
func (d *Database) ResetClientPoint(username string, poolIdx int, extraPoolKey string) error { func (d *Database) ResetClientPoint(username string, poolIdx int, extraPoolKey string) error {
var query string if extraPoolKey != "" {
switch { err := d.GDB.Exec(`
case extraPoolKey != "": UPDATE clients SET points_extra = points_extra - ?, updated_at = CURRENT_TIMESTAMP
_, err := d.Exec( WHERE username = ?`, extraPoolKey, username).Error
`UPDATE clients SET points_extra = points_extra - $2, updated_at = CURRENT_TIMESTAMP WHERE username = $1`,
username, extraPoolKey,
)
if err != nil { if err != nil {
log.Printf("❌ [ResetClientPointAdmin] Erreur UPDATE extra: %v", err) log.Printf("❌ [ResetClientPointAdmin] Erreur UPDATE extra: %v", err)
} else { } else {
@@ -495,25 +466,20 @@ func (d *Database) ResetClientPoint(username string, poolIdx int, extraPoolKey s
Redis.Del(RedisCtx, cacheKey) Redis.Del(RedisCtx, cacheKey)
} }
return err 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) // -1 ou poolIdx sans clé → reset total
if err != nil { result := d.GDB.Exec(`
log.Printf("❌ [ResetClientPointAdmin] Erreur UPDATE: %v", err) UPDATE clients SET points_extra = '{}'::jsonb, updated_at = CURRENT_TIMESTAMP
return fmt.Errorf("erreur reset points: %w", err) WHERE username = ?`, username)
if result.Error != nil {
log.Printf("❌ [ResetClientPointAdmin] Erreur UPDATE: %v", result.Error)
return fmt.Errorf("erreur reset points: %w", result.Error)
} }
if result.RowsAffected == 0 {
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("erreur vérification: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("client non trouvé") return fmt.Errorf("client non trouvé")
} }
// Invalider le cache Redis du client
cacheKey := fmt.Sprintf("client:%s", username) cacheKey := fmt.Sprintf("client:%s", username)
Redis.Del(RedisCtx, cacheKey) Redis.Del(RedisCtx, cacheKey)
@@ -525,33 +491,20 @@ func (d *Database) ResetClientPenalties(username string, resetCancellationsCount
var query string var query string
if resetCancellationsCount { if resetCancellationsCount {
query = `UPDATE clients query = `UPDATE clients SET amende = 0, cancellations_count = 0, updated_at = CURRENT_TIMESTAMP WHERE username = ?`
SET amende = 0,
cancellations_count = 0,
updated_at = CURRENT_TIMESTAMP
WHERE username = $1`
} else { } else {
query = `UPDATE clients query = `UPDATE clients SET amende = 0, updated_at = CURRENT_TIMESTAMP WHERE username = ?`
SET amende = 0,
updated_at = CURRENT_TIMESTAMP
WHERE username = $1`
} }
result, err := d.Exec(query, username) result := d.GDB.Exec(query, username)
if err != nil { if result.Error != nil {
log.Printf("❌ [ResetClientPenalties] Erreur UPDATE: %v", err) log.Printf("❌ [ResetClientPenalties] Erreur UPDATE: %v", result.Error)
return fmt.Errorf("erreur reset pénalités: %w", err) return fmt.Errorf("erreur reset pénalités: %w", result.Error)
} }
if result.RowsAffected == 0 {
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("erreur vérification: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("client non trouvé") return fmt.Errorf("client non trouvé")
} }
// Invalider le cache Redis du client
cacheKey := fmt.Sprintf("client:%s", username) cacheKey := fmt.Sprintf("client:%s", username)
Redis.Del(RedisCtx, cacheKey) Redis.Del(RedisCtx, cacheKey)
@@ -559,38 +512,29 @@ func (d *Database) ResetClientPenalties(username string, resetCancellationsCount
} }
func (d *Database) GetAllClientsWithPenalties() ([]map[string]interface{}, error) { 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 SELECT username, amende, COALESCE(cancellations_count, 0) as cancellations_count, updated_at
FROM clients FROM clients
WHERE amende > 0 WHERE amende > 0
ORDER BY amende DESC ORDER BY amende DESC`).Scan(&rows).Error
`
rows, err := d.Query(query)
if err != nil { if err != nil {
log.Printf("❌ [GetAllClientsWithPenalties] Erreur query: %v", err) log.Printf("❌ [GetAllClientsWithPenalties] Erreur query: %v", err)
return nil, fmt.Errorf("erreur récupération clients: %w", 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{}{ clients = append(clients, map[string]interface{}{
"username": username, "username": row.Username,
"total_penalty": amende, "total_penalty": row.Amende,
"cancellations_count": cancellationsCount, "cancellations_count": row.CancellationsCount,
"last_updated": updatedAt, "last_updated": row.UpdatedAt,
}) })
} }
@@ -600,51 +544,42 @@ func (d *Database) GetAllClientsWithPenalties() ([]map[string]interface{}, error
} }
func (d *Database) GetClientPenaltiesStats() (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 SELECT
COUNT(CASE WHEN amende > 0 THEN 1 END) as clients_with_penalties, COUNT(CASE WHEN amende > 0 THEN 1 END) as clients_with_penalties,
COALESCE(SUM(amende), 0) as total_penalties, COALESCE(SUM(amende), 0) as total_penalties,
COALESCE(AVG(amende), 0) as avg_penalty, COALESCE(AVG(amende), 0) as avg_penalty,
COALESCE(MAX(amende), 0) as max_penalty, COALESCE(MAX(amende), 0) as max_penalty,
COUNT(*) as total_clients COUNT(*) as total_clients
FROM clients FROM clients`).Scan(&result).Error
`
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,
)
if err != nil { if err != nil {
log.Printf("❌ [GetClientPenaltiesStats] Erreur: %v", err) log.Printf("❌ [GetClientPenaltiesStats] Erreur: %v", err)
return nil, fmt.Errorf("erreur récupération stats: %w", err) return nil, fmt.Errorf("erreur récupération stats: %w", err)
} }
result := map[string]interface{}{ stats := map[string]interface{}{
"clients_with_penalties": stats.ClientsWithPenalties, "clients_with_penalties": result.ClientsWithPenalties,
"total_penalties": stats.TotalPenalties, "total_penalties": result.TotalPenalties,
"average_penalty": stats.AvgPenalty, "average_penalty": result.AvgPenalty,
"max_penalty": stats.MaxPenalty, "max_penalty": result.MaxPenalty,
"total_clients": stats.TotalClients, "total_clients": result.TotalClients,
} }
log.Printf("📊 [GetClientPenaltiesStats] Stats: %d/%d clients avec pénalités", 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) log.Printf("💰 [CalcPointsTx] START - cmd=%d, user=%s", commandID, username)
// Charger les paramètres globaux // 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 // ✅ É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 SELECT ci.quantite, ci.prix, COALESCE(p.category, '') as category
FROM command_items ci FROM command_items ci
LEFT JOIN products p ON ci.product_id = p.id LEFT JOIN products p ON ci.product_id = p.id
WHERE ci.command_id = $1 WHERE ci.command_id = ?
`, commandID) `, commandID).Scan(&items).Error; err != nil {
if err != nil {
log.Printf("❌ [CalcPointsTx] Erreur query items: %v", err) log.Printf("❌ [CalcPointsTx] Erreur query items: %v", err)
return 0, "", fmt.Errorf("erreur récupération items: %w", err) return 0, "", fmt.Errorf("erreur récupération items: %w", err)
} }
defer rows.Close()
var itemCount int if len(items) == 0 {
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 {
log.Printf("⚠️ [CalcPointsTx] Aucun item trouvé pour cmd %d", commandID) log.Printf("⚠️ [CalcPointsTx] Aucun item trouvé pour cmd %d", commandID)
return 0, "", nil 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 { for i, t := range poolTotals {
log.Printf("📊 [CalcPointsTx] Pool[%d] (%s): %.2f€", i, pools[i].Name, t) 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 { if poolPts[i] == 0 {
continue continue
} }
_, err = tx.Exec(` if err := tx.Exec(`
UPDATE clients UPDATE clients
SET points_extra = jsonb_set( SET points_extra = jsonb_set(
COALESCE(points_extra, '{}'::jsonb), COALESCE(points_extra, '{}'::jsonb),
ARRAY[$2], ARRAY[?],
to_jsonb(COALESCE((points_extra->>$2)::int, 0) + $3) to_jsonb(COALESCE((points_extra->>?)::int, 0) + ?)
), updated_at = CURRENT_TIMESTAMP ), updated_at = CURRENT_TIMESTAMP
WHERE username = $1 WHERE username = ?
`, username, pool.Key, poolPts[i]) `, pool.Key, pool.Key, poolPts[i], username).Error; err != nil {
if err != nil {
log.Printf("❌ [CalcPointsTx] Erreur UPDATE points_extra pool[%d] (%s): %v", i, pool.Key, err) 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) 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 // 🚚 Livreur : seulement commandes assignées
if role == "livreur" { if role == "livreur" {
err := d.QueryRow(` err := d.GDB.Raw(`
SELECT EXISTS( SELECT EXISTS(
SELECT 1 FROM commandes 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 return exists, err
} }
// 👤 User : seulement SES commandes // 👤 User : seulement SES commandes
err := d.QueryRow(` err := d.GDB.Raw(`
SELECT EXISTS( SELECT EXISTS(
SELECT 1 FROM commandes 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 return exists, err
} }
+142 -157
View File
@@ -1,7 +1,6 @@
package db package db
import ( import (
"database/sql"
"fmt" "fmt"
"log" "log"
"strings" "strings"
@@ -151,9 +150,7 @@ func (d *Database) InsertCommandItemWithClientInfo(
// ✅ VÉRIFIER QUE LA COMMANDE EXISTE // ✅ VÉRIFIER QUE LA COMMANDE EXISTE
var exists bool var exists bool
checkQuery := `SELECT EXISTS(SELECT 1 FROM commandes WHERE id = $1)` if err := d.GDB.Raw(`SELECT EXISTS(SELECT 1 FROM commandes WHERE id = ?)`, commandID).Scan(&exists).Error; err != nil {
err := d.QueryRow(checkQuery, commandID).Scan(&exists)
if err != nil {
log.Printf("❌ Erreur vérification commande: %v", err) log.Printf("❌ Erreur vérification commande: %v", err)
return fmt.Errorf("erreur vérification commande: %w", err) return fmt.Errorf("erreur vérification commande: %w", err)
} }
@@ -162,16 +159,15 @@ func (d *Database) InsertCommandItemWithClientInfo(
} }
// ✅ INSERT // ✅ INSERT
query := `INSERT INTO command_items ( err := d.GDB.Exec(`
command_id, produit, product_id, quantite, prix, INSERT INTO command_items (
client_username, client_nom, client_prenom, client_telephone, delivery_address, command_id, produit, product_id, quantite, prix,
status, created_at, updated_at client_username, client_nom, client_prenom, client_telephone, delivery_address,
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, 'pending', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)` status, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
_, err = d.Exec(query,
commandID, produit, productID, quantite, prix, commandID, produit, productID, quantite, prix,
clientUsername, clientNom, clientPrenom, clientTelephone, deliveryAddress, clientUsername, clientNom, clientPrenom, clientTelephone, deliveryAddress,
) ).Error
if err != nil { if err != nil {
log.Printf("❌ Erreur INSERT command_items: %v", err) log.Printf("❌ Erreur INSERT command_items: %v", err)
return fmt.Errorf("erreur insertion item: %w", 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 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 SELECT
ci.id, ci.id,
ci.command_id, ci.command_id,
@@ -220,94 +240,93 @@ func (d *Database) GetCommandItems(commandID int) ([]map[string]interface{}, err
FROM command_items ci FROM command_items ci
LEFT JOIN commandes c ON ci.command_id = c.id LEFT JOIN commandes c ON ci.command_id = c.id
LEFT JOIN products p ON ci.product_id = p.id LEFT JOIN products p ON ci.product_id = p.id
WHERE ci.command_id = $1 WHERE ci.command_id = ?
ORDER BY ci.id ASC` ORDER BY ci.id ASC`, commandID).Scan(&rows).Error
rows, err := d.Query(query, commandID)
if err != nil { if err != nil {
log.Printf("❌ Erreur query: %v", err) log.Printf("❌ Erreur query: %v", err)
return nil, fmt.Errorf("erreur récupération items: %w", err) return nil, fmt.Errorf("erreur récupération items: %w", err)
} }
defer rows.Close()
var items []map[string]interface{} items := make([]map[string]interface{}, 0, len(rows))
for _, row := range rows {
for rows.Next() { productIDValue := 0
var id, commandID int if row.ProductID != nil {
var quantite float64 productIDValue = int(*row.ProductID)
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)
} }
// ✅ CONVERTIR sql.NullInt64 en int (0 si NULL) var commandCreatedAt interface{}
productIDValue := 0 if row.CommandCreatedAt != nil {
if productID.Valid { commandCreatedAt = *row.CommandCreatedAt
productIDValue = int(productID.Int64)
} }
item := map[string]interface{}{ item := map[string]interface{}{
"id": id, "id": row.ID,
"command_id": commandID, "command_id": row.CommandID,
"produit": produit, "produit": row.Produit,
"product_id": productIDValue, // ✅ FIX: Utiliser la valeur convertie "product_id": productIDValue,
"quantite": quantite, "quantite": row.Quantite,
"prix": prix, "prix": row.Prix,
"client_username": clientUsername, "client_username": row.ClientUsername,
"client_nom": clientNom, "client_nom": row.ClientNom,
"client_prenom": clientPrenom, "client_prenom": row.ClientPrenom,
"client_telephone": clientTelephone, "client_telephone": row.ClientTelephone,
"delivery_address": deliveryAddress.String, "delivery_address": ptrStr(row.DeliveryAddress),
"status": status.String, "status": ptrStr(row.Status),
"created_at": createdAt, "created_at": row.CreatedAt,
"updated_at": updatedAt, "updated_at": row.UpdatedAt,
// Infos commande // Infos commande
"command_status": commandStatus.String, "command_status": ptrStr(row.CommandStatus),
"command_address": commandAddress.String, "command_address": ptrStr(row.CommandAddress),
"total_prix": totalPrix, "total_prix": row.TotalPrix,
"referral_used": referralUsed, "referral_used": row.ReferralUsed,
"livreur_assign": livreurAssign.String, "livreur_assign": ptrStr(row.LivreurAssign),
"command_created_at": commandCreatedAt, "command_created_at": commandCreatedAt,
"category": category, "category": row.Category,
} }
items = append(items, item) 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)) log.Printf("✅ %d items récupérés avec infos client et catégories", len(items))
return items, nil 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) { func (d *Database) GetCommandItemsByUsername(username string) ([]map[string]interface{}, error) {
if err := validateUsername(username); err != nil { if err := validateUsername(username); err != nil {
log.Printf("❌ [GetCommandItemsByUsername] %v", err) log.Printf("❌ [GetCommandItemsByUsername] %v", err)
return nil, 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 SELECT
ci.id, ci.id,
ci.command_id, ci.command_id,
@@ -330,73 +349,49 @@ func (d *Database) GetCommandItemsByUsername(username string) ([]map[string]inte
c.created_at as command_created_at c.created_at as command_created_at
FROM command_items ci FROM command_items ci
LEFT JOIN commandes c ON ci.command_id = c.id LEFT JOIN commandes c ON ci.command_id = c.id
WHERE ci.client_username = $1 WHERE ci.client_username = ?
ORDER BY ci.command_id DESC, ci.id ASC` ORDER BY ci.command_id DESC, ci.id ASC`, username).Scan(&rows).Error
rows, err := d.Query(query, username)
if err != nil { if err != nil {
log.Printf("❌ Erreur query: %v", err) log.Printf("❌ Erreur query: %v", err)
return nil, fmt.Errorf("erreur récupération items: %w", err) return nil, fmt.Errorf("erreur récupération items: %w", err)
} }
defer rows.Close()
var items []map[string]interface{} items := make([]map[string]interface{}, 0, len(rows))
for _, row := range rows {
for rows.Next() { productIDValue := 0
var id, commandID int if row.ProductID != nil {
var quantite float64 productIDValue = int(*row.ProductID)
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)
} }
productIDValue := 0 var commandCreatedAt interface{}
if productID.Valid { if row.CommandCreatedAt != nil {
productIDValue = int(productID.Int64) commandCreatedAt = *row.CommandCreatedAt
} }
item := map[string]interface{}{ item := map[string]interface{}{
"id": id, "id": row.ID,
"command_id": commandID, "command_id": row.CommandID,
"produit": produit, "produit": row.Produit,
"product_id": productIDValue, "product_id": productIDValue,
"quantite": quantite, "quantite": row.Quantite,
"prix": prix, "prix": row.Prix,
"client_username": clientUsername, "client_username": row.ClientUsername,
"client_nom": clientNom, "client_nom": row.ClientNom,
"client_prenom": clientPrenom, "client_prenom": row.ClientPrenom,
"client_telephone": clientTelephone, "client_telephone": row.ClientTelephone,
"delivery_address": deliveryAddress.String, "delivery_address": ptrStr(row.DeliveryAddress),
"status": status.String, "status": ptrStr(row.Status),
"created_at": createdAt, "created_at": row.CreatedAt,
"updated_at": updatedAt, "updated_at": row.UpdatedAt,
"command_status": commandStatus.String, "command_status": ptrStr(row.CommandStatus),
"command_address": commandAddress.String, "command_address": ptrStr(row.CommandAddress),
"total_prix": totalPrix, "total_prix": row.TotalPrix,
"livreur_assign": livreurAssign.String, "livreur_assign": ptrStr(row.LivreurAssign),
"command_created_at": commandCreatedAt, "command_created_at": commandCreatedAt,
} }
items = append(items, item) 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) log.Printf("✅ %d items récupérés pour l'utilisateur %s", len(items), username)
return items, nil 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 // Récupérer le prix et la quantité avant suppression pour mettre à jour le total
var prix, quantite float64 var result struct {
checkQuery := `SELECT prix, quantite FROM command_items WHERE id = $1 AND command_id = $2` Prix float64 `gorm:"column:prix"`
err := d.QueryRow(checkQuery, itemID, commandID).Scan(&prix, &quantite) Quantite float64 `gorm:"column:quantite"`
if err == sql.ErrNoRows {
return fmt.Errorf("item %d non trouvé dans la commande %d", itemID, commandID)
} }
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) 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 // Supprimer l'item
_, err = d.Exec(`DELETE FROM command_items WHERE id = $1`, itemID) if err := d.GDB.Exec(`DELETE FROM command_items WHERE id = ?`, itemID).Error; err != nil {
if err != nil {
log.Printf("❌ Erreur DELETE command_items: %v", err) log.Printf("❌ Erreur DELETE command_items: %v", err)
return fmt.Errorf("erreur suppression item: %w", err) return fmt.Errorf("erreur suppression item: %w", err)
} }
// Recalculer le total de la commande // Recalculer le total de la commande
_, err = d.Exec( if err := d.GDB.Exec(
`UPDATE commandes SET total_prix = GREATEST(0, total_prix - $1) WHERE id = $2`, `UPDATE commandes SET total_prix = GREATEST(0, total_prix - ?) WHERE id = ?`,
prix*quantite, commandID, result.Prix*result.Quantite, commandID,
) ).Error; err != nil {
if err != nil {
log.Printf("⚠️ [DeleteCommandItem] Erreur maj total commande: %v", err) 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 var exists bool
checkQuery := `SELECT EXISTS(SELECT 1 FROM command_items WHERE id = $1)` if err := d.GDB.Raw(`SELECT EXISTS(SELECT 1 FROM command_items WHERE id = ?)`, itemID).Scan(&exists).Error; err != nil {
err := d.QueryRow(checkQuery, itemID).Scan(&exists)
if err != nil {
log.Printf("❌ Erreur vérification: %v", err) log.Printf("❌ Erreur vérification: %v", err)
return fmt.Errorf("erreur vérification item: %w", 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) return fmt.Errorf("item %d non trouvé", itemID)
} }
query := `UPDATE command_items result := d.GDB.Exec(`
SET status = $1, updated_at = CURRENT_TIMESTAMP UPDATE command_items
WHERE id = $2` SET status = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?`, status, itemID)
result, err := d.Exec(query, status, itemID) if result.Error != nil {
if err != nil { log.Printf("❌ Erreur UPDATE: %v", result.Error)
log.Printf("❌ Erreur UPDATE: %v", err) return fmt.Errorf("erreur mise à jour statut: %w", result.Error)
return fmt.Errorf("erreur mise à jour statut: %w", err)
} }
if result.RowsAffected == 0 {
rowsAffected, err := result.RowsAffected()
if err != nil {
log.Printf("❌ Erreur RowsAffected: %v", err)
return fmt.Errorf("erreur vérification: %w", err)
}
if rowsAffected == 0 {
log.Printf("❌ Item %d non trouvé", itemID) log.Printf("❌ Item %d non trouvé", itemID)
return fmt.Errorf("item non trouvé") return fmt.Errorf("item non trouvé")
} }
+85 -168
View File
@@ -6,228 +6,149 @@
package db package db
import ( import (
"database/sql"
"fmt" "fmt"
"gestion/models" "gestion/models"
"log" "log"
"slices" "slices"
"time"
) )
// GetAllCommandsOldestFirst récupère les commandes triées par ancienneté (plus anciennes en premier) // 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, query := `SELECT c.id, c.username, c.status, c.adresse, c.total_prix,
c.livreur_assign, c.created_at, c.updated_at c.livreur_assign, c.created_at, c.updated_at
FROM commandes c FROM commandes c
WHERE 1=1` WHERE 1=1`
args := []interface{}{} args := []any{}
argPosition := 1
// Filtrer par status
if status != "" { if status != "" {
validStatuses := []string{"pending", "assigned", "en_route", "livre", "approved", "cancelled", "disabled"} validStatuses := []string{"pending", "assigned", "en_route", "livre", "approved", "cancelled", "disabled"}
if !slices.Contains(validStatuses, status) {
isValid := slices.Contains(validStatuses, status)
if !isValid {
return nil, fmt.Errorf("statut invalide: %s", status) return nil, fmt.Errorf("statut invalide: %s", status)
} }
query += " AND c.status = ?"
query += fmt.Sprintf(" AND c.status = $%d", argPosition)
args = append(args, status) args = append(args, status)
argPosition++
} }
// Filtrer par username si fourni
if username != "" { if username != "" {
query += fmt.Sprintf(" AND c.username = $%d", argPosition) query += " AND c.username = ?"
args = append(args, username) args = append(args, username)
argPosition++
} }
query += " ORDER BY c.created_at ASC" query += " ORDER BY c.created_at ASC"
rows, err := d.Query(query, args...) var commands []map[string]any
if err != nil { if err := d.GDB.Raw(query, args...).Scan(&commands).Error; err != nil {
return nil, fmt.Errorf("erreur récupération commandes prioritaires: %w", err) 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 return commands, nil
} }
// GetOldestPendingCommand récupère la commande pending la plus ancienne // GetOldestPendingCommand récupère la commande pending la plus ancienne
func (d *Database) GetOldestPendingCommand() (map[string]interface{}, error) { func (d *Database) GetOldestPendingCommand() (map[string]any, error) {
query := `SELECT c.id, c.username, c.status, c.adresse, c.total_prix, var commands []map[string]any
c.livreur_assign, c.created_at, c.updated_at err := d.GDB.Raw(`
FROM commandes c SELECT c.id, c.username, c.status, c.adresse, c.total_prix,
WHERE c.status = 'pending' c.livreur_assign, c.created_at, c.updated_at
ORDER BY c.created_at ASC FROM commandes c
LIMIT 1` WHERE c.status = 'pending'
ORDER BY c.created_at ASC
var id int LIMIT 1`).Scan(&commands).Error
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
}
if err != nil { if err != nil {
return nil, fmt.Errorf("erreur récupération commande la plus ancienne: %w", err) return nil, fmt.Errorf("erreur récupération commande la plus ancienne: %w", err)
} }
if len(commands) == 0 {
command := map[string]interface{}{ return nil, nil
"id": id,
"username": username,
"status": status,
"adresse": adresse,
"total_prix": totalPrix,
"created_at": createdAt,
"updated_at": updatedAt,
} }
return commands[0], nil
if livreurAssign.Valid {
command["livreur_assign"] = livreurAssign.String
} else {
command["livreur_assign"] = nil
}
return command, nil
} }
// GetPendingCommandsWithPriority récupère les commandes pending avec calcul de priorité // GetPendingCommandsWithPriority récupère les commandes pending avec calcul de priorité
func (d *Database) GetPendingCommandsWithPriority() ([]*models.CommandPriority, error) { func (d *Database) GetPendingCommandsWithPriority() ([]*models.CommandPriority, error) {
query := `SELECT c.id, c.username, c.status, c.adresse, c.total_prix, var rows []struct {
c.created_at, c.updated_at, ID int `gorm:"column:id"`
EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - c.created_at)) as waiting_seconds Username string `gorm:"column:username"`
FROM commandes c Status string `gorm:"column:status"`
WHERE c.status = 'pending' Adresse string `gorm:"column:adresse"`
ORDER BY c.created_at ASC` 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 { if err != nil {
return nil, fmt.Errorf("erreur récupération commandes avec priorité: %w", err) 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{ cmd := &models.CommandPriority{
ID: id, ID: row.ID,
Username: username, Username: row.Username,
Status: status, Status: row.Status,
Address: adresse, Address: row.Adresse,
TotalPrice: totalPrix, TotalPrice: row.TotalPrix,
CreatedAt: createdAt, WaitingSeconds: int(row.WaitingSeconds),
UpdatedAt: updatedAt, WaitingMinutes: int(row.WaitingSeconds / 60),
WaitingSeconds: int(waitingSeconds),
WaitingMinutes: int(waitingSeconds / 60),
} }
commands = append(commands, cmd) 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 return commands, nil
} }
// GetCommandWaitingTime récupère le temps d'attente d'une commande // GetCommandWaitingTime récupère le temps d'attente d'une commande
func (d *Database) GetCommandWaitingTime(commandID int) (int, error) { func (d *Database) GetCommandWaitingTime(commandID int) (int, error) {
query := `SELECT EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - created_at))::INTEGER as waiting_seconds var result struct {
FROM commandes WaitingSeconds int `gorm:"column:waiting_seconds"`
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")
} }
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 { if err != nil {
return 0, fmt.Errorf("erreur récupération temps d'attente: %w", err) return 0, fmt.Errorf("erreur récupération temps d'attente: %w", err)
} }
if result.WaitingSeconds == 0 {
return waitingSeconds, nil // 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 // GetPendingCommandsStats récupère des statistiques sur les commandes en attente
func (d *Database) GetPendingCommandsStats() (map[string]interface{}, error) { func (d *Database) GetPendingCommandsStats() (map[string]any, error) {
query := `SELECT var result struct {
COUNT(*) as total_pending, TotalPending int `gorm:"column:total_pending"`
AVG(EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - created_at))) as avg_waiting_seconds, AvgWaitingSeconds *float64 `gorm:"column:avg_waiting_seconds"`
MIN(created_at) as oldest_command_date, OldestCommandDate *string `gorm:"column:oldest_command_date"`
MAX(created_at) as newest_command_date NewestCommandDate *string `gorm:"column:newest_command_date"`
FROM commandes }
WHERE status = 'pending'`
var totalPending int err := d.GDB.Raw(`
var avgWaitingSeconds sql.NullFloat64 SELECT
var oldestDate, newestDate sql.NullTime COUNT(*) as total_pending,
AVG(EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - created_at))) as avg_waiting_seconds,
err := d.QueryRow(query).Scan(&totalPending, &avgWaitingSeconds, &oldestDate, &newestDate) 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 { if err != nil {
return nil, fmt.Errorf("erreur récupération stats: %w", err) return nil, fmt.Errorf("erreur récupération stats: %w", err)
} }
stats := map[string]interface{}{ stats := map[string]any{
"total_pending": totalPending, "total_pending": result.TotalPending,
"avg_waiting_seconds": 0, "avg_waiting_seconds": 0,
"avg_waiting_minutes": 0, "avg_waiting_minutes": 0,
"oldest_command_date": nil, "oldest_command_date": nil,
@@ -235,25 +156,21 @@ func (d *Database) GetPendingCommandsStats() (map[string]interface{}, error) {
"oldest_waiting_minutes": 0, "oldest_waiting_minutes": 0,
} }
if avgWaitingSeconds.Valid { if result.AvgWaitingSeconds != nil {
stats["avg_waiting_seconds"] = int(avgWaitingSeconds.Float64) stats["avg_waiting_seconds"] = int(*result.AvgWaitingSeconds)
stats["avg_waiting_minutes"] = int(avgWaitingSeconds.Float64 / 60) stats["avg_waiting_minutes"] = int(*result.AvgWaitingSeconds / 60)
} }
if oldestDate.Valid { if result.OldestCommandDate != nil {
stats["oldest_command_date"] = oldestDate.Time stats["oldest_command_date"] = *result.OldestCommandDate
waitingTime := time.Since(oldestDate.Time)
stats["oldest_waiting_minutes"] = int(waitingTime.Minutes())
} }
if newestDate.Valid { if result.NewestCommandDate != nil {
stats["newest_command_date"] = newestDate.Time stats["newest_command_date"] = *result.NewestCommandDate
} }
log.Printf("📊 [STATS] Commandes pending: %d | Attente moyenne: %d min | Plus ancienne: %d min", log.Printf("📊 [STATS] Commandes pending: %d | Attente moyenne: %d min",
totalPending, result.TotalPending, stats["avg_waiting_minutes"])
stats["avg_waiting_minutes"],
stats["oldest_waiting_minutes"])
return stats, nil return stats, nil
} }
File diff suppressed because it is too large Load Diff
+139 -244
View File
@@ -1,46 +1,24 @@
package db package db
import ( import (
"database/sql"
"fmt" "fmt"
"log" "log"
"slices" "slices"
"time"
"gorm.io/gorm"
) )
// GetAvailableDeliveryPersons récupère tous les livreurs disponibles // GetAvailableDeliveryPersons récupère tous les livreurs disponibles
func (d *Database) GetAvailableDeliveryPersons() ([]map[string]any, error) { func (d *Database) GetAvailableDeliveryPersons() ([]map[string]any, error) {
query := `SELECT id, username, total, livraison var livreurs []map[string]any
FROM users err := d.GDB.Raw(`
WHERE role = 'livreur' SELECT id, username, total, livraison
ORDER BY username` FROM users
WHERE role = 'livreur'
rows, err := d.Query(query) ORDER BY username`).Scan(&livreurs).Error
if err != nil { if err != nil {
return nil, fmt.Errorf("erreur lors de la récupération des livreurs: %w", err) 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 return livreurs, nil
} }
@@ -48,255 +26,172 @@ func (d *Database) GetAvailableDeliveryPersons() ([]map[string]any, error) {
func (d *Database) AssignDeliveryPerson(commandID int, livreurUsername string) error { func (d *Database) AssignDeliveryPerson(commandID int, livreurUsername string) error {
log.Printf("📦 [AssignDeliveryPerson] START - commandID=%d, livreur=%s", commandID, livreurUsername) log.Printf("📦 [AssignDeliveryPerson] START - commandID=%d, livreur=%s", commandID, livreurUsername)
tx, err := d.Begin() return d.GDB.Transaction(func(tx *gorm.DB) error {
if err != nil { var roleResult struct {
log.Printf("❌ Erreur démarrage transaction: %v", err) Role string `gorm:"column:role"`
return fmt.Errorf("erreur démarrage transaction: %w", err) }
} err := tx.Raw(`SELECT role FROM users WHERE username = ? FOR UPDATE`, livreurUsername).Scan(&roleResult).Error
defer tx.Rollback() 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 var cmdResult struct {
checkQuery := `SELECT role FROM users WHERE username = $1 FOR UPDATE` Status string `gorm:"column:status"`
err = tx.QueryRow(checkQuery, livreurUsername).Scan(&role) LivreurAssign *string `gorm:"column:livreur_assign"`
if err == sql.ErrNoRows { }
log.Printf("❌ Livreur '%s' non trouvé", livreurUsername) err = tx.Raw(`SELECT status, livreur_assign FROM commandes WHERE id = ? FOR UPDATE`, commandID).Scan(&cmdResult).Error
return fmt.Errorf("livreur non trouvé") if err != nil {
} log.Printf("❌ Erreur vérification commande: %v", err)
if err != nil { return fmt.Errorf("erreur lors de la vérification de la commande: %w", err)
log.Printf("❌ Erreur vérification livreur: %v", err) }
return fmt.Errorf("erreur lors de la vérification du livreur: %w", err) if cmdResult.Status == "" {
} log.Printf("❌ Commande %d non trouvée", commandID)
if role != "livreur" { return fmt.Errorf("commande non trouvée")
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 currentStatus string validStatusesForAssignment := []string{"pending", "assigned"}
var currentLivreur sql.NullString if !slices.Contains(validStatusesForAssignment, cmdResult.Status) {
statusQuery := `SELECT status, livreur_assign log.Printf("❌ Statut invalide pour assignation: %s", cmdResult.Status)
FROM commandes return fmt.Errorf("commande en statut '%s', impossible d'assigner un livreur", cmdResult.Status)
WHERE id = $1 }
FOR UPDATE`
err = tx.QueryRow(statusQuery, commandID).Scan(&currentStatus, &currentLivreur) result := tx.Exec(`
if err == sql.ErrNoRows { UPDATE commandes
log.Printf("❌ Commande %d non trouvée", commandID) SET livreur_assign = ?,
return fmt.Errorf("commande non trouvée") status = 'assigned',
} updated_at = CURRENT_TIMESTAMP
if err != nil { WHERE id = ?
log.Printf("❌ Erreur vérification commande: %v", err) AND status IN ('pending', 'assigned')`, livreurUsername, commandID)
return fmt.Errorf("erreur lors de la vérification de la commande: %w", err) 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"} if err := tx.Exec(`
isValidStatus := slices.Contains(validStatusesForAssignment, currentStatus) 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 { return nil
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
} }
// GetDeliveryPersonCommands récupère les commandes assignées à un livreur // GetDeliveryPersonCommands récupère les commandes assignées à un livreur
func (d *Database) GetDeliveryPersonCommands(livreurUsername string, status string) ([]map[string]any, error) { 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 FROM commandes
WHERE livreur_assign = $1` WHERE livreur_assign = ?`
args := []any{livreurUsername} args := []any{livreurUsername}
if status != "" { if status != "" {
query += " AND status = $2" query += " AND status = ?"
args = append(args, status) args = append(args, status)
} }
query += " ORDER BY created_at DESC" query += " ORDER BY created_at DESC"
rows, err := d.Query(query, args...) var commands []map[string]any
if err != nil { 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) 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 return commands, nil
} }
func (d *Database) IncrementLivreurDeliveryCount(livreurUsername string) error { func (d *Database) IncrementLivreurDeliveryCount(livreurUsername string) error {
query := `UPDATE users result := d.GDB.Exec(`
SET livraison = livraison + 1, UPDATE users
total = total + 1, SET livraison = livraison + 1,
updated_at = CURRENT_TIMESTAMP total = total + 1,
WHERE username = $1 AND role = 'livreur'` updated_at = CURRENT_TIMESTAMP
WHERE username = ? AND role = 'livreur'`, livreurUsername)
result, err := d.Exec(query, livreurUsername) if result.Error != nil {
if err != nil { return fmt.Errorf("erreur lors de l'incrémentation des livraisons: %w", result.Error)
return fmt.Errorf("erreur lors de l'incrémentation des livraisons: %w", err)
} }
if result.RowsAffected == 0 {
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("erreur lors de la vérification: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("livreur non trouvé") return fmt.Errorf("livreur non trouvé")
} }
return nil return nil
} }
func (d *Database) ApproveDelivery(commandID int, clientUsername string) error { func (d *Database) ApproveDelivery(commandID int, clientUsername string) error {
tx, err := d.Begin() return d.GDB.Transaction(func(tx *gorm.DB) error {
if err != nil { var cmdResult struct {
log.Printf("❌ Erreur démarrage transaction: %v", err) Username string `gorm:"column:username"`
return fmt.Errorf("erreur démarrage transaction: %w", err) Status string `gorm:"column:status"`
} LivreurAssign *string `gorm:"column:livreur_assign"`
defer tx.Rollback() }
err := tx.Raw(`
var commandUsername, currentStatus string SELECT username, status, livreur_assign
var livreurAssign sql.NullString FROM commandes WHERE id = ? FOR UPDATE`, commandID).Scan(&cmdResult).Error
checkQuery := `SELECT username, status, livreur_assign
FROM commandes
WHERE id = $1
FOR UPDATE`
err = tx.QueryRow(checkQuery, commandID).Scan(&commandUsername, &currentStatus, &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)
if err != nil { if err != nil {
log.Printf("⚠️ Erreur incrémentation livreur: %v", err) return fmt.Errorf("erreur lors de la vérification de la commande: %w", err)
} else { }
rows, _ := result.RowsAffected() if cmdResult.Username == "" {
if rows > 0 { return fmt.Errorf("commande non trouvée")
log.Printf(" ✅ Compteur livreur incrémenté: %s", livreurAssign.String) }
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) if err := tx.Exec(`
VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP)` 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", return nil
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
} }
+17 -73
View File
@@ -7,96 +7,40 @@ import (
) )
func (d *Database) GetDeliveryIssues(status string) ([]models.DeliveryIssue, error) { func (d *Database) GetDeliveryIssues(status string) ([]models.DeliveryIssue, error) {
query := ` q := d.GDB.Order("created_at DESC")
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
if status != "" { if status != "" {
query += " WHERE status = $1" q = q.Where("status = ?", status)
args = append(args, status)
} }
var issues []models.DeliveryIssue
query += " ORDER BY created_at DESC" if err := q.Find(&issues).Error; err != nil {
rows, err := d.Query(query, args...)
if err != nil {
return nil, fmt.Errorf("erreur récupération problèmes: %w", err) 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 return issues, nil
} }
// CreateDeliveryIssue crée un nouveau problème
func (d *Database) CreateDeliveryIssue(commandID int, issueType, description, reportedBy string) (*models.DeliveryIssue, error) { func (d *Database) CreateDeliveryIssue(commandID int, issueType, description, reportedBy string) (*models.DeliveryIssue, error) {
query := ` issue := models.DeliveryIssue{
INSERT INTO delivery_issues (command_id, issue_type, description, status, reported_by, created_at, updated_at) CommandID: commandID,
VALUES ($1, $2, $3, 'open', $4, NOW(), NOW()) IssueType: issueType,
RETURNING id, command_id, issue_type, description, status, reported_by, created_at, updated_at Description: description,
` Status: "open",
ReportedBy: reportedBy,
var issue models.DeliveryIssue }
err := d.QueryRow(query, commandID, issueType, description, reportedBy).Scan( if err := d.GDB.Create(&issue).Error; err != nil {
&issue.ID,
&issue.CommandID,
&issue.IssueType,
&issue.Description,
&issue.Status,
&issue.ReportedBy,
&issue.CreatedAt,
&issue.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("erreur création problème: %w", err) return nil, fmt.Errorf("erreur création problème: %w", err)
} }
return &issue, nil return &issue, nil
} }
// UpdateDeliveryIssue met à jour un problème
func (d *Database) UpdateDeliveryIssue(issueID int, status, resolution, resolvedBy string) error { func (d *Database) UpdateDeliveryIssue(issueID int, status, resolution, resolvedBy string) error {
query := ` result := d.GDB.Model(&models.DeliveryIssue{}).Where("id = ?", issueID).
UPDATE delivery_issues Updates(map[string]any{"status": status, "resolution": resolution, "resolved_by": resolvedBy})
SET status = $1, resolution = $2, resolved_by = $3, updated_at = NOW() if result.Error != nil {
WHERE id = $4 return fmt.Errorf("erreur mise à jour problème: %w", result.Error)
`
result, err := d.Exec(query, status, resolution, resolvedBy, issueID)
if err != nil {
return fmt.Errorf("erreur mise à jour problème: %w", err)
} }
if result.RowsAffected == 0 {
rowsAffected, _ := result.RowsAffected()
if rowsAffected == 0 {
return fmt.Errorf("problème non trouvé") return fmt.Errorf("problème non trouvé")
} }
log.Printf("✅ Problème %d mis à jour: status=%s", issueID, status) log.Printf("✅ Problème %d mis à jour: status=%s", issueID, status)
return nil return nil
} }
+39 -145
View File
@@ -6,7 +6,6 @@
package db package db
import ( import (
"database/sql"
"fmt" "fmt"
"log" "log"
"strings" "strings"
@@ -22,52 +21,38 @@ var allowedStatuses = map[string]bool{
// CountDeliveriesByStatus compte les livraisons d'un livreur par statut // CountDeliveriesByStatus compte les livraisons d'un livreur par statut
func (d *Database) CountDeliveriesByStatus(livreurUsername string, statuses string) (int, error) { func (d *Database) CountDeliveriesByStatus(livreurUsername string, statuses string) (int, error) {
if statuses == "" { if statuses == "" {
// Cas simple: toutes les livraisons var result struct {
query := `SELECT COUNT(*) Count int `gorm:"column:count"`
FROM commandes }
WHERE livreur_assign = $1` err := d.GDB.Raw(`SELECT COUNT(*) as count FROM commandes WHERE livreur_assign = ?`, livreurUsername).Scan(&result).Error
var count int
err := d.QueryRow(query, livreurUsername).Scan(&count)
if err != nil { if err != nil {
log.Printf("❌ [CountDeliveries] Erreur: %v", err) log.Printf("❌ [CountDeliveries] Erreur: %v", err)
return 0, fmt.Errorf("erreur comptage livraisons: %w", err) return 0, fmt.Errorf("erreur comptage livraisons: %w", err)
} }
return count, nil return result.Count, nil
} }
// ✅ Valider les statuts
cleanStatuses, err := ValidateStatuses(statuses) cleanStatuses, err := ValidateStatuses(statuses)
if err != nil { if err != nil {
log.Printf("❌ [CountDeliveries] Validation échouée: %v", err) log.Printf("❌ [CountDeliveries] Validation échouée: %v", err)
return 0, err return 0, err
} }
// ✅ Construire la requête avec IN et placeholders var result struct {
placeholders := make([]string, len(cleanStatuses)) Count int `gorm:"column:count"`
args := []interface{}{livreurUsername}
for i, status := range cleanStatuses {
placeholders[i] = fmt.Sprintf("$%d", i+2)
args = append(args, status)
} }
err = d.GDB.Raw(
query := fmt.Sprintf(`SELECT COUNT(*) `SELECT COUNT(*) as count FROM commandes WHERE livreur_assign = ? AND status IN ?`,
FROM commandes livreurUsername, cleanStatuses,
WHERE livreur_assign = $1 ).Scan(&result).Error
AND status IN (%s)`, strings.Join(placeholders, ","))
var count int
err = d.QueryRow(query, args...).Scan(&count)
if err != nil { if err != nil {
log.Printf("❌ [CountDeliveries] Erreur: %v", err) log.Printf("❌ [CountDeliveries] Erreur: %v", err)
return 0, fmt.Errorf("erreur comptage livraisons: %w", err) return 0, fmt.Errorf("erreur comptage livraisons: %w", err)
} }
log.Printf("✅ [CountDeliveries] %d livraisons pour %s avec statuts %v", log.Printf("✅ [CountDeliveries] %d livraisons pour %s avec statuts %v",
count, livreurUsername, cleanStatuses) result.Count, livreurUsername, cleanStatuses)
return result.Count, nil
return count, nil
} }
func ValidateStatuses(statuses string) ([]string, error) { func ValidateStatuses(statuses string) ([]string, error) {
@@ -77,7 +62,6 @@ func ValidateStatuses(statuses string) ([]string, error) {
statusList := strings.Split(statuses, ",") statusList := strings.Split(statuses, ",")
// Liste blanche complète
validStatusMap := map[string]bool{ validStatusMap := map[string]bool{
"pending": true, "pending": true,
"assigned": true, "assigned": true,
@@ -93,12 +77,9 @@ func ValidateStatuses(statuses string) ([]string, error) {
for _, status := range statusList { for _, status := range statusList {
status = strings.TrimSpace(status) status = strings.TrimSpace(status)
// Ignorer les chaînes vides
if status == "" { if status == "" {
continue continue
} }
if validStatusMap[status] { if validStatusMap[status] {
cleanStatuses = append(cleanStatuses, status) cleanStatuses = append(cleanStatuses, status)
} else { } else {
@@ -106,7 +87,6 @@ func ValidateStatuses(statuses string) ([]string, error) {
} }
} }
// Logger les statuts invalides
if len(invalidStatuses) > 0 { if len(invalidStatuses) > 0 {
log.Printf("⚠️ [ValidateStatuses] Statuts invalides ignorés: %v", invalidStatuses) 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 // GetLastDeliveryDate récupère la date de la dernière livraison d'un livreur
func (d *Database) GetLastDeliveryDate(livreurUsername string) (*time.Time, error) { func (d *Database) GetLastDeliveryDate(livreurUsername string) (*time.Time, error) {
query := `SELECT MAX(updated_at) var result struct {
FROM commandes LastDate *time.Time `gorm:"column:last_date"`
WHERE livreur_assign = $1 }
AND status = 'approved'` err := d.GDB.Raw(`
SELECT MAX(updated_at) as last_date
var lastDate sql.NullTime FROM commandes
err := d.QueryRow(query, livreurUsername).Scan(&lastDate) WHERE livreur_assign = ? AND status = 'approved'`, livreurUsername).Scan(&result).Error
if err != nil { if err != nil {
return nil, fmt.Errorf("erreur récupération dernière livraison: %w", err) return nil, fmt.Errorf("erreur récupération dernière livraison: %w", err)
} }
return result.LastDate, nil
if !lastDate.Valid {
return nil, nil // Aucune livraison
}
t := lastDate.Time
return &t, nil
} }
// GetCurrentCommand récupère l'ID de la commande en cours d'un livreur // GetCurrentCommand récupère l'ID de la commande en cours d'un livreur
func (d *Database) GetCurrentCommand(livreurUsername string) (int, error) { func (d *Database) GetCurrentCommand(livreurUsername string) (int, error) {
// Récupérer depuis Redis
currentKey := fmt.Sprintf("delivery:current:%s", livreurUsername) currentKey := fmt.Sprintf("delivery:current:%s", livreurUsername)
currentIDStr, err := Redis.Get(RedisCtx, currentKey).Result() currentIDStr, err := Redis.Get(RedisCtx, currentKey).Result()
if err != nil { if err != nil {
return 0, nil // Pas de commande en cours return 0, nil
} }
var currentID int var currentID int
@@ -157,93 +130,40 @@ func (d *Database) GetCurrentCommand(livreurUsername string) (int, error) {
return currentID, nil return currentID, nil
} }
// ============================================
// 📜 HISTORIQUE DES LIVRAISONS
// ============================================
// GetDeliveryPersonHistory récupère l'historique paginé des livraisons d'un livreur // 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) { func (d *Database) GetDeliveryPersonHistory(livreurUsername string, limit, offset int) ([]map[string]any, error) {
query := ` var history []map[string]any
err := d.GDB.Raw(`
SELECT SELECT
c.id as command_id, c.id as command_id,
c.username as client, c.username as client,
c.status, c.status,
c.adresse, c.adresse,
c.total_prix, c.total_prix::float8 as total_prix,
c.created_at as assigned_at, c.created_at as assigned_at,
c.updated_at as completed_at c.updated_at as completed_at
FROM commandes c FROM commandes c
WHERE c.livreur_assign = $1 WHERE c.livreur_assign = ?
ORDER BY c.created_at DESC ORDER BY c.created_at DESC
LIMIT $2 OFFSET $3 LIMIT ? OFFSET ?`, livreurUsername, limit, offset).Scan(&history).Error
`
rows, err := d.Query(query, livreurUsername, limit, offset)
if err != nil { if err != nil {
return nil, fmt.Errorf("erreur récupération historique: %w", err) 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 return history, nil
} }
// GetDeliveryPersonStatus récupère le statut d'un livreur // GetDeliveryPersonStatus récupère le statut d'un livreur
func (d *Database) GetDeliveryPersonStatus(livreurUsername string) (string, error) { func (d *Database) GetDeliveryPersonStatus(livreurUsername string) (string, error) {
statusKey := fmt.Sprintf("delivery:status:%s", livreurUsername) statusKey := fmt.Sprintf("delivery:status:%s", livreurUsername)
status, err := Redis.Get(RedisCtx, statusKey).Result() status, err := Redis.Get(RedisCtx, statusKey).Result()
if err != nil { if err != nil {
// Statut par défaut si non trouvé
return "offline", nil return "offline", nil
} }
return status, nil return status, nil
} }
// UpdateDeliveryPersonStatus met à jour le statut d'un livreur // UpdateDeliveryPersonStatus met à jour le statut d'un livreur
func (d *Database) UpdateDeliveryPersonStatus(livreurUsername string, status string) error { func (d *Database) UpdateDeliveryPersonStatus(livreurUsername string, status string) error {
// 🔹 Valider le statut
if !allowedStatuses[status] { if !allowedStatuses[status] {
return fmt.Errorf("statut invalide: %s", 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) log.Printf("🔄 [UpdateStatus] Mise à jour: %s → %s", livreurUsername, status)
statusKey := fmt.Sprintf("delivery:status:%s", livreurUsername) statusKey := fmt.Sprintf("delivery:status:%s", livreurUsername)
if err := Redis.Set(RedisCtx, statusKey, status, 0).Err(); err != nil {
err := Redis.Set(RedisCtx, statusKey, status, 0).Err()
if err != nil {
log.Printf("❌ [UpdateStatus] Erreur Redis: %v", err) log.Printf("❌ [UpdateStatus] Erreur Redis: %v", err)
return fmt.Errorf("erreur mise à jour statut: %w", 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 // GetDeliverymanQueueSize récupère la taille de la queue d'un livreur
func (d *Database) GetDeliverymanQueueSize(livreurUsername string) (int, error) { func (d *Database) GetDeliverymanQueueSize(livreurUsername string) (int, error) {
queueKey := fmt.Sprintf("delivery:queue:%s", livreurUsername) queueKey := fmt.Sprintf("delivery:queue:%s", livreurUsername)
size, err := Redis.LLen(RedisCtx, queueKey).Result() size, err := Redis.LLen(RedisCtx, queueKey).Result()
if err != nil { if err != nil {
return 0, fmt.Errorf("erreur récupération taille queue: %w", err) return 0, fmt.Errorf("erreur récupération taille queue: %w", err)
} }
return int(size), nil return int(size), nil
} }
// GetDeliverymanQueue récupère la queue complète d'un livreur // GetDeliverymanQueue récupère la queue complète d'un livreur
func (d *Database) GetDeliverymanQueue(livreurUsername string) ([]int, error) { func (d *Database) GetDeliverymanQueue(livreurUsername string) ([]int, error) {
queueKey := fmt.Sprintf("delivery:queue:%s", livreurUsername) queueKey := fmt.Sprintf("delivery:queue:%s", livreurUsername)
commands, err := Redis.LRange(RedisCtx, queueKey, 0, -1).Result() commands, err := Redis.LRange(RedisCtx, queueKey, 0, -1).Result()
if err != nil { if err != nil {
return nil, fmt.Errorf("erreur récupération queue: %w", err) 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) fmt.Sscanf(cmdStr, "%d", &cmdID)
queue = append(queue, cmdID) queue = append(queue, cmdID)
} }
return queue, nil return queue, nil
} }
// UpdateCommandLivreur met à jour le livreur assigné à une commande // UpdateCommandLivreur met à jour le livreur assigné à une commande
func (d *Database) UpdateCommandLivreur(commandID int, livreurUsername string) error { func (d *Database) UpdateCommandLivreur(commandID int, livreurUsername string) error {
query := `UPDATE commandes result := d.GDB.Exec(`
SET livreur_assign = $1, updated_at = CURRENT_TIMESTAMP UPDATE commandes
WHERE id = $2` SET livreur_assign = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?`, livreurUsername, commandID)
result, err := d.Exec(query, livreurUsername, commandID) if result.Error != nil {
if err != nil { return fmt.Errorf("erreur mise à jour livreur: %w", result.Error)
return fmt.Errorf("erreur mise à jour livreur: %w", err)
} }
if result.RowsAffected == 0 {
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("erreur vérification: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("commande non trouvée") return fmt.Errorf("commande non trouvée")
} }
return nil return nil
} }
// GetAllDeliveryPersonsStats récupère les stats de tous les livreurs // GetAllDeliveryPersonsStats récupère les stats de tous les livreurs
func (d *Database) GetAllDeliveryPersonsStats() ([]map[string]any, error) { func (d *Database) GetAllDeliveryPersonsStats() ([]map[string]any, error) {
// Récupérer tous les livreurs
livreurs, err := d.GetAvailableDeliveryPersons() livreurs, err := d.GetAvailableDeliveryPersons()
if err != nil { if err != nil {
return nil, fmt.Errorf("erreur récupération livreurs: %w", err) return nil, fmt.Errorf("erreur récupération livreurs: %w", err)
} }
var stats []map[string]any var stats []map[string]any
for _, livreur := range livreurs { for _, livreur := range livreurs {
username := livreur["username"].(string) username := livreur["username"].(string)
// Compter les livraisons
totalDeliveries, _ := d.CountDeliveriesByStatus(username, "") totalDeliveries, _ := d.CountDeliveriesByStatus(username, "")
completedDeliveries, _ := d.CountDeliveriesByStatus(username, "approved") completedDeliveries, _ := d.CountDeliveriesByStatus(username, "approved")
queueSize, _ := d.GetDeliverymanQueueSize(username) queueSize, _ := d.GetDeliverymanQueueSize(username)
status, _ := d.GetDeliveryPersonStatus(username) status, _ := d.GetDeliveryPersonStatus(username)
statEntry := map[string]any{ stats = append(stats, map[string]any{
"username": username, "username": username,
"total_deliveries": totalDeliveries, "total_deliveries": totalDeliveries,
"completed_deliveries": completedDeliveries, "completed_deliveries": completedDeliveries,
"queue_size": queueSize, "queue_size": queueSize,
"status": status, "status": status,
} })
stats = append(stats, statEntry)
} }
return stats, nil return stats, nil
} }
// GetDeliveryPersonsByStatus récupère les livreurs par statut // GetDeliveryPersonsByStatus récupère les livreurs par statut
func (d *Database) GetDeliveryPersonsByStatus(status string) ([]string, error) { func (d *Database) GetDeliveryPersonsByStatus(status string) ([]string, error) {
// Récupérer tous les livreurs
livreurs, err := d.GetAvailableDeliveryPersons() livreurs, err := d.GetAvailableDeliveryPersons()
if err != nil { if err != nil {
return nil, fmt.Errorf("erreur récupération livreurs: %w", err) return nil, fmt.Errorf("erreur récupération livreurs: %w", err)
} }
var filteredLivreurs []string var filteredLivreurs []string
for _, livreur := range livreurs { for _, livreur := range livreurs {
username := livreur["username"].(string) username := livreur["username"].(string)
currentStatus, _ := d.GetDeliveryPersonStatus(username) currentStatus, _ := d.GetDeliveryPersonStatus(username)
if currentStatus == status { if currentStatus == status {
filteredLivreurs = append(filteredLivreurs, username) filteredLivreurs = append(filteredLivreurs, username)
} }
} }
return filteredLivreurs, nil return filteredLivreurs, nil
} }
@@ -376,7 +273,6 @@ func (d *Database) GetAvailableDeliveryPersonsCount() (int, error) {
if err != nil { if err != nil {
return 0, err return 0, err
} }
return len(availableLivreurs), nil return len(availableLivreurs), nil
} }
@@ -384,7 +280,6 @@ func (d *Database) GetAvailableDeliveryPersonsCount() (int, error) {
func (d *Database) ClearDeliveryPersonData(livreurUsername string) error { func (d *Database) ClearDeliveryPersonData(livreurUsername string) error {
log.Printf("🗑️ [ClearDeliveryData] Nettoyage données pour: %s", livreurUsername) log.Printf("🗑️ [ClearDeliveryData] Nettoyage données pour: %s", livreurUsername)
// Supprimer de Redis
keys := []string{ keys := []string{
fmt.Sprintf("delivery:status:%s", livreurUsername), fmt.Sprintf("delivery:status:%s", livreurUsername),
fmt.Sprintf("delivery:location:%s", livreurUsername), fmt.Sprintf("delivery:location:%s", livreurUsername),
@@ -394,8 +289,7 @@ func (d *Database) ClearDeliveryPersonData(livreurUsername string) error {
} }
for _, key := range keys { for _, key := range keys {
err := Redis.Del(RedisCtx, key).Err() if err := Redis.Del(RedisCtx, key).Err(); err != nil {
if err != nil {
log.Printf("⚠️ [ClearDeliveryData] Erreur suppression clé %s: %v", key, err) log.Printf("⚠️ [ClearDeliveryData] Erreur suppression clé %s: %v", key, err)
} }
} }
+6 -22
View File
@@ -5,34 +5,21 @@ import (
"log" "log"
) )
// GetCommandCategories retourne les catégories distinctes des produits d'une commande
func (d *Database) GetCommandCategories(commandID int) ([]string, error) { func (d *Database) GetCommandCategories(commandID int) ([]string, error) {
query := ` var categories []string
err := d.GDB.Raw(`
SELECT DISTINCT p.category SELECT DISTINCT p.category
FROM command_items ci FROM command_items ci
JOIN products p ON p.id = ci.product_id JOIN products p ON p.id = ci.product_id
WHERE ci.command_id = $1 AND p.category IS NOT NULL AND p.category != ''` WHERE ci.command_id = ? AND p.category IS NOT NULL AND p.category != ''`,
commandID,
rows, err := d.Query(query, commandID) ).Scan(&categories).Error
if err != nil { if err != nil {
return nil, fmt.Errorf("erreur lecture catégories commande %d: %w", commandID, err) return nil, fmt.Errorf("erreur lecture catégories commande %d: %w", commandID, err)
} }
defer rows.Close() return categories, nil
var categories []string
for rows.Next() {
var cat string
if err := rows.Scan(&cat); err == nil {
categories = append(categories, cat)
}
}
return categories, rows.Err()
} }
// 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) { func (d *Database) GetEligibleDeliverymenForCommand(commandID int) ([]string, error) {
settings, err := d.GetSettings() settings, err := d.GetSettings()
if err != nil { if err != nil {
@@ -55,20 +42,17 @@ func (d *Database) GetEligibleDeliverymenForCommand(commandID int) ([]string, er
return allActive() return allActive()
} }
// Mode category_based : récupérer les catégories de la commande
categories, err := d.GetCommandCategories(commandID) categories, err := d.GetCommandCategories(commandID)
if err != nil || len(categories) == 0 { if err != nil || len(categories) == 0 {
log.Printf("⚠️ [DELIVERY_MODE] Cmd %d: catégories non trouvées — fallback single", commandID) log.Printf("⚠️ [DELIVERY_MODE] Cmd %d: catégories non trouvées — fallback single", commandID)
return allActive() return allActive()
} }
// Construire un set des catégories de la commande
catSet := make(map[string]bool, len(categories)) catSet := make(map[string]bool, len(categories))
for _, c := range categories { for _, c := range categories {
catSet[c] = true catSet[c] = true
} }
// Trouver les livreurs dont la route intersecte les catégories
eligible := make(map[string]bool) eligible := make(map[string]bool)
for _, route := range settings.DeliveryMode.CategoryRoutes { for _, route := range settings.DeliveryMode.CategoryRoutes {
for _, routeCat := range route.Categories { for _, routeCat := range route.Categories {
+23 -193
View File
@@ -6,11 +6,10 @@
package db package db
import ( import (
"database/sql"
"fmt" "fmt"
"log" "log"
"maps" "maps"
"time" "strconv"
) )
// GetCompletedCommandsByUsername récupère toutes les commandes terminées (approved) d'un utilisateur // 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 := ` query := `
SELECT SELECT
id, id,
client_order_id AS client_order_number,
username, username,
status, status,
adresse, adresse,
total_prix, total_prix::float8 as total_prix,
livreur_assign, livreur_assign,
created_at, created_at,
updated_at updated_at
FROM commandes FROM commandes
WHERE username = $1 AND status = 'approved' WHERE username = ? AND status = 'approved'
ORDER BY created_at DESC ORDER BY created_at DESC
` `
rows, err := d.Query(query, username) var commands []map[string]any
if err != nil { if err := d.GDB.Raw(query, username).Scan(&commands).Error; err != nil {
log.Printf("❌ [GetCompletedCommands] Erreur query: %v", err) log.Printf("❌ [GetCompletedCommands] Erreur query: %v", err)
return nil, fmt.Errorf("erreur lors de la récupération des commandes terminées: %w", 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 return commands, nil
} }
@@ -98,12 +48,11 @@ func (d *Database) GetCompletedCommandsWithItems(username string) ([]map[string]
var enrichedCommands []map[string]any var enrichedCommands []map[string]any
for _, command := range commands { for _, command := range commands {
commandID, ok := command["id"].(int) commandID, _ := strconv.Atoi(fmt.Sprintf("%v", command["id"]))
if !ok { if commandID == 0 {
continue continue
} }
// Récupérer les items
items, err := d.GetCommandItems(commandID) items, err := d.GetCommandItems(commandID)
if err != nil { if err != nil {
log.Printf("⚠️ [GetCompletedWithItems] Erreur items pour cmd %d: %v", commandID, err) 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, COUNT(*) as total_count,
COALESCE(SUM(total_prix) FILTER (WHERE status = 'approved'), 0) as total_spent COALESCE(SUM(total_prix) FILTER (WHERE status = 'approved'), 0) as total_spent
FROM commandes FROM commandes
WHERE username = $1 WHERE username = ?
` `
var approvedCount, pendingCount, assignedCount, enRouteCount, livreCount, cancelledCount, totalCount int var result map[string]any
var totalSpent float64 if err := d.GDB.Raw(query, username).Scan(&result).Error; err != nil {
err := d.QueryRow(query, username).Scan(
&approvedCount,
&pendingCount,
&assignedCount,
&enRouteCount,
&livreCount,
&cancelledCount,
&totalCount,
&totalSpent,
)
if err != nil {
log.Printf("❌ [GetCommandsStats] Erreur: %v", err) log.Printf("❌ [GetCommandsStats] Erreur: %v", err)
return nil, fmt.Errorf("erreur lors de la récupération des statistiques: %w", err) return nil, fmt.Errorf("erreur lors de la récupération des statistiques: %w", err)
} }
stats := map[string]any{ return result, nil
"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
} }
// GetCommandsByStatus récupère les commandes d'un utilisateur par statut // 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 := ` query := `
SELECT SELECT
id, id,
client_order_id AS client_order_number,
username, username,
status, status,
adresse, adresse,
total_prix, total_prix::float8 as total_prix,
livreur_assign, livreur_assign,
created_at, created_at,
updated_at updated_at
FROM commandes FROM commandes
WHERE username = $1 AND status = $2 WHERE username = ? AND status = ?
ORDER BY created_at DESC 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 var commands []map[string]any
if err := d.GDB.Raw(query, username, status).Scan(&commands).Error; err != nil {
for rows.Next() { return nil, fmt.Errorf("erreur lors de la récupération des commandes: %w", err)
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)
} }
return commands, nil return commands, nil
} }
// GetRecentCompletedOrders récupère les N dernières commandes terminées d'un utilisateur // 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) { func (d *Database) GetRecentCompletedOrders(username string, limit int) ([]map[string]any, error) {
query := ` query := `
SELECT SELECT
id, id,
client_order_id AS client_order_number,
username, username,
status, status,
adresse, adresse,
total_prix, total_prix::float8 as total_prix,
livreur_assign, livreur_assign,
created_at, created_at,
updated_at updated_at
FROM commandes FROM commandes
WHERE username = $1 AND status = 'approved' WHERE username = ? AND status = 'approved'
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT $2 LIMIT ?
` `
rows, err := d.Query(query, username, limit) var commands []map[string]any
if err != nil { if err := d.GDB.Raw(query, username, limit).Scan(&commands).Error; err != nil {
log.Printf("❌ [GetRecentCompleted] Erreur query: %v", err) log.Printf("❌ [GetRecentCompleted] Erreur query: %v", err)
return nil, fmt.Errorf("erreur lors de la récupération: %w", 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 return commands, nil
} }
+32 -11
View File
@@ -8,6 +8,9 @@ import (
"time" "time"
_ "github.com/lib/pq" _ "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 // Database encapsule la connexion à la base de données
type Database struct { type Database struct {
*sql.DB *sql.DB
GDB *gorm.DB
} }
// DB est l'instance globale de la base de données // 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") 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 // Créer l'instance Database
database := &Database{db} database := &Database{db, gormDB}
// Assigner à la variable globale // Assigner à la variable globale
DB = database DB = database
@@ -76,16 +90,6 @@ func InitDB() *Database {
log.Fatalf("❌ Erreur migration must_change_password: %v", err) 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 // 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 { 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) log.Fatalf("❌ Erreur migration proposed_address: %v", err)
@@ -166,6 +170,23 @@ func InitDB() *Database {
log.Fatalf("❌ Erreur migration commandes.payment_method: %v", err) 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 // Migration: table de suivi des paiements crypto
if _, err = database.Exec(` if _, err = database.Exec(`
CREATE TABLE IF NOT EXISTS crypto_payments ( CREATE TABLE IF NOT EXISTS crypto_payments (
+73 -113
View File
@@ -1,167 +1,127 @@
package db package db
import ( import (
"database/sql"
"fmt" "fmt"
"log" "log"
"time" "time"
"gorm.io/gorm"
) )
func (d *Database) SaveToken(userID int, userType string, token string, expiresAt time.Time) error { type jwtToken struct {
// Valider le user_type ID int `gorm:"primaryKey;autoIncrement"`
validTypes := map[string]bool{ UserID int `gorm:"column:user_id;index"`
"client": true, UserType string `gorm:"column:user_type"`
"admin": true, Token string `gorm:"column:token;uniqueIndex"`
"cabine": true, DateSave time.Time `gorm:"column:date_save;autoCreateTime"`
"livreur": true, 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) return fmt.Errorf("type d'utilisateur invalide: %s", userType)
} }
t := jwtToken{UserID: userID, UserType: userType, Token: token, DateFin: expiresAt}
query := `INSERT INTO jwt_tokens (user_id, user_type, token, date_save, date_fin) if err := d.GDB.Create(&t).Error; err != nil {
VALUES ($1, $2, $3, CURRENT_TIMESTAMP, $4)`
_, err := d.Exec(query, userID, userType, token, expiresAt)
if err != nil {
return fmt.Errorf("erreur lors de l'enregistrement du token: %w", err) return fmt.Errorf("erreur lors de l'enregistrement du token: %w", err)
} }
log.Printf("✅ Token enregistré pour %s ID: %d", userType, userID) log.Printf("✅ Token enregistré pour %s ID: %d", userType, userID)
return nil return nil
} }
// IsTokenValid vérifie si un token existe et n'est pas expiré
func (d *Database) IsTokenValid(token string) (bool, error) { func (d *Database) IsTokenValid(token string) (bool, error) {
query := `SELECT COUNT(*) FROM jwt_tokens var count int64
WHERE token = $1 AND date_fin > $2` err := d.GDB.Model(&jwtToken{}).
Where("token = ? AND date_fin > ?", token, time.Now()).
var count int Count(&count).Error
err := d.QueryRow(query, token, time.Now()).Scan(&count) return count > 0, err
if err != nil {
return false, fmt.Errorf("erreur lors de la vérification du token: %w", err)
}
return count > 0, nil
} }
// RevokeToken révoque un token (le supprime de la base)
func (d *Database) RevokeToken(token string) error { func (d *Database) RevokeToken(token string) error {
query := `DELETE FROM jwt_tokens WHERE token = $1` result := d.GDB.Where("token = ?", token).Delete(&jwtToken{})
if result.Error != nil {
result, err := d.Exec(query, token) return fmt.Errorf("erreur lors de la révocation du token: %w", result.Error)
if err != nil {
return fmt.Errorf("erreur lors de la révocation du token: %w", err)
} }
if result.RowsAffected > 0 {
rowsAffected, _ := result.RowsAffected()
if rowsAffected > 0 {
log.Printf("✅ Token révoqué avec succès") log.Printf("✅ Token révoqué avec succès")
} }
return nil return nil
} }
// RevokeAllUserTokens révoque tous les tokens d'un utilisateur
func (d *Database) RevokeAllUserTokens(userID int, userType string) error { func (d *Database) RevokeAllUserTokens(userID int, userType string) error {
query := `DELETE FROM jwt_tokens WHERE user_id = $1 AND user_type = $2` result := d.GDB.Where("user_id = ? AND user_type = ?", userID, userType).Delete(&jwtToken{})
if result.Error != nil {
result, err := d.Exec(query, userID, userType) return fmt.Errorf("erreur lors de la révocation des tokens: %w", result.Error)
if err != nil {
return fmt.Errorf("erreur lors de la révocation des tokens: %w", err)
} }
log.Printf("✅ %d token(s) révoqué(s) pour %s ID: %d", result.RowsAffected, userType, userID)
rowsAffected, _ := result.RowsAffected()
log.Printf("✅ %d token(s) révoqué(s) pour %s ID: %d", rowsAffected, userType, userID)
return nil 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) { func (d *Database) GetUserActiveTokens(userID int, userType string) ([]map[string]any, error) {
query := `SELECT id, token, date_save, date_fin var tokens []jwtToken
FROM jwt_tokens err := d.GDB.Where("user_id = ? AND user_type = ? AND date_fin > ?", userID, userType, time.Now()).
WHERE user_id = $1 AND user_type = $2 AND date_fin > $3 Order("date_save DESC").Find(&tokens).Error
ORDER BY date_save DESC`
rows, err := d.Query(query, userID, userType, time.Now())
if err != nil { if err != nil {
return nil, fmt.Errorf("erreur lors de la récupération des tokens: %w", err) return nil, fmt.Errorf("erreur lors de la récupération des tokens: %w", err)
} }
defer rows.Close()
var tokens []map[string]any result := make([]map[string]any, len(tokens))
for rows.Next() { for i, t := range tokens {
var id int truncated := t.Token
var token string if len(truncated) > 20 {
var dateSave, dateFin time.Time truncated = truncated[:20] + "..."
}
err := rows.Scan(&id, &token, &dateSave, &dateFin) result[i] = map[string]any{
if err != nil { "id": t.ID,
return nil, fmt.Errorf("erreur lors du scan: %w", err) "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 result, nil
return tokens, nil
} }
func (d *Database) GetTokenInfo(token string) (map[string]any, error) { func (d *Database) GetTokenInfo(token string) (map[string]any, error) {
query := `SELECT user_id, user_type, date_save, date_fin var t jwtToken
FROM jwt_tokens err := d.GDB.Where("token = ? AND date_fin > ?", token, time.Now()).First(&t).Error
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é")
}
if err != nil { 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) return nil, fmt.Errorf("erreur lors de la récupération des infos du token: %w", err)
} }
return map[string]any{
tokenInfo := map[string]any{ "user_id": t.UserID,
"user_id": userID, "user_type": t.UserType,
"user_type": userType, "date_save": t.DateSave,
"date_save": dateSave, "date_fin": t.DateFin,
"date_fin": dateFin, }, nil
}
return tokenInfo, nil
} }
func (d *Database) CountActiveTokensByType() (map[string]int, error) { func (d *Database) CountActiveTokensByType() (map[string]int, error) {
query := `SELECT user_type, COUNT(*) as count type row struct {
FROM jwt_tokens UserType string
WHERE date_fin > $1 Count int
GROUP BY user_type` }
var rows []row
rows, err := d.Query(query, time.Now()) 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 { if err != nil {
return nil, fmt.Errorf("erreur lors du comptage des tokens: %w", err) return nil, fmt.Errorf("erreur lors du comptage des tokens: %w", err)
} }
defer rows.Close() counts := make(map[string]int, len(rows))
for _, r := range rows {
counts := make(map[string]int) counts[r.UserType] = r.Count
for rows.Next() {
var userType string
var count int
if err := rows.Scan(&userType, &count); err != nil {
return nil, err
}
counts[userType] = count
} }
return counts, nil return counts, nil
} }
+53 -154
View File
@@ -1,7 +1,6 @@
package db package db
import ( import (
"database/sql"
"fmt" "fmt"
"gestion/models" "gestion/models"
"log" "log"
@@ -42,7 +41,6 @@ func validateProductID(productID int) error {
// validateMediaType vérifie le type de média // validateMediaType vérifie le type de média
func validateMediaType(mediaType string) error { func validateMediaType(mediaType string) error {
validTypes := []string{"image", "video"} validTypes := []string{"image", "video"}
mediaType = strings.ToLower(strings.TrimSpace(mediaType)) mediaType = strings.ToLower(strings.TrimSpace(mediaType))
if slices.Contains(validTypes, mediaType) { if slices.Contains(validTypes, mediaType) {
return nil return nil
@@ -55,46 +53,29 @@ func validateMediaURL(url string) error {
if len(url) == 0 { if len(url) == 0 {
return fmt.Errorf("URL vide") return fmt.Errorf("URL vide")
} }
if len(url) > 500 { if len(url) > 500 {
return fmt.Errorf("URL trop longue (max 500 caractères)") return fmt.Errorf("URL trop longue (max 500 caractères)")
} }
// ✅ PROTECTION PATH TRAVERSAL
if strings.Contains(url, "..") || strings.Contains(url, "...") || strings.Contains(url, "..//") { if strings.Contains(url, "..") || strings.Contains(url, "...") || strings.Contains(url, "..//") {
return fmt.Errorf("path traversal détecté dans l'URL") return fmt.Errorf("path traversal détecté dans l'URL")
} }
// ✅ VÉRIFIER QUE L'URL COMMENCE PAR /uploads/
if !strings.HasPrefix(url, "/uploads/") { if !strings.HasPrefix(url, "/uploads/") {
return fmt.Errorf("URL doit commencer par /uploads/") return fmt.Errorf("URL doit commencer par /uploads/")
} }
dangerousChars := []string{"<", ">", "\"", "'", ";", "|", "&", "$", "`", "\\"}
// ✅ VÉRIFIER QU'IL N'Y A PAS DE CARACTÈRES DANGEREUX
dangerousChars := []string{
"<", ">", "\"", "'", ";", "|", "&", "$", "`", "\\",
}
for _, char := range dangerousChars { for _, char := range dangerousChars {
if strings.Contains(url, char) { if strings.Contains(url, char) {
return fmt.Errorf("caractères interdits dans l'URL") return fmt.Errorf("caractères interdits dans l'URL")
} }
} }
return nil return nil
} }
// ============================================ func (d *Database) CreateMedia(media any) error {
// CREATE MEDIA - VERSION SÉCURISÉE
// ============================================
func (db *Database) CreateMedia(media any) error {
log.Printf("🔒 [CreateMedia] START - Type: %T", media) log.Printf("🔒 [CreateMedia] START - Type: %T", media)
// ✅ TYPE ASSERTION SÉCURISÉE
m, ok := media.(MediaInterface) m, ok := media.(MediaInterface)
if !ok { if !ok {
// Vérifier si c'est un pointeur vers models.Media
if mediaPtr, isPtr := media.(*models.Media); isPtr { if mediaPtr, isPtr := media.(*models.Media); isPtr {
m = mediaPtr m = mediaPtr
ok = true ok = true
@@ -116,29 +97,25 @@ func (db *Database) CreateMedia(media any) error {
log.Printf("❌ [CreateMedia] %v", err) log.Printf("❌ [CreateMedia] %v", err)
return err return err
} }
if err := validateMediaType(mediaType); err != nil { if err := validateMediaType(mediaType); err != nil {
log.Printf("❌ [CreateMedia] %v", err) log.Printf("❌ [CreateMedia] %v", err)
return err return err
} }
if err := validateMediaURL(mediaURL); err != nil { if err := validateMediaURL(mediaURL); err != nil {
log.Printf("❌ [CreateMedia] %v", err) log.Printf("❌ [CreateMedia] %v", err)
return 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) log.Printf("❌ [InsertMedia] %v", err)
return err return err
} }
return nil 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 var exists bool
checkQuery := `SELECT EXISTS(SELECT 1 FROM products WHERE id = $1)` if err := d.GDB.Raw(`SELECT EXISTS(SELECT 1 FROM products WHERE id = ?)`, productID).Scan(&exists).Error; err != nil {
err := db.QueryRow(checkQuery, productID).Scan(&exists)
if err != nil {
log.Printf("❌ [CreateMedia] Erreur vérification produit: %v", err) log.Printf("❌ [CreateMedia] Erreur vérification produit: %v", err)
return fmt.Errorf("erreur vérification produit: %w", 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) return fmt.Errorf("produit %d n'existe pas", productID)
} }
query := `INSERT INTO media (product_id, url, type, created_at) var result struct {
VALUES ($1, $2, $3, $4) RETURNING id` ID int `gorm:"column:id"`
}
var mediaID int err := d.GDB.Raw(`
now := time.Now() INSERT INTO media (product_id, url, type, created_at)
VALUES (?, ?, ?, ?) RETURNING id`,
err = db.QueryRow(query, productID, mediaURL, mediaType, now).Scan(&mediaID) productID, mediaURL, mediaType, time.Now(),
).Scan(&result).Error
if err != nil { if err != nil {
log.Printf("❌ [CreateMedia] Erreur INSERT: %v", err) log.Printf("❌ [CreateMedia] Erreur INSERT: %v", err)
return fmt.Errorf("erreur création média: %w", err) return fmt.Errorf("erreur création média: %w", err)
} }
if mi, ok := m.(MediaInterface); ok { 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 return nil
} }
// ============================================ func (d *Database) GetMediaByID(mediaID int) (*models.Media, error) {
// GET MEDIA BY ID - VERSION SÉCURISÉE
// ============================================
func (db *Database) GetMediaByID(mediaID int) (*models.Media, error) {
log.Printf("🔍 [GetMediaByID] START - ID=%d", mediaID) log.Printf("🔍 [GetMediaByID] START - ID=%d", mediaID)
// ✅ VALIDATION
if err := validateMediaID(mediaID); err != nil { if err := validateMediaID(mediaID); err != nil {
log.Printf("❌ [GetMediaByID] %v", err) log.Printf("❌ [GetMediaByID] %v", err)
return nil, err return nil, err
} }
var media models.Media var media models.Media
err := d.GDB.Raw(`
query := `SELECT id, product_id, url, type, created_at SELECT id, product_id, url, type, created_at
FROM media FROM media WHERE id = ?`, mediaID).Scan(&media).Error
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é")
}
if err != nil { if err != nil {
log.Printf("❌ [GetMediaByID] Erreur query: %v", err) log.Printf("❌ [GetMediaByID] Erreur query: %v", err)
return nil, fmt.Errorf("erreur récupération média: %w", 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) log.Printf("✅ [GetMediaByID] Média trouvé: Type=%s", media.Type)
return &media, nil return &media, nil
} }
// ============================================ func (d *Database) GetMediaByProductID(productID int) ([]models.Media, error) {
// GET MEDIA BY PRODUCT ID - VERSION SÉCURISÉE
// ============================================
func (db *Database) GetMediaByProductID(productID int) ([]models.Media, error) {
log.Printf("🖼️ [GetMediaByProductID] START - ProductID=%d", productID) log.Printf("🖼️ [GetMediaByProductID] START - ProductID=%d", productID)
// ✅ VALIDATION
if err := validateProductID(productID); err != nil { if err := validateProductID(productID); err != nil {
log.Printf("❌ [GetMediaByProductID] %v", err) log.Printf("❌ [GetMediaByProductID] %v", err)
return nil, err return nil, err
} }
query := `SELECT id, product_id, url, type, created_at var mediaList []models.Media
FROM media err := d.GDB.Raw(`
WHERE product_id = $1 SELECT id, product_id, url, type, created_at
ORDER BY id ASC` FROM media WHERE product_id = ?
ORDER BY id ASC`, productID).Scan(&mediaList).Error
rows, err := db.Query(query, productID)
if err != nil { if err != nil {
log.Printf("❌ [GetMediaByProductID] Erreur query: %v", err) log.Printf("❌ [GetMediaByProductID] Erreur query: %v", err)
return nil, fmt.Errorf("erreur récupération médias: %w", 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)) log.Printf("✅ [GetMediaByProductID] %d médias trouvés", len(mediaList))
return mediaList, nil return mediaList, nil
} }
// ============================================ func (d *Database) UpdateMedia(media *models.Media) error {
// UPDATE MEDIA - VERSION SÉCURISÉE
// ============================================
func (db *Database) UpdateMedia(media *models.Media) error {
if media == nil { if media == nil {
return fmt.Errorf("média nil") return fmt.Errorf("média nil")
} }
if err := validateMediaID(media.ID); err != nil { if err := validateMediaID(media.ID); err != nil {
log.Printf("❌ [UpdateMedia] %v", err) log.Printf("❌ [UpdateMedia] %v", err)
return err return err
} }
if err := validateMediaType(media.Type); err != nil { if err := validateMediaType(media.Type); err != nil {
log.Printf("❌ [UpdateMedia] %v", err) log.Printf("❌ [UpdateMedia] %v", err)
return err return err
} }
if err := validateMediaURL(media.URL); err != nil { if err := validateMediaURL(media.URL); err != nil {
log.Printf("❌ [UpdateMedia] %v", err) log.Printf("❌ [UpdateMedia] %v", err)
return err return err
} }
if err := d.CheckMediaExists(media); err != nil {
// ✅ VÉRIFIER QUE LE MÉDIA EXISTE
if err := db.CheckMediaExists(media); err != nil {
log.Printf("❌ [UpdateMedia] %v", err) log.Printf("❌ [UpdateMedia] %v", err)
return err return err
} }
// ✅ UPDATE result := d.GDB.Exec(`UPDATE media SET url = ?, type = ? WHERE id = ?`, media.URL, media.Type, media.ID)
query := `UPDATE media if result.Error != nil {
SET url = $1, type = $2 log.Printf("❌ [UpdateMedia] Erreur UPDATE: %v", result.Error)
WHERE id = $3` return fmt.Errorf("erreur mise à jour média: %w", result.Error)
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)
} }
if result.RowsAffected == 0 {
rowsAffected, _ := result.RowsAffected()
if rowsAffected == 0 {
log.Printf("❌ [UpdateMedia] Aucune ligne affectée") log.Printf("❌ [UpdateMedia] Aucune ligne affectée")
return fmt.Errorf("média non trouvé") return fmt.Errorf("média non trouvé")
} }
log.Printf("✅ [UpdateMedia] Média %d mis à jour", media.ID) log.Printf("✅ [UpdateMedia] Média %d mis à jour", media.ID)
return nil return nil
} }
func (db *Database) CheckMediaExists(media *models.Media) error { func (d *Database) CheckMediaExists(media *models.Media) error {
var exists bool var exists bool
checkQuery := `SELECT EXISTS(SELECT 1 FROM media WHERE id = $1)` if err := d.GDB.Raw(`SELECT EXISTS(SELECT 1 FROM media WHERE id = ?)`, media.ID).Scan(&exists).Error; err != nil {
err := db.QueryRow(checkQuery, media.ID).Scan(&exists)
if err != nil {
log.Printf("❌ [UpdateMedia] Erreur vérification: %v", err) log.Printf("❌ [UpdateMedia] Erreur vérification: %v", err)
return fmt.Errorf("erreur vérification média: %w", 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 return nil
} }
func (db *Database) DeleteMedia(mediaID int) error { func (d *Database) DeleteMedia(mediaID int) error {
if err := validateMediaID(mediaID); err != nil { if err := validateMediaID(mediaID); err != nil {
log.Printf("❌ [DeleteMedia] %v", err) log.Printf("❌ [DeleteMedia] %v", err)
return err return err
} }
media := models.Media{ID: mediaID} media := models.Media{ID: mediaID}
if err := db.CheckMediaExists(&media); err != nil { if err := d.CheckMediaExists(&media); err != nil {
log.Printf("❌ [DeleteMedia] %v", err) log.Printf("❌ [DeleteMedia] %v", err)
return err return err
} }
query := `DELETE FROM media WHERE id = $1` result := d.GDB.Exec(`DELETE FROM media WHERE id = ?`, mediaID)
if result.Error != nil {
result, err := db.Exec(query, mediaID) log.Printf("❌ [DeleteMedia] Erreur DELETE: %v", result.Error)
if err != nil { return fmt.Errorf("erreur suppression média: %w", result.Error)
log.Printf("❌ [DeleteMedia] Erreur DELETE: %v", err)
return fmt.Errorf("erreur suppression média: %w", err)
} }
if result.RowsAffected == 0 {
rowsAffected, _ := result.RowsAffected()
if rowsAffected == 0 {
log.Printf("❌ [DeleteMedia] Aucune ligne affectée") log.Printf("❌ [DeleteMedia] Aucune ligne affectée")
return fmt.Errorf("média non trouvé") return fmt.Errorf("média non trouvé")
} }
log.Printf("✅ [DeleteMedia] Média %d supprimé", mediaID) log.Printf("✅ [DeleteMedia] Média %d supprimé", mediaID)
return nil return nil
} }
func (db *Database) DeleteMediaByProductID(productID int) error { func (d *Database) DeleteMediaByProductID(productID int) error {
log.Printf("🗑️ [DeleteMediaByProductID] START - ProductID=%d", productID) log.Printf("🗑️ [DeleteMediaByProductID] START - ProductID=%d", productID)
if err := validateProductID(productID); err != nil { if err := validateProductID(productID); err != nil {
@@ -369,9 +274,7 @@ func (db *Database) DeleteMediaByProductID(productID int) error {
} }
var exists bool var exists bool
checkQuery := `SELECT EXISTS(SELECT 1 FROM products WHERE id = $1)` if err := d.GDB.Raw(`SELECT EXISTS(SELECT 1 FROM products WHERE id = ?)`, productID).Scan(&exists).Error; err != nil {
err := db.QueryRow(checkQuery, productID).Scan(&exists)
if err != nil {
log.Printf("❌ [DeleteMediaByProductID] Erreur vérification: %v", err) log.Printf("❌ [DeleteMediaByProductID] Erreur vérification: %v", err)
return fmt.Errorf("erreur vérification produit: %w", 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) return fmt.Errorf("produit %d non trouvé", productID)
} }
query := `DELETE FROM media WHERE product_id = $1` result := d.GDB.Exec(`DELETE FROM media WHERE product_id = ?`, productID)
if result.Error != nil {
result, err := db.Exec(query, productID) log.Printf("❌ [DeleteMediaByProductID] Erreur DELETE: %v", result.Error)
if err != nil { return fmt.Errorf("erreur suppression médias: %w", result.Error)
log.Printf("❌ [DeleteMediaByProductID] Erreur DELETE: %v", err)
return fmt.Errorf("erreur suppression médias: %w", err)
} }
rowsAffected, _ := result.RowsAffected()
log.Printf("✅ [DeleteMediaByProductID] %d média(s) supprimé(s) pour produit %d", log.Printf("✅ [DeleteMediaByProductID] %d média(s) supprimé(s) pour produit %d",
rowsAffected, productID) result.RowsAffected, productID)
return nil return nil
} }
+18 -36
View File
@@ -9,7 +9,6 @@ import (
) )
func (d *Database) NotifyClient(username string, commandID int, notifType, message string) error { func (d *Database) NotifyClient(username string, commandID int, notifType, message string) error {
// Sauvegarder la notification dans Redis
notifKey := fmt.Sprintf("notifications:%s", username) notifKey := fmt.Sprintf("notifications:%s", username)
notification := map[string]any{ notification := map[string]any{
@@ -24,7 +23,6 @@ func (d *Database) NotifyClient(username string, commandID int, notifType, messa
Redis.LPush(RedisCtx, notifKey, notifJSON) Redis.LPush(RedisCtx, notifKey, notifJSON)
Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour) 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 services.TelegramBot != nil && services.TelegramBot.IsConfigured() {
if chatID, ok, err := d.GetClientTelegramChatID(username); err == nil && ok { if chatID, ok, err := d.GetClientTelegramChatID(username); err == nil && ok {
go services.TelegramBot.SendMessage(chatID, fmt.Sprintf("🔔 <b>Notification</b>\n\n%s", message)) go services.TelegramBot.SendMessage(chatID, fmt.Sprintf("🔔 <b>Notification</b>\n\n%s", message))
@@ -51,7 +49,6 @@ func (d *Database) NotifyLivreur(username string, commandID int, notifType, mess
Redis.LPush(RedisCtx, notifKey, notifJSON) Redis.LPush(RedisCtx, notifKey, notifJSON)
Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour) 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 services.TelegramBot != nil && services.TelegramBot.IsConfigured() {
if chatID, ok, err := d.GetUserTelegramChatID(username); err == nil && ok { if chatID, ok, err := d.GetUserTelegramChatID(username); err == nil && ok {
go services.TelegramBot.SendMessage(chatID, fmt.Sprintf("🔔 <b>Notification</b>\n\n%s", message)) go services.TelegramBot.SendMessage(chatID, fmt.Sprintf("🔔 <b>Notification</b>\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 // 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) { func (d *Database) NotifyAllAdminCabine(commandID int, clientUsername, deliveryAddr string) {
rows, err := d.Query( var users []struct {
`SELECT username FROM users WHERE role IN ('admin','cabine')`, Username string `gorm:"column:username"`
) }
if err != nil { 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) log.Printf("❌ [ADMIN_NOTIF] Erreur lecture users admin/cabine: %v", err)
return return
} }
defer rows.Close()
msg := fmt.Sprintf("Nouvelle commande #%d de %s — %s", commandID, clientUsername, deliveryAddr) 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) notifJSON, _ := json.Marshal(notification)
count := 0 count := 0
for rows.Next() { for _, u := range users {
var username string notifKey := fmt.Sprintf("notifications:%s", u.Username)
if err := rows.Scan(&username); err != nil {
continue
}
notifKey := fmt.Sprintf("notifications:%s", username)
Redis.LPush(RedisCtx, notifKey, notifJSON) Redis.LPush(RedisCtx, notifKey, notifJSON)
Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour) Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour)
// Diffusion Telegram individuelle
if services.TelegramBot != nil && services.TelegramBot.IsConfigured() { 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 capturedChatID := chatID
capturedMsg := msg capturedMsg := msg
go services.TelegramBot.SendMessage(capturedChatID, fmt.Sprintf("🔔 <b>Nouvelle commande</b>\n\n%s", capturedMsg)) go services.TelegramBot.SendMessage(capturedChatID, fmt.Sprintf("🔔 <b>Nouvelle commande</b>\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) log.Printf("📬 [ADMIN_NOTIF] Notif Redis (%d users) pour commande #%d", count, commandID)
} }
// NotifyAllAdminCabineAlert envoie une notification Redis + push à tous les admins/cabines // NotifyAllAdminCabineAlert envoie une notification Redis à tous les admins/cabines lors d'une alerte
// lors du déclenchement d'une alerte par un livreur.
func (d *Database) NotifyAllAdminCabineAlert(alertID int, livreurUsername, alertMessage string) { func (d *Database) NotifyAllAdminCabineAlert(alertID int, livreurUsername, alertMessage string) {
rows, err := d.Query( var users []struct {
`SELECT username FROM users WHERE role IN ('admin','cabine')`, Username string `gorm:"column:username"`
) }
if err != nil { 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) log.Printf("❌ [ALERT_NOTIF] Erreur lecture users admin/cabine: %v", err)
return return
} }
defer rows.Close()
body := fmt.Sprintf("%s — livreur : %s", alertMessage, livreurUsername) body := fmt.Sprintf("%s — livreur : %s", alertMessage, livreurUsername)
@@ -132,18 +120,13 @@ func (d *Database) NotifyAllAdminCabineAlert(alertID int, livreurUsername, alert
notifJSON, _ := json.Marshal(notification) notifJSON, _ := json.Marshal(notification)
count := 0 count := 0
for rows.Next() { for _, u := range users {
var username string notifKey := fmt.Sprintf("notifications:%s", u.Username)
if err := rows.Scan(&username); err != nil {
continue
}
notifKey := fmt.Sprintf("notifications:%s", username)
Redis.LPush(RedisCtx, notifKey, notifJSON) Redis.LPush(RedisCtx, notifKey, notifJSON)
Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour) Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour)
// Diffusion Telegram individuelle
if services.TelegramBot != nil && services.TelegramBot.IsConfigured() { 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 capturedChatID := chatID
capturedBody := body capturedBody := body
go services.TelegramBot.SendMessage(capturedChatID, fmt.Sprintf("🚨 <b>Alerte livreur</b>\n\n%s", capturedBody)) go services.TelegramBot.SendMessage(capturedChatID, fmt.Sprintf("🚨 <b>Alerte livreur</b>\n\n%s", capturedBody))
@@ -156,11 +139,10 @@ func (d *Database) NotifyAllAdminCabineAlert(alertID int, livreurUsername, alert
// AddDeliveryRating ajoute une note pour un livreur // AddDeliveryRating ajoute une note pour un livreur
func (d *Database) AddDeliveryRating(livreurUsername string, commandID, rating int, comment string) error { 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) INSERT INTO delivery_ratings (livreur_username, command_id, rating, comment, created_at)
VALUES (?, ?, ?, ?, NOW()) VALUES (?, ?, ?, ?, NOW())`,
` livreurUsername, commandID, rating, comment).Error
_, err := d.Exec(query, livreurUsername, commandID, rating, comment)
if err != nil { if err != nil {
log.Printf("⚠️ Erreur sauvegarde note livreur: %v", err) log.Printf("⚠️ Erreur sauvegarde note livreur: %v", err)
return err return err
+41 -78
View File
@@ -1,21 +1,25 @@
package db package db
import ( import (
"database/sql"
"fmt" "fmt"
"gestion/models" "gestion/models"
"log" "log"
"gorm.io/gorm"
) )
func (d *Database) CreateCryptoPayment(commandID int, nowPaymentID, status, priceCurrency, payCurrency, payAddress string, priceAmount, payAmount float64) (*models.CryptoPayment, error) { func (d *Database) CreateCryptoPayment(commandID int, nowPaymentID, status, priceCurrency, payCurrency, payAddress string, priceAmount, payAmount float64) (*models.CryptoPayment, error) {
var p models.CryptoPayment p := models.CryptoPayment{
err := d.DB.QueryRow(` CommandID: commandID,
INSERT INTO crypto_payments (command_id, nowpayment_id, status, price_amount, price_currency, pay_currency, pay_address, pay_amount) NowPaymentID: nowPaymentID,
VALUES ($1, $2, $3, $4, $5, $6, $7, $8) Status: status,
RETURNING id, command_id, nowpayment_id, status, price_amount, price_currency, pay_currency, pay_address, pay_amount, created_at, updated_at`, PriceAmount: priceAmount,
commandID, nowPaymentID, status, priceAmount, priceCurrency, payCurrency, payAddress, payAmount, PriceCurrency: priceCurrency,
).Scan(&p.ID, &p.CommandID, &p.NowPaymentID, &p.Status, &p.PriceAmount, &p.PriceCurrency, &p.PayCurrency, &p.PayAddress, &p.PayAmount, &p.CreatedAt, &p.UpdatedAt) PayCurrency: payCurrency,
if err != nil { PayAddress: payAddress,
PayAmount: payAmount,
}
if err := d.GDB.Create(&p).Error; err != nil {
return nil, fmt.Errorf("CreateCryptoPayment: %w", err) return nil, fmt.Errorf("CreateCryptoPayment: %w", err)
} }
return &p, nil 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) { func (d *Database) GetCryptoPaymentByCommandID(commandID int) (*models.CryptoPayment, error) {
var p models.CryptoPayment var p models.CryptoPayment
err := d.DB.QueryRow(` err := d.GDB.Where("command_id = ?", commandID).Order("created_at DESC").First(&p).Error
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
}
if err != nil { if err != nil {
if isNotFound(err) {
return nil, nil
}
return nil, err return nil, err
} }
return &p, nil return &p, nil
@@ -39,84 +39,47 @@ func (d *Database) GetCryptoPaymentByCommandID(commandID int) (*models.CryptoPay
func (d *Database) GetCryptoPaymentByNowPaymentID(nowPaymentID string) (*models.CryptoPayment, error) { func (d *Database) GetCryptoPaymentByNowPaymentID(nowPaymentID string) (*models.CryptoPayment, error) {
var p models.CryptoPayment var p models.CryptoPayment
err := d.DB.QueryRow(` err := d.GDB.Where("nowpayment_id = ?", nowPaymentID).First(&p).Error
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
}
if err != nil { if err != nil {
if isNotFound(err) {
return nil, nil
}
return nil, err return nil, err
} }
return &p, nil return &p, nil
} }
func (d *Database) GetPendingCryptoPayments() ([]models.CryptoPayment, error) { 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 var payments []models.CryptoPayment
for rows.Next() { err := d.GDB.Where("status NOT IN ?", []string{"finished", "failed", "expired", "refunded"}).
var p models.CryptoPayment Order("created_at ASC").Find(&payments).Error
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 { return payments, err
continue
}
payments = append(payments, p)
}
return payments, nil
} }
func (d *Database) UpdateCryptoPaymentStatus(id int, status string, payAmount float64) error { func (d *Database) UpdateCryptoPaymentStatus(id int, status string, payAmount float64) error {
_, err := d.DB.Exec(` return d.GDB.Model(&models.CryptoPayment{}).Where("id = ?", id).
UPDATE crypto_payments SET status = $1, pay_amount = $2, updated_at = NOW() Updates(map[string]any{"status": status, "pay_amount": payAmount}).Error
WHERE id = $3`, status, payAmount, id)
return err
} }
// ActivateCryptoCommand passe la commande de 'pending_payment' → 'pending' une fois le paiement confirmé
func (d *Database) ActivateCryptoCommand(commandID int) error { func (d *Database) ActivateCryptoCommand(commandID int) error {
_, err := d.DB.Exec(` return d.GDB.Exec(`UPDATE commandes SET status = 'pending', updated_at = NOW() WHERE id = ? AND status = 'pending_payment'`, commandID).Error
UPDATE commandes SET status = 'pending', updated_at = NOW()
WHERE id = $1 AND status = 'pending_payment'`, commandID)
return err
} }
// CancelCryptoCommand annule une commande en attente de paiement et restaure le stock
func (d *Database) CancelCryptoCommand(commandID int) error { func (d *Database) CancelCryptoCommand(commandID int) error {
tx, err := d.DB.Begin() return d.GDB.Transaction(func(tx *gorm.DB) error {
if err != nil { type item struct {
return err ProductID int
} Quantite float64
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
} }
if _, err := tx.Exec(`UPDATE products SET stock = stock + $1 WHERE id = $2`, qty, productID); err != nil { var items []item
log.Printf("[CANCEL CRYPTO] erreur restauration stock produit %d: %v", productID, err) if err := tx.Raw(`SELECT product_id, quantite FROM command_items WHERE command_id = ?`, commandID).Scan(&items).Error; err != nil {
return err
} }
} for _, it := range items {
if err := tx.Exec(`UPDATE products SET stock = stock + ? WHERE id = ?`, it.Quantite, it.ProductID).Error; err != nil {
if _, err := tx.Exec(`UPDATE commandes SET status = 'cancelled', updated_at = NOW() WHERE id = $1 AND status = 'pending_payment'`, commandID); err != nil { log.Printf("[CANCEL CRYPTO] erreur restauration stock produit %d: %v", it.ProductID, err)
return err }
} }
return tx.Exec(`UPDATE commandes SET status = 'cancelled', updated_at = NOW() WHERE id = ? AND status = 'pending_payment'`, commandID).Error
return tx.Commit() })
} }
+73 -122
View File
@@ -1,7 +1,6 @@
package db package db
import ( import (
"database/sql"
"fmt" "fmt"
"gestion/models" "gestion/models"
"log" "log"
@@ -9,7 +8,7 @@ import (
) )
// CreateProduct crée un nouveau produit avec ses prix // 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] Type reçu: %T", product)
log.Printf("🔍 [DB CreateProduct] Valeur: %+v", product) log.Printf("🔍 [DB CreateProduct] Valeur: %+v", product)
@@ -29,7 +28,7 @@ func (db *Database) CreateProduct(product any) error {
if !ok { if !ok {
if prodPtr, isPtr := product.(*models.Product); isPtr { if prodPtr, isPtr := product.(*models.Product); isPtr {
log.Printf("✅ [DB CreateProduct] C'est un *models.Product, utilisons-le directement") 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 ok = true
} else { } else {
return fmt.Errorf("type de produit invalide: reçu %T, attendu ProductInterface", product) 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())) 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() now := time.Now()
var productID int var result struct {
var createdAt, updatedAt time.Time 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). err := d.GDB.Raw(`
Scan(&productID, &createdAt, &updatedAt) 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 { if err != nil {
log.Printf("❌ [DB CreateProduct] Erreur INSERT: %v", err) log.Printf("❌ [DB CreateProduct] Erreur INSERT: %v", err)
return fmt.Errorf("erreur création produit: %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(result.ID)
p.SetID(productID) p.SetCreatedAt(result.CreatedAt)
p.SetCreatedAt(createdAt) p.SetUpdatedAt(result.UpdatedAt)
p.SetUpdatedAt(updatedAt)
// Insérer les prix for i, price := range p.GetPrices() {
prices := p.GetPrices() err := d.GDB.Exec(`INSERT INTO product_prices (product_id, quantity, price) VALUES (?, ?, ?)`,
for i, price := range prices { result.ID, price.Quantity, price.Price).Error
priceQuery := `INSERT INTO product_prices (product_id, quantity, price)
VALUES ($1, $2, $3)`
_, err := db.Exec(priceQuery, productID, price.Quantity, price.Price)
if err != nil { if err != nil {
log.Printf("❌ [DB CreateProduct] Erreur insertion prix[%d]: %v", i, err) log.Printf("❌ [DB CreateProduct] Erreur insertion prix[%d]: %v", i, err)
return fmt.Errorf("erreur insertion prix: %v", err) return fmt.Errorf("erreur insertion prix: %v", err)
} }
log.Printf("✅ [DB CreateProduct] Prix[%d] inséré: quantity=%g, price=%.2f", log.Printf("✅ [DB CreateProduct] Prix[%d] inséré: quantity=%g, price=%.2f", i, price.Quantity, price.Price)
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 return nil
} }
@@ -91,27 +87,21 @@ func (d *Database) GetProductByID(id int) (models.Product, error) {
log.Printf("📦 [GetProductByID] START - ID=%d", id) log.Printf("📦 [GetProductByID] START - ID=%d", id)
var p models.Product var p models.Product
err := d.GDB.Raw(`
// ✅ AJOUTER stock dans le SELECT
err := d.QueryRow(`
SELECT id, name, category, description, stock, unit, created_at, updated_at SELECT id, name, category, description, stock, unit, created_at, updated_at
FROM products FROM products
WHERE id=$1 WHERE id = ?`, id).Scan(&p).Error
`, id).Scan(&p.ID, &p.Name, &p.Category, &p.Description, &p.Stock, &p.Unit, &p.CreatedAt, &p.UpdatedAt)
if err != nil { if err != nil {
log.Printf("❌ [GetProductByID] Erreur query: %v", err) log.Printf("❌ [GetProductByID] Erreur query: %v", err)
return p, err return p, err
} }
log.Printf("📊 [GetProductByID] Product scanned: ID=%d, Name=%s, Stock=%.2f", log.Printf("📊 [GetProductByID] Product scanned: ID=%d, Name=%s, Stock=%.2f", p.ID, p.Name, p.Stock)
p.ID, p.Name, p.Stock)
// ✅ CHARGER LES PRICES
prices, err := d.GetProductPrices(p.ID) prices, err := d.GetProductPrices(p.ID)
if err != nil { if err != nil {
log.Printf("⚠️ [GetProductByID] Erreur loading prices: %v", err) log.Printf("⚠️ [GetProductByID] Erreur loading prices: %v", err)
p.Prices = []models.ProductPrice{} // Tableau vide au lieu de nil p.Prices = []models.ProductPrice{}
} else { } else {
p.Prices = prices p.Prices = prices
log.Printf("✅ [GetProductByID] Loaded %d prices for product %d", len(prices), p.ID) 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 return p, nil
} }
func (d *Database) GetAllProducts() ([]models.Product, error) { func (d *Database) GetAllProducts() ([]models.Product, error) {
log.Println("📦 [GetAllProducts] START") 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 SELECT id, name, category, description, stock, unit, created_at, updated_at
FROM products FROM products
ORDER BY id ASC ORDER BY id ASC`).Scan(&products).Error
`)
if err != nil { if err != nil {
log.Printf("❌ [GetAllProducts] Erreur query: %v", err) log.Printf("❌ [GetAllProducts] Erreur query: %v", err)
return nil, err return nil, err
} }
defer rows.Close()
var products []models.Product for i := range products {
for rows.Next() { prices, err := d.GetProductPrices(products[i].ID)
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)
if err != nil { if err != nil {
log.Printf("⚠️ [GetAllProducts] Erreur loading prices for product %d: %v", p.ID, err) log.Printf("⚠️ [GetAllProducts] Erreur loading prices for product %d: %v", products[i].ID, err)
p.Prices = []models.ProductPrice{} products[i].Prices = []models.ProductPrice{}
} else { } else {
p.Prices = prices products[i].Prices = prices
log.Printf("✅ [GetAllProducts] Loaded %d prices for product %d", len(prices), p.ID) log.Printf("✅ [GetAllProducts] Loaded %d prices for product %d", len(prices), products[i].ID)
} }
// ✅ CHARGER LES MÉDIAS (MANQUANT AVANT!) media, err := d.GetMediaByProductID(products[i].ID)
media, err := d.GetMediaByProductID(p.ID)
if err != nil { if err != nil {
log.Printf("⚠️ [GetAllProducts] Erreur loading media for product %d: %v", p.ID, err) log.Printf("⚠️ [GetAllProducts] Erreur loading media for product %d: %v", products[i].ID, err)
p.Media = []models.Media{} products[i].Media = []models.Media{}
} else { } else {
p.Media = media products[i].Media = media
log.Printf("✅ [GetAllProducts] Loaded %d media for product %d", len(media), p.ID) 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)) 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 // 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) 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 SELECT id, name, category, description, stock, unit, created_at, updated_at
FROM products FROM products
WHERE category = $1 WHERE category = ?
ORDER BY created_at DESC ORDER BY created_at DESC`, category).Scan(&products).Error
`, category)
if err != nil { if err != nil {
log.Printf("❌ [GetProductsByCategory] Erreur query: %v", err) log.Printf("❌ [GetProductsByCategory] Erreur query: %v", err)
return nil, fmt.Errorf("erreur lors de la récupération des produits: %w", err) return nil, fmt.Errorf("erreur lors de la récupération des produits: %w", err)
} }
defer rows.Close()
var products []models.Product for i := range products {
for rows.Next() { prices, err := d.GetProductPrices(products[i].ID)
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)
if err != nil { if err != nil {
log.Printf("⚠️ [GetProductsByCategory] Erreur loading prices for product %d: %v", p.ID, err) log.Printf("⚠️ [GetProductsByCategory] Erreur loading prices for product %d: %v", products[i].ID, err)
p.Prices = []models.ProductPrice{} // Tableau vide au lieu de nil products[i].Prices = []models.ProductPrice{}
} else { } else {
p.Prices = prices products[i].Prices = prices
log.Printf("✅ [GetProductsByCategory] Loaded %d prices for product %d", len(prices), p.ID) 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)) log.Printf("✅ [GetProductsByCategory] Total products loaded: %d", len(products))
return products, nil return products, nil
} }
func (db *Database) UpdateProduct(productID int, name, category, description, unit string, stock float64, prices []models.ProductPrice) error { func (d *Database) UpdateProduct(productID int, name, category, description, unit string, stock float64, prices []models.ProductPrice) error {
_, err := db.Exec(` err := d.GDB.Exec(`
UPDATE products UPDATE products
SET name = $1, category = $2, description = $3, stock = $4, unit = $5, updated_at = $6 SET name = ?, category = ?, description = ?, stock = ?, unit = ?, updated_at = ?
WHERE id = $7 WHERE id = ?`,
`, name, category, description, stock, unit, time.Now(), productID) name, category, description, stock, unit, time.Now(), productID).Error
if err != nil { if err != nil {
return fmt.Errorf("erreur mise à jour produit: %w", err) 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 { for _, price := range prices {
_, err := db.Exec(` if err := d.GDB.Exec(`INSERT INTO product_prices (product_id, quantity, price) VALUES (?, ?, ?)`,
INSERT INTO product_prices (product_id, quantity, price) productID, price.Quantity, price.Price).Error; err != nil {
VALUES ($1, $2, $3)
`, productID, price.Quantity, price.Price)
if err != nil {
log.Printf("❌ [UpdateProduct] Erreur prix: %v", err) 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 // DeleteProduct supprime un produit
func (db *Database) DeleteProduct(productID int) error { func (d *Database) DeleteProduct(productID int) error {
query := `DELETE FROM products WHERE id = $1` result := d.GDB.Exec(`DELETE FROM products WHERE id = ?`, productID)
result, err := db.Exec(query, productID) if result.Error != nil {
if err != nil { return fmt.Errorf("erreur suppression produit: %v", result.Error)
return fmt.Errorf("erreur suppression produit: %v", err)
} }
if result.RowsAffected == 0 {
rowsAffected, _ := result.RowsAffected()
if rowsAffected == 0 {
return fmt.Errorf("produit introuvable") return fmt.Errorf("produit introuvable")
} }
return nil return nil
} }
func (d *Database) GetProductNameByID(productID int) (string, error) { func (d *Database) GetProductNameByID(productID int) (string, error) {
var name string var result struct {
query := `SELECT name FROM products WHERE id = $1` Name string `gorm:"column:name"`
err := d.QueryRow(query, productID).Scan(&name) }
err := d.GDB.Raw(`SELECT name FROM products WHERE id = ?`, productID).Scan(&result).Error
if err != nil { if err != nil {
if err == sql.ErrNoRows {
return "", fmt.Errorf("produit non trouvé")
}
return "", err return "", err
} }
return name, nil if result.Name == "" {
return "", fmt.Errorf("produit non trouvé")
}
return result.Name, nil
} }
+18 -54
View File
@@ -3,79 +3,43 @@ package db
import ( import (
"fmt" "fmt"
"gestion/models" "gestion/models"
"log"
) )
// GetProductPrices récupère tous les prix d'un produit func (d *Database) GetProductPrices(productID int) ([]models.ProductPrice, error) {
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()
var prices []models.ProductPrice var prices []models.ProductPrice
for rows.Next() { if err := d.GDB.Where("product_id = ?", productID).Order("quantity ASC").Find(&prices).Error; err != nil {
var price models.ProductPrice return nil, fmt.Errorf("erreur récupération prix: %w", err)
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)
} }
log.Printf("✅ [GetProductPrices] Found %d prices for product %d", len(prices), productID)
return prices, nil return prices, nil
} }
// CreateProductPrice ajoute un nouveau prix pour un produit func (d *Database) CreateProductPrice(productID int, quantity float64, price float64) error {
func (db *Database) CreateProductPrice(productID int, quantity float64, price float64) error { p := models.ProductPrice{ProductID: productID, Quantity: quantity, Price: price}
query := `INSERT INTO product_prices (product_id, quantity, price) if err := d.GDB.Create(&p).Error; err != nil {
VALUES ($1, $2, $3)` return fmt.Errorf("erreur création prix: %w", err)
_, err := db.Exec(query, productID, quantity, price)
if err != nil {
return fmt.Errorf("erreur création prix: %v", err)
} }
return nil return nil
} }
// UpdateProductPrice met à jour un prix func (d *Database) UpdateProductPrice(priceID int, quantity float64, price float64) error {
func (db *Database) UpdateProductPrice(priceID int, quantity float64, price float64) error { result := d.GDB.Model(&models.ProductPrice{}).Where("id = ?", priceID).
query := `UPDATE product_prices SET quantity = $1, price = $2 WHERE id = $3` Updates(map[string]any{"quantity": quantity, "price": price})
result, err := db.Exec(query, quantity, price, priceID) if result.Error != nil {
if err != nil { return fmt.Errorf("erreur mise à jour prix: %w", result.Error)
return fmt.Errorf("erreur mise à jour prix: %v", err)
} }
if result.RowsAffected == 0 {
rowsAffected, _ := result.RowsAffected()
if rowsAffected == 0 {
return fmt.Errorf("prix introuvable") return fmt.Errorf("prix introuvable")
} }
return nil return nil
} }
// DeleteProductPrice supprime un prix func (d *Database) DeleteProductPrice(priceID int) error {
func (db *Database) DeleteProductPrice(priceID int) error { result := d.GDB.Delete(&models.ProductPrice{}, priceID)
query := `DELETE FROM product_prices WHERE id = $1` if result.Error != nil {
result, err := db.Exec(query, priceID) return fmt.Errorf("erreur suppression prix: %w", result.Error)
if err != nil {
return fmt.Errorf("erreur suppression prix: %v", err)
} }
if result.RowsAffected == 0 {
rowsAffected, _ := result.RowsAffected()
if rowsAffected == 0 {
return fmt.Errorf("prix introuvable") return fmt.Errorf("prix introuvable")
} }
return nil return nil
} }
+23 -57
View File
@@ -1,94 +1,60 @@
package db package db
import ( import (
"database/sql"
"fmt" "fmt"
"gorm.io/gorm"
) )
// GetClientReferralBalance retourne le solde parrainage d'un client.
func (d *Database) GetClientReferralBalance(username string) (float64, error) { func (d *Database) GetClientReferralBalance(username string) (float64, error) {
var balance float64 var balance float64
err := d.QueryRow( err := d.GDB.Table("clients").Select("referral_balance").Where("username = ?", username).Scan(&balance).Error
`SELECT referral_balance FROM clients WHERE username = $1`, if err != nil {
username, return 0, err
).Scan(&balance)
if err == sql.ErrNoRows {
return 0, fmt.Errorf("client non trouvé")
} }
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 { func (d *Database) CreditClientReferral(username string, amount float64) error {
if amount <= 0 { if amount <= 0 {
return fmt.Errorf("le montant doit être positif") return fmt.Errorf("le montant doit être positif")
} }
res, err := d.Exec( result := d.GDB.Exec(`UPDATE clients SET referral_balance = referral_balance + ? WHERE username = ?`, amount, username)
`UPDATE clients SET referral_balance = referral_balance + $1 WHERE username = $2`, if result.Error != nil {
amount, username, return result.Error
)
if err != nil {
return err
} }
n, _ := res.RowsAffected() if result.RowsAffected == 0 {
if n == 0 {
return fmt.Errorf("client non trouvé") return fmt.Errorf("client non trouvé")
} }
return nil 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 { func (d *Database) DebitReferralBalance(username string, amount float64) error {
if amount <= 0 { if amount <= 0 {
return nil return nil
} }
tx, err := d.Begin() return d.GDB.Transaction(func(tx *gorm.DB) error {
if err != nil { var balance float64
return fmt.Errorf("erreur transaction: %w", err) 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é")
var balance float64 }
if err := tx.QueryRow( if balance < amount {
`SELECT referral_balance FROM clients WHERE username = $1 FOR UPDATE`, return fmt.Errorf("solde parrainage insuffisant (disponible: %.2f€)", balance)
username, }
).Scan(&balance); err != nil { return tx.Exec(`UPDATE clients SET referral_balance = referral_balance - ? WHERE username = ?`, amount, username).Error
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()
} }
// UseClientReferralBalance déduit un montant du solde parrainage dans une transaction. func (d *Database) UseClientReferralBalance(tx *gorm.DB, username string, amount float64) error {
func (d *Database) UseClientReferralBalance(tx *sql.Tx, username string, amount float64) error {
if amount <= 0 { if amount <= 0 {
return nil return nil
} }
var balance float64 var balance float64
err := tx.QueryRow( if err := tx.Raw(`SELECT referral_balance FROM clients WHERE username = ? FOR UPDATE`, username).Scan(&balance).Error; err != nil {
`SELECT referral_balance FROM clients WHERE username = $1 FOR UPDATE`,
username,
).Scan(&balance)
if err != nil {
return fmt.Errorf("client non trouvé") return fmt.Errorf("client non trouvé")
} }
if balance < amount { if balance < amount {
return fmt.Errorf("solde parrainage insuffisant (disponible: %.2f€)", balance) return fmt.Errorf("solde parrainage insuffisant (disponible: %.2f€)", balance)
} }
_, err = tx.Exec( return tx.Exec(`UPDATE clients SET referral_balance = referral_balance - ? WHERE username = ?`, amount, username).Error
`UPDATE clients SET referral_balance = referral_balance - $1 WHERE username = $2`,
amount, username,
)
return err
} }
+23 -36
View File
@@ -14,36 +14,29 @@ import (
// GetClientCancellationsCount récupère le nombre d'annulations tardives d'un client // GetClientCancellationsCount récupère le nombre d'annulations tardives d'un client
func (d *Database) GetClientCancellationsCount(username string) (int, error) { func (d *Database) GetClientCancellationsCount(username string) (int, error) {
var count int var result struct {
query := `SELECT COALESCE(cancellations_count, 0) FROM clients WHERE username = $1` Count int `gorm:"column:count"`
}
err := d.QueryRow(query, username).Scan(&count) err := d.GDB.Raw(`SELECT COALESCE(cancellations_count, 0) as count FROM clients WHERE username = ?`, username).Scan(&result).Error
if err != nil { if err != nil {
log.Printf("❌ [GetCancellationsCount] Erreur: %v", err) log.Printf("❌ [GetCancellationsCount] Erreur: %v", err)
return 0, fmt.Errorf("erreur récupération compteur: %w", err) return 0, fmt.Errorf("erreur récupération compteur: %w", err)
} }
return result.Count, nil
return count, nil
} }
// IncrementClientCancellationsCount incrémente le compteur d'annulations // IncrementClientCancellationsCount incrémente le compteur d'annulations
func (d *Database) IncrementClientCancellationsCount(username string) error { func (d *Database) IncrementClientCancellationsCount(username string) error {
query := `UPDATE clients result := d.GDB.Exec(`
SET cancellations_count = COALESCE(cancellations_count, 0) + 1, UPDATE clients
updated_at = CURRENT_TIMESTAMP SET cancellations_count = COALESCE(cancellations_count, 0) + 1,
WHERE username = $1` updated_at = CURRENT_TIMESTAMP
WHERE username = ?`, username)
result, err := d.Exec(query, username) if result.Error != nil {
if err != nil { log.Printf("❌ [IncrementCancellations] Erreur: %v", result.Error)
log.Printf("❌ [IncrementCancellations] Erreur: %v", err) return fmt.Errorf("erreur incrémentation: %w", result.Error)
return fmt.Errorf("erreur incrémentation: %w", err)
} }
if result.RowsAffected == 0 {
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("erreur vérification: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("client non trouvé") return fmt.Errorf("client non trouvé")
} }
@@ -105,22 +98,16 @@ func (d *Database) ApplyCancellationPenalty(username string) (int, error) {
return 0, err return 0, err
} }
query := `UPDATE clients result := d.GDB.Exec(`
SET amende = $1, UPDATE clients
updated_at = CURRENT_TIMESTAMP SET amende = ?,
WHERE username = $2` updated_at = CURRENT_TIMESTAMP
WHERE username = ?`, float64(penalty), username)
result, err := d.Exec(query, float64(penalty), username) if result.Error != nil {
if err != nil { log.Printf("❌ [ApplyCancellationPenalty] Erreur UPDATE: %v", result.Error)
log.Printf("❌ [ApplyCancellationPenalty] Erreur UPDATE: %v", err) return 0, fmt.Errorf("erreur application pénalité: %w", result.Error)
return 0, fmt.Errorf("erreur application pénalité: %w", err)
} }
if result.RowsAffected == 0 {
rowsAffected, err := result.RowsAffected()
if err != nil {
return 0, fmt.Errorf("erreur vérification: %w", err)
}
if rowsAffected == 0 {
return 0, fmt.Errorf("client non trouvé") return 0, fmt.Errorf("client non trouvé")
} }
+60 -66
View File
@@ -4,6 +4,8 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"gestion/models" "gestion/models"
"gorm.io/gorm"
) )
// DefaultDeliverySchedule retourne un planning de livraison par défaut (tous les jours, 9h-20h) // 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) { func (d *Database) GetSettings() (models.AppSettings, error) {
settings := DefaultSettings() settings := DefaultSettings()
rows, err := d.Query(`SELECT key, value FROM app_settings`) var rows []struct {
if err != nil { 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) return settings, fmt.Errorf("erreur lecture settings: %w", err)
} }
defer rows.Close()
for rows.Next() { for _, row := range rows {
var key, value string switch row.Key {
if err := rows.Scan(&key, &value); err != nil {
continue
}
switch key {
case "penalties_enabled": case "penalties_enabled":
settings.PenaltiesEnabled = value == "true" settings.PenaltiesEnabled = row.Value == "true"
case "show_amende_score": case "show_amende_score":
settings.ShowAmendeScore = value == "true" settings.ShowAmendeScore = row.Value == "true"
case "points_enabled": case "points_enabled":
settings.PointsEnabled = value == "true" settings.PointsEnabled = row.Value == "true"
case "points_pools": case "points_pools":
var pools []models.PointsPool 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 settings.PointsPools = pools
} }
case "referral_enabled": case "referral_enabled":
settings.ReferralEnabled = value == "true" settings.ReferralEnabled = row.Value == "true"
case "crypto_payment_enabled": case "crypto_payment_enabled":
settings.CryptoPaymentEnabled = value == "true" settings.CryptoPaymentEnabled = row.Value == "true"
case "crypto_only": case "crypto_only":
settings.CryptoOnly = value == "true" settings.CryptoOnly = row.Value == "true"
case "nowpayments_api_key": case "nowpayments_api_key":
settings.NowPaymentsAPIKey = value settings.NowPaymentsAPIKey = row.Value
case "nowpayments_ipn_secret": case "nowpayments_ipn_secret":
settings.NowPaymentsIPNSecret = value settings.NowPaymentsIPNSecret = row.Value
case "nowpayments_currencies": case "nowpayments_currencies":
var currencies []string var currencies []string
if err := json.Unmarshal([]byte(value), &currencies); err == nil { if err := json.Unmarshal([]byte(row.Value), &currencies); err == nil {
settings.NowPaymentsCurrencies = currencies settings.NowPaymentsCurrencies = currencies
} }
case "delivery_schedule": case "delivery_schedule":
var sched models.DeliverySchedule 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 settings.DeliverySchedule = sched
} }
case "postal_zones": case "postal_zones":
var zones []models.PostalZone 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 settings.PostalZones = zones
} }
case "telegram_bot_token": case "telegram_bot_token":
settings.TelegramBotToken = value settings.TelegramBotToken = row.Value
case "telegram_bot_username": case "telegram_bot_username":
settings.TelegramBotUsername = value settings.TelegramBotUsername = row.Value
case "delivery_mode": case "delivery_mode":
var mode models.DeliveryModeConfig 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 settings.DeliveryMode = mode
} }
} }
} }
return settings, rows.Err() return settings, nil
} }
// UpdateSettings sauvegarde les paramètres dans la DB // UpdateSettings sauvegarde les paramètres dans la DB
@@ -158,7 +158,6 @@ func (d *Database) UpdateSettings(s models.AppSettings) error {
if s.PointsPools == nil { if s.PointsPools == nil {
s.PointsPools = []models.PointsPool{} s.PointsPools = []models.PointsPool{}
} }
// S'assurer que chaque pool a des slices non-nil
for i := range s.PointsPools { for i := range s.PointsPools {
if s.PointsPools[i].Categories == nil { if s.PointsPools[i].Categories == nil {
s.PointsPools[i].Categories = []string{} 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) 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 { if s.NowPaymentsCurrencies == nil {
s.NowPaymentsCurrencies = []string{} 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) 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{ pairs := [][2]string{
{"penalties_enabled", boolStr(s.PenaltiesEnabled)}, {"penalties_enabled", boolStr(s.PenaltiesEnabled)},
{"show_amende_score", boolStr(s.ShowAmendeScore)}, {"show_amende_score", boolStr(s.ShowAmendeScore)},
@@ -201,39 +212,22 @@ func (d *Database) UpdateSettings(s models.AppSettings) error {
{"nowpayments_api_key", s.NowPaymentsAPIKey}, {"nowpayments_api_key", s.NowPaymentsAPIKey},
{"nowpayments_ipn_secret", s.NowPaymentsIPNSecret}, {"nowpayments_ipn_secret", s.NowPaymentsIPNSecret},
{"nowpayments_currencies", string(currenciesJSON)}, {"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) upsert := `INSERT INTO app_settings (key, value) VALUES (?, ?)
if err != nil { ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value`
return fmt.Errorf("erreur sérialisation delivery_schedule: %w", err)
}
pairs = append(pairs, [2]string{"delivery_schedule", string(schedJSON)})
if s.PostalZones == nil { return d.GDB.Transaction(func(tx *gorm.DB) error {
s.PostalZones = []models.PostalZone{} for _, p := range pairs {
} if err := tx.Exec(upsert, p[0], p[1]).Error; err != nil {
zonesJSON, err := json.Marshal(s.PostalZones) return fmt.Errorf("erreur upsert %s: %w", p[0], err)
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 nil
})
return tx.Commit()
} }
+32 -22
View File
@@ -2,7 +2,6 @@ package db
import ( import (
"crypto/rand" "crypto/rand"
"database/sql"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
@@ -18,7 +17,7 @@ func (d *Database) MigrateAddTelegramColumns() {
`ALTER TABLE users ADD COLUMN IF NOT EXISTS telegram_chat_id BIGINT`, `ALTER TABLE users ADD COLUMN IF NOT EXISTS telegram_chat_id BIGINT`,
} }
for _, q := range migrations { 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) 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 { 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 d.GDB.Exec(`UPDATE clients SET telegram_chat_id = ? WHERE username = ?`, chatID, username).Error
return err
} }
func (d *Database) GetClientTelegramChatID(username string) (int64, bool, error) { func (d *Database) GetClientTelegramChatID(username string) (int64, bool, error) {
var chatID sql.NullInt64 var result struct {
err := d.QueryRow(`SELECT telegram_chat_id FROM clients WHERE username = $1`, username).Scan(&chatID) 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 { if err != nil {
return 0, false, err 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 { func (d *Database) DeleteClientTelegramChatID(username string) error {
_, err := d.Exec(`UPDATE clients SET telegram_chat_id = NULL WHERE username = $1`, username) return d.GDB.Exec(`UPDATE clients SET telegram_chat_id = NULL WHERE username = ?`, username).Error
return err
} }
func (d *Database) SaveUserTelegramChatID(username string, chatID int64) 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 d.GDB.Exec(`UPDATE users SET telegram_chat_id = ? WHERE username = ?`, chatID, username).Error
return err
} }
func (d *Database) GetUserTelegramChatID(username string) (int64, bool, error) { func (d *Database) GetUserTelegramChatID(username string) (int64, bool, error) {
var chatID sql.NullInt64 var result struct {
err := d.QueryRow(`SELECT telegram_chat_id FROM users WHERE username = $1`, username).Scan(&chatID) 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 { if err != nil {
return 0, false, err 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 { func (d *Database) DeleteUserTelegramChatID(username string) error {
_, err := d.Exec(`UPDATE users SET telegram_chat_id = NULL WHERE username = $1`, username) return d.GDB.Exec(`UPDATE users SET telegram_chat_id = NULL WHERE username = ?`, username).Error
return err
} }
// GetUserByTelegramChatID retrouve un utilisateur (clients + users) par chat_id // GetUserByTelegramChatID retrouve un utilisateur (clients + users) par chat_id
func (d *Database) GetUserByTelegramChatID(chatID int64) (username, role string, err error) { 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) var clientResult struct {
if err == nil { Username string `gorm:"column:username"`
return username, "client", nil }
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) var userResult struct {
if err == nil { Username string `gorm:"column:username"`
return username, role, nil 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") return "", "", fmt.Errorf("aucun compte lié à ce chat_id")
+34 -149
View File
@@ -1,205 +1,90 @@
package db package db
import ( import (
"database/sql"
"fmt" "fmt"
"gestion/models" "gestion/models"
"log" "log"
"time"
) )
func (d *Database) CreateUser(user *models.User) error { func (d *Database) CreateUser(user *models.User) error {
query := `INSERT INTO users (username, password, role, created_at, updated_at) if err := d.GDB.Create(user).Error; err != nil {
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 {
return fmt.Errorf("erreur lors de la création de l'utilisateur: %w", err) return fmt.Errorf("erreur lors de la création de l'utilisateur: %w", err)
} }
return nil return nil
} }
// GetAllUsers récupère tous les utilisateurs
func (d *Database) GetAllUsers() ([]*models.User, error) { func (d *Database) GetAllUsers() ([]*models.User, error) {
query := `SELECT id, username, password, role, created_at, updated_at var users []*models.User
FROM users ORDER BY created_at DESC` if err := d.GDB.Order("created_at DESC").Find(&users).Error; err != nil {
rows, err := d.Query(query)
if err != nil {
return nil, fmt.Errorf("erreur lors de la récupération des utilisateurs: %w", err) 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 return users, nil
} }
func (d *Database) GetAllDeliveryMen() ([]*models.User, error) { 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 var users []*models.User
if err := d.GDB.Where("role = ?", "livreur").Find(&users).Error; err != nil {
for rows.Next() { return nil, fmt.Errorf("erreur lors de la récupération des livreurs: %w", err)
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 = rows.Err(); err != nil {
return nil, fmt.Errorf("erreur lors de l'itération des résultats: %w", err)
}
return users, nil return users, nil
} }
// UpdateUser met à jour un utilisateur existant
func (d *Database) UpdateUser(user *models.User) error { func (d *Database) UpdateUser(user *models.User) error {
query := `UPDATE users result := d.GDB.Model(user).Updates(models.User{
SET username = $1, password = $2, role = $3, updated_at = CURRENT_TIMESTAMP Username: user.Username,
WHERE id = $4` Password: user.Password,
Role: user.Role,
result, err := d.Exec(query, user.Username, user.Password, user.Role, user.ID) })
if err != nil { if result.Error != nil {
return fmt.Errorf("erreur lors de la mise à jour de l'utilisateur: %w", err) return fmt.Errorf("erreur lors de la mise à jour de l'utilisateur: %w", result.Error)
} }
if result.RowsAffected == 0 {
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("erreur lors de la vérification des lignes affectées: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("utilisateur non trouvé") return fmt.Errorf("utilisateur non trouvé")
} }
return nil return nil
} }
// DeleteUser supprime un utilisateur
func (d *Database) DeleteUser(id int) error { func (d *Database) DeleteUser(id int) error {
var role string var user models.User
err := d.QueryRow(`SELECT role FROM users WHERE id = $1`, id).Scan(&role) if err := d.GDB.First(&user, id).Error; err != nil {
if err != nil { if isNotFound(err) {
if err == sql.ErrNoRows {
return fmt.Errorf("utilisateur non trouvé") return fmt.Errorf("utilisateur non trouvé")
} }
return fmt.Errorf("erreur lors de la récupération du rôle: %w", err) 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 := d.GDB.Delete(&models.User{}, id)
if result.Error != nil {
result, err := d.Exec(query, id) return fmt.Errorf("erreur lors de la suppression de l'utilisateur: %w", result.Error)
if err != nil {
return fmt.Errorf("erreur lors de la suppression de l'utilisateur: %w", err)
} }
if result.RowsAffected == 0 {
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("erreur lors de la vérification des lignes affectées: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("utilisateur non trouvé") 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 return nil
} }
// GetUserByID récupère un utilisateur par son ID
func (d *Database) GetUserByID(id int) (*models.User, error) { func (d *Database) GetUserByID(id int) (*models.User, error) {
user := &models.User{} var user models.User
query := `SELECT id, username, password, role, created_at, updated_at if err := d.GDB.First(&user, id).Error; err != nil {
FROM users WHERE id = $1` if isNotFound(err) {
return nil, fmt.Errorf("utilisateur non trouvé")
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 {
return nil, fmt.Errorf("erreur lors de la récupération de l'utilisateur: %w", err) 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) { func (d *Database) GetUserByUsername(username string) (*models.User, error) {
user := &models.User{} var user models.User
query := `SELECT id, username, password, role if err := d.GDB.Where("username = ?", username).First(&user).Error; err != nil {
FROM users WHERE username = $1` if isNotFound(err) {
return nil, fmt.Errorf("utilisateur non trouvé")
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 {
return nil, fmt.Errorf("erreur lors de la récupération de l'utilisateur: %w", err) return nil, fmt.Errorf("erreur lors de la récupération de l'utilisateur: %w", err)
} }
return &user, nil
return user, nil
} }
+8
View File
@@ -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
}
+8
View File
@@ -30,6 +30,12 @@ require (
github.com/gorilla/context v1.1.2 // indirect github.com/gorilla/context v1.1.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/sessions v1.4.0 // 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/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.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/text v0.27.0 // indirect
golang.org/x/tools v0.34.0 // indirect golang.org/x/tools v0.34.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect google.golang.org/protobuf v1.36.9 // indirect
gorm.io/driver/postgres v1.6.0 // indirect
gorm.io/gorm v1.31.1 // indirect
) )
+17
View File
@@ -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/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 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= 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 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 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.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.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.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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=
+3 -1
View File
@@ -176,11 +176,13 @@ func RegisterClient(c *gin.Context) {
func AdminCreateClient(c *gin.Context) { func AdminCreateClient(c *gin.Context) {
var req models.RegisterClientRequest var req models.RegisterClientRequest
if err := c.ShouldBindJSON(&req); err != nil { 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 return
} }
if !utils.ValidatePhoneNumber(req.Telephone) { 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"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Numéro de téléphone invalide"})
return return
} }
+1 -1
View File
@@ -325,7 +325,7 @@ func GetAllCancelledOrders(c *gin.Context) {
// ✅ ENRICHIR les données (sans exposer d'infos sensibles inutiles) // ✅ ENRICHIR les données (sans exposer d'infos sensibles inutiles)
var enrichedOrders []map[string]any var enrichedOrders []map[string]any
for _, order := range cancelledOrders { for _, order := range cancelledOrders {
orderID, _ := order["id"].(int) orderID, _ := strconv.Atoi(fmt.Sprintf("%v", order["id"]))
items, _ := database.GetCommandItems(orderID) items, _ := database.GetCommandItems(orderID)
logs, _ := database.GetCommandLogs(orderID) logs, _ := database.GetCommandLogs(orderID)
+1 -1
View File
@@ -41,7 +41,7 @@ func GetMyDeliveries(c *gin.Context) {
filteredCommands := make([]gin.H, len(commands)) filteredCommands := make([]gin.H, len(commands))
for i, cmd := range commands { for i, cmd := range commands {
commandID, _ := cmd["id"].(int) commandID, _ := strconv.Atoi(fmt.Sprintf("%v", cmd["id"]))
items, _ := database.GetCommandItems(commandID) items, _ := database.GetCommandItems(commandID)
// Client info SANS téléphone // Client info SANS téléphone
+3 -2
View File
@@ -10,6 +10,7 @@ import (
"gestion/db" "gestion/db"
"log" "log"
"net/http" "net/http"
"strconv"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -128,8 +129,8 @@ func GetMyCompletedOrdersWithItems(c *gin.Context) {
var enrichedCommands []map[string]interface{} var enrichedCommands []map[string]interface{}
for _, command := range commands { for _, command := range commands {
commandID, ok := command["id"].(int) commandID, _ := strconv.Atoi(fmt.Sprintf("%v", command["id"]))
if !ok { if commandID == 0 {
continue continue
} }
-30
View File
@@ -22,32 +22,26 @@ import (
) )
func main() { func main() {
// Forcer la timezone Europe/Paris (UTC+1/+2)
if loc, err := time.LoadLocation("Europe/Paris"); err == nil { if loc, err := time.LoadLocation("Europe/Paris"); err == nil {
time.Local = loc time.Local = loc
} else { } else {
log.Printf("⚠️ Impossible de charger la timezone Europe/Paris: %v", err) log.Printf("⚠️ Impossible de charger la timezone Europe/Paris: %v", err)
} }
// Chargement des variables d'environnement
if err := godotenv.Load(); err != nil { if err := godotenv.Load(); err != nil {
log.Println("⚠️ Aucun fichier .env trouvé, utilisation des valeurs par défaut.") log.Println("⚠️ Aucun fichier .env trouvé, utilisation des valeurs par défaut.")
} }
// Initialisation de la base de données
database := db.InitDB() database := db.InitDB()
defer database.Close() defer database.Close()
log.Printf("✅ Database initialisée: %+v", database) log.Printf("✅ Database initialisée: %+v", database)
// Initialisation de Redis
db.InitRedis() db.InitRedis()
defer db.Redis.Close() defer db.Redis.Close()
log.Println("✅ Redis initialisé avec succès") log.Println("✅ Redis initialisé avec succès")
// Initialisation du service de géolocalisation
geoService := services.NewGeoService(db.Redis, db.RedisCtx) geoService := services.NewGeoService(db.Redis, db.RedisCtx)
log.Println("✅ Service de géolocalisation initialisé") log.Println("✅ Service de géolocalisation initialisé")
// Initialisation du service Telegram
telegramService := services.NewTelegramService() telegramService := services.NewTelegramService()
if telegramService.IsConfigured() { if telegramService.IsConfigured() {
log.Println("✅ Service Telegram initialisé") log.Println("✅ Service Telegram initialisé")
@@ -60,15 +54,12 @@ func main() {
log.Println("️ Service Telegram désactivé (TELEGRAM_BOT_TOKEN non défini)") log.Println("️ Service Telegram désactivé (TELEGRAM_BOT_TOKEN non défini)")
} }
// Migration: ajout des colonnes telegram_chat_id
database.MigrateAddTelegramColumns() database.MigrateAddTelegramColumns()
// Priorité DB > .env pour la config Telegram
if dbSettings, err := database.GetSettings(); err == nil { if dbSettings, err := database.GetSettings(); err == nil {
telegramService.Reload(dbSettings.TelegramBotToken, dbSettings.TelegramBotUsername) telegramService.Reload(dbSettings.TelegramBotToken, dbSettings.TelegramBotUsername)
if dbSettings.TelegramBotToken != "" { if dbSettings.TelegramBotToken != "" {
log.Printf("✅ [TELEGRAM] Config chargée depuis la DB (username: %s)", dbSettings.TelegramBotUsername) 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 webhookURL := os.Getenv("TELEGRAM_WEBHOOK_URL"); webhookURL != "" {
if err := telegramService.SetWebhook(webhookURL); err != nil { if err := telegramService.SetWebhook(webhookURL); err != nil {
log.Printf("⚠️ [TELEGRAM] Erreur enregistrement webhook (DB reload): %v", err) 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("")
log.Println("🧹 Démarrage du nettoyage des commandes invalides...") log.Println("🧹 Démarrage du nettoyage des commandes invalides...")
removed, err := database.CleanupInvalidQueueCommands() removed, err := database.CleanupInvalidQueueCommands()
@@ -93,32 +81,21 @@ func main() {
} }
} }
// ============================================
// 🔄 DÉMARRAGE DU SCHEDULER DE NETTOYAGE AUTO
// ============================================
go database.StartQueueCleanupScheduler() go database.StartQueueCleanupScheduler()
log.Println("✅ Scheduler de nettoyage démarré (toutes les 5 min)") log.Println("✅ Scheduler de nettoyage démarré (toutes les 5 min)")
// ============================================
// 🔄 SYNCHRONISATION DES STATUTS LIVREURS
// ============================================
if err := database.SyncAllDeliverymanStatuses(); err != nil { if err := database.SyncAllDeliverymanStatuses(); err != nil {
log.Printf("⚠️ Erreur synchronisation statuts: %v", err) log.Printf("⚠️ Erreur synchronisation statuts: %v", err)
} else { } else {
log.Println("✅ Synchronisation des statuts livreurs terminée") log.Println("✅ Synchronisation des statuts livreurs terminée")
} }
// ============================================
// ⭐ DÉMARRAGE DU CRON JOB AUTO-ASSIGNATION
// ============================================
go workers.StartAutoAssignmentCron(database, geoService) go workers.StartAutoAssignmentCron(database, geoService)
log.Println("✅ Cron job auto-assignation démarré (5 min)") log.Println("✅ Cron job auto-assignation démarré (5 min)")
// Démarrage des workers Redis en arrière-plan
go workers.StartRedisWorkers(database) go workers.StartRedisWorkers(database)
log.Println("✅ Workers Redis démarrés") 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 { workers.StartDynamicPaymentChecker(database, func() *services.NowPaymentsClient {
s, err := database.GetSettings() s, err := database.GetSettings()
if err != nil || !s.CryptoPaymentEnabled || s.NowPaymentsAPIKey == "" { if err != nil || !s.CryptoPaymentEnabled || s.NowPaymentsAPIKey == "" {
@@ -130,7 +107,6 @@ func main() {
gin.SetMode(gin.ReleaseMode) gin.SetMode(gin.ReleaseMode)
r := gin.Default() r := gin.Default()
// Configuration des sessions
store := cookie.NewStore([]byte(os.Getenv("SESSION_SECRET"))) store := cookie.NewStore([]byte(os.Getenv("SESSION_SECRET")))
store.Options(sessions.Options{ store.Options(sessions.Options{
Path: "/", Path: "/",
@@ -142,7 +118,6 @@ func main() {
}) })
r.Use(sessions.Sessions("mysession", store)) r.Use(sessions.Sessions("mysession", store))
// Configuration CORS
r.Use(cors.New(cors.Config{ 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"}, 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"}, AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"},
@@ -151,7 +126,6 @@ func main() {
AllowCredentials: true, AllowCredentials: true,
})) }))
// Middleware pour injecter la base de données et geoService
r.Use(func(c *gin.Context) { r.Use(func(c *gin.Context) {
c.Set("database", database) c.Set("database", database)
c.Set("geoService", geoService) c.Set("geoService", geoService)
@@ -167,12 +141,8 @@ func main() {
c.Next() c.Next()
}) })
// Fichiers statiques
r.Static("/uploads", "./uploads") r.Static("/uploads", "./uploads")
// ============================================
// ENREGISTRER TOUTES LES ROUTES
// ============================================
routes.SetupRoutes(r, database, geoService) routes.SetupRoutes(r, database, geoService)
if err := r.Run(":8080"); err != nil { if err := r.Run(":8080"); err != nil {
+7 -5
View File
@@ -3,9 +3,11 @@ package models
import "time" import "time"
type Address struct { type Address struct {
ID int64 `db:"id"` ID int64 `db:"id" gorm:"primaryKey;autoIncrement"`
InvalidAddress string `db:"invalid_address"` InvalidAddress string `db:"invalid_address" gorm:"column:invalid_address"`
CorrectAddress string `db:"correct_address"` CorrectAddress string `db:"correct_address" gorm:"column:correct_address"`
CreatedAt time.Time `db:"created_at"` CreatedAt time.Time `db:"created_at" gorm:"autoCreateTime"`
UpdatedAt time.Time `db:"updated_at"` UpdatedAt time.Time `db:"updated_at" gorm:"autoUpdateTime"`
} }
func (Address) TableName() string { return "adresse_correction" }
+10 -6
View File
@@ -1,10 +1,14 @@
package models package models
import "time"
type AlertPolicy struct { type AlertPolicy struct {
ID int `json:"id"` ID int `json:"id" gorm:"primaryKey;autoIncrement"`
Username string `json:"username"` Username string `json:"username" gorm:"column:username"`
Status string `json:"status"` Status string `json:"status" gorm:"column:status"`
Message string `json:"message"` Message string `json:"message" gorm:"column:message"`
CreatedAt string `json:"created_at"` CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
UpdatedAt string `json:"updated_at"` UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
} }
func (AlertPolicy) TableName() string { return "alerte_policy" }
+16 -15
View File
@@ -4,19 +4,20 @@ package models
import "time" import "time"
type Client struct { type Client struct {
ID int `json:"id"` ID int `gorm:"primaryKey;autoIncrement" json:"id"`
Username string `json:"username"` Username string `gorm:"column:username" json:"username"`
Password string `json:"-"` // Ne jamais exposer le password dans le JSON Password string `gorm:"column:password" json:"-"`
Nom string `json:"nom"` Nom string `gorm:"column:nom" json:"nom"`
Prenom string `json:"prenom"` Prenom string `gorm:"column:prenom" json:"prenom"`
Telephone string `json:"telephone"` Telephone string `gorm:"column:telephone" json:"telephone"`
Command int `json:"command"` Command int `gorm:"column:command" json:"command"`
PointsExtra map[string]int `json:"points_extra"` PointsExtra map[string]int `gorm:"-" json:"points_extra"`
Amende float64 `json:"amende"` Amende float64 `gorm:"column:amende" json:"amende"`
CancellationsCount int `json:"cancellations_count"` CancellationsCount int `gorm:"column:cancellations_count" json:"cancellations_count"`
LastPenaltyReason string `json:"last_penalty_reason"` LastPenaltyReason string `gorm:"column:last_penalty_reason" json:"last_penalty_reason"`
MustChangePassword bool `json:"must_change_password"` MustChangePassword bool `gorm:"column:must_change_password" json:"must_change_password"`
PushToken string `json:"-"` ReferralBalance float64 `gorm:"column:referral_balance" json:"referral_balance"`
ReferralBalance float64 `json:"referral_balance"` CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
CreatedAt time.Time `json:"created_at"`
} }
func (Client) TableName() string { return "clients" }
+12 -10
View File
@@ -10,14 +10,16 @@ type LivreurPosition struct {
} }
type DeliveryIssue struct { type DeliveryIssue struct {
ID int `json:"id"` ID int `json:"id" gorm:"primaryKey;autoIncrement"`
CommandID int `json:"command_id"` CommandID int `json:"command_id" gorm:"column:command_id;index"`
IssueType string `json:"issue_type"` IssueType string `json:"issue_type" gorm:"column:issue_type"`
Description string `json:"description"` Description string `json:"description" gorm:"column:description"`
Status string `json:"status"` // "open", "in_progress", "resolved" Status string `json:"status" gorm:"column:status"`
ReportedBy string `json:"reported_by"` ReportedBy string `json:"reported_by" gorm:"column:reported_by"`
ResolvedBy string `json:"resolved_by,omitempty"` ResolvedBy string `json:"resolved_by,omitempty" gorm:"column:resolved_by"`
Resolution string `json:"resolution,omitempty"` Resolution string `json:"resolution,omitempty" gorm:"column:resolution"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
} }
func (DeliveryIssue) TableName() string { return "delivery_issues" }
+7 -5
View File
@@ -3,13 +3,15 @@ package models
import "time" import "time"
type Media struct { type Media struct {
ID int `json:"id"` ID int `gorm:"primaryKey;autoIncrement" json:"id"`
ProductID int `json:"product_id"` ProductID int `gorm:"column:product_id" json:"product_id"`
Type string `json:"type"` Type string `gorm:"column:type" json:"type"`
URL string `json:"url"` URL string `gorm:"column:url" json:"url"`
CreatedAt time.Time `json:"created_at"` // ✅ Ajouté CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
} }
func (Media) TableName() string { return "media" }
// Méthodes pour l'interface Database // Méthodes pour l'interface Database
func (m *Media) GetProductID() int { return m.ProductID } func (m *Media) GetProductID() int { return m.ProductID }
func (m *Media) GetType() string { return m.Type } func (m *Media) GetType() string { return m.Type }
+13 -11
View File
@@ -4,15 +4,17 @@ import "time"
// CryptoPayment suit un paiement crypto NowPayments lié à une commande // CryptoPayment suit un paiement crypto NowPayments lié à une commande
type CryptoPayment struct { type CryptoPayment struct {
ID int `json:"id"` ID int `json:"id" gorm:"primaryKey;autoIncrement"`
CommandID int `json:"command_id"` CommandID int `json:"command_id" gorm:"column:command_id;index"`
NowPaymentID string `json:"nowpayment_id"` NowPaymentID string `json:"nowpayment_id" gorm:"column:nowpayment_id;uniqueIndex"`
Status string `json:"status"` // waiting, confirming, confirmed, sending, partially_paid, finished, failed, refunded, expired Status string `json:"status" gorm:"column:status"`
PriceAmount float64 `json:"price_amount"` PriceAmount float64 `json:"price_amount" gorm:"column:price_amount"`
PriceCurrency string `json:"price_currency"` PriceCurrency string `json:"price_currency" gorm:"column:price_currency"`
PayCurrency string `json:"pay_currency"` PayCurrency string `json:"pay_currency" gorm:"column:pay_currency"`
PayAddress string `json:"pay_address"` PayAddress string `json:"pay_address" gorm:"column:pay_address"`
PayAmount float64 `json:"pay_amount"` PayAmount float64 `json:"pay_amount" gorm:"column:pay_amount"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
} }
func (CryptoPayment) TableName() string { return "crypto_payments" }
+19 -15
View File
@@ -3,26 +3,30 @@ package models
import "time" import "time"
type Product struct { type Product struct {
ID int `json:"id"` ID int `json:"id" gorm:"primaryKey;autoIncrement"`
Name string `json:"name" binding:"required"` Name string `json:"name" gorm:"column:name" binding:"required"`
Category string `json:"category" binding:"required"` Category string `json:"category" gorm:"column:category" binding:"required"`
Description string `json:"description"` Description string `json:"description" gorm:"column:description"`
Stock float64 `json:"stock"` // ← ajouter le stock ici Stock float64 `json:"stock" gorm:"column:stock"`
Unit string `json:"unit"` // kg | g | bag | l | cl | pcs | u Unit string `json:"unit" gorm:"column:unit"`
Prices []ProductPrice `json:"prices"` Prices []ProductPrice `json:"prices" gorm:"foreignKey:ProductID"`
Media []Media `json:"media,omitempty"` Media []Media `json:"media,omitempty" gorm:"foreignKey:ProductID"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
} }
func (Product) TableName() string { return "products" }
type ProductPrice struct { type ProductPrice struct {
ID int `json:"id"` ID int `json:"id" gorm:"primaryKey;autoIncrement"`
ProductID int `json:"product_id"` ProductID int `json:"product_id" gorm:"column:product_id;index"`
Quantity float64 `json:"quantity" binding:"required"` Quantity float64 `json:"quantity" gorm:"column:quantity" binding:"required"`
Price float64 `json:"price" binding:"required"` Price float64 `json:"price" gorm:"column:price" binding:"required"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
} }
func (ProductPrice) TableName() string { return "product_prices" }
type StockInfo struct { type StockInfo struct {
ProductID int `json:"product_id"` ProductID int `json:"product_id"`
ProductName string `json:"product_name"` ProductName string `json:"product_name"`
+10 -4
View File
@@ -1,8 +1,14 @@
package models package models
import "time"
type User struct { type User struct {
ID int `json:"id"` ID int `json:"id" gorm:"primaryKey;autoIncrement"`
Username string `json:"username"` Username string `json:"username" gorm:"column:username;uniqueIndex"`
Password string `json:"password,omitempty"` // omitempty pour ne pas l'exposer dans les réponses JSON Password string `json:"password,omitempty" gorm:"column:password"`
Role string `json:"role"` // "user" ou "admin" 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" }
+5 -8
View File
@@ -5,6 +5,7 @@ import (
"gestion/db" "gestion/db"
"gestion/services" "gestion/services"
"log" "log"
"strconv"
"time" "time"
) )
@@ -57,14 +58,10 @@ func processAutoAssignmentWithPriority(database *db.Database, geoService *servic
skippedCount := 0 skippedCount := 0
for i, cmd := range commands { for i, cmd := range commands {
commandID, ok := cmd["id"].(int) commandID, _ := strconv.Atoi(fmt.Sprintf("%v", cmd["id"]))
if !ok { if commandID == 0 {
if idFloat, ok := cmd["id"].(float64); ok { skippedCount++
commandID = int(idFloat) continue
} else {
skippedCount++
continue
}
} }
// Log la position dans la queue de priorité // Log la position dans la queue de priorité
+1
View File
@@ -226,6 +226,7 @@ export interface OrderItem {
*/ */
export interface OrderDetail { export interface OrderDetail {
id: number; id: number;
client_order_number?: number;
username: string; username: string;
status: string; status: string;
delivery_address?: string; delivery_address?: string;
@@ -327,7 +327,7 @@ export default function OrderDetailsScreen() {
> >
<View style={styles.headerCard}> <View style={styles.headerCard}>
<View style={styles.headerRow}> <View style={styles.headerRow}>
<Text style={styles.title}>Commande #{order.id}</Text> <Text style={styles.title}>Commande #{order.client_order_number ?? order.id}</Text>
<StatusBadge status={status} /> <StatusBadge status={status} />
</View> </View>
<Text style={styles.date}> <Text style={styles.date}>
@@ -370,7 +370,7 @@ export default function OrderHistoryScreen() {
> >
<View style={styles.orderHeader}> <View style={styles.orderHeader}>
<Text style={styles.orderIdText}> <Text style={styles.orderIdText}>
Commande #{order.id} Commande #{order.client_order_number ?? order.id}
</Text> </Text>
<StatusBadge status={order.status} /> <StatusBadge status={order.status} />
</View> </View>
@@ -406,7 +406,7 @@ export default function OrderTrackingScreen() {
> >
<View style={styles.cardHeader}> <View style={styles.cardHeader}>
<Text style={styles.orderId}> <Text style={styles.orderId}>
Commande #{order.id} Commande #{order.client_order_number ?? order.id}
</Text> </Text>
<StatusBadge status={order.status} /> <StatusBadge status={order.status} />
</View> </View>
+114 -1
View File
@@ -6,6 +6,7 @@ import {
StyleSheet, StyleSheet,
TouchableOpacity, TouchableOpacity,
Alert, Alert,
Modal,
KeyboardAvoidingView, KeyboardAvoidingView,
Platform, Platform,
Linking, Linking,
@@ -45,6 +46,9 @@ export default function ProfileScreen() {
const [telegramEnabled, setTelegramEnabled] = useState(false); const [telegramEnabled, setTelegramEnabled] = useState(false);
const [telegramLoading, setTelegramLoading] = useState(false); const [telegramLoading, setTelegramLoading] = useState(false);
// Modal confirmation infos par défaut
const [showSaveModal, setShowSaveModal] = useState(false);
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
setLoadingProfile(true); setLoadingProfile(true);
const [savedAddress, savedPhone, savedSignal, profileRes, tgStatus] = await Promise.all([ 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_PHONE, defaultPhone.trim()),
AsyncStorage.setItem(STORAGE_SIGNAL, signalPseudo.trim()), AsyncStorage.setItem(STORAGE_SIGNAL, signalPseudo.trim()),
]); ]);
setShowSaveModal(false);
Alert.alert("Enregistré", "Informations par défaut sauvegardées"); 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 }, saveBtnText: { color: "#fff", fontSize: fontSize.sm, fontWeight: fontWeight.semibold },
saveBtnTextSecondary: { color: colors.accent }, 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]); }), [colors]);
if (loadingProfile) { if (loadingProfile) {
@@ -316,7 +389,7 @@ export default function ProfileScreen() {
</View> </View>
<TouchableOpacity <TouchableOpacity
style={[styles.saveBtn, styles.saveBtnSecondary]} style={[styles.saveBtn, styles.saveBtnSecondary]}
onPress={saveLocal} onPress={() => setShowSaveModal(true)}
> >
<Ionicons name="save-outline" size={16} color={colors.accent} /> <Ionicons name="save-outline" size={16} color={colors.accent} />
<Text style={[styles.saveBtnText, styles.saveBtnTextSecondary]}> <Text style={[styles.saveBtnText, styles.saveBtnTextSecondary]}>
@@ -365,6 +438,46 @@ export default function ProfileScreen() {
)} )}
</ScrollView> </ScrollView>
<Modal
visible={showSaveModal}
transparent
animationType="fade"
onRequestClose={() => setShowSaveModal(false)}
>
<TouchableOpacity
style={styles.modalOverlay}
activeOpacity={1}
onPress={() => setShowSaveModal(false)}
>
<TouchableOpacity activeOpacity={1} onPress={() => {}}>
<View style={styles.modalBox}>
<View style={styles.modalIconWrap}>
<Ionicons name="save-outline" size={24} color={colors.accent} />
</View>
<Text style={styles.modalTitle}>Enregistrer les infos par défaut ?</Text>
<Text style={styles.modalBody}>
Adresse, téléphone de livraison et pseudo Signal seront sauvegardés et pré-remplis lors de vos prochaines commandes.
</Text>
<View style={styles.modalActions}>
<TouchableOpacity
style={styles.modalBtnCancel}
onPress={() => setShowSaveModal(false)}
>
<Text style={styles.modalBtnCancelText}>Annuler</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.modalBtnConfirm}
onPress={saveLocal}
>
<Text style={styles.modalBtnConfirmText}>Confirmer</Text>
</TouchableOpacity>
</View>
</View>
</TouchableOpacity>
</TouchableOpacity>
</Modal>
</KeyboardAvoidingView> </KeyboardAvoidingView>
); );
} }