chore: update id order
This commit is contained in:
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,164 +1,155 @@
|
||||
// ============================================
|
||||
// db/cancel_commands_db.go
|
||||
// FONCTIONS DB ATOMIQUES POUR L'ANNULATION
|
||||
// VERSION 100% SÉCURISÉE - FIX ETA CHECK
|
||||
// ============================================
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"slices"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ============================================
|
||||
// ANNULATION ATOMIQUE
|
||||
// ============================================
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func (d *Database) CancelCommandAtomic(commandID int, username, reason string, force bool) (int, error) {
|
||||
log.Printf("🔒 [CancelAtomic] START - cmd=%d, user=%s, force=%v", commandID, username, force)
|
||||
|
||||
tx, err := d.Begin()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("erreur transaction: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
var penalty int
|
||||
|
||||
var currentStatus, cmdUsername, livreurAssign string
|
||||
err = tx.QueryRow(`
|
||||
SELECT status, username, COALESCE(livreur_assign, '')
|
||||
FROM commandes
|
||||
WHERE id = $1
|
||||
FOR UPDATE
|
||||
`, commandID).Scan(¤tStatus, &cmdUsername, &livreurAssign)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return 0, fmt.Errorf("commande non trouvée")
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
log.Printf("📋 [CancelAtomic] Trouvée - status=%s, owner=%s, livreur=%s", currentStatus, cmdUsername, livreurAssign)
|
||||
|
||||
if cmdUsername != username {
|
||||
return 0, fmt.Errorf("commande ne vous appartient pas")
|
||||
}
|
||||
|
||||
nonCancellableStatuses := []string{"livre", "approved", "cancelled", "disabled"}
|
||||
if slices.Contains(nonCancellableStatuses, currentStatus) {
|
||||
return 0, fmt.Errorf("impossible d'annuler")
|
||||
}
|
||||
|
||||
isLateCancel := false
|
||||
if livreurAssign != "" {
|
||||
if currentStatus == "en_route" || currentStatus == "arrived" {
|
||||
isLateCancel = true
|
||||
log.Printf("⚠️ [CancelAtomic] Annulation TARDIVE détectée - Statut: %s", currentStatus)
|
||||
} else if d.CheckCommandETAExistsAndValid(commandID) {
|
||||
isLateCancel = true
|
||||
log.Printf("⚠️ [CancelAtomic] Annulation TARDIVE détectée - ETA définie")
|
||||
} else {
|
||||
log.Printf("✅ [CancelAtomic] Annulation SANS PÉNALITÉ - Statut: %s, Pas d'ETA valide", currentStatus)
|
||||
err := d.GDB.Transaction(func(tx *gorm.DB) error {
|
||||
var cmdResult struct {
|
||||
Status string `gorm:"column:status"`
|
||||
Username string `gorm:"column:username"`
|
||||
LivreurAssign string `gorm:"column:livreur_assign"`
|
||||
}
|
||||
} else {
|
||||
log.Printf("✅ [CancelAtomic] Annulation SANS PÉNALITÉ - Aucun livreur assigné")
|
||||
}
|
||||
|
||||
if isLateCancel && !force {
|
||||
return 0, fmt.Errorf("confirmation requise")
|
||||
}
|
||||
|
||||
result, err := tx.Exec(`
|
||||
UPDATE commandes
|
||||
SET status = 'cancelled', updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $1 AND status = $2 AND username = $3
|
||||
`, commandID, currentStatus, username)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if rows, _ := result.RowsAffected(); rows == 0 {
|
||||
return 0, fmt.Errorf("commande déjà modifiée")
|
||||
}
|
||||
log.Printf("✅ [CancelAtomic] Statut mis à jour: %s → cancelled", currentStatus)
|
||||
|
||||
if _, err = tx.Exec(`
|
||||
UPDATE products p
|
||||
SET stock = stock + ci.quantite, updated_at = CURRENT_TIMESTAMP
|
||||
FROM command_items ci
|
||||
WHERE ci.command_id = $1 AND ci.product_id = p.id
|
||||
`, commandID); err != nil {
|
||||
log.Printf("⚠️ [CancelAtomic] Erreur remboursement stock: %v", err)
|
||||
} else {
|
||||
log.Printf("✅ [CancelAtomic] Stock remboursé")
|
||||
}
|
||||
|
||||
penalty := 0
|
||||
if isLateCancel {
|
||||
log.Printf("⚠️ [CancelAtomic] Annulation tardive confirmée - Application pénalité")
|
||||
penalty, _ = d.CalculateCancellationPenalty(username)
|
||||
if _, err = tx.Exec(`
|
||||
UPDATE clients
|
||||
SET amende = amende + $1,
|
||||
cancellations_count = COALESCE(cancellations_count, 0) + 1,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE username = $2
|
||||
`, penalty, username); err != nil {
|
||||
log.Printf("❌ [CancelAtomic] Erreur pénalité: %v", err)
|
||||
} else {
|
||||
log.Printf("⚠️ [CancelAtomic] Pénalité: %d appliquée à %s", penalty, username)
|
||||
err := tx.Raw(`
|
||||
SELECT status, username, COALESCE(livreur_assign, '') as livreur_assign
|
||||
FROM commandes WHERE id = ? FOR UPDATE`, commandID).Scan(&cmdResult).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cmdResult.Username == "" {
|
||||
return fmt.Errorf("commande non trouvée")
|
||||
}
|
||||
}
|
||||
|
||||
if _, err = tx.Exec(`
|
||||
INSERT INTO command_logs (command_id, status, message, author, created_at)
|
||||
VALUES ($1, 'cancelled', $2, $3, CURRENT_TIMESTAMP)
|
||||
`, commandID, fmt.Sprintf("Annulée par %s - Raison: %s", username, reason), username); err != nil {
|
||||
log.Printf("⚠️ [CancelAtomic] Erreur log: %v", err)
|
||||
}
|
||||
log.Printf("📋 [CancelAtomic] Trouvée - status=%s, owner=%s, livreur=%s",
|
||||
cmdResult.Status, cmdResult.Username, cmdResult.LivreurAssign)
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return 0, fmt.Errorf("erreur commit: %w", err)
|
||||
}
|
||||
if cmdResult.Username != username {
|
||||
return fmt.Errorf("commande ne vous appartient pas")
|
||||
}
|
||||
|
||||
if livreurAssign != "" {
|
||||
go func() {
|
||||
if err := d.CleanupCompletedCommandFromQueue(commandID, livreurAssign); err != nil {
|
||||
log.Printf("⚠️ [CancelAtomic] Erreur cleanup queue: %v", err)
|
||||
nonCancellableStatuses := []string{"livre", "approved", "cancelled", "disabled"}
|
||||
if slices.Contains(nonCancellableStatuses, cmdResult.Status) {
|
||||
return fmt.Errorf("impossible d'annuler")
|
||||
}
|
||||
|
||||
isLateCancel := false
|
||||
if cmdResult.LivreurAssign != "" {
|
||||
if cmdResult.Status == "en_route" || cmdResult.Status == "arrived" {
|
||||
isLateCancel = true
|
||||
log.Printf("⚠️ [CancelAtomic] Annulation TARDIVE détectée - Statut: %s", cmdResult.Status)
|
||||
} else if d.CheckCommandETAExistsAndValid(commandID) {
|
||||
isLateCancel = true
|
||||
log.Printf("⚠️ [CancelAtomic] Annulation TARDIVE détectée - ETA définie")
|
||||
} else {
|
||||
log.Printf("✅ [CancelAtomic] Annulation SANS PÉNALITÉ - Statut: %s, Pas d'ETA valide", cmdResult.Status)
|
||||
}
|
||||
} else {
|
||||
log.Printf("✅ [CancelAtomic] Annulation SANS PÉNALITÉ - Aucun livreur assigné")
|
||||
}
|
||||
|
||||
if isLateCancel && !force {
|
||||
return fmt.Errorf("confirmation requise")
|
||||
}
|
||||
|
||||
result := tx.Exec(`
|
||||
UPDATE commandes
|
||||
SET status = 'cancelled', updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ? AND status = ? AND username = ?`, commandID, cmdResult.Status, username)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("commande déjà modifiée")
|
||||
}
|
||||
log.Printf("✅ [CancelAtomic] Statut mis à jour: %s → cancelled", cmdResult.Status)
|
||||
|
||||
if err := tx.Exec(`
|
||||
UPDATE products p
|
||||
SET stock = stock + ci.quantite, updated_at = CURRENT_TIMESTAMP
|
||||
FROM command_items ci
|
||||
WHERE ci.command_id = ? AND ci.product_id = p.id`, commandID).Error; err != nil {
|
||||
log.Printf("⚠️ [CancelAtomic] Erreur remboursement stock: %v", err)
|
||||
} else {
|
||||
log.Printf("✅ [CancelAtomic] Stock remboursé")
|
||||
}
|
||||
|
||||
if isLateCancel {
|
||||
log.Printf("⚠️ [CancelAtomic] Annulation tardive confirmée - Application pénalité")
|
||||
penalty, _ = d.CalculateCancellationPenalty(username)
|
||||
if err := tx.Exec(`
|
||||
UPDATE clients
|
||||
SET amende = amende + ?,
|
||||
cancellations_count = COALESCE(cancellations_count, 0) + 1,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE username = ?`, penalty, username).Error; err != nil {
|
||||
log.Printf("❌ [CancelAtomic] Erreur pénalité: %v", err)
|
||||
} else {
|
||||
log.Printf("⚠️ [CancelAtomic] Pénalité: %d appliquée à %s", penalty, username)
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Exec(`
|
||||
INSERT INTO command_logs (command_id, status, message, author, created_at)
|
||||
VALUES (?, 'cancelled', ?, ?, CURRENT_TIMESTAMP)`,
|
||||
commandID,
|
||||
fmt.Sprintf("Annulée par %s - Raison: %s", username, reason),
|
||||
username,
|
||||
).Error; err != nil {
|
||||
log.Printf("⚠️ [CancelAtomic] Erreur log: %v", err)
|
||||
}
|
||||
|
||||
// Nettoyage async après commit
|
||||
livreur := cmdResult.LivreurAssign
|
||||
if livreur != "" {
|
||||
go func() {
|
||||
if err := d.CleanupCompletedCommandFromQueue(commandID, livreur); err != nil {
|
||||
log.Printf("⚠️ [CancelAtomic] Erreur cleanup queue: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
go func() {
|
||||
Redis.Del(RedisCtx,
|
||||
fmt.Sprintf("command:%d", commandID),
|
||||
fmt.Sprintf("client:%s", username),
|
||||
fmt.Sprintf("client:%s:commands", username),
|
||||
)
|
||||
}()
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
go func() {
|
||||
Redis.Del(RedisCtx,
|
||||
fmt.Sprintf("command:%d", commandID),
|
||||
fmt.Sprintf("client:%s", username),
|
||||
fmt.Sprintf("client:%s:commands", username),
|
||||
)
|
||||
}()
|
||||
|
||||
log.Printf("🎉 [CancelAtomic] SUCCÈS - Commande %d annulée", commandID)
|
||||
return penalty, nil
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// ✅ NOUVELLE FONCTION: CHECK ETA VALIDE
|
||||
// ============================================
|
||||
|
||||
// CheckCommandETAExistsAndValid vérifie si une ETA RÉELLE existe (> 0 minutes, non expirée)
|
||||
func (d *Database) CheckCommandETAExistsAndValid(commandID int) bool {
|
||||
etaKey := fmt.Sprintf("command:eta:%d", commandID)
|
||||
|
||||
// Récupérer l'ETA depuis Redis
|
||||
etaMinutesStr, err := Redis.Get(RedisCtx, etaKey).Result()
|
||||
if err != nil {
|
||||
log.Printf("⚠️ [CheckETA] Pas d'ETA trouvée pour cmd %d", commandID)
|
||||
return false
|
||||
}
|
||||
|
||||
// Parser l'ETA
|
||||
var etaMinutes int
|
||||
_, err = fmt.Sscanf(etaMinutesStr, "%d", &etaMinutes)
|
||||
if err != nil || etaMinutes <= 0 {
|
||||
@@ -166,7 +157,6 @@ func (d *Database) CheckCommandETAExistsAndValid(commandID int) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Vérifier le TTL (si l'ETA existe, elle doit avoir un TTL)
|
||||
ttl, err := Redis.TTL(RedisCtx, etaKey).Result()
|
||||
if err != nil || ttl <= 0 {
|
||||
log.Printf("⚠️ [CheckETA] ETA expirée pour cmd %d", commandID)
|
||||
@@ -177,106 +167,76 @@ func (d *Database) CheckCommandETAExistsAndValid(commandID int) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SUPPRESSION ATOMIQUE
|
||||
// ============================================
|
||||
|
||||
func (d *Database) DeleteCommandAtomic(commandID int, deletedBy, role string) error {
|
||||
log.Printf("🔒 [DeleteAtomic] START - cmd=%d, by=%s (%s)", commandID, deletedBy, role)
|
||||
|
||||
// ✅ TRANSACTION
|
||||
tx, err := d.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("erreur transaction: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
return d.GDB.Transaction(func(tx *gorm.DB) error {
|
||||
var cmdResult struct {
|
||||
Status string `gorm:"column:status"`
|
||||
Username string `gorm:"column:username"`
|
||||
LivreurAssign string `gorm:"column:livreur_assign"`
|
||||
}
|
||||
err := tx.Raw(`
|
||||
SELECT status, username, COALESCE(livreur_assign, '') as livreur_assign
|
||||
FROM commandes WHERE id = ? FOR UPDATE`, commandID).Scan(&cmdResult).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cmdResult.Username == "" {
|
||||
return fmt.Errorf("commande non trouvée")
|
||||
}
|
||||
|
||||
// ✅ SELECT FOR UPDATE
|
||||
var currentStatus, cmdUsername, livreurAssign string
|
||||
err = tx.QueryRow(`
|
||||
SELECT status, username, COALESCE(livreur_assign, '')
|
||||
FROM commandes
|
||||
WHERE id = $1
|
||||
FOR UPDATE
|
||||
`, commandID).Scan(¤tStatus, &cmdUsername, &livreurAssign)
|
||||
log.Printf("📋 [DeleteAtomic] Trouvée - status=%s, client=%s", cmdResult.Status, cmdResult.Username)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return fmt.Errorf("commande non trouvée")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Exec(`
|
||||
UPDATE products p
|
||||
SET stock = stock + ci.quantite, updated_at = CURRENT_TIMESTAMP
|
||||
FROM command_items ci
|
||||
WHERE ci.command_id = ? AND ci.product_id = p.id`, commandID).Error; err != nil {
|
||||
log.Printf("⚠️ [DeleteAtomic] Erreur remboursement: %v", err)
|
||||
} else {
|
||||
log.Printf("✅ [DeleteAtomic] Stock remboursé")
|
||||
}
|
||||
|
||||
log.Printf("📋 [DeleteAtomic] Trouvée - status=%s, client=%s", currentStatus, cmdUsername)
|
||||
tx.Exec(`
|
||||
INSERT INTO command_logs (command_id, status, message, author, created_at)
|
||||
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)`,
|
||||
commandID, "deleted",
|
||||
fmt.Sprintf("Supprimée par %s (%s) - Ancien statut: %s", deletedBy, role, cmdResult.Status),
|
||||
deletedBy)
|
||||
|
||||
// ✅ REMBOURSER STOCK ATOMIQUEMENT
|
||||
_, err = tx.Exec(`
|
||||
UPDATE products p
|
||||
SET stock = stock + ci.quantite, updated_at = CURRENT_TIMESTAMP
|
||||
FROM command_items ci
|
||||
WHERE ci.command_id = $1 AND ci.product_id = p.id
|
||||
`, commandID)
|
||||
if err := tx.Exec(`DELETE FROM command_items WHERE command_id = ?`, commandID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Printf("⚠️ [DeleteAtomic] Erreur remboursement: %v", err)
|
||||
} else {
|
||||
log.Printf("✅ [DeleteAtomic] Stock remboursé")
|
||||
}
|
||||
result := tx.Exec(`DELETE FROM commandes WHERE id = ?`, commandID)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("commande non trouvée")
|
||||
}
|
||||
|
||||
// ✅ LOG AVANT SUPPRESSION
|
||||
_, err = tx.Exec(`
|
||||
INSERT INTO command_logs (command_id, status, message, author, created_at)
|
||||
VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP)
|
||||
`, commandID, "deleted",
|
||||
fmt.Sprintf("Supprimée par %s (%s) - Ancien statut: %s", deletedBy, role, currentStatus),
|
||||
deletedBy)
|
||||
log.Printf("✅ [DeleteAtomic] Supprimée de la DB")
|
||||
|
||||
// ✅ SUPPRIMER ITEMS
|
||||
_, err = tx.Exec(`DELETE FROM command_items WHERE command_id = $1`, commandID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
livreur := cmdResult.LivreurAssign
|
||||
cmdUsername := cmdResult.Username
|
||||
|
||||
// ✅ SUPPRIMER COMMANDE
|
||||
result, err := tx.Exec(`DELETE FROM commandes WHERE id = $1`, commandID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if livreur != "" {
|
||||
go d.RemoveCommandFromAllQueues(commandID, livreur)
|
||||
}
|
||||
go func() {
|
||||
Redis.Del(RedisCtx,
|
||||
fmt.Sprintf("command:%d", commandID),
|
||||
fmt.Sprintf("client:%s", cmdUsername),
|
||||
fmt.Sprintf("client:%s:commands", cmdUsername),
|
||||
)
|
||||
}()
|
||||
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows == 0 {
|
||||
return fmt.Errorf("commande non trouvée")
|
||||
}
|
||||
|
||||
log.Printf("✅ [DeleteAtomic] Supprimée de la DB")
|
||||
|
||||
// ✅ COMMIT
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("erreur commit: %w", err)
|
||||
}
|
||||
|
||||
// ✅ NETTOYER QUEUES (async)
|
||||
if livreurAssign != "" {
|
||||
go d.RemoveCommandFromAllQueues(commandID, livreurAssign)
|
||||
}
|
||||
|
||||
// ✅ INVALIDER CACHES (async)
|
||||
go func() {
|
||||
Redis.Del(RedisCtx,
|
||||
fmt.Sprintf("command:%d", commandID),
|
||||
fmt.Sprintf("client:%s", cmdUsername),
|
||||
fmt.Sprintf("client:%s:commands", cmdUsername),
|
||||
)
|
||||
}()
|
||||
|
||||
log.Printf("🎉 [DeleteAtomic] SUCCÈS - Commande %d supprimée", commandID)
|
||||
return nil
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// FONCTIONS HELPERS (déjà sécurisées)
|
||||
// ============================================
|
||||
|
||||
func (d *Database) GetCommandPositionInQueue(livreurUsername string, commandID int) (int, error) {
|
||||
queueKey := fmt.Sprintf("queue:deliveryman:%s", livreurUsername)
|
||||
commandIDStr := fmt.Sprintf("%d", commandID)
|
||||
@@ -285,104 +245,61 @@ func (d *Database) GetCommandPositionInQueue(livreurUsername string, commandID i
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("commande non trouvée dans la queue")
|
||||
}
|
||||
|
||||
return int(rank) + 1, nil
|
||||
}
|
||||
|
||||
func (d *Database) GetCancelledCommands(username string, limit int) ([]map[string]interface{}, error) {
|
||||
func (d *Database) GetCancelledCommands(username string, limit int) ([]map[string]any, error) {
|
||||
query := `
|
||||
SELECT id, username, status, adresse, total_prix, created_at, updated_at
|
||||
SELECT id, client_order_id AS client_order_number, username, status, adresse, total_prix::float8 as total_prix, created_at, updated_at
|
||||
FROM commandes
|
||||
WHERE status = 'cancelled'
|
||||
`
|
||||
WHERE status = 'cancelled'`
|
||||
|
||||
args := []interface{}{}
|
||||
argPos := 1
|
||||
args := []any{}
|
||||
|
||||
if username != "" {
|
||||
query += fmt.Sprintf(" AND username = $%d", argPos)
|
||||
query += " AND username = ?"
|
||||
args = append(args, username)
|
||||
argPos++
|
||||
}
|
||||
|
||||
query += " ORDER BY updated_at DESC"
|
||||
|
||||
if limit > 0 {
|
||||
query += fmt.Sprintf(" LIMIT $%d", argPos)
|
||||
query += " LIMIT ?"
|
||||
args = append(args, limit)
|
||||
}
|
||||
|
||||
rows, err := d.Query(query, args...)
|
||||
if err != nil {
|
||||
var commands []map[string]any
|
||||
if err := d.GDB.Raw(query, args...).Scan(&commands).Error; err != nil {
|
||||
return nil, fmt.Errorf("erreur récupération: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var commands []map[string]interface{}
|
||||
for rows.Next() {
|
||||
var id int
|
||||
var username, status, adresse string
|
||||
var totalPrix float64
|
||||
var createdAt, updatedAt time.Time
|
||||
|
||||
err := rows.Scan(&id, &username, &status, &adresse, &totalPrix, &createdAt, &updatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
commands = append(commands, map[string]interface{}{
|
||||
"id": id,
|
||||
"username": username,
|
||||
"status": status,
|
||||
"adresse": adresse,
|
||||
"total_prix": totalPrix,
|
||||
"created_at": createdAt,
|
||||
"updated_at": updatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
return commands, nil
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// GESTION DES PÉNALITÉS
|
||||
// ============================================
|
||||
|
||||
// AddClientPenalty ajoute une pénalité à un client
|
||||
func (d *Database) AddClientPenalty(username string, points int) error {
|
||||
log.Printf("⚠️ [AddPenalty] Ajout pénalité: %d points pour client %s", points, username)
|
||||
|
||||
// ✅ Validation
|
||||
if points <= 0 {
|
||||
return fmt.Errorf("points invalides: %d", points)
|
||||
}
|
||||
|
||||
if username == "" {
|
||||
return fmt.Errorf("username vide")
|
||||
}
|
||||
|
||||
// ✅ UPDATE dans PostgreSQL
|
||||
query := `UPDATE clients
|
||||
SET amende = amende + $1, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE username = $2`
|
||||
|
||||
result, err := d.Exec(query, points, username)
|
||||
if err != nil {
|
||||
log.Printf("❌ [AddPenalty] Erreur UPDATE: %v", err)
|
||||
return fmt.Errorf("erreur ajout pénalité: %w", err)
|
||||
result := d.GDB.Exec(`
|
||||
UPDATE clients
|
||||
SET amende = amende + ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE username = ?`, points, username)
|
||||
if result.Error != nil {
|
||||
log.Printf("❌ [AddPenalty] Erreur UPDATE: %v", result.Error)
|
||||
return fmt.Errorf("erreur ajout pénalité: %w", result.Error)
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("erreur vérification: %w", err)
|
||||
}
|
||||
if rowsAffected == 0 {
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("client non trouvé: %s", username)
|
||||
}
|
||||
|
||||
log.Printf("✅ [AddPenalty] Pénalité ajoutée: +%d points pour %s", points, username)
|
||||
|
||||
// ✅ Invalider le cache Redis du client
|
||||
cacheKey := fmt.Sprintf("client:%s", username)
|
||||
Redis.Del(RedisCtx, cacheKey)
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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é")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+412
-505
File diff suppressed because it is too large
Load Diff
+139
-244
@@ -1,46 +1,24 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// GetAvailableDeliveryPersons récupère tous les livreurs disponibles
|
||||
func (d *Database) GetAvailableDeliveryPersons() ([]map[string]any, error) {
|
||||
query := `SELECT id, username, total, livraison
|
||||
FROM users
|
||||
WHERE role = 'livreur'
|
||||
ORDER BY username`
|
||||
|
||||
rows, err := d.Query(query)
|
||||
var livreurs []map[string]any
|
||||
err := d.GDB.Raw(`
|
||||
SELECT id, username, total, livraison
|
||||
FROM users
|
||||
WHERE role = 'livreur'
|
||||
ORDER BY username`).Scan(&livreurs).Error
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("erreur lors de la récupération des livreurs: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var livreurs []map[string]any
|
||||
for rows.Next() {
|
||||
var id int
|
||||
var username string
|
||||
var total, livraison float64
|
||||
|
||||
err := rows.Scan(&id, &username, &total, &livraison)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("erreur lors du scan du livreur: %w", err)
|
||||
}
|
||||
|
||||
livreur := map[string]any{
|
||||
"id": id,
|
||||
"username": username,
|
||||
"total": total,
|
||||
"livraison": livraison,
|
||||
}
|
||||
livreurs = append(livreurs, livreur)
|
||||
}
|
||||
|
||||
return livreurs, nil
|
||||
}
|
||||
|
||||
@@ -48,255 +26,172 @@ func (d *Database) GetAvailableDeliveryPersons() ([]map[string]any, error) {
|
||||
func (d *Database) AssignDeliveryPerson(commandID int, livreurUsername string) error {
|
||||
log.Printf("📦 [AssignDeliveryPerson] START - commandID=%d, livreur=%s", commandID, livreurUsername)
|
||||
|
||||
tx, err := d.Begin()
|
||||
if err != nil {
|
||||
log.Printf("❌ Erreur démarrage transaction: %v", err)
|
||||
return fmt.Errorf("erreur démarrage transaction: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
return d.GDB.Transaction(func(tx *gorm.DB) error {
|
||||
var roleResult struct {
|
||||
Role string `gorm:"column:role"`
|
||||
}
|
||||
err := tx.Raw(`SELECT role FROM users WHERE username = ? FOR UPDATE`, livreurUsername).Scan(&roleResult).Error
|
||||
if err != nil {
|
||||
log.Printf("❌ Erreur vérification livreur: %v", err)
|
||||
return fmt.Errorf("erreur lors de la vérification du livreur: %w", err)
|
||||
}
|
||||
if roleResult.Role == "" {
|
||||
log.Printf("❌ Livreur '%s' non trouvé", livreurUsername)
|
||||
return fmt.Errorf("livreur non trouvé")
|
||||
}
|
||||
if roleResult.Role != "livreur" {
|
||||
log.Printf("❌ L'utilisateur '%s' n'est pas un livreur (role=%s)", livreurUsername, roleResult.Role)
|
||||
return fmt.Errorf("l'utilisateur n'est pas un livreur")
|
||||
}
|
||||
|
||||
var role string
|
||||
checkQuery := `SELECT role FROM users WHERE username = $1 FOR UPDATE`
|
||||
err = tx.QueryRow(checkQuery, livreurUsername).Scan(&role)
|
||||
if err == sql.ErrNoRows {
|
||||
log.Printf("❌ Livreur '%s' non trouvé", livreurUsername)
|
||||
return fmt.Errorf("livreur non trouvé")
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("❌ Erreur vérification livreur: %v", err)
|
||||
return fmt.Errorf("erreur lors de la vérification du livreur: %w", err)
|
||||
}
|
||||
if role != "livreur" {
|
||||
log.Printf("❌ L'utilisateur '%s' n'est pas un livreur (role=%s)", livreurUsername, role)
|
||||
return fmt.Errorf("l'utilisateur n'est pas un livreur")
|
||||
}
|
||||
var cmdResult struct {
|
||||
Status string `gorm:"column:status"`
|
||||
LivreurAssign *string `gorm:"column:livreur_assign"`
|
||||
}
|
||||
err = tx.Raw(`SELECT status, livreur_assign FROM commandes WHERE id = ? FOR UPDATE`, commandID).Scan(&cmdResult).Error
|
||||
if err != nil {
|
||||
log.Printf("❌ Erreur vérification commande: %v", err)
|
||||
return fmt.Errorf("erreur lors de la vérification de la commande: %w", err)
|
||||
}
|
||||
if cmdResult.Status == "" {
|
||||
log.Printf("❌ Commande %d non trouvée", commandID)
|
||||
return fmt.Errorf("commande non trouvée")
|
||||
}
|
||||
|
||||
var currentStatus string
|
||||
var currentLivreur sql.NullString
|
||||
statusQuery := `SELECT status, livreur_assign
|
||||
FROM commandes
|
||||
WHERE id = $1
|
||||
FOR UPDATE`
|
||||
validStatusesForAssignment := []string{"pending", "assigned"}
|
||||
if !slices.Contains(validStatusesForAssignment, cmdResult.Status) {
|
||||
log.Printf("❌ Statut invalide pour assignation: %s", cmdResult.Status)
|
||||
return fmt.Errorf("commande en statut '%s', impossible d'assigner un livreur", cmdResult.Status)
|
||||
}
|
||||
|
||||
err = tx.QueryRow(statusQuery, commandID).Scan(¤tStatus, ¤tLivreur)
|
||||
if err == sql.ErrNoRows {
|
||||
log.Printf("❌ Commande %d non trouvée", commandID)
|
||||
return fmt.Errorf("commande non trouvée")
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("❌ Erreur vérification commande: %v", err)
|
||||
return fmt.Errorf("erreur lors de la vérification de la commande: %w", err)
|
||||
}
|
||||
result := tx.Exec(`
|
||||
UPDATE commandes
|
||||
SET livreur_assign = ?,
|
||||
status = 'assigned',
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
AND status IN ('pending', 'assigned')`, livreurUsername, commandID)
|
||||
if result.Error != nil {
|
||||
log.Printf("❌ Erreur UPDATE: %v", result.Error)
|
||||
return fmt.Errorf("erreur lors de l'assignation du livreur: %w", result.Error)
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
log.Printf("❌ Impossible d'assigner: conditions non remplies")
|
||||
return fmt.Errorf("impossible d'assigner la commande (déjà assignée ou statut changé)")
|
||||
}
|
||||
|
||||
validStatusesForAssignment := []string{"pending", "assigned"}
|
||||
isValidStatus := slices.Contains(validStatusesForAssignment, currentStatus)
|
||||
if err := tx.Exec(`
|
||||
INSERT INTO command_logs (command_id, status, message, author, created_at)
|
||||
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)`,
|
||||
commandID, "assigned",
|
||||
fmt.Sprintf("Livraison assignée au livreur %s", livreurUsername),
|
||||
"admin",
|
||||
).Error; err != nil {
|
||||
log.Printf("⚠️ Erreur ajout log: %v", err)
|
||||
// Non bloquant
|
||||
}
|
||||
|
||||
if !isValidStatus {
|
||||
log.Printf("❌ Statut invalide pour assignation: %s", currentStatus)
|
||||
return fmt.Errorf("commande en statut '%s', impossible d'assigner un livreur", currentStatus)
|
||||
}
|
||||
|
||||
updateQuery := `UPDATE commandes
|
||||
SET livreur_assign = $1,
|
||||
status = 'assigned',
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $2
|
||||
AND status IN ('pending', 'assigned')`
|
||||
|
||||
result, err := tx.Exec(updateQuery, livreurUsername, commandID)
|
||||
if err != nil {
|
||||
log.Printf("❌ Erreur UPDATE: %v", err)
|
||||
return fmt.Errorf("erreur lors de l'assignation du livreur: %w", err)
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
log.Printf("❌ Erreur RowsAffected: %v", err)
|
||||
return fmt.Errorf("erreur lors de la vérification: %w", err)
|
||||
}
|
||||
if rowsAffected == 0 {
|
||||
log.Printf("❌ Impossible d'assigner: conditions non remplies")
|
||||
return fmt.Errorf("impossible d'assigner la commande (déjà assignée ou statut changé)")
|
||||
}
|
||||
|
||||
logQuery := `INSERT INTO command_logs (command_id, status, message, author, created_at)
|
||||
VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP)`
|
||||
|
||||
_, err = tx.Exec(logQuery, commandID, "assigned",
|
||||
fmt.Sprintf("Livraison assignée au livreur %s", livreurUsername),
|
||||
"admin")
|
||||
if err != nil {
|
||||
log.Printf("⚠️ Erreur ajout log: %v", err)
|
||||
// Non bloquant
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
log.Printf("❌ Erreur COMMIT: %v", err)
|
||||
return fmt.Errorf("erreur commit transaction: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// GetDeliveryPersonCommands récupère les commandes assignées à un livreur
|
||||
func (d *Database) GetDeliveryPersonCommands(livreurUsername string, status string) ([]map[string]any, error) {
|
||||
query := `SELECT id, username, status, adresse, total_prix, livreur_assign, created_at, updated_at
|
||||
query := `SELECT id, username, status, adresse, total_prix::float8 as total_prix, livreur_assign, created_at, updated_at
|
||||
FROM commandes
|
||||
WHERE livreur_assign = $1`
|
||||
WHERE livreur_assign = ?`
|
||||
|
||||
args := []any{livreurUsername}
|
||||
|
||||
if status != "" {
|
||||
query += " AND status = $2"
|
||||
query += " AND status = ?"
|
||||
args = append(args, status)
|
||||
}
|
||||
|
||||
query += " ORDER BY created_at DESC"
|
||||
|
||||
rows, err := d.Query(query, args...)
|
||||
if err != nil {
|
||||
var commands []map[string]any
|
||||
if err := d.GDB.Raw(query, args...).Scan(&commands).Error; err != nil {
|
||||
return nil, fmt.Errorf("erreur lors de la récupération des commandes: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var commands []map[string]any
|
||||
for rows.Next() {
|
||||
var id int
|
||||
var username, status, adresse string
|
||||
var livreurAssign sql.NullString
|
||||
var totalPrix float64
|
||||
var createdAt, updatedAt time.Time
|
||||
|
||||
err := rows.Scan(&id, &username, &status, &adresse, &totalPrix, &livreurAssign, &createdAt, &updatedAt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("erreur lors du scan: %w", err)
|
||||
}
|
||||
|
||||
command := map[string]any{
|
||||
"id": id,
|
||||
"username": username,
|
||||
"status": status,
|
||||
"adresse": adresse,
|
||||
"total_prix": totalPrix,
|
||||
"livreur_assign": livreurAssign.String,
|
||||
"created_at": createdAt,
|
||||
"updated_at": updatedAt,
|
||||
}
|
||||
commands = append(commands, command)
|
||||
}
|
||||
|
||||
return commands, nil
|
||||
}
|
||||
|
||||
func (d *Database) IncrementLivreurDeliveryCount(livreurUsername string) error {
|
||||
query := `UPDATE users
|
||||
SET livraison = livraison + 1,
|
||||
total = total + 1,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE username = $1 AND role = 'livreur'`
|
||||
|
||||
result, err := d.Exec(query, livreurUsername)
|
||||
if err != nil {
|
||||
return fmt.Errorf("erreur lors de l'incrémentation des livraisons: %w", err)
|
||||
result := d.GDB.Exec(`
|
||||
UPDATE users
|
||||
SET livraison = livraison + 1,
|
||||
total = total + 1,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE username = ? AND role = 'livreur'`, livreurUsername)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("erreur lors de l'incrémentation des livraisons: %w", result.Error)
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("erreur lors de la vérification: %w", err)
|
||||
}
|
||||
if rowsAffected == 0 {
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("livreur non trouvé")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Database) ApproveDelivery(commandID int, clientUsername string) error {
|
||||
tx, err := d.Begin()
|
||||
if err != nil {
|
||||
log.Printf("❌ Erreur démarrage transaction: %v", err)
|
||||
return fmt.Errorf("erreur démarrage transaction: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
var commandUsername, currentStatus string
|
||||
var livreurAssign sql.NullString
|
||||
|
||||
checkQuery := `SELECT username, status, livreur_assign
|
||||
FROM commandes
|
||||
WHERE id = $1
|
||||
FOR UPDATE`
|
||||
|
||||
err = tx.QueryRow(checkQuery, commandID).Scan(&commandUsername, ¤tStatus, &livreurAssign)
|
||||
if err == sql.ErrNoRows {
|
||||
return fmt.Errorf("commande non trouvée")
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("erreur lors de la vérification de la commande: %w", err)
|
||||
}
|
||||
|
||||
if commandUsername != clientUsername {
|
||||
return fmt.Errorf("cette commande ne vous appartient pas")
|
||||
}
|
||||
|
||||
if currentStatus != "livre" {
|
||||
return fmt.Errorf("cette commande n'est pas encore livrée (statut actuel: %s)", currentStatus)
|
||||
}
|
||||
|
||||
updateQuery := `UPDATE commandes
|
||||
SET status = 'approved',
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $1
|
||||
AND status = 'livre'
|
||||
AND username = $2`
|
||||
|
||||
result, err := tx.Exec(updateQuery, commandID, clientUsername)
|
||||
if err != nil {
|
||||
return fmt.Errorf("erreur lors de l'approbation de la livraison: %w", err)
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("erreur lors de la vérification: %w", err)
|
||||
}
|
||||
if rowsAffected == 0 {
|
||||
return fmt.Errorf("impossible d'approuver: statut changé ou commande introuvable")
|
||||
}
|
||||
|
||||
if livreurAssign.Valid && livreurAssign.String != "" {
|
||||
incrementQuery := `UPDATE users
|
||||
SET livraison = livraison + 1,
|
||||
total = total + 1,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE username = $1 AND role = 'livreur'`
|
||||
|
||||
result, err := tx.Exec(incrementQuery, livreurAssign.String)
|
||||
return d.GDB.Transaction(func(tx *gorm.DB) error {
|
||||
var cmdResult struct {
|
||||
Username string `gorm:"column:username"`
|
||||
Status string `gorm:"column:status"`
|
||||
LivreurAssign *string `gorm:"column:livreur_assign"`
|
||||
}
|
||||
err := tx.Raw(`
|
||||
SELECT username, status, livreur_assign
|
||||
FROM commandes WHERE id = ? FOR UPDATE`, commandID).Scan(&cmdResult).Error
|
||||
if err != nil {
|
||||
log.Printf("⚠️ Erreur incrémentation livreur: %v", err)
|
||||
} else {
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows > 0 {
|
||||
log.Printf(" ✅ Compteur livreur incrémenté: %s", livreurAssign.String)
|
||||
return fmt.Errorf("erreur lors de la vérification de la commande: %w", err)
|
||||
}
|
||||
if cmdResult.Username == "" {
|
||||
return fmt.Errorf("commande non trouvée")
|
||||
}
|
||||
|
||||
if cmdResult.Username != clientUsername {
|
||||
return fmt.Errorf("cette commande ne vous appartient pas")
|
||||
}
|
||||
if cmdResult.Status != "livre" {
|
||||
return fmt.Errorf("cette commande n'est pas encore livrée (statut actuel: %s)", cmdResult.Status)
|
||||
}
|
||||
|
||||
result := tx.Exec(`
|
||||
UPDATE commandes
|
||||
SET status = 'approved', updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ? AND status = 'livre' AND username = ?`, commandID, clientUsername)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("erreur lors de l'approbation de la livraison: %w", result.Error)
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("impossible d'approuver: statut changé ou commande introuvable")
|
||||
}
|
||||
|
||||
if cmdResult.LivreurAssign != nil && *cmdResult.LivreurAssign != "" {
|
||||
res := tx.Exec(`
|
||||
UPDATE users
|
||||
SET livraison = livraison + 1,
|
||||
total = total + 1,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE username = ? AND role = 'livreur'`, *cmdResult.LivreurAssign)
|
||||
if res.Error != nil {
|
||||
log.Printf("⚠️ Erreur incrémentation livreur: %v", res.Error)
|
||||
} else if res.RowsAffected > 0 {
|
||||
log.Printf(" ✅ Compteur livreur incrémenté: %s", *cmdResult.LivreurAssign)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logQuery := `INSERT INTO command_logs (command_id, status, message, author, created_at)
|
||||
VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP)`
|
||||
if err := tx.Exec(`
|
||||
INSERT INTO command_logs (command_id, status, message, author, created_at)
|
||||
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)`,
|
||||
commandID, "approved",
|
||||
fmt.Sprintf("Livraison approuvée par le client %s", clientUsername),
|
||||
clientUsername,
|
||||
).Error; err != nil {
|
||||
log.Printf("⚠️ Erreur ajout log: %v", err)
|
||||
}
|
||||
|
||||
_, err = tx.Exec(logQuery, commandID, "approved",
|
||||
fmt.Sprintf("Livraison approuvée par le client %s", clientUsername),
|
||||
clientUsername)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ Erreur ajout log: %v", err)
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
log.Printf("❌ Erreur COMMIT: %v", err)
|
||||
return fmt.Errorf("erreur commit transaction: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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é")
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"gestion/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// DefaultDeliverySchedule retourne un planning de livraison par défaut (tous les jours, 9h-20h)
|
||||
@@ -84,66 +86,64 @@ func DefaultSettings() models.AppSettings {
|
||||
func (d *Database) GetSettings() (models.AppSettings, error) {
|
||||
settings := DefaultSettings()
|
||||
|
||||
rows, err := d.Query(`SELECT key, value FROM app_settings`)
|
||||
if err != nil {
|
||||
var rows []struct {
|
||||
Key string `gorm:"column:key"`
|
||||
Value string `gorm:"column:value"`
|
||||
}
|
||||
if err := d.GDB.Raw(`SELECT key, value FROM app_settings`).Scan(&rows).Error; err != nil {
|
||||
return settings, fmt.Errorf("erreur lecture settings: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var key, value string
|
||||
if err := rows.Scan(&key, &value); err != nil {
|
||||
continue
|
||||
}
|
||||
switch key {
|
||||
for _, row := range rows {
|
||||
switch row.Key {
|
||||
case "penalties_enabled":
|
||||
settings.PenaltiesEnabled = value == "true"
|
||||
settings.PenaltiesEnabled = row.Value == "true"
|
||||
case "show_amende_score":
|
||||
settings.ShowAmendeScore = value == "true"
|
||||
settings.ShowAmendeScore = row.Value == "true"
|
||||
case "points_enabled":
|
||||
settings.PointsEnabled = value == "true"
|
||||
settings.PointsEnabled = row.Value == "true"
|
||||
case "points_pools":
|
||||
var pools []models.PointsPool
|
||||
if err := json.Unmarshal([]byte(value), &pools); err == nil {
|
||||
if err := json.Unmarshal([]byte(row.Value), &pools); err == nil {
|
||||
settings.PointsPools = pools
|
||||
}
|
||||
case "referral_enabled":
|
||||
settings.ReferralEnabled = value == "true"
|
||||
settings.ReferralEnabled = row.Value == "true"
|
||||
case "crypto_payment_enabled":
|
||||
settings.CryptoPaymentEnabled = value == "true"
|
||||
settings.CryptoPaymentEnabled = row.Value == "true"
|
||||
case "crypto_only":
|
||||
settings.CryptoOnly = value == "true"
|
||||
settings.CryptoOnly = row.Value == "true"
|
||||
case "nowpayments_api_key":
|
||||
settings.NowPaymentsAPIKey = value
|
||||
settings.NowPaymentsAPIKey = row.Value
|
||||
case "nowpayments_ipn_secret":
|
||||
settings.NowPaymentsIPNSecret = value
|
||||
settings.NowPaymentsIPNSecret = row.Value
|
||||
case "nowpayments_currencies":
|
||||
var currencies []string
|
||||
if err := json.Unmarshal([]byte(value), ¤cies); err == nil {
|
||||
if err := json.Unmarshal([]byte(row.Value), ¤cies); err == nil {
|
||||
settings.NowPaymentsCurrencies = currencies
|
||||
}
|
||||
case "delivery_schedule":
|
||||
var sched models.DeliverySchedule
|
||||
if err := json.Unmarshal([]byte(value), &sched); err == nil {
|
||||
if err := json.Unmarshal([]byte(row.Value), &sched); err == nil {
|
||||
settings.DeliverySchedule = sched
|
||||
}
|
||||
case "postal_zones":
|
||||
var zones []models.PostalZone
|
||||
if err := json.Unmarshal([]byte(value), &zones); err == nil {
|
||||
if err := json.Unmarshal([]byte(row.Value), &zones); err == nil {
|
||||
settings.PostalZones = zones
|
||||
}
|
||||
case "telegram_bot_token":
|
||||
settings.TelegramBotToken = value
|
||||
settings.TelegramBotToken = row.Value
|
||||
case "telegram_bot_username":
|
||||
settings.TelegramBotUsername = value
|
||||
settings.TelegramBotUsername = row.Value
|
||||
case "delivery_mode":
|
||||
var mode models.DeliveryModeConfig
|
||||
if err := json.Unmarshal([]byte(value), &mode); err == nil {
|
||||
if err := json.Unmarshal([]byte(row.Value), &mode); err == nil {
|
||||
settings.DeliveryMode = mode
|
||||
}
|
||||
}
|
||||
}
|
||||
return settings, rows.Err()
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
// UpdateSettings sauvegarde les paramètres dans la DB
|
||||
@@ -158,7 +158,6 @@ func (d *Database) UpdateSettings(s models.AppSettings) error {
|
||||
if s.PointsPools == nil {
|
||||
s.PointsPools = []models.PointsPool{}
|
||||
}
|
||||
// S'assurer que chaque pool a des slices non-nil
|
||||
for i := range s.PointsPools {
|
||||
if s.PointsPools[i].Categories == nil {
|
||||
s.PointsPools[i].Categories = []string{}
|
||||
@@ -173,15 +172,6 @@ func (d *Database) UpdateSettings(s models.AppSettings) error {
|
||||
return fmt.Errorf("erreur sérialisation pools: %w", err)
|
||||
}
|
||||
|
||||
tx, err := d.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
upsert := `INSERT INTO app_settings (key, value) VALUES ($1, $2)
|
||||
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value`
|
||||
|
||||
if s.NowPaymentsCurrencies == nil {
|
||||
s.NowPaymentsCurrencies = []string{}
|
||||
}
|
||||
@@ -190,6 +180,27 @@ func (d *Database) UpdateSettings(s models.AppSettings) error {
|
||||
return fmt.Errorf("erreur sérialisation nowpayments_currencies: %w", err)
|
||||
}
|
||||
|
||||
schedJSON, err := json.Marshal(s.DeliverySchedule)
|
||||
if err != nil {
|
||||
return fmt.Errorf("erreur sérialisation delivery_schedule: %w", err)
|
||||
}
|
||||
|
||||
if s.PostalZones == nil {
|
||||
s.PostalZones = []models.PostalZone{}
|
||||
}
|
||||
zonesJSON, err := json.Marshal(s.PostalZones)
|
||||
if err != nil {
|
||||
return fmt.Errorf("erreur sérialisation postal_zones: %w", err)
|
||||
}
|
||||
|
||||
if s.DeliveryMode.CategoryRoutes == nil {
|
||||
s.DeliveryMode.CategoryRoutes = []models.CategoryRoute{}
|
||||
}
|
||||
deliveryModeJSON, err := json.Marshal(s.DeliveryMode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("erreur sérialisation delivery_mode: %w", err)
|
||||
}
|
||||
|
||||
pairs := [][2]string{
|
||||
{"penalties_enabled", boolStr(s.PenaltiesEnabled)},
|
||||
{"show_amende_score", boolStr(s.ShowAmendeScore)},
|
||||
@@ -201,39 +212,22 @@ func (d *Database) UpdateSettings(s models.AppSettings) error {
|
||||
{"nowpayments_api_key", s.NowPaymentsAPIKey},
|
||||
{"nowpayments_ipn_secret", s.NowPaymentsIPNSecret},
|
||||
{"nowpayments_currencies", string(currenciesJSON)},
|
||||
{"delivery_schedule", string(schedJSON)},
|
||||
{"postal_zones", string(zonesJSON)},
|
||||
{"telegram_bot_token", s.TelegramBotToken},
|
||||
{"telegram_bot_username", s.TelegramBotUsername},
|
||||
{"delivery_mode", string(deliveryModeJSON)},
|
||||
}
|
||||
|
||||
schedJSON, err := json.Marshal(s.DeliverySchedule)
|
||||
if err != nil {
|
||||
return fmt.Errorf("erreur sérialisation delivery_schedule: %w", err)
|
||||
}
|
||||
pairs = append(pairs, [2]string{"delivery_schedule", string(schedJSON)})
|
||||
upsert := `INSERT INTO app_settings (key, value) VALUES (?, ?)
|
||||
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value`
|
||||
|
||||
if s.PostalZones == nil {
|
||||
s.PostalZones = []models.PostalZone{}
|
||||
}
|
||||
zonesJSON, err := json.Marshal(s.PostalZones)
|
||||
if err != nil {
|
||||
return fmt.Errorf("erreur sérialisation postal_zones: %w", err)
|
||||
}
|
||||
pairs = append(pairs, [2]string{"postal_zones", string(zonesJSON)})
|
||||
pairs = append(pairs, [2]string{"telegram_bot_token", s.TelegramBotToken})
|
||||
pairs = append(pairs, [2]string{"telegram_bot_username", s.TelegramBotUsername})
|
||||
|
||||
if s.DeliveryMode.CategoryRoutes == nil {
|
||||
s.DeliveryMode.CategoryRoutes = []models.CategoryRoute{}
|
||||
}
|
||||
deliveryModeJSON, err := json.Marshal(s.DeliveryMode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("erreur sérialisation delivery_mode: %w", err)
|
||||
}
|
||||
pairs = append(pairs, [2]string{"delivery_mode", string(deliveryModeJSON)})
|
||||
|
||||
for _, p := range pairs {
|
||||
if _, err = tx.Exec(upsert, p[0], p[1]); err != nil {
|
||||
return fmt.Errorf("erreur upsert %s: %w", p[0], err)
|
||||
return d.GDB.Transaction(func(tx *gorm.DB) error {
|
||||
for _, p := range pairs {
|
||||
if err := tx.Exec(upsert, p[0], p[1]).Error; err != nil {
|
||||
return fmt.Errorf("erreur upsert %s: %w", p[0], err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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,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é
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user