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