chore: refacto

This commit is contained in:
2026-04-20 19:41:52 +02:00
parent 5fc52c72ee
commit 2e9827af98
46 changed files with 497 additions and 648 deletions
+15 -4
View File
@@ -65,10 +65,14 @@ func (d *Database) CancelCommandAtomic(commandID int, username, reason string, f
return fmt.Errorf("confirmation requise")
}
cancelMsg := reason
if cancelMsg == "Annulation par le client" {
cancelMsg = ""
}
result := tx.Exec(`
UPDATE commandes
SET status = 'cancelled', updated_at = CURRENT_TIMESTAMP
WHERE id = ? AND status = ? AND username = ?`, commandID, cmdResult.Status, username)
SET status = 'cancelled', cancel_reason = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ? AND status = ? AND username = ?`, cancelMsg, commandID, cmdResult.Status, username)
if result.Error != nil {
return result.Error
}
@@ -87,13 +91,20 @@ func (d *Database) CancelCommandAtomic(commandID int, username, reason string, f
log.Printf("✅ [CancelAtomic] Stock remboursé")
}
if err := tx.Exec(`
UPDATE clients
SET cancellations_count = COALESCE(cancellations_count, 0) + 1,
updated_at = CURRENT_TIMESTAMP
WHERE username = ?`, username).Error; err != nil {
log.Printf("⚠️ [CancelAtomic] Erreur incrémentation count: %v", err)
}
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)
@@ -250,7 +261,7 @@ func (d *Database) GetCommandPositionInQueue(livreurUsername string, commandID i
func (d *Database) GetCancelledCommands(username string, limit int) ([]map[string]any, error) {
query := `
SELECT id, client_order_id AS client_order_number, username, status, adresse, total_prix::float8 as 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, COALESCE(cancel_reason, '') AS cancel_reason
FROM commandes
WHERE status = 'cancelled'`
+1 -1
View File
@@ -58,7 +58,7 @@ func (d *Database) UpdateCategory(id int, name, color string, isComingSoon bool)
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 {
if err := d.GDB.Model(&c).Updates(map[string]interface{}{"name": name, "color": color, "is_coming_soon": isComingSoon}).Error; err != nil {
return nil, err
}
return &c, nil
+20 -17
View File
@@ -88,11 +88,13 @@ func (d *Database) GetAllClients() ([]*models.Client, error) {
Command int `gorm:"column:command"`
Amende float64 `gorm:"column:amende"`
ReferralBalance float64 `gorm:"column:referral_balance"`
CancellationsCount int `gorm:"column:cancellations_count"`
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(cancellations_count, 0) as cancellations_count,
COALESCE(points_extra, '{}'::jsonb) as points_extra, created_at
FROM clients ORDER BY created_at DESC`).Scan(&rows).Error
if err != nil {
@@ -111,6 +113,7 @@ func (d *Database) GetAllClients() ([]*models.Client, error) {
Command: row.Command,
Amende: row.Amende,
ReferralBalance: row.ReferralBalance,
CancellationsCount: row.CancellationsCount,
CreatedAt: row.CreatedAt,
}
client.PointsExtra = map[string]int{}
@@ -125,14 +128,15 @@ func (d *Database) GetAllClients() ([]*models.Client, error) {
// UpdateClient met à jour un client existant
func (d *Database) UpdateClient(client *models.Client) error {
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,
)
result := d.GDB.Model(&models.Client{}).Where("id = ?", client.ID).Updates(map[string]any{
"username": client.Username,
"password": client.Password,
"nom": client.Nom,
"prenom": client.Prenom,
"telephone": client.Telephone,
"command": client.Command,
"amende": client.Amende,
})
if result.Error != nil {
return fmt.Errorf("erreur lors de la mise à jour du client: %w", result.Error)
}
@@ -147,7 +151,7 @@ func (d *Database) UpdateClient(client *models.Client) error {
func (d *Database) DeleteClient(id int) error {
_ = d.RevokeAllUserTokens(id, "client")
result := d.GDB.Exec(`DELETE FROM clients WHERE id = ?`, id)
result := d.GDB.Delete(&models.Client{}, id)
if result.Error != nil {
return fmt.Errorf("erreur lors de la suppression du client: %w", result.Error)
}
@@ -160,7 +164,7 @@ 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 {
result := d.GDB.Exec(`UPDATE clients SET password = ? WHERE id = ?`, hashedPassword, clientID)
result := d.GDB.Model(&models.Client{}).Where("id = ?", clientID).Update("password", hashedPassword)
if result.Error != nil {
return fmt.Errorf("erreur lors de la mise à jour du mot de passe: %w", result.Error)
}
@@ -173,9 +177,10 @@ 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 {
result := d.GDB.Exec(`
UPDATE clients SET password = ?, must_change_password = FALSE, updated_at = CURRENT_TIMESTAMP
WHERE id = ?`, hashedPassword, clientID)
result := d.GDB.Model(&models.Client{}).Where("id = ?", clientID).Updates(map[string]any{
"password": hashedPassword,
"must_change_password": false,
})
if result.Error != nil {
return fmt.Errorf("erreur lors de la mise à jour du mot de passe: %w", result.Error)
}
@@ -254,9 +259,7 @@ func (d *Database) PayClientPenalties(username string, amountPaid float64) error
return fmt.Errorf("montant insuffisant: %.2f payé, %.2f requis", amountPaid, currentAmount)
}
result := d.GDB.Exec(`
UPDATE clients SET amende = 0, updated_at = CURRENT_TIMESTAMP
WHERE username = ?`, username)
result := d.GDB.Model(&models.Client{}).Where("username = ?", username).Update("amende", 0.0)
if result.Error != nil {
log.Printf("❌ [PayClientPenalties] Erreur UPDATE: %v", result.Error)
return fmt.Errorf("erreur paiement pénalités: %w", result.Error)
@@ -273,7 +276,7 @@ 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 {
result := d.GDB.Exec(`UPDATE clients SET command = command + 1 WHERE username = ?`, username)
result := d.GDB.Model(&models.Client{}).Where("username = ?", username).UpdateColumn("command", gorm.Expr("command + 1"))
if result.Error != nil {
return fmt.Errorf("erreur lors de l'incrémentation du compteur: %w", result.Error)
}
+4 -1
View File
@@ -212,6 +212,7 @@ func (d *Database) GetCommandItems(commandID int) ([]map[string]interface{}, err
LivreurAssign *string `gorm:"column:livreur_assign"`
CommandCreatedAt *time.Time `gorm:"column:command_created_at"`
Category string `gorm:"column:category"`
ClientOrderNumber int `gorm:"column:client_order_number"`
}
err := d.GDB.Raw(`
@@ -236,7 +237,8 @@ func (d *Database) GetCommandItems(commandID int) ([]map[string]interface{}, err
c.referral_used,
c.livreur_assign,
c.created_at as command_created_at,
p.category
p.category,
c.client_order_id as client_order_number
FROM command_items ci
LEFT JOIN commandes c ON ci.command_id = c.id
LEFT JOIN products p ON ci.product_id = p.id
@@ -282,6 +284,7 @@ func (d *Database) GetCommandItems(commandID int) ([]map[string]interface{}, err
"livreur_assign": ptrStr(row.LivreurAssign),
"command_created_at": commandCreatedAt,
"category": row.Category,
"client_order_number": row.ClientOrderNumber,
}
items = append(items, item)
}
+84 -80
View File
@@ -52,10 +52,9 @@ type basketItem struct {
func (d *Database) fetchBasketItems(username string) ([]basketItem, float64, error) {
var items []basketItem
if err := d.GDB.Raw(`SELECT product_id, quantity, price FROM baskets WHERE username = ?`, username).Scan(&items).Error; err != nil {
if err := d.GDB.Table("baskets").Select("product_id, quantity, price").Where("username = ?", username).Scan(&items).Error; err != nil {
return nil, 0, fmt.Errorf("erreur récupération panier: %w", err)
}
total := 0.0
for _, item := range items {
total += item.Price
@@ -84,12 +83,10 @@ func validateCommandStatus(status string) error {
}
func (d *Database) CreateCommand(username string) (*models.Command, error) {
var addrResult struct {
Username string `gorm:"column:username"`
}
adresse := "Adresse non spécifiée"
if err := d.GDB.Raw(`SELECT username FROM clients WHERE username = ?`, username).Scan(&addrResult).Error; err == nil && addrResult.Username != "" {
adresse = addrResult.Username
var clientCheck models.Client
if err := d.GDB.Select("username").Where("username = ?", username).First(&clientCheck).Error; err == nil && clientCheck.Username != "" {
adresse = clientCheck.Username
}
basketItems, totalPrix, err := d.fetchBasketItems(username)
@@ -103,13 +100,14 @@ func (d *Database) CreateCommand(username string) (*models.Command, error) {
var cmdResult struct {
ID int `gorm:"column:id"`
ClientOrderID int `gorm:"column:client_order_id"`
CreatedAt time.Time `gorm:"column:created_at"`
UpdatedAt time.Time `gorm:"column:updated_at"`
}
err = d.GDB.Raw(`
INSERT INTO commandes (username, status, adresse, total_prix, client_order_id, created_at, updated_at)
VALUES (?, ?, ?, ?, (SELECT COALESCE(MAX(client_order_id), 0) + 1 FROM commandes WHERE username = ?), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
RETURNING id, created_at, updated_at`,
RETURNING id, client_order_id, created_at, updated_at`,
username, "pending", adresse, totalPrix, username).Scan(&cmdResult).Error
if err != nil {
return nil, fmt.Errorf("erreur lors de la création de la commande: %w", err)
@@ -123,12 +121,17 @@ func (d *Database) CreateCommand(username string) (*models.Command, error) {
productName = "Produit inconnu"
}
if err := d.GDB.Exec(`
INSERT INTO command_items (command_id, produit, product_id, quantite, prix)
VALUES (?, ?, ?, ?, ?)`,
commandID, productName, item.ProductID, item.Quantity, item.Price).Error; err != nil {
cmdItem := models.CommandItem{
CommandID: commandID,
Produit: productName,
ProductID: item.ProductID,
Quantity: item.Quantity,
Price: item.Price,
}
if err := d.GDB.Create(&cmdItem).Error; err != nil {
return nil, fmt.Errorf("erreur lors de l'insertion des items: %w", err)
}
}
if err := d.GDB.Exec(`DELETE FROM baskets WHERE username = ?`, username).Error; err != nil {
@@ -137,6 +140,7 @@ func (d *Database) CreateCommand(username string) (*models.Command, error) {
command := &models.Command{
ID: commandID,
ClientOrderID: cmdResult.ClientOrderID,
Status: "pending",
Total: totalPrix,
}
@@ -189,13 +193,14 @@ func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (*
var cmdResult struct {
ID int `gorm:"column:id"`
ClientOrderID int `gorm:"column:client_order_id"`
CreatedAt time.Time `gorm:"column:created_at"`
UpdatedAt time.Time `gorm:"column:updated_at"`
}
err = d.GDB.Raw(`
INSERT INTO commandes (username, status, adresse, total_prix, client_order_id, created_at, updated_at)
VALUES (?, ?, ?, ?, (SELECT COALESCE(MAX(client_order_id), 0) + 1 FROM commandes WHERE username = ?), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
RETURNING id, created_at, updated_at`,
RETURNING id, client_order_id, created_at, updated_at`,
username, "pending", deliveryAddress, totalPrix, username).Scan(&cmdResult).Error
if err != nil {
return nil, fmt.Errorf("erreur création commande: %w", err)
@@ -226,7 +231,7 @@ func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (*
return nil, fmt.Errorf("erreur insertion items: %w", err)
}
result := d.GDB.Exec(`UPDATE products SET stock = stock - ? WHERE id = ? AND stock >= ?`, item.Quantity, item.ProductID, item.Quantity)
result := d.GDB.Model(&models.Product{}).Where("id = ? AND stock >= ?", item.ProductID, item.Quantity).UpdateColumn("stock", gorm.Expr("stock - ?", item.Quantity))
if result.Error != nil {
log.Printf("⚠️ Erreur décrémentation stock produit %d: %v", item.ProductID, result.Error)
return nil, fmt.Errorf("erreur mise à jour stock: %w", result.Error)
@@ -236,7 +241,7 @@ func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (*
}
}
if err := d.GDB.Exec(`DELETE FROM baskets WHERE username = ?`, username).Error; err != nil {
if err := d.GDB.Delete(&models.Panier{}, "username = ?", username).Error; err != nil {
log.Printf("⚠️ Erreur vidage panier: %v", err)
}
@@ -248,6 +253,7 @@ func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (*
command := &models.Command{
ID: commandID,
ClientOrderID: cmdResult.ClientOrderID,
Username: username,
Status: "pending",
Total: totalPrix,
@@ -272,29 +278,6 @@ func (d *Database) GetAllCommands(status, username string) ([]map[string]any, er
}
}
query := `SELECT c.id, c.username, c.status, c.adresse, c.total_prix,
c.livreur_assign, c.created_at, c.updated_at,
c.proposed_address, c.address_proposal_status,
c.client_order_id AS client_order_number
FROM commandes c
WHERE 1=1`
args := []interface{}{}
if status == "" {
query += ` AND c.status IN ('pending', 'assigned', 'en_route', 'arrived', 'livre')`
} else {
query += ` AND c.status = ?`
args = append(args, status)
}
if username != "" {
query += ` AND c.username = ?`
args = append(args, username)
}
query += " ORDER BY c.created_at DESC LIMIT 1000"
var rows []struct {
ID int `gorm:"column:id"`
Username string `gorm:"column:username"`
@@ -309,7 +292,23 @@ func (d *Database) GetAllCommands(status, username string) ([]map[string]any, er
ClientOrderNumber int `gorm:"column:client_order_number"`
}
if err := d.GDB.Raw(query, args...).Scan(&rows).Error; err != nil {
gdb := d.GDB.Table("commandes c").
Select(`c.id, c.username, c.status, c.adresse, c.total_prix,
c.livreur_assign, c.created_at, c.updated_at,
c.proposed_address, c.address_proposal_status,
c.client_order_id AS client_order_number`)
if status == "" {
gdb = gdb.Where("c.status IN ?", []string{"pending", "assigned", "en_route", "arrived", "livre"})
} else {
gdb = gdb.Where("c.status = ?", status)
}
if username != "" {
gdb = gdb.Where("c.username = ?", username)
}
if err := gdb.Order("c.created_at DESC").Limit(1000).Scan(&rows).Error; err != nil {
return nil, fmt.Errorf("erreur lors de la récupération des commandes: %w", err)
}
@@ -346,14 +345,11 @@ func (d *Database) GetAllCommands(status, username string) ([]map[string]any, er
}
func (d *Database) GetCommandCount() (int, error) {
var result struct {
Count int `gorm:"column:count"`
}
err := d.GDB.Raw("SELECT COUNT(*) as count FROM commandes").Scan(&result).Error
if err != nil {
var count int64
if err := d.GDB.Model(&models.Command{}).Count(&count).Error; err != nil {
return 0, fmt.Errorf("erreur récupération count commandes: %w", err)
}
return result.Count, nil
return int(count), nil
}
func (d *Database) SetCommandReferralUsed(commandID int, amount float64) error {
@@ -374,14 +370,16 @@ func (d *Database) GetCommandByID(id int) (map[string]any, error) {
AddressProposalStatus string `gorm:"column:address_proposal_status"`
ReferralUsed float64 `gorm:"column:referral_used"`
ClientOrderNumber int `gorm:"column:client_order_number"`
CancelReason string `gorm:"column:cancel_reason"`
}
err := d.GDB.Raw(`
SELECT c.id, c.username, c.status, c.adresse, c.total_prix, c.livreur_assign, c.created_at, c.updated_at,
c.proposed_address, c.address_proposal_status, c.referral_used,
c.client_order_id AS client_order_number
FROM commandes c WHERE c.id = ?`, id).Scan(&row).Error
if err != nil {
if err := d.GDB.Table("commandes c").
Select(`c.id, c.username, c.status, c.adresse, c.total_prix, c.livreur_assign,
c.created_at, c.updated_at, c.proposed_address, c.address_proposal_status,
c.referral_used, c.client_order_id AS client_order_number,
COALESCE(c.cancel_reason, '') AS cancel_reason`).
Where("c.id = ?", id).
First(&row).Error; err != nil {
return nil, fmt.Errorf("erreur lors de la récupération de la commande: %w", err)
}
if row.ID == 0 {
@@ -399,6 +397,7 @@ func (d *Database) GetCommandByID(id int) (map[string]any, error) {
"address_proposal_status": row.AddressProposalStatus,
"referral_used": row.ReferralUsed,
"client_order_number": row.ClientOrderNumber,
"cancel_reason": row.CancelReason,
}
if row.LivreurAssign != nil {
@@ -416,12 +415,23 @@ func (d *Database) GetCommandByID(id int) (map[string]any, error) {
return command, nil
}
// GetClientOrderID retourne le client_order_id (numéro perso du client) pour un commandID global.
// Retourne commandID en fallback si introuvable.
func (d *Database) GetClientOrderID(commandID int) int {
var result struct {
ClientOrderID int `gorm:"column:client_order_id"`
}
if err := d.GDB.Model(&models.Command{}).Select("client_order_id").Where("id = ?", commandID).First(&result).Error; err != nil || result.ClientOrderID == 0 {
return commandID
}
return result.ClientOrderID
}
func (d *Database) GetCommandAddress(commandID int) (string, error) {
var result struct {
Adresse string `gorm:"column:adresse"`
}
err := d.GDB.Raw(`SELECT adresse FROM commandes WHERE id = ?`, commandID).Scan(&result).Error
if err != nil {
if err := d.GDB.Model(&models.Command{}).Select("adresse").Where("id = ?", commandID).First(&result).Error; err != nil {
return "", fmt.Errorf("erreur lors de la récupération de l'adresse: %w", err)
}
if result.Adresse == "" {
@@ -439,9 +449,10 @@ func (d *Database) UpdateCommandAddress(commandID int, deliveryAddress string) e
return fmt.Errorf("adresse vide non autorisée")
}
result := d.GDB.Exec(`
UPDATE commandes SET adresse = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?`, deliveryAddress, commandID)
result := d.GDB.Model(&models.Command{}).Where("id = ?", commandID).Updates(map[string]any{
"adresse": deliveryAddress,
"updated_at": time.Now(),
})
if result.Error != nil {
return fmt.Errorf("erreur lors de la mise à jour de l'adresse: %w", result.Error)
}
@@ -459,10 +470,11 @@ func (d *Database) ProposeAddressChange(commandID int, proposedAddress, proposed
return err
}
result := d.GDB.Exec(`
UPDATE commandes
SET proposed_address = ?, address_proposal_status = 'pending', updated_at = CURRENT_TIMESTAMP
WHERE id = ?`, proposedAddress, commandID)
result := d.GDB.Model(&models.Command{}).Where("id = ?", commandID).Updates(map[string]any{
"proposed_address": proposedAddress,
"address_proposal_status": "pending",
"updated_at": time.Now(),
})
if result.Error != nil {
return fmt.Errorf("erreur proposition adresse: %w", result.Error)
}
@@ -519,7 +531,10 @@ func (d *Database) UpdateCommandStatus(commandID int, status string) error {
return err
}
result := d.GDB.Exec(`UPDATE commandes SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, status, commandID)
result := d.GDB.Model(&models.Command{}).Where("id = ?", commandID).Updates(map[string]any{
"status": status,
"updated_at": time.Now(),
})
if result.Error != nil {
return fmt.Errorf("erreur lors de la mise à jour du statut: %w", result.Error)
}
@@ -534,10 +549,12 @@ func (d *Database) AddCommandLog(commandID int, status, message, author string)
sanitizedMessage := sanitizeLogMessage(message)
sanitizedAuthor := sanitizeLogMessage(author)
if err := d.GDB.Exec(`
INSERT INTO command_logs (command_id, status, message, author, created_at)
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)`,
commandID, status, sanitizedMessage, sanitizedAuthor).Error; err != nil {
if err := d.GDB.Create(&models.CommandLog{
CommandID: commandID,
Status: status,
Message: sanitizedMessage,
Author: sanitizedAuthor,
}).Error; err != nil {
log.Printf("⚠️ Avertissement: impossible d'ajouter le log (table command_logs peut-être manquante): %v", err)
return nil
}
@@ -547,22 +564,9 @@ func (d *Database) AddCommandLog(commandID int, status, message, author string)
// GetCommandLogs récupère tous les logs d'une commande
func (d *Database) GetCommandLogs(commandID int) ([]map[string]any, error) {
var rows []struct {
ID int `gorm:"column:id"`
CommandID int `gorm:"column:command_id"`
Status string `gorm:"column:status"`
Message string `gorm:"column:message"`
Author string `gorm:"column:author"`
CreatedAt time.Time `gorm:"column:created_at"`
}
var rows []models.CommandLog
err := d.GDB.Raw(`
SELECT id, command_id, status, message, author, created_at
FROM command_logs
WHERE command_id = ?
ORDER BY created_at ASC`, commandID).Scan(&rows).Error
if err != nil {
// Si la table n'existe pas, retourner un tableau vide au lieu d'une erreur
if err := d.GDB.Where("command_id = ?", commandID).Order("created_at ASC").Find(&rows).Error; err != nil {
log.Printf("⚠️ Avertissement: impossible de récupérer les logs: %v", err)
return []map[string]any{}, nil
}
+20 -2
View File
@@ -187,6 +187,25 @@ func InitDB() *Database {
log.Fatalf("❌ Erreur backfill commandes.client_order_id: %v", err)
}
// Migration: raison d'annulation par le client
if _, err = database.Exec(`ALTER TABLE commandes ADD COLUMN IF NOT EXISTS cancel_reason TEXT`); err != nil {
log.Fatalf("❌ Erreur migration commandes.cancel_reason: %v", err)
}
// Migration: coordonnées GPS de destination et du livreur
if _, err = database.Exec(`ALTER TABLE commandes ADD COLUMN IF NOT EXISTS dest_latitude DOUBLE PRECISION`); err != nil {
log.Fatalf("❌ Erreur migration commandes.dest_latitude: %v", err)
}
if _, err = database.Exec(`ALTER TABLE commandes ADD COLUMN IF NOT EXISTS dest_longitude DOUBLE PRECISION`); err != nil {
log.Fatalf("❌ Erreur migration commandes.dest_longitude: %v", err)
}
if _, err = database.Exec(`ALTER TABLE commandes ADD COLUMN IF NOT EXISTS livreur_latitude DOUBLE PRECISION`); err != nil {
log.Fatalf("❌ Erreur migration commandes.livreur_latitude: %v", err)
}
if _, err = database.Exec(`ALTER TABLE commandes ADD COLUMN IF NOT EXISTS livreur_longitude DOUBLE PRECISION`); err != nil {
log.Fatalf("❌ Erreur migration commandes.livreur_longitude: %v", err)
}
// Migration: table de suivi des paiements crypto
if _, err = database.Exec(`
CREATE TABLE IF NOT EXISTS crypto_payments (
@@ -264,7 +283,6 @@ func (db *Database) createTables() error {
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);`,
`ALTER TABLE clients ADD COLUMN IF NOT EXISTS referral_balance NUMERIC(10,2) DEFAULT 0.0;`,
// ============================
// TABLE jwt_tokens
@@ -423,7 +441,7 @@ func (db *Database) createTables() error {
`CREATE INDEX IF NOT EXISTS idx_command_items_client_username ON command_items(client_username);`,
`CREATE INDEX IF NOT EXISTS idx_command_items_status ON command_items(status);`,
`CREATE INDEX IF NOT EXISTS idx_command_items_produit ON command_items(produit);`,
`CREATE INDEX IF NOT EXISTS idx_jwt_user_id ON jwt_tokens(user_id);`,
`CREATE INDEX IF NOT EXISTS idx_jwt_user_id ON jwt_tokens(user_id);`,
`CREATE INDEX IF NOT EXISTS idx_jwt_user_type ON jwt_tokens(user_type);`,
`CREATE INDEX IF NOT EXISTS idx_jwt_token ON jwt_tokens(token);`,
`CREATE INDEX IF NOT EXISTS idx_jwt_date_fin ON jwt_tokens(date_fin);`,
+3 -17
View File
@@ -3,6 +3,7 @@ package db
import (
"encoding/json"
"fmt"
"gestion/models"
"gestion/services"
"log"
"time"
@@ -64,7 +65,7 @@ func (d *Database) NotifyAllAdminCabine(commandID int, clientUsername, deliveryA
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 {
if err := d.GDB.Model(&models.User{}).Select("username").Where("role IN ?", []string{"admin", "cabine"}).Scan(&users).Error; err != nil {
log.Printf("❌ [ADMIN_NOTIF] Erreur lecture users admin/cabine: %v", err)
return
}
@@ -103,7 +104,7 @@ func (d *Database) NotifyAllAdminCabineAlert(alertID int, livreurUsername, alert
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 {
if err := d.GDB.Model(&models.User{}).Select("username").Where("role IN ?", []string{"admin", "cabine"}).Scan(&users).Error; err != nil {
log.Printf("❌ [ALERT_NOTIF] Erreur lecture users admin/cabine: %v", err)
return
}
@@ -136,18 +137,3 @@ func (d *Database) NotifyAllAdminCabineAlert(alertID int, livreurUsername, alert
}
log.Printf("🚨 [ALERT_NOTIF] Notif Redis (%d users) pour alerte #%d de %s", count, alertID, livreurUsername)
}
// AddDeliveryRating ajoute une note pour un livreur
func (d *Database) AddDeliveryRating(livreurUsername string, commandID, rating int, comment string) error {
err := d.GDB.Exec(`
INSERT INTO delivery_ratings (livreur_username, command_id, rating, comment, created_at)
VALUES (?, ?, ?, ?, NOW())`,
livreurUsername, commandID, rating, comment).Error
if err != nil {
log.Printf("⚠️ Erreur sauvegarde note livreur: %v", err)
return err
}
log.Printf("⭐ Note %d/5 ajoutée pour livreur %s (commande %d)", rating, livreurUsername, commandID)
return nil
}
+4 -1
View File
@@ -2,6 +2,7 @@ package db
import (
"fmt"
"gestion/models"
"gorm.io/gorm"
)
@@ -19,7 +20,9 @@ func (d *Database) CreditClientReferral(username string, amount float64) error {
if amount <= 0 {
return fmt.Errorf("le montant doit être positif")
}
result := d.GDB.Exec(`UPDATE clients SET referral_balance = referral_balance + ? WHERE username = ?`, amount, username)
result := d.GDB.Model(&models.Client{}).Where("username = ?", username).Updates(map[string]any{
"referral_balance": gorm.Expr("referral_balance + ?", amount),
})
if result.Error != nil {
return result.Error
}
+7 -11
View File
@@ -10,6 +10,8 @@ import (
"gestion/models"
"log"
"sort"
"gorm.io/gorm"
)
// GetClientCancellationsCount récupère le nombre d'annulations tardives d'un client
@@ -17,7 +19,7 @@ func (d *Database) GetClientCancellationsCount(username string) (int, error) {
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
err := d.GDB.Table("clients").Select("COALESCE(cancellations_count, 0) as count").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)
@@ -27,11 +29,9 @@ func (d *Database) GetClientCancellationsCount(username string) (int, error) {
// IncrementClientCancellationsCount incrémente le compteur d'annulations
func (d *Database) IncrementClientCancellationsCount(username string) error {
result := d.GDB.Exec(`
UPDATE clients
SET cancellations_count = COALESCE(cancellations_count, 0) + 1,
updated_at = CURRENT_TIMESTAMP
WHERE username = ?`, username)
result := d.GDB.Model(&models.Client{}).Where("username = ?", username).Updates(map[string]any{
"cancellations_count": gorm.Expr("COALESCE(cancellations_count, 0) + 1"),
})
if result.Error != nil {
log.Printf("❌ [IncrementCancellations] Erreur: %v", result.Error)
return fmt.Errorf("erreur incrémentation: %w", result.Error)
@@ -98,11 +98,7 @@ func (d *Database) ApplyCancellationPenalty(username string) (int, error) {
return 0, err
}
result := d.GDB.Exec(`
UPDATE clients
SET amende = ?,
updated_at = CURRENT_TIMESTAMP
WHERE username = ?`, float64(penalty), username)
result := d.GDB.Model(&models.Client{}).Where("username = ?", username).Update("amende", float64(penalty))
if result.Error != nil {
log.Printf("❌ [ApplyCancellationPenalty] Erreur UPDATE: %v", result.Error)
return 0, fmt.Errorf("erreur application pénalité: %w", result.Error)
+8 -1
View File
@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"gestion/models"
"strconv"
"gorm.io/gorm"
)
@@ -40,6 +41,7 @@ func DefaultSettings() models.AppSettings {
},
PointsEnabled: true,
ReferralEnabled: true,
ReferralAmount: 0,
PointsPools: []models.PointsPool{
{
Key: "pool_0",
@@ -90,7 +92,7 @@ func (d *Database) GetSettings() (models.AppSettings, error) {
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 {
if err := d.GDB.Table("app_settings").Select("key, value").Scan(&rows).Error; err != nil {
return settings, fmt.Errorf("erreur lecture settings: %w", err)
}
@@ -109,6 +111,10 @@ func (d *Database) GetSettings() (models.AppSettings, error) {
}
case "referral_enabled":
settings.ReferralEnabled = row.Value == "true"
case "referral_amount":
if v, err := strconv.ParseFloat(row.Value, 64); err == nil {
settings.ReferralAmount = v
}
case "crypto_payment_enabled":
settings.CryptoPaymentEnabled = row.Value == "true"
case "crypto_only":
@@ -207,6 +213,7 @@ func (d *Database) UpdateSettings(s models.AppSettings) error {
{"points_enabled", boolStr(s.PointsEnabled)},
{"points_pools", string(poolsJSON)},
{"referral_enabled", boolStr(s.ReferralEnabled)},
{"referral_amount", strconv.FormatFloat(s.ReferralAmount, 'f', 2, 64)},
{"crypto_payment_enabled", boolStr(s.CryptoPaymentEnabled)},
{"crypto_only", boolStr(s.CryptoOnly)},
{"nowpayments_api_key", s.NowPaymentsAPIKey},
+8 -10
View File
@@ -67,15 +67,14 @@ func ValidateAndConsumeLinkToken(token string) (username, role string, err error
}
func (d *Database) SaveClientTelegramChatID(username string, chatID int64) error {
return d.GDB.Exec(`UPDATE clients SET telegram_chat_id = ? WHERE username = ?`, chatID, username).Error
return d.GDB.Model(&models.Client{}).Where("username = ?", username).Update("telegram_chat_id", chatID).Error
}
func (d *Database) GetClientTelegramChatID(username string) (int64, bool, error) {
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 {
if err := d.GDB.Table("clients").Select("telegram_chat_id").Where("username = ?", username).Limit(1).Scan(&result).Error; err != nil {
return 0, false, err
}
if result.ChatID == nil {
@@ -85,19 +84,18 @@ func (d *Database) GetClientTelegramChatID(username string) (int64, bool, error)
}
func (d *Database) DeleteClientTelegramChatID(username string) error {
return d.GDB.Exec(`UPDATE clients SET telegram_chat_id = NULL WHERE username = ?`, username).Error
return d.GDB.Model(&models.Client{}).Where("username = ?", username).Update("telegram_chat_id", nil).Error
}
func (d *Database) SaveUserTelegramChatID(username string, chatID int64) error {
return d.GDB.Exec(`UPDATE users SET telegram_chat_id = ? WHERE username = ?`, chatID, username).Error
return d.GDB.Model(&models.User{}).Where("username = ?", username).Update("telegram_chat_id", chatID).Error
}
func (d *Database) GetUserTelegramChatID(username string) (int64, bool, error) {
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 {
if err := d.GDB.Table("users").Select("telegram_chat_id").Where("username = ?", username).Limit(1).Scan(&result).Error; err != nil {
return 0, false, err
}
if result.ChatID == nil {
@@ -107,7 +105,7 @@ func (d *Database) GetUserTelegramChatID(username string) (int64, bool, error) {
}
func (d *Database) DeleteUserTelegramChatID(username string) error {
return d.GDB.Exec(`UPDATE users SET telegram_chat_id = NULL WHERE username = ?`, username).Error
return d.GDB.Model(&models.User{}).Where("username = ?", username).Update("telegram_chat_id", nil).Error
}
// GetUserByTelegramChatID retrouve un utilisateur (clients + users) par chat_id
@@ -115,7 +113,7 @@ func (d *Database) GetUserByTelegramChatID(chatID int64) (username, role string,
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 != "" {
if err = d.GDB.Table("clients").Select("username").Where("telegram_chat_id = ?", chatID).Limit(1).Scan(&clientResult).Error; err == nil && clientResult.Username != "" {
return clientResult.Username, "client", nil
}
@@ -123,7 +121,7 @@ func (d *Database) GetUserByTelegramChatID(chatID int64) (username, role string,
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 != "" {
if err = d.GDB.Model(&models.User{}).Select("username, role").Where("telegram_chat_id = ?", chatID).Limit(1).Scan(&userResult).Error; err == nil && userResult.Username != "" {
return userResult.Username, userResult.Role, nil
}
+1 -1
View File
@@ -112,7 +112,7 @@ func (d *Database) ProcessScheduledNotifications() error {
// SendETANotification envoie une notification ETA
func (d *Database) SendETANotification(commandID int, notifType string) {
message := fmt.Sprintf("Votre commande #%d arrive dans %s", commandID, notifType)
message := fmt.Sprintf("Votre commande #%d arrive dans %s", d.GetClientOrderID(commandID), notifType)
channel := fmt.Sprintf("notifications:command:%d", commandID)
Redis.Publish(RedisCtx, channel, message)
+1 -1
View File
@@ -2,7 +2,7 @@ package db
import "fmt"
func extractCommandID(member interface{}) int {
func extractCommandID(member any) int {
switch v := member.(type) {
case int:
return v
@@ -70,7 +70,6 @@ func (d *Database) AssignCommandToDeliverymanQueue(commandID int, deliveryman st
d.UpdateCommandStatus(commandID, "assigned")
d.AssignDeliveryPerson(commandID, deliveryman)
// ✅ MODIFIÉ: Position dans la queue pour info seulement
d.SetCommandETAWithDetails(commandID, totalETA, int(currentQueueSize)+1)
limitInfo := ""
@@ -93,10 +92,8 @@ func (d *Database) AssignCommandToDeliverymanQueueUnlimited(deliveryman string,
queueKey := fmt.Sprintf("queue:deliveryman:%s", deliveryman)
currentQueueSize, _ := Redis.ZCard(RedisCtx, queueKey).Result()
// ✅ MODIFIÉ: Calculer le temps de trajet direct depuis la position du livreur
travelTime := d.CalculateETAForDeliveryman(deliveryman, queueItem.Lat, queueItem.Lng)
// ✅ ETA = temps de trajet direct uniquement
queueItem.EstimatedETA = travelTime
err := d.AddToDeliverymanQueue(deliveryman, queueItem)
+4 -4
View File
@@ -216,17 +216,17 @@ func (d *Database) StartQueueCleanupScheduler() {
// ============================================
// GetQueueValidationReport génère un rapport de validation sans supprimer
func (d *Database) GetQueueValidationReport() (map[string]interface{}, error) {
func (d *Database) GetQueueValidationReport() (map[string]any, error) {
keys, err := Redis.Keys(RedisCtx, "queue:pending:*").Result()
if err != nil {
return nil, err
}
report := map[string]interface{}{
report := map[string]any{
"total_commands": len(keys),
"valid_commands": 0,
"invalid_commands": 0,
"invalid_details": []map[string]interface{}{},
"invalid_details": []map[string]any{},
"validation_results": []string{},
}
@@ -259,7 +259,7 @@ func (d *Database) GetQueueValidationReport() (map[string]interface{}, error) {
if len(issues) > 0 {
report["invalid_commands"] = report["invalid_commands"].(int) + 1
report["invalid_details"] = append(report["invalid_details"].([]map[string]interface{}), map[string]interface{}{
report["invalid_details"] = append(report["invalid_details"].([]map[string]any), map[string]any{
"command_id": queueItem.CommandID,
"issues": issues,
"data": queueItem,
@@ -11,10 +11,6 @@ import (
"github.com/redis/go-redis/v9"
)
// ============================================
// 🆕 GESTION AUTOMATIQUE DU STATUT BUSY
// ============================================
// UpdateDeliverymanStatusBasedOnQueue met à jour automatiquement le statut
func (d *Database) UpdateDeliverymanStatusBasedOnQueue(deliveryman string) error {
queueKey := fmt.Sprintf("queue:deliveryman:%s", deliveryman)
+2 -2
View File
@@ -13,6 +13,8 @@ require (
github.com/lib/pq v1.10.9
github.com/redis/go-redis/v9 v9.17.0
golang.org/x/crypto v0.40.0
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.31.1
)
require (
@@ -56,6 +58,4 @@ 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
)
+6 -5
View File
@@ -2,6 +2,7 @@ package handlers
import (
"gestion/db"
"gestion/utils"
"net/http"
"github.com/gin-gonic/gin"
@@ -21,12 +22,12 @@ func AddAddress(c *gin.Context) {
InvalidAddress string `json:"invalid_address" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
utils.BindErr(c, err)
return
}
if err := database.AddAddress(req.CorrectAddress, req.InvalidAddress); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
utils.ServerErr(c, "Impossible d'ajouter l'adresse", err)
return
}
@@ -46,12 +47,12 @@ func DeleteAddress(c *gin.Context) {
InvalidAddress string `json:"invalid_address" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
utils.BindErr(c, err)
return
}
if err := database.DeleteAddress(req.InvalidAddress, req.CorrectAddress); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
utils.ServerErr(c, "Impossible de supprimer l'adresse", err)
return
}
@@ -68,7 +69,7 @@ func GetAllAddress(c *gin.Context) {
getAddress, err := database.AllAddress()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
utils.ServerErr(c, "Impossible de récupérer les adresses", err)
return
}
+13 -16
View File
@@ -2,6 +2,7 @@ package handlers
import (
"gestion/db"
"gestion/utils"
"net/http"
"strconv"
@@ -32,14 +33,13 @@ func AlertPolice(c *gin.Context) {
usernameStr := username.(string)
alert, err := database.CreateAlert(usernameStr, req.Message)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
utils.ServerErr(c, "Impossible de créer l'alerte", err)
return
}
// Notifier tous les admins/cabines en temps réel
go database.NotifyAllAdminCabineAlert(alert.ID, usernameStr, req.Message)
c.JSON(200, gin.H{
c.JSON(http.StatusCreated, gin.H{
"success": true,
"message": "Police alert created",
"alert_id": alert.ID,
@@ -61,13 +61,12 @@ func DeleteAlert(c *gin.Context) {
c.JSON(http.StatusForbidden, gin.H{"error": "Accès réservé aux livreurs"})
return
}
err = database.DeleteAlertPolicy(alertID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
if err = database.DeleteAlertPolicy(alertID); err != nil {
utils.ServerErr(c, "Impossible de supprimer l'alerte", err)
return
}
c.JSON(200, gin.H{
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Alert deleted",
})
@@ -89,11 +88,11 @@ func GetAlert(c *gin.Context) {
}
alert, err := database.GetAlertPolicy(alertID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
c.JSON(http.StatusNotFound, gin.H{"error": "Alerte non trouvée"})
return
}
c.JSON(200, gin.H{
c.JSON(http.StatusOK, gin.H{
"success": true,
"alert": alert,
})
@@ -124,7 +123,6 @@ func EndAlert(c *gin.Context) {
usernameStr := username.(string)
// Vérifier que l'alerte appartient bien à ce livreur
alert, err := database.GetAlertPolicy(alertID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Alerte non trouvée"})
@@ -136,9 +134,8 @@ func EndAlert(c *gin.Context) {
return
}
err = database.EndAlert(alertID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Impossible de mettre fin à l'alerte", "details": err.Error()})
if err = database.EndAlert(alertID); err != nil {
utils.ServerErr(c, "Impossible de mettre fin à l'alerte", err)
return
}
@@ -169,7 +166,7 @@ func GetMyAlerts(c *gin.Context) {
alerts, err := database.GetAlertsByUsername(usernameStr)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Impossible de récupérer les alertes", "details": err.Error()})
utils.ServerErr(c, "Impossible de récupérer les alertes", err)
return
}
@@ -192,7 +189,7 @@ func GetAllAlerts(c *gin.Context) {
alerts, err := database.GetAllAlerts()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Impossible de récupérer les alertes", "details": err.Error()})
utils.ServerErr(c, "Impossible de récupérer les alertes", err)
return
}
@@ -214,7 +211,7 @@ func GetActiveAlerts(c *gin.Context) {
alerts, err := database.GetActiveAlerts()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Impossible de récupérer les alertes", "details": err.Error()})
utils.ServerErr(c, "Impossible de récupérer les alertes", err)
return
}
+1 -2
View File
@@ -77,7 +77,6 @@ func RegisterClient(c *gin.Context) {
log.Printf("❌ [REGISTER_CLIENT] Erreur binding: %v", err)
c.JSON(http.StatusBadRequest, gin.H{
"error": "Données invalides",
"details": err.Error(),
})
return
}
@@ -301,7 +300,7 @@ func ChangePassword(c *gin.Context) {
NewPassword string `json:"new_password" binding:"required,min=8"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides", "details": err.Error()})
utils.BindErr(c, err)
return
}
+1 -11
View File
@@ -85,7 +85,6 @@ func SetCommandDestinationCoordinates(c *gin.Context) {
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur stockage Redis",
"details": err.Error(),
})
return
}
@@ -217,7 +216,6 @@ func UpdateCommandAddressCabine(c *gin.Context) {
if err := database.UpdateCommandAddress(commandID, req.DeliveryAddress); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur lors de la mise à jour de l'adresse",
"details": err.Error(),
})
return
}
@@ -264,7 +262,6 @@ func GetLivreurPosition(c *gin.Context) {
"success": false,
"livreur": livreurUsername,
"message": "Position non disponible (GPS désactivé ou livraison terminée)",
"details": err.Error(),
})
return
}
@@ -394,7 +391,6 @@ func GetDeliveryIssues(c *gin.Context) {
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur récupération problèmes",
"details": err.Error(),
})
return
}
@@ -431,7 +427,6 @@ func CreateDeliveryIssue(c *gin.Context) {
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur création problème",
"details": err.Error(),
})
return
}
@@ -468,7 +463,6 @@ func UpdateDeliveryIssue(c *gin.Context) {
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur mise à jour",
"details": err.Error(),
})
return
}
@@ -516,7 +510,6 @@ func AddDeliverySupport(c *gin.Context) {
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur ajout support",
"details": err.Error(),
})
return
}
@@ -540,7 +533,6 @@ func GetCommandLogs(c *gin.Context) {
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur récupération logs",
"details": err.Error(),
})
return
}
@@ -580,7 +572,6 @@ func ForceValidateDelivery(c *gin.Context) {
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Raison requise pour validation forcée",
"details": err.Error(),
"example": gin.H{
"reason": "Client confirmé par téléphone",
},
@@ -632,7 +623,6 @@ func ForceValidateDelivery(c *gin.Context) {
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur lors de la validation forcée",
"details": err.Error(),
})
return
}
@@ -641,7 +631,7 @@ func ForceValidateDelivery(c *gin.Context) {
livreurAssign, _ := command["livreur_assign"].(string)
if clientUsername != "" {
clientMsg := fmt.Sprintf("Votre commande #%d a été livrée. Merci !", commandID)
clientMsg := fmt.Sprintf("Ta commande #%d a bien été livrée ! Bonne dégustation l'ami et à bientôt 😊", database.GetClientOrderID(commandID))
database.NotifyClient(clientUsername, commandID, "livre", clientMsg)
}
@@ -339,6 +339,7 @@ func GetAllCancelledOrders(c *gin.Context) {
}
}
cancelReason, _ := order["cancel_reason"].(string)
enrichedOrder := map[string]any{
"id": order["id"],
"username": order["username"],
@@ -346,12 +347,14 @@ func GetAllCancelledOrders(c *gin.Context) {
"created_at": order["created_at"],
"updated_at": order["updated_at"],
"items_count": len(items),
"cancel_reason": cancelReason,
}
if cancellationLog != nil {
enrichedOrder["cancellation"] = gin.H{
"cancelled_at": cancellationLog["created_at"],
"cancelled_by": cancellationLog["author"],
"reason": cancelReason,
}
}
+3 -3
View File
@@ -46,7 +46,7 @@ func CreateCategory(c *gin.Context) {
}
if err := db.ValidateCategoryColor(req.Color); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "Couleur invalide (format hex requis, ex: #ff0000)"})
return
}
@@ -91,7 +91,7 @@ func UpdateCategory(c *gin.Context) {
}
if err := db.ValidateCategoryColor(req.Color); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "Couleur invalide (format hex requis, ex: #ff0000)"})
return
}
@@ -121,7 +121,7 @@ func DeleteCategory(c *gin.Context) {
if err := database.DeleteCategory(id); err != nil {
log.Printf("❌ [CATEGORIES] Suppression erreur: %v", err)
if strings.Contains(err.Error(), "utilisée par") {
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
c.JSON(http.StatusConflict, gin.H{"error": "Catégorie utilisée par des produits existants"})
} else {
c.JSON(http.StatusNotFound, gin.H{"error": "Catégorie non trouvée"})
}
+2 -1
View File
@@ -6,6 +6,7 @@ import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
@@ -90,7 +91,6 @@ func GetMyCommandsWithTracking(c *gin.Context) {
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur récupération",
"details": err.Error(),
})
return
}
@@ -118,6 +118,7 @@ func GetMyCommandsWithTracking(c *gin.Context) {
enrichedCommands[i] = gin.H{
"id": cmd["id"],
"client_order_number": cmd["client_order_number"],
"status": cmd["status"],
"status_message": getStatusMessage(cmd["status"].(string)),
"adresse": cmd["adresse"],
+12 -24
View File
@@ -81,7 +81,7 @@ func UpdateCommandAddress(c *gin.Context) {
// ✅ Récupération sécurisée du username
adminUsername, err := safeGetUsername(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
@@ -161,7 +161,7 @@ func ProposeAddressChange(c *gin.Context) {
staffUsername, err := safeGetUsername(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
@@ -198,15 +198,14 @@ func ProposeAddressChange(c *gin.Context) {
}
if err := database.ProposeAddressChange(commandID, req.ProposedAddress, staffUsername); err != nil {
log.Printf("❌ [PROPOSE_ADDR] Erreur: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
utils.ServerErr(c, "Impossible de proposer l'adresse", err)
return
}
// Notifier le client
clientUsername, _ := command["username"].(string)
if clientUsername != "" {
msg := fmt.Sprintf("📍 Une nouvelle adresse de livraison vous est proposée pour la commande #%d : %s. Veuillez l'accepter ou la refuser dans le suivi de commande.", commandID, req.ProposedAddress)
msg := fmt.Sprintf("📍 Une nouvelle adresse de livraison vous est proposée pour votre commande #%d : %s. Veuillez l'accepter ou la refuser dans le suivi de commande.", database.GetClientOrderID(commandID), req.ProposedAddress)
database.NotifyClient(clientUsername, commandID, "address_proposal", msg)
}
@@ -248,8 +247,7 @@ func RespondToAddressProposal(c *gin.Context) {
}
if err := database.RespondToAddressProposal(commandID, clientUsername, req.Accepted); err != nil {
log.Printf("❌ [RESPOND_ADDR] Erreur: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
utils.ServerErr(c, "Impossible de traiter la réponse", err)
return
}
@@ -296,7 +294,6 @@ func GetAllCommands(c *gin.Context) {
log.Printf("❌ [GET_CMDS] Erreur: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur lors de la récupération des commandes",
"details": err.Error(),
})
return
}
@@ -444,7 +441,7 @@ func StaffApproveDelivery(c *gin.Context) {
totalPoints, pointCategory, clientUsername, err := database.ApproveDeliveryAtomicByStaff(commandID, staffUsername)
if err != nil {
log.Printf("❌ [STAFF_APPROVE] Erreur: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Impossible de confirmer la réception: " + err.Error()})
utils.ServerErr(c, "Impossible de confirmer la réception", err)
return
}
@@ -474,7 +471,7 @@ func ValidateDelivery(c *gin.Context) {
adminUsername, err := safeGetUsername(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
@@ -604,7 +601,6 @@ func GetAvailableDeliveryPersons(c *gin.Context) {
log.Printf("❌ [GET_LIVREURS] Erreur: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur lors de la récupération des livreurs",
"details": err.Error(),
})
return
}
@@ -651,7 +647,6 @@ func AssignDeliveryPerson(c *gin.Context) {
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Données invalides",
"details": err.Error(),
})
return
}
@@ -665,7 +660,6 @@ func AssignDeliveryPerson(c *gin.Context) {
log.Printf("❌ [ASSIGN] Erreur: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur assignation livreur",
"details": err.Error(),
})
return
}
@@ -704,7 +698,6 @@ func GetClientCommandsHistory(c *gin.Context) {
log.Printf("❌ [HISTORY] Erreur: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur récupération historique",
"details": err.Error(),
})
return
}
@@ -805,7 +798,7 @@ func NotifyClientToDescend(c *gin.Context) {
}
staffUsername, _ := c.Get("username")
msg := fmt.Sprintf("Votre commande #%d est prête ! Vous pouvez descendre la récupérer.", commandID)
msg := fmt.Sprintf("Votre commande #%d est prête ! Vous pouvez descendre la récupérer.", database.GetClientOrderID(commandID))
database.NotifyClient(clientUsername, commandID, "ready_pickup", msg)
database.AddCommandLog(commandID, "notification", fmt.Sprintf("Client notifié de descendre par %s", staffUsername), staffUsername.(string))
@@ -859,7 +852,6 @@ func ShowItems(c *gin.Context) {
log.Printf("❌ [ITEMS] Erreur: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur lors de la récupération des items",
"details": err.Error(),
})
return
}
@@ -951,7 +943,6 @@ func GetCommandItemsWithDetails(c *gin.Context) {
log.Printf("❌ [ITEMS_DETAILED] Erreur DB: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur récupération items",
"details": err.Error(),
})
return
}
@@ -970,6 +961,7 @@ func GetCommandItemsWithDetails(c *gin.Context) {
"total_prix": items[0]["total_prix"],
"livreur_assign": items[0]["livreur_assign"],
"command_created_at": items[0]["command_created_at"],
"client_order_number": items[0]["client_order_number"],
}
c.JSON(http.StatusOK, gin.H{
@@ -1007,7 +999,6 @@ func UpdateItemStatus(c *gin.Context) {
log.Printf("❌ [UPD_ITEM] Erreur JSON: %v", err)
c.JSON(http.StatusBadRequest, gin.H{
"error": "Status requis",
"details": err.Error(),
})
return
}
@@ -1038,7 +1029,6 @@ func UpdateItemStatus(c *gin.Context) {
log.Printf("❌ [UPD_ITEM] Erreur: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur lors de la mise à jour",
"details": err.Error(),
})
return
}
@@ -1065,7 +1055,7 @@ func DeleteCommandItem(c *gin.Context) {
adminUsername, err := safeGetUsername(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
@@ -1084,8 +1074,7 @@ func DeleteCommandItem(c *gin.Context) {
log.Printf("🗑️ [DEL_ITEM] Admin %s supprime item %d de cmd %d", adminUsername, itemID, commandID)
if err := database.DeleteCommandItem(commandID, itemID); err != nil {
log.Printf("❌ [DEL_ITEM] Erreur: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
utils.ServerErr(c, "Impossible de supprimer l'item", err)
return
}
@@ -1137,8 +1126,7 @@ func UpdateCommandStatusAdmin(c *gin.Context) {
}
if err := database.UpdateCommandStatus(commandID, req.Status); err != nil {
log.Printf("❌ [STATUS_ADMIN] Erreur: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
utils.ServerErr(c, "Impossible de mettre à jour le statut", err)
return
}
+5 -8
View File
@@ -34,7 +34,6 @@ func GetMyDeliveries(c *gin.Context) {
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur récupération",
"details": err.Error(),
})
return
}
@@ -190,7 +189,6 @@ func UpdateDeliveryStatus(c *gin.Context) {
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Données invalides",
"details": err.Error(),
})
return
}
@@ -261,7 +259,6 @@ func UpdateDeliveryStatus(c *gin.Context) {
if err := database.UpdateCommandStatus(commandID, req.Status); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur mise à jour",
"details": err.Error(),
})
return
}
@@ -361,16 +358,16 @@ func UpdateDeliveryStatus(c *gin.Context) {
} else {
etaStr = fmt.Sprintf("%d min", etaMinutes)
}
clientMsg = fmt.Sprintf("🛵 Votre commande #%d est en route ! Arrivée dans ~%s", commandID, etaStr)
clientMsg = fmt.Sprintf("Un livreur vient de prendre en charge ta commande #%d, il est en route (~%s)", database.GetClientOrderID(commandID), etaStr)
} else {
clientMsg = fmt.Sprintf("🛵 Votre commande #%d est en route !", commandID)
clientMsg = fmt.Sprintf("Un livreur vient de prendre en charge ta commande #%d, il est en route.", database.GetClientOrderID(commandID))
}
case "arrived":
clientMsg = fmt.Sprintf("🛵 Votre livreur est là ! Il sera chez vous dans 5 minutes (commande #%d)", commandID)
clientMsg = fmt.Sprintf("Descend, le livreur est là dans 3min (commande #%d) 🛵", database.GetClientOrderID(commandID))
case "livre":
clientMsg = fmt.Sprintf("Votre commande #%d a été livrée. Merci !", commandID)
clientMsg = fmt.Sprintf("Ta commande #%d a bien été livrée ! Bonne dégustation l'ami et à bientôt 😊", database.GetClientOrderID(commandID))
case "cancelled":
clientMsg = fmt.Sprintf("Votre commande #%d a été annulée par le livreur", commandID)
clientMsg = fmt.Sprintf("Votre commande #%d a été annulée par le livreur", database.GetClientOrderID(commandID))
}
if clientMsg != "" {
database.NotifyClient(clientUsername, commandID, req.Status, clientMsg)
@@ -40,7 +40,6 @@ func GetDeliveryPersonDetails(c *gin.Context) {
log.Printf("❌ [GET_DELIVERY_DETAILS] Livreur non trouvé: %v", err)
c.JSON(http.StatusNotFound, gin.H{
"error": "Livreur non trouvé",
"details": err.Error(),
})
return
}
@@ -118,7 +117,6 @@ func UpdateDeliveryPersonStatusAdmin(c *gin.Context) {
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Statut requis",
"details": err.Error(),
})
return
}
@@ -163,7 +161,6 @@ func UpdateDeliveryPersonStatusAdmin(c *gin.Context) {
log.Printf("❌ [UPDATE_DELIVERY_STATUS] Erreur mise à jour: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur mise à jour statut",
"details": err.Error(),
})
return
}
@@ -325,7 +322,6 @@ func GetDeliveryPersonHistory(c *gin.Context) {
log.Printf("❌ [GET_DELIVERY_HISTORY] Erreur récupération: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur récupération historique",
"details": err.Error(),
})
return
}
@@ -373,7 +369,6 @@ func UpdateDeliveryPersonLocationAdmin(c *gin.Context) {
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Coordonnées GPS requises",
"details": err.Error(),
})
return
}
@@ -421,7 +416,6 @@ func UpdateDeliveryPersonLocationAdmin(c *gin.Context) {
log.Printf("❌ [UPDATE_DELIVERY_LOCATION] Erreur mise à jour: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur mise à jour position",
"details": err.Error(),
})
return
}
@@ -515,7 +509,6 @@ func RemoveCommandFromQueue(c *gin.Context) {
log.Printf("❌ [REMOVE_FROM_QUEUE] Erreur suppression: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur suppression de la queue",
"details": err.Error(),
})
return
}
+6 -60
View File
@@ -21,34 +21,24 @@ import (
// GÉOCODAGE D'ADRESSES
// ============================================
// GeocodeAddress convertit une adresse en coordonnées GPS
func GeocodeAddress(c *gin.Context) {
geoService := c.MustGet("geoService").(*services.GeoService)
var req struct {
Address string `json:"address" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Adresse requise",
"details": err.Error(),
})
c.JSON(http.StatusBadRequest, gin.H{"error": "Adresse requise"})
return
}
location, err := geoService.GeocodeAddress(req.Address)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"error": "Impossible de géocoder cette adresse",
"details": err.Error(),
})
c.JSON(http.StatusNotFound, gin.H{"error": "Impossible de géocoder cette adresse"})
return
}
log.Printf("📍 Adresse géocodée: %s -> (%.6f, %.6f)",
req.Address, location.Latitude, location.Longitude)
log.Printf("📍 Adresse géocodée: %s -> (%.6f, %.6f)", req.Address, location.Latitude, location.Longitude)
c.JSON(http.StatusOK, gin.H{
"success": true,
"latitude": location.Latitude,
@@ -77,7 +67,6 @@ func FindNearestDeliveryPerson(c *gin.Context) {
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Données invalides",
"details": err.Error(),
})
return
}
@@ -90,7 +79,6 @@ func FindNearestDeliveryPerson(c *gin.Context) {
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Impossible de géocoder l'adresse",
"details": err.Error(),
})
return
}
@@ -111,7 +99,6 @@ func FindNearestDeliveryPerson(c *gin.Context) {
if err := services.ValidateCoordinates(targetCoords); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Coordonnées invalides",
"details": err.Error(),
})
return
}
@@ -136,7 +123,6 @@ func FindNearestDeliveryPerson(c *gin.Context) {
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"error": "Aucun livreur avec position GPS valide",
"details": err.Error(),
})
return
}
@@ -194,7 +180,6 @@ func GetAllDeliveryDistances(c *gin.Context) {
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Données invalides",
"details": err.Error(),
})
return
}
@@ -206,7 +191,6 @@ func GetAllDeliveryDistances(c *gin.Context) {
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Impossible de géocoder l'adresse",
"details": err.Error(),
})
return
}
@@ -225,7 +209,6 @@ func GetAllDeliveryDistances(c *gin.Context) {
if err := services.ValidateCoordinates(targetCoords); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Coordonnées invalides",
"details": err.Error(),
})
return
}
@@ -247,7 +230,6 @@ func GetAllDeliveryDistances(c *gin.Context) {
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur calcul des distances",
"details": err.Error(),
})
return
}
@@ -317,7 +299,6 @@ func AutoAssignNearestDeliveryPerson(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Impossible de géocoder l'adresse de livraison",
"address": address,
"details": err.Error(),
})
return
}
@@ -376,7 +357,6 @@ func AutoAssignNearestDeliveryPerson(c *gin.Context) {
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur calcul ETA",
"details": err.Error(),
})
return
}
@@ -386,7 +366,6 @@ func AutoAssignNearestDeliveryPerson(c *gin.Context) {
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur lors de l'assignation",
"details": err.Error(),
})
return
}
@@ -441,7 +420,6 @@ func AutoAssignNearestDeliveryPerson(c *gin.Context) {
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur calcul ETA",
"details": err.Error(),
})
return
}
@@ -451,7 +429,6 @@ func AutoAssignNearestDeliveryPerson(c *gin.Context) {
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur lors de l'assignation forcée",
"details": err.Error(),
})
return
}
@@ -507,7 +484,6 @@ func AutoAssignNearestDeliveryPerson(c *gin.Context) {
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"error": "Aucun livreur avec position GPS valide",
"details": err.Error(),
})
return
}
@@ -528,7 +504,6 @@ func AutoAssignNearestDeliveryPerson(c *gin.Context) {
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur lors de l'assignation",
"details": err.Error(),
})
return
}
@@ -583,7 +558,6 @@ func AutoAssignAllPendingCommands(c *gin.Context) {
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur récupération des commandes",
"details": err.Error(),
})
return
}
@@ -743,7 +717,6 @@ func GetAllDeliveryQueues(c *gin.Context) {
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur récupération des queues",
"details": err.Error(),
})
return
}
@@ -803,7 +776,6 @@ func GetDeliverymanQueue(c *gin.Context) {
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur récupération de la queue",
"details": err.Error(),
})
return
}
@@ -814,41 +786,23 @@ func GetDeliverymanQueue(c *gin.Context) {
})
}
// ============================================
// VALIDATION D'ADRESSE
// ============================================
// ValidateAddress vérifie si une adresse peut être géocodée
// POST /api/v1/validate-address
// Body: {"address": "123 Main St, Paris"}
func ValidateAddress(c *gin.Context) {
geoService := c.MustGet("geoService").(*services.GeoService)
var req struct {
Address string `json:"address" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Adresse requise",
"details": err.Error(),
})
c.JSON(http.StatusBadRequest, gin.H{"error": "Adresse requise"})
return
}
isValid := geoService.IsValidAddress(req.Address)
if !isValid {
c.JSON(http.StatusOK, gin.H{
"valid": false,
"message": "Adresse introuvable ou invalide",
})
if !geoService.IsValidAddress(req.Address) {
c.JSON(http.StatusOK, gin.H{"valid": false, "message": "Adresse introuvable ou invalide"})
return
}
// Récupérer les détails
location, _ := geoService.GeocodeAddress(req.Address)
c.JSON(http.StatusOK, gin.H{
"valid": true,
"message": "Adresse valide",
@@ -858,13 +812,7 @@ func ValidateAddress(c *gin.Context) {
})
}
// ============================================
// HELPER FUNCTION - CALCUL ETA AVEC TOMTOM
// ============================================
// calculateTravelTimeWithTomTom calcule l'ETA avec TomTom ou fallback local
func calculateTravelTimeWithTomTom(geoService *services.GeoService, deliverymanUsername string, targetLat, targetLon float64) (int, float64, error) {
// Récupérer position du livreur
deliverymanLoc, err := geoService.GetDeliveryPersonLocation(deliverymanUsername)
if err != nil {
return 0, 0, fmt.Errorf("position du livreur introuvable: %w", err)
@@ -875,10 +823,8 @@ func calculateTravelTimeWithTomTom(geoService *services.GeoService, deliverymanU
Longitude: targetLon,
}
// Calculer ETA avec TomTom (avec fallback automatique intégré)
travelTime, distance, err := services.GetETAWithTraffic(*deliverymanLoc, targetCoords)
if err != nil {
// Fallback sur calcul local
distance = services.CalculateDistance(*deliverymanLoc, targetCoords)
travelTime = services.CalculateETA(distance)
log.Printf("⚠️ TomTom indisponible pour %s, fallback: %.2f km -> %d min",
+1 -2
View File
@@ -13,6 +13,7 @@ import (
"net/url"
"strconv"
"github.com/gin-gonic/gin"
)
@@ -43,7 +44,6 @@ func GetDeliveryPersonMapLinks(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{
"success": false,
"error": "Position GPS non disponible pour ce livreur",
"details": err.Error(),
"message": "Le livreur n'a pas encore partagé sa position ou est hors ligne",
})
return
@@ -117,7 +117,6 @@ func GetCommandNavigationLinks(c *gin.Context) {
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur génération des liens",
"details": err.Error(),
})
return
}
+1 -2
View File
@@ -12,6 +12,7 @@ import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
@@ -41,7 +42,6 @@ func GetMyCompletedOrders(c *gin.Context) {
log.Printf("❌ [HISTORY] Erreur: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur lors de la récupération de l'historique",
"details": err.Error(),
})
return
}
@@ -120,7 +120,6 @@ func GetMyCompletedOrdersWithItems(c *gin.Context) {
log.Printf("❌ [HISTORY_DETAILED] Erreur: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur lors de la récupération de l'historique",
"details": err.Error(),
})
return
}
+17 -32
View File
@@ -9,6 +9,7 @@ import (
"gestion/db"
"gestion/models"
"gestion/services"
"gestion/utils"
"log"
"net/http"
@@ -32,7 +33,7 @@ func AddProductsBasket(c *gin.Context) {
var req BasketsRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Requête invalide", "details": err.Error()})
utils.BindErr(c, err)
return
}
@@ -62,17 +63,17 @@ func AddProductsBasket(c *gin.Context) {
}
if err := database.DecrementProductStockByID(req.ProductID, req.Quantity); err != nil {
log.Printf("❌ [ADD_PANIER] Erreur décrement stock product_id=%d: %v", req.ProductID, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Impossible d'ajouter le produit au panier", "details": err.Error()})
utils.ServerErr(c, "Impossible d'ajouter le produit au panier", err)
return
}
panier, err := database.AddProductInBasketByID(req.Username, req.ProductID, req.Quantity)
if err != nil {
log.Printf("❌ [ADD_PANIER] Erreur ajout product_id=%d qty=%.3f: %v", req.ProductID, req.Quantity, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Impossible d'ajouter le produit au panier", "details": err.Error()})
utils.ServerErr(c, "Impossible d'ajouter le produit au panier", err)
return
}
c.JSON(http.StatusCreated, gin.H{"success": true, "message": "Produit ajouté au panier avec succès", "panier": panier})
c.JSON(http.StatusOK, gin.H{"success": true, "message": "Produit ajouté au panier avec succès", "panier": panier})
return
}
@@ -128,11 +129,7 @@ func GetAllBaskets(c *gin.Context) {
baskets, err := database.GetAllProductsInBasket(username)
if err != nil {
log.Printf("❌ [GET_PANIER] Erreur récupération: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur lors de la récupération du panier",
"details": err.Error(),
})
utils.ServerErr(c, "Erreur lors de la récupération du panier", err)
return
}
@@ -165,11 +162,7 @@ func DeleteProductFromBasket(c *gin.Context) {
}
if err := c.ShouldBindJSON(&req); err != nil {
log.Printf("❌ [DEL_PANIER] Erreur JSON: %v", err)
c.JSON(http.StatusBadRequest, gin.H{
"error": "Données requises manquantes",
"details": err.Error(),
})
utils.BindErr(c, err)
return
}
@@ -211,11 +204,7 @@ func DeleteProductFromBasket(c *gin.Context) {
// Supprimer l'article
err = database.DeleteProductFromBasket(req.ID)
if err != nil {
log.Printf("❌ [DEL_PANIER] Erreur suppression: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur lors de la suppression",
"details": err.Error(),
})
utils.ServerErr(c, "Erreur lors de la suppression", err)
return
}
@@ -259,11 +248,7 @@ func ClearBasket(c *gin.Context) {
err = database.ClearBasket(authUsernameStr)
if err != nil {
log.Printf("❌ [CLEAR_PANIER] Erreur vidage: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur lors du vidage du panier",
"details": err.Error(),
})
utils.ServerErr(c, "Erreur lors du vidage du panier", err)
return
}
@@ -305,7 +290,7 @@ func ValidateBasket(c *gin.Context) {
cmd := &models.Command{DeliveryAddress: req.DeliveryAddress}
if err := database.CheckAddress(cmd); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(), "corrected_address": cmd.DeliveryAddress})
c.JSON(http.StatusBadRequest, gin.H{"error": "Adresse non reconnue", "corrected_address": cmd.DeliveryAddress})
return
}
req.DeliveryAddress = cmd.DeliveryAddress
@@ -323,8 +308,7 @@ func ValidateBasket(c *gin.Context) {
// ============================================
items, err := database.GetBasketItems(usernameStr)
if err != nil {
log.Printf("❌ [CHECKOUT] Erreur récupération panier: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Impossible de récupérer le panier", "details": err.Error()})
utils.ServerErr(c, "Impossible de récupérer le panier", err)
return
}
@@ -487,7 +471,7 @@ func ValidateBasket(c *gin.Context) {
_, _ = database.CreateCryptoPayment(commandID, payResp.PaymentID.String(), payResp.Status, payResp.PriceCurrency, payResp.PayCurrency, payResp.PayAddress, priceAmt, payAmt)
log.Printf("✅ [CHECKOUT] Commande %d en attente paiement crypto (%s)", commandID, req.PayCurrency)
c.JSON(http.StatusOK, gin.H{
c.JSON(http.StatusCreated, gin.H{
"success": true,
"command_id": commandID,
"payment_method": "crypto",
@@ -510,8 +494,7 @@ func ValidateBasket(c *gin.Context) {
// ============================================
err = database.ClearBasket(usernameStr)
if err != nil {
log.Printf("❌ [CHECKOUT] Erreur vidage panier: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Impossible de vider le panier", "details": err.Error()})
utils.ServerErr(c, "Impossible de vider le panier", err)
return
}
log.Printf("🧹 [CHECKOUT] Panier vidé")
@@ -585,7 +568,8 @@ func ValidateBasket(c *gin.Context) {
}
// Notifier le client
clientMsg := fmt.Sprintf("Votre commande #%d est confirmée ! Un livreur est en route.", commandID)
clientOrderID := database.GetClientOrderID(commandID)
clientMsg := fmt.Sprintf("Ta commande #%d est prise en compte ! Merci de rester branché et vigilant sur les notifs à venir.", clientOrderID)
database.NotifyClient(usernameStr, commandID, "assigned", clientMsg)
assigned = true
@@ -613,6 +597,7 @@ func ValidateBasket(c *gin.Context) {
resp := gin.H{
"success": true,
"command_id": commandID,
"client_order_number": command.ClientOrderID,
"delivery_address": req.DeliveryAddress,
"status": "pending",
"referral_used": referralUsed,
@@ -631,7 +616,7 @@ func ValidateBasket(c *gin.Context) {
log.Printf("✅ [CHECKOUT] Réponse 200 - Commande %d en attente", commandID)
}
c.JSON(http.StatusOK, resp)
c.JSON(http.StatusCreated, resp)
}
// getBaseURL construit l'URL de base depuis la requête en cours
+17 -67
View File
@@ -10,6 +10,7 @@ import (
"fmt"
"gestion/db"
"gestion/services"
"gestion/utils"
"log"
"net/http"
"strconv"
@@ -94,10 +95,7 @@ func AutoAssignNextCommand(c *gin.Context) {
err = database.AutoAssignCommand(nextCommand.CommandID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur lors de l'assignation automatique",
"details": err.Error(),
})
utils.ServerErr(c, "Erreur lors de l'assignation automatique", err)
return
}
@@ -138,11 +136,7 @@ func UpdateLivreurLocation(c *gin.Context) {
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Données invalides - latitude et longitude requises",
"details": err.Error(),
"format": gin.H{
"latitude": "number (required)",
"longitude": "number (required)",
},
"format": gin.H{"latitude": "number (required)", "longitude": "number (required)"},
})
return
}
@@ -169,10 +163,7 @@ func UpdateLivreurLocation(c *gin.Context) {
// ✅ 1. Mettre à jour la position GPS dans Redis
err := database.UpdateDeliveryPersonLocation(usernameStr, req.Latitude, req.Longitude)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur lors de la mise à jour de la position",
"details": err.Error(),
})
utils.ServerErr(c, "Erreur lors de la mise à jour de la position", err)
return
}
@@ -251,7 +242,6 @@ func GetMyLocation(c *gin.Context) {
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"error": "Position non disponible",
"details": err.Error(),
"message": "Veuillez d'abord mettre à jour votre position",
})
return
@@ -286,10 +276,7 @@ func GetDeliveryPersonLocation(c *gin.Context) {
position, err := database.GetLivreurPosition(username)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"error": "Position non trouvée pour ce livreur",
"details": err.Error(),
})
c.JSON(http.StatusNotFound, gin.H{"error": "Position non trouvée pour ce livreur"})
return
}
@@ -373,7 +360,6 @@ func GetDeliverymanLocationForCommand(c *gin.Context) {
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"error": "Position du livreur non disponible",
"details": err.Error(),
"message": "Le livreur n'a pas encore partagé sa position",
"command_info": gin.H{
"command_id": commandID,
@@ -501,10 +487,7 @@ func UpdateDeliveryPersonStatus(c *gin.Context) {
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Données invalides",
"details": err.Error(),
})
utils.BindErr(c, err)
return
}
@@ -531,10 +514,7 @@ func UpdateDeliveryPersonStatus(c *gin.Context) {
err := database.SetDeliveryPersonStatus(usernameStr, req.Status, 0)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur lors de la mise à jour du statut",
"details": err.Error(),
})
utils.ServerErr(c, "Erreur lors de la mise à jour du statut", err)
return
}
@@ -613,10 +593,7 @@ func GetMyQueue(c *gin.Context) {
queueInfo, err := database.GetDeliverymanQueueInfo(usernameStr)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur récupération de la queue",
"details": err.Error(),
})
utils.ServerErr(c, "Erreur récupération de la queue", err)
return
}
@@ -640,10 +617,7 @@ func GetAvailableDeliveryPersonsRealtime(c *gin.Context) {
livreurs, err := database.GetAvailableDeliveryPersonsRedis()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur lors de la récupération des livreurs",
"details": err.Error(),
})
utils.ServerErr(c, "Erreur lors de la récupération des livreurs", err)
return
}
@@ -687,10 +661,7 @@ func SetCommandETAHandler(c *gin.Context) {
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Données invalides",
"details": err.Error(),
})
utils.BindErr(c, err)
return
}
@@ -724,10 +695,7 @@ func SetCommandETAHandler(c *gin.Context) {
// Mettre à jour l'ETA dans Redis
err = database.SetCommandETA(commandID, req.ETAMinutes)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur lors de la mise à jour de l'ETA",
"details": err.Error(),
})
utils.ServerErr(c, "Erreur lors de la mise à jour de l'ETA", err)
return
}
@@ -830,10 +798,7 @@ func GetMyPenalties(c *gin.Context) {
// ✅ UTILISE LA MÉTHODE DÉDIÉE GetClientPenaltiesInfo
penaltiesInfo, err := database.GetClientPenaltiesInfo(usernameStr)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur lors de la récupération des pénalités",
"details": err.Error(),
})
utils.ServerErr(c, "Erreur lors de la récupération des pénalités", err)
return
}
@@ -869,10 +834,7 @@ func GetClientPenaltiesAdmin(c *gin.Context) {
// ✅ UTILISE GetClientPenaltiesInfo
penaltiesInfo, err := database.GetClientPenaltiesInfo(username)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur lors de la récupération des pénalités",
"details": err.Error(),
})
utils.ServerErr(c, "Erreur lors de la récupération des pénalités", err)
return
}
@@ -896,10 +858,7 @@ func GetAllClientsWithPenalties(c *gin.Context) {
// ✅ UTILISE GetAllClientsWithPenalties
clients, err := database.GetAllClientsWithPenalties()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur lors de la récupération des clients",
"details": err.Error(),
})
utils.ServerErr(c, "Erreur lors de la récupération des clients", err)
return
}
@@ -926,10 +885,7 @@ func GetPenaltiesStats(c *gin.Context) {
// ✅ UTILISE GetClientPenaltiesStats
stats, err := database.GetClientPenaltiesStats()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur lors de la récupération des statistiques",
"details": err.Error(),
})
utils.ServerErr(c, "Erreur lors de la récupération des statistiques", err)
return
}
@@ -970,10 +926,7 @@ func ResetClientPointAdmin(c *gin.Context) {
err := database.ResetClientPoint(username, req.Pool, extraPoolKey)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur lors de la réinitialisation",
"details": err.Error(),
})
utils.ServerErr(c, "Erreur lors de la réinitialisation", err)
return
}
@@ -1008,10 +961,7 @@ func ResetClientPenaltiesAdmin(c *gin.Context) {
// ✅ UTILISE ResetClientPenalties
err := database.ResetClientPenalties(username, req.ResetCancellationsCount)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur lors de la réinitialisation",
"details": err.Error(),
})
utils.ServerErr(c, "Erreur lors de la réinitialisation", err)
return
}
+4 -4
View File
@@ -2,6 +2,7 @@ package handlers
import (
"gestion/db"
"gestion/utils"
"log"
"net/http"
@@ -26,7 +27,7 @@ func GetMyReferralBalance(c *gin.Context) {
balance, err := database.GetClientReferralBalance(username.(string))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
c.JSON(http.StatusNotFound, gin.H{"error": "Solde de parrainage introuvable"})
return
}
@@ -47,8 +48,7 @@ func CreditClientReferralAdmin(c *gin.Context) {
}
if err := database.CreditClientReferral(targetUsername, req.Amount); err != nil {
log.Printf("❌ [REFERRAL] Crédit échoué pour %s: %v", targetUsername, err)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
utils.ServerErr(c, "Impossible de créditer le solde", err)
return
}
@@ -68,7 +68,7 @@ func GetClientReferralAdmin(c *gin.Context) {
balance, err := database.GetClientReferralBalance(targetUsername)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
c.JSON(http.StatusNotFound, gin.H{"error": "Client introuvable"})
return
}
+1
View File
@@ -38,6 +38,7 @@ func GetPublicSettings(c *gin.Context) {
"pool_names": poolNames,
"pool_keys": poolKeys,
"referral_enabled": settings.ReferralEnabled,
"referral_amount": settings.ReferralAmount,
"delivery_schedule": settings.DeliverySchedule,
"crypto_payment_enabled": settings.CryptoPaymentEnabled,
"crypto_only": settings.CryptoOnly,
@@ -34,7 +34,6 @@ func UpdateMyProfile(c *gin.Context) {
log.Printf("❌ [UPDATE_MY_PROFILE] Erreur binding: %v", err)
c.JSON(http.StatusBadRequest, gin.H{
"error": "Données invalides",
"details": err.Error(),
})
return
}
@@ -186,7 +185,6 @@ func UpdateClientByAdmin(c *gin.Context) {
log.Printf("❌ [UPDATE_CLIENT_ADMIN] Erreur binding: %v", err)
c.JSON(http.StatusBadRequest, gin.H{
"error": "Données invalides",
"details": err.Error(),
})
return
}
@@ -9,6 +9,7 @@ import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
@@ -56,7 +57,6 @@ func ValidateDeliveryByLivreur(c *gin.Context) {
log.Printf("❌ [VALIDATE_LIVREUR] Erreur JSON: %v", err)
c.JSON(http.StatusBadRequest, gin.H{
"error": "Coordonnées GPS requises",
"details": err.Error(),
})
return
}
@@ -99,7 +99,6 @@ func ValidateDeliveryByLivreur(c *gin.Context) {
log.Printf("❌ [VALIDATE_LIVREUR] Erreur update statut: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur validation",
"details": err.Error(),
})
return
}
@@ -299,7 +298,6 @@ func StartDelivery(c *gin.Context) {
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Coordonnées GPS requises",
"details": err.Error(),
})
return
}
@@ -336,7 +334,6 @@ func StartDelivery(c *gin.Context) {
if err := database.UpdateCommandStatus(commandID, "en_route"); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur mise à jour statut",
"details": err.Error(),
})
return
}
@@ -354,7 +351,7 @@ func StartDelivery(c *gin.Context) {
// Notifier le client
if clientUsername, _ := command["username"].(string); clientUsername != "" {
msg := fmt.Sprintf("🛵 Votre commande #%d est en route !", commandID)
msg := fmt.Sprintf("Un livreur vient de prendre en charge ta commande #%d, il est en route.", database.GetClientOrderID(commandID))
database.NotifyClient(clientUsername, commandID, "en_route", msg)
}
@@ -47,11 +47,6 @@ type AdminClaims struct {
var (
userJWTSecret = []byte(os.Getenv("USER_JWT_SECRET")) // ✅ Pour clients
adminJWTSecret = []byte(os.Getenv("ADMIN_JWT_SECRET")) // ✅ Pour admin/cabine/livreur
roleHierarchy = map[string][]string{
"admin": {"admin", "cabine", "livreur", "client"},
"cabine": {"cabine", "livreur", "client"},
"livreur": {"livreur", "client"},
}
)
// ============================================
@@ -68,7 +63,7 @@ func validateClientToken(tokenString string, database *db.Database) (*ClientClai
log.Printf("🔍 [VALIDATE-CLIENT] Validating client token...")
// Parser JWT EN PREMIER avec userJWTSecret (CLIENT)
token, err := jwt.ParseWithClaims(tokenString, &ClientClaims{}, func(token *jwt.Token) (interface{}, error) {
token, err := jwt.ParseWithClaims(tokenString, &ClientClaims{}, func(token *jwt.Token) (any, error) {
// Vérifier explicitement l'algorithme
if token.Method.Alg() != jwt.SigningMethodHS256.Alg() {
return nil, fmt.Errorf("unexpected signing algorithm: %v", token.Method.Alg())
@@ -116,8 +111,7 @@ func validateAdminToken(tokenString string, database *db.Database) (*AdminClaims
log.Printf("🔍 [VALIDATE-ADMIN] Validating admin token...")
// Parser JWT EN PREMIER avec adminJWTSecret (ADMIN/CABINE/LIVREUR)
token, err := jwt.ParseWithClaims(tokenString, &AdminClaims{}, func(token *jwt.Token) (interface{}, error) {
token, err := jwt.ParseWithClaims(tokenString, &AdminClaims{}, func(token *jwt.Token) (any, error) {
if token.Method.Alg() != jwt.SigningMethodHS256.Alg() {
return nil, fmt.Errorf("unexpected signing algorithm: %v", token.Method.Alg())
}
@@ -155,11 +149,6 @@ func validateAdminToken(tokenString string, database *db.Database) (*AdminClaims
return claims, nil
}
// ============================================
// MIDDLEWARE AUTHENTIFICATION CLIENT
// ============================================
// ClientMiddleware valide le JWT d'un client
func ClientMiddleware(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
@@ -180,7 +169,6 @@ func ClientMiddleware(c *gin.Context) {
return
}
// ✅ NOUVEAU : Vérifier que le token n'a pas été révoqué
valid, err := database.IsTokenValid(tokenStr)
if err != nil {
log.Printf("❌ [CLIENT-MWARE] Erreur vérification token DB: %v", err)
@@ -195,7 +183,6 @@ func ClientMiddleware(c *gin.Context) {
return
}
// Stocker les infos du client dans le contexte
c.Set("client_id", claims.ClientID)
c.Set("username", claims.Username)
c.Set("role", claims.Role)
@@ -206,12 +193,6 @@ func ClientMiddleware(c *gin.Context) {
c.Next()
}
// ============================================
// MIDDLEWARE AUTHENTIFICATION ADMIN
// ============================================
// AdminMiddleware valide le JWT d'un admin (role == "admin" SEULEMENT)
// ✅ Remplace le AdminMiddleware de handlers/auth.go
func AdminMiddleware(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
@@ -232,7 +213,6 @@ func AdminMiddleware(c *gin.Context) {
return
}
// ✅ Check révocation
valid, err := database.IsTokenValid(tokenStr)
if err != nil {
log.Printf("❌ [ADMIN-MWARE] Erreur vérification token DB: %v", err)
@@ -247,23 +227,14 @@ func AdminMiddleware(c *gin.Context) {
return
}
// Vérifier rôle
validRoles := []string{"admin", "cabine", "livreur"}
isValid := false
for _, role := range validRoles {
if claims.Role == role {
isValid = true
break
}
}
if !isValid {
log.Printf("❌ [ADMIN-MWARE] Role invalide: %s", claims.Role)
// Vérifier rôle — admin uniquement
if claims.Role != "admin" {
log.Printf("❌ [ADMIN-MWARE] Role invalide: %s (admin requis)", claims.Role)
c.JSON(http.StatusForbidden, gin.H{"error": "Droits insuffisants - Accès admin requis"})
c.Abort()
return
}
// Stocker les infos de l'admin dans le contexte
c.Set("user_id", claims.UserID)
c.Set("username", claims.Username)
c.Set("role", claims.Role)
@@ -275,12 +246,6 @@ func AdminMiddleware(c *gin.Context) {
c.Next()
}
// ============================================
// MIDDLEWARE AUTHENTIFICATION CABINE
// ============================================
// CabineMiddleware valide que l'utilisateur a accès à la cabine
// ✅ Remplace le CabineMiddleware de handlers/auth.go
func CabineMiddleware(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
@@ -316,22 +281,10 @@ func CabineMiddleware(c *gin.Context) {
return
}
// Vérifier hiérarchie des rôles
allowedRoles := roleHierarchy[claims.Role]
authorized := false
for _, r := range allowedRoles {
if r == "cabine" {
authorized = true
break
}
}
if !authorized {
// Vérifier rôle — admin ou cabine uniquement
if claims.Role != "admin" && claims.Role != "cabine" {
log.Printf("❌ [CABINE-MWARE] Role non autorisé: %s", claims.Role)
c.JSON(http.StatusForbidden, gin.H{
"error": "Droits insuffisants - Accès cabine requis",
"your_role": claims.Role,
"allowed_roles": "admin, cabine",
})
c.JSON(http.StatusForbidden, gin.H{"error": "Droits insuffisants - Accès cabine requis"})
c.Abort()
return
}
@@ -347,12 +300,6 @@ func CabineMiddleware(c *gin.Context) {
c.Next()
}
// ============================================
// MIDDLEWARE AUTHENTIFICATION LIVREUR
// ============================================
// LivreurMiddleware valide que l'utilisateur est livreur
// ✅ Remplace le LivreurMiddleware de handlers/auth.go
func LivreurMiddleware(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
@@ -388,22 +335,10 @@ func LivreurMiddleware(c *gin.Context) {
return
}
// Vérifier hiérarchie des rôles
allowedRoles := roleHierarchy[claims.Role]
authorized := false
for _, r := range allowedRoles {
if r == "livreur" {
authorized = true
break
}
}
if !authorized {
// Vérifier rôle — admin ou livreur uniquement
if claims.Role != "admin" && claims.Role != "livreur" {
log.Printf("❌ [LIVREUR-MWARE] Role non autorisé: %s", claims.Role)
c.JSON(http.StatusForbidden, gin.H{
"error": "Droits insuffisants - Accès livreur requis",
"your_role": claims.Role,
"allowed_roles": "admin, livreur",
})
c.JSON(http.StatusForbidden, gin.H{"error": "Droits insuffisants - Accès livreur requis"})
c.Abort()
return
}
@@ -419,11 +354,6 @@ func LivreurMiddleware(c *gin.Context) {
c.Next()
}
// ============================================
// SESSION MIDDLEWARE CLIENT (Existant)
// ============================================
// ClientSessionMiddleware valide la session Redis du client
func ClientSessionMiddleware(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -507,17 +437,10 @@ func ClientSessionMiddleware(c *gin.Context) {
c.Next()
}
// ============================================
// RATE LIMITING MIDDLEWARE
// ============================================
// RateLimitMiddleware limite le nombre de requêtes par client
// Config: 100 requêtes par minute par client
func RateLimitMiddleware(c *gin.Context) {
// Récupérer le client_id
clientID, ok := c.Get("client_id")
if !ok {
// Pas de client_id (requête publique), pas de rate limit
c.Next()
return
}
@@ -533,12 +456,10 @@ func RateLimitMiddleware(c *gin.Context) {
return
}
// Initialiser le TTL à la première requête
if count == 1 {
db.Redis.Expire(db.RedisCtx, rateLimitKey, 60*time.Second) // 1 minute
}
// Vérifier si dépassement (100 requêtes/min)
if count > 100 {
log.Printf("❌ [RATELIMIT] Client %d dépassé le limite: %d requêtes/min", clientIDInt, count)
c.JSON(http.StatusTooManyRequests, gin.H{
@@ -548,7 +469,6 @@ func RateLimitMiddleware(c *gin.Context) {
return
}
// Ajouter le header du remaining
c.Header("X-RateLimit-Remaining", strconv.FormatInt(100-count, 10))
log.Printf("📊 [RATELIMIT] Client %d: %d/%d requêtes", clientIDInt, count, 100)
@@ -556,6 +476,40 @@ func RateLimitMiddleware(c *gin.Context) {
c.Next()
}
// LoginRateLimitMiddleware limite les tentatives de connexion par IP.
// Config: 10 tentatives par 15 minutes.
func LoginRateLimitMiddleware(c *gin.Context) {
ip := c.GetHeader("X-Real-IP")
if ip == "" {
ip = c.ClientIP()
}
rateLimitKey := "ratelimit:login:" + ip
count, err := db.Redis.Incr(db.RedisCtx, rateLimitKey).Result()
if err != nil {
log.Printf("⚠️ [LOGIN-RATELIMIT] Erreur Redis: %v", err)
c.Next()
return
}
if count == 1 {
db.Redis.Expire(db.RedisCtx, rateLimitKey, 15*time.Minute)
}
if count > 10 {
log.Printf("❌ [LOGIN-RATELIMIT] IP %s bloquée: %d tentatives/15min", ip, count)
c.JSON(http.StatusTooManyRequests, gin.H{
"error": "Trop de tentatives de connexion - Réessayez dans 15 minutes",
})
c.Abort()
return
}
c.Header("X-RateLimit-Remaining", strconv.FormatInt(10-count, 10))
c.Next()
}
// ============================================
// HELPER MIDDLEWARE
// ============================================
@@ -640,11 +594,6 @@ func LoadClientContext(c *gin.Context, database *db.Database) (*db.SessionData,
return session, nil
}
// ============================================
// DATABASE MIDDLEWARE
// ============================================
// DatabaseMiddleware injecte la base de données dans le contexte
func DatabaseMiddleware(db *db.Database) gin.HandlerFunc {
return func(c *gin.Context) {
c.Set("database", db)
+11 -8
View File
@@ -5,19 +5,22 @@ import "time"
type Client struct {
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
Username string `gorm:"column:username" json:"username"`
Username string `gorm:"column:username;not null" 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"`
Telephone string `gorm:"column:telephone;not null" json:"telephone"`
Command int `gorm:"column:command;default:0" json:"command"`
CancelCommande int `gorm:"column:cancel_commande;default:0" json:"cancel_commande"`
Amende float64 `gorm:"column:amende;default:0" json:"amende"`
CancellationsCount int `gorm:"column:cancellations_count;not null;default:0" 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"`
MustChangePassword bool `gorm:"column:must_change_password;default:false" json:"must_change_password"`
ReferralBalance float64 `gorm:"column:referral_balance;default:0" json:"referral_balance"`
// points_extra est un JSONB géré manuellement (type non supporté nativement par GORM)
PointsExtra map[string]int `gorm:"-" json:"points_extra"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
func (Client) TableName() string { return "clients" }
+31 -16
View File
@@ -3,28 +3,43 @@ package models
import "time"
type Command struct {
ID int `json:"id"`
UserID int `json:"user_id"`
Username string `json:"username"`
Status string `json:"status"` // "pending", "assigned", "livre", "approved", "cancelled", "disabled"
Total float64 `json:"total"`
DeliveryAddress string `json:"delivery_address"` // Adresse de livraison pour cette commande
LivreurAssign string `json:"livreur_assign,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
ClientOrderID int `gorm:"column:client_order_id" json:"client_order_id"`
UserID int `gorm:"column:user_id" json:"user_id"`
Username string `gorm:"column:username" json:"username"`
Status string `gorm:"column:status" json:"status"`
Total float64 `gorm:"column:total_prix" json:"total"`
DeliveryAddress string `gorm:"column:adresse" json:"delivery_address"`
LivreurAssign string `gorm:"column:livreur_assign" json:"livreur_assign,omitempty"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
func (Command) TableName() string { return "commandes" }
// CommandItem représente un produit dans une commande
type CommandItem struct {
ID int `json:"id"`
CommandID int `json:"command_id"`
ProductID int `json:"product_id"`
Quantity int `json:"quantity"`
Price float64 `json:"price"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
CommandID int `gorm:"column:command_id" json:"command_id"`
Produit string `gorm:"column:produit" json:"produit"`
ProductID int `gorm:"column:product_id" json:"product_id"`
Quantity float64 `gorm:"column:quantite" json:"quantity"`
Price float64 `gorm:"column:prix" json:"price"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
type CommandLog struct {
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
CommandID int `gorm:"column:command_id" json:"command_id"`
Status string `gorm:"column:status" json:"status"`
Message string `gorm:"column:message" json:"message"`
Author string `gorm:"column:author" json:"author"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
}
func (CommandLog) TableName() string { return "command_logs" }
type CommandPriority struct {
ID int `json:"id"`
Username string `json:"username"`
+1 -1
View File
@@ -6,7 +6,7 @@ import "time"
type CryptoPayment struct {
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"`
NowPaymentID string `json:"nowpayment_id" gorm:"column:nowpayment_id;not null"`
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"`
+1
View File
@@ -67,6 +67,7 @@ type AppSettings struct {
PointsEnabled bool `json:"points_enabled"` // afficher/activer le système de points
PointsPools []PointsPool `json:"points_pools"` // types de points personnalisés
ReferralEnabled bool `json:"referral_enabled"` // activer/désactiver le système de parrainage
ReferralAmount float64 `json:"referral_amount"` // montant crédité par parrainage
CryptoPaymentEnabled bool `json:"crypto_payment_enabled"` // activer/désactiver le paiement crypto
CryptoOnly bool `json:"crypto_only"` // forcer le paiement crypto uniquement (pas d'espèces)
NowPaymentsAPIKey string `json:"nowpayments_api_key"` // clé API NowPayments
+3 -1
View File
@@ -4,9 +4,11 @@ import "time"
type User struct {
ID int `json:"id" gorm:"primaryKey;autoIncrement"`
Username string `json:"username" gorm:"column:username;uniqueIndex"`
Username string `json:"username" gorm:"column:username;not null"`
Password string `json:"password,omitempty" gorm:"column:password"`
Role string `json:"role" gorm:"column:role"`
Total float64 `json:"total" gorm:"column:total;default:0"`
Livraison float64 `json:"livraison" gorm:"column:livraison;default:0"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
}
+8 -16
View File
@@ -32,7 +32,7 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
// ============================================
authGroupV1 := router.Group("/api/v1/auth")
{
authGroupV1.POST("/login", handlers.LoginClient)
authGroupV1.POST("/login", middleware.LoginRateLimitMiddleware, handlers.LoginClient)
authGroupV1.POST("/logout", handlers.LogoutClient)
}
@@ -61,21 +61,22 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
cartGroupV1 := router.Group("/api/v1")
cartGroupV1.Use(middleware.ClientMiddleware)
cartGroupV1.Use(middleware.ClientSessionMiddleware)
cartGroupV1.Use(middleware.RateLimitMiddleware)
{
// Panier
cartGroupV1.POST("/panier/add", handlers.AddProductsBasket)
cartGroupV1.GET("/panier/:username", handlers.GetAllBaskets)
cartGroupV1.DELETE("/panier/remove", handlers.DeleteProductFromBasket)
cartGroupV1.DELETE("/panier/clear", handlers.ClearBasket) // ✅ CORRIGÉ - Sans :username
cartGroupV1.DELETE("/panier/clear", handlers.ClearBasket)
// Commandes
cartGroupV1.POST("/checkout", middleware.OrderHoursMiddleware, middleware.BlockClientIfPenalty, handlers.ValidateBasket) // ✅ Auto-assign GPS
cartGroupV1.GET("/my-commands", handlers.GetMyCommandsWithTracking) // ✅ Avec suivi
// ⭐ NOUVEAUX - SUIVI CLIENT TEMPS RÉEL
cartGroupV1.GET("/commands/:id/eta", handlers.GetOrderETA) // ✅ AJOUTÉ
cartGroupV1.GET("/commands/:id/status", handlers.GetCommandStatus) // ✅ AJOUTÉ
cartGroupV1.GET("/commands/:id/tracking", handlers.GetCommandTracking) // ✅ AJOUTÉ
cartGroupV1.GET("/commands/:id/eta", handlers.GetOrderETA)
cartGroupV1.GET("/commands/:id/status", handlers.GetCommandStatus)
cartGroupV1.GET("/commands/:id/tracking", handlers.GetCommandTracking)
cartGroupV1.GET("/commands/:id", handlers.GetCommandByID)
cartGroupV1.GET("/commands/:id/items", handlers.GetCommandItemsWithDetails)
// Approbation livraison
@@ -123,15 +124,6 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
// ============================================
router.POST("/webhook/telegram", handlers.TelegramWebhook)
// ============================================
// 🌍 GÉOCODAGE PUBLIC (v1)
// ============================================
geoGroupV1 := router.Group("/api/v1")
{
geoGroupV1.POST("/geocode", handlers.GeocodeAddress)
geoGroupV1.POST("/validate-address", handlers.ValidateAddress)
}
// ============================================
// 📋 PATTERN v2: ADMIN API
// ============================================
@@ -141,8 +133,8 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
// ============================================
adminAuthGroupV2 := router.Group("/api/v2/admin/auth")
{
adminAuthGroupV2.POST("/register", handlers.RegisterAdmin)
adminAuthGroupV2.POST("/login", handlers.LoginAdmin)
//adminAuthGroupV2.POST("/register", handlers.RegisterAdmin)
adminAuthGroupV2.POST("/login", middleware.LoginRateLimitMiddleware, handlers.LoginAdmin)
adminAuthGroupV2.POST("/logout", handlers.LogoutAdmin)
}
+20
View File
@@ -0,0 +1,20 @@
package utils
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
)
// BindErr log l'erreur de binding et renvoie 400 sans détails internes.
func BindErr(c *gin.Context, err error) {
log.Printf("⚠️ [BIND] %s %s: %v", c.Request.Method, c.Request.URL.Path, err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"})
}
// ServerErr log l'erreur interne et renvoie 500 avec un message générique.
func ServerErr(c *gin.Context, msg string, err error) {
log.Printf("❌ [SERVER] %s %s — %s: %v", c.Request.Method, c.Request.URL.Path, msg, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": msg})
}
+2 -1
View File
@@ -189,7 +189,8 @@ func tryAssignCommandWithPriority(
// 9. Notifier le client
if clientUsername != "" {
clientMsg := fmt.Sprintf("Votre commande #%d est confirmée ! Un livreur est en route.", commandID)
clientOrderID := database.GetClientOrderID(commandID)
clientMsg := fmt.Sprintf("Ta commande #%d est prise en compte ! Merci de rester branché et vigilant sur les notifs à venir.", clientOrderID)
database.NotifyClient(clientUsername, commandID, "assigned", clientMsg)
}
@@ -240,7 +240,7 @@ function ProductDetail() {
<div className="product-detail-content">
<div className={`product-image-section ${isOutOfStock ? "out-of-stock" : ""}`}>
<img
src={product.image || ""}
src={product.media?.find(m => m.type === "image")?.url || ""}
alt={product.name}
className="product-detail-image"
/>