From 2e9827af98acd2811074d1009171a087e9055c12 Mon Sep 17 00:00:00 2001 From: Xor290 Date: Mon, 20 Apr 2026 19:41:52 +0200 Subject: [PATCH] chore: refacto --- backend/gestion/db/db_cancel_command.go | 19 +- backend/gestion/db/db_categories.go | 2 +- backend/gestion/db/db_clients.go | 79 ++++--- backend/gestion/db/db_command_items.go | 9 +- backend/gestion/db/db_commands.go | 216 +++++++++--------- backend/gestion/db/db_init.go | 22 +- backend/gestion/db/db_notifications.go | 20 +- backend/gestion/db/db_referral.go | 5 +- backend/gestion/db/db_sanction_client.go | 18 +- backend/gestion/db/db_settings.go | 9 +- backend/gestion/db/db_telegram.go | 18 +- backend/gestion/db/redis_eta_management.go | 2 +- backend/gestion/db/redis_helpers.go | 2 +- backend/gestion/db/redis_queue_assignment.go | 3 - backend/gestion/db/redis_queue_clean_up.go | 8 +- backend/gestion/db/redis_queue_management.go | 4 - backend/gestion/go.mod | 4 +- backend/gestion/handlers/address.go | 11 +- backend/gestion/handlers/alert.go | 29 ++- backend/gestion/handlers/auth.go | 3 +- backend/gestion/handlers/cabine.go | 12 +- backend/gestion/handlers/cancel_command.go | 15 +- backend/gestion/handlers/categories.go | 6 +- backend/gestion/handlers/client_tracking.go | 3 +- backend/gestion/handlers/commands.go | 48 ++-- backend/gestion/handlers/deleviry.go | 13 +- backend/gestion/handlers/delivery_admin.go | 7 - backend/gestion/handlers/geoloca.go | 100 ++------ backend/gestion/handlers/gps.go | 3 +- backend/gestion/handlers/history.go | 3 +- backend/gestion/handlers/panier.go | 61 ++--- backend/gestion/handlers/redis_services.go | 86 ++----- backend/gestion/handlers/referral.go | 8 +- backend/gestion/handlers/settings.go | 1 + backend/gestion/handlers/update_profile.go | 2 - .../gestion/handlers/validation_deleviry.go | 7 +- .../gestion/middleware/session_middleware.go | 141 ++++-------- backend/gestion/models/client.go | 31 +-- backend/gestion/models/commands.go | 47 ++-- backend/gestion/models/payment.go | 2 +- backend/gestion/models/settings.go | 1 + backend/gestion/models/user.go | 12 +- backend/gestion/routes/routes.go | 28 +-- backend/gestion/utils/errors.go | 20 ++ backend/gestion/workers/cron_auto_assign.go | 3 +- .../src/pages/User/ProductDetail.tsx | 2 +- 46 files changed, 497 insertions(+), 648 deletions(-) create mode 100644 backend/gestion/utils/errors.go diff --git a/backend/gestion/db/db_cancel_command.go b/backend/gestion/db/db_cancel_command.go index 9c6b459c..5736ed4b 100644 --- a/backend/gestion/db/db_cancel_command.go +++ b/backend/gestion/db/db_cancel_command.go @@ -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'` diff --git a/backend/gestion/db/db_categories.go b/backend/gestion/db/db_categories.go index 3e412f58..427b989f 100644 --- a/backend/gestion/db/db_categories.go +++ b/backend/gestion/db/db_categories.go @@ -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 diff --git a/backend/gestion/db/db_clients.go b/backend/gestion/db/db_clients.go index 96da7667..e7a6702a 100644 --- a/backend/gestion/db/db_clients.go +++ b/backend/gestion/db/db_clients.go @@ -79,20 +79,22 @@ func (d *Database) GetClientByID(id int) (*models.Client, error) { // GetAllClients récupère tous les clients func (d *Database) GetAllClients() ([]*models.Client, error) { var rows []struct { - ID int `gorm:"column:id"` - Username string `gorm:"column:username"` - Password string `gorm:"column:password"` - Nom string `gorm:"column:nom"` - Prenom string `gorm:"column:prenom"` - Telephone string `gorm:"column:telephone"` - Command int `gorm:"column:command"` - Amende float64 `gorm:"column:amende"` - ReferralBalance float64 `gorm:"column:referral_balance"` - PointsExtraJSON []byte `gorm:"column:points_extra"` - CreatedAt time.Time `gorm:"column:created_at"` + ID int `gorm:"column:id"` + Username string `gorm:"column:username"` + Password string `gorm:"column:password"` + Nom string `gorm:"column:nom"` + Prenom string `gorm:"column:prenom"` + Telephone string `gorm:"column:telephone"` + Command int `gorm:"column:command"` + Amende float64 `gorm:"column:amende"` + ReferralBalance float64 `gorm:"column:referral_balance"` + 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 { @@ -102,16 +104,17 @@ func (d *Database) GetAllClients() ([]*models.Client, error) { clients := make([]*models.Client, 0, len(rows)) for _, row := range rows { client := &models.Client{ - ID: row.ID, - Username: row.Username, - Password: row.Password, - Nom: row.Nom, - Prenom: row.Prenom, - Telephone: row.Telephone, - Command: row.Command, - Amende: row.Amende, - ReferralBalance: row.ReferralBalance, - CreatedAt: row.CreatedAt, + ID: row.ID, + Username: row.Username, + Password: row.Password, + Nom: row.Nom, + Prenom: row.Prenom, + Telephone: row.Telephone, + Command: row.Command, + Amende: row.Amende, + ReferralBalance: row.ReferralBalance, + CancellationsCount: row.CancellationsCount, + CreatedAt: row.CreatedAt, } client.PointsExtra = map[string]int{} if len(row.PointsExtraJSON) > 0 { @@ -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) } diff --git a/backend/gestion/db/db_command_items.go b/backend/gestion/db/db_command_items.go index 8052636f..512f5e7d 100644 --- a/backend/gestion/db/db_command_items.go +++ b/backend/gestion/db/db_command_items.go @@ -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 @@ -280,8 +282,9 @@ func (d *Database) GetCommandItems(commandID int) ([]map[string]interface{}, err "total_prix": row.TotalPrix, "referral_used": row.ReferralUsed, "livreur_assign": ptrStr(row.LivreurAssign), - "command_created_at": commandCreatedAt, - "category": row.Category, + "command_created_at": commandCreatedAt, + "category": row.Category, + "client_order_number": row.ClientOrderNumber, } items = append(items, item) } diff --git a/backend/gestion/db/db_commands.go b/backend/gestion/db/db_commands.go index 4ad81f39..b1a6a336 100644 --- a/backend/gestion/db/db_commands.go +++ b/backend/gestion/db/db_commands.go @@ -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) @@ -102,14 +99,15 @@ func (d *Database) CreateCommand(username string) (*models.Command, error) { } var cmdResult struct { - ID int `gorm:"column:id"` - CreatedAt time.Time `gorm:"column:created_at"` - UpdatedAt time.Time `gorm:"column:updated_at"` + 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 { @@ -136,9 +139,10 @@ func (d *Database) CreateCommand(username string) (*models.Command, error) { } command := &models.Command{ - ID: commandID, - Status: "pending", - Total: totalPrix, + ID: commandID, + ClientOrderID: cmdResult.ClientOrderID, + Status: "pending", + Total: totalPrix, } return command, nil @@ -188,14 +192,15 @@ func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (* } var cmdResult struct { - ID int `gorm:"column:id"` - CreatedAt time.Time `gorm:"column:created_at"` - UpdatedAt time.Time `gorm:"column:updated_at"` + 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,44 +278,37 @@ 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` + var rows []struct { + ID int `gorm:"column:id"` + Username string `gorm:"column:username"` + Status string `gorm:"column:status"` + Adresse string `gorm:"column:adresse"` + TotalPrix float64 `gorm:"column:total_prix"` + LivreurAssign *string `gorm:"column:livreur_assign"` + CreatedAt time.Time `gorm:"column:created_at"` + UpdatedAt time.Time `gorm:"column:updated_at"` + ProposedAddress *string `gorm:"column:proposed_address"` + AddressProposalStatus string `gorm:"column:address_proposal_status"` + ClientOrderNumber int `gorm:"column:client_order_number"` + } - args := []interface{}{} + 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 == "" { - query += ` AND c.status IN ('pending', 'assigned', 'en_route', 'arrived', 'livre')` + gdb = gdb.Where("c.status IN ?", []string{"pending", "assigned", "en_route", "arrived", "livre"}) } else { - query += ` AND c.status = ?` - args = append(args, status) + gdb = gdb.Where("c.status = ?", status) } if username != "" { - query += ` AND c.username = ?` - args = append(args, username) + gdb = gdb.Where("c.username = ?", username) } - query += " ORDER BY c.created_at DESC LIMIT 1000" - - var rows []struct { - ID int `gorm:"column:id"` - Username string `gorm:"column:username"` - Status string `gorm:"column:status"` - Adresse string `gorm:"column:adresse"` - TotalPrix float64 `gorm:"column:total_prix"` - LivreurAssign *string `gorm:"column:livreur_assign"` - CreatedAt time.Time `gorm:"column:created_at"` - UpdatedAt time.Time `gorm:"column:updated_at"` - ProposedAddress *string `gorm:"column:proposed_address"` - AddressProposalStatus string `gorm:"column:address_proposal_status"` - ClientOrderNumber int `gorm:"column:client_order_number"` - } - - if err := d.GDB.Raw(query, args...).Scan(&rows).Error; err != nil { + 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 { @@ -362,26 +358,28 @@ func (d *Database) SetCommandReferralUsed(commandID int, amount float64) error { func (d *Database) GetCommandByID(id int) (map[string]any, error) { var row struct { - ID int `gorm:"column:id"` - Username string `gorm:"column:username"` - Status string `gorm:"column:status"` - Adresse string `gorm:"column:adresse"` - TotalPrix float64 `gorm:"column:total_prix"` - LivreurAssign *string `gorm:"column:livreur_assign"` - CreatedAt time.Time `gorm:"column:created_at"` - UpdatedAt time.Time `gorm:"column:updated_at"` - ProposedAddress *string `gorm:"column:proposed_address"` - AddressProposalStatus string `gorm:"column:address_proposal_status"` - ReferralUsed float64 `gorm:"column:referral_used"` - ClientOrderNumber int `gorm:"column:client_order_number"` + ID int `gorm:"column:id"` + Username string `gorm:"column:username"` + Status string `gorm:"column:status"` + Adresse string `gorm:"column:adresse"` + TotalPrix float64 `gorm:"column:total_prix"` + LivreurAssign *string `gorm:"column:livreur_assign"` + CreatedAt time.Time `gorm:"column:created_at"` + UpdatedAt time.Time `gorm:"column:updated_at"` + ProposedAddress *string `gorm:"column:proposed_address"` + AddressProposalStatus string `gorm:"column:address_proposal_status"` + ReferralUsed float64 `gorm:"column:referral_used"` + ClientOrderNumber int `gorm:"column:client_order_number"` + 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 } diff --git a/backend/gestion/db/db_init.go b/backend/gestion/db/db_init.go index 9468c5c6..6b98ac48 100644 --- a/backend/gestion/db/db_init.go +++ b/backend/gestion/db/db_init.go @@ -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);`, diff --git a/backend/gestion/db/db_notifications.go b/backend/gestion/db/db_notifications.go index 1b282b93..05165ab7 100644 --- a/backend/gestion/db/db_notifications.go +++ b/backend/gestion/db/db_notifications.go @@ -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 -} diff --git a/backend/gestion/db/db_referral.go b/backend/gestion/db/db_referral.go index a17c8eb7..e34c250a 100644 --- a/backend/gestion/db/db_referral.go +++ b/backend/gestion/db/db_referral.go @@ -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 } diff --git a/backend/gestion/db/db_sanction_client.go b/backend/gestion/db/db_sanction_client.go index cf307deb..d7cd437d 100644 --- a/backend/gestion/db/db_sanction_client.go +++ b/backend/gestion/db/db_sanction_client.go @@ -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) diff --git a/backend/gestion/db/db_settings.go b/backend/gestion/db/db_settings.go index ed0507b3..41fdaaa1 100644 --- a/backend/gestion/db/db_settings.go +++ b/backend/gestion/db/db_settings.go @@ -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}, diff --git a/backend/gestion/db/db_telegram.go b/backend/gestion/db/db_telegram.go index b4fc9206..c8ecef2b 100644 --- a/backend/gestion/db/db_telegram.go +++ b/backend/gestion/db/db_telegram.go @@ -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 } diff --git a/backend/gestion/db/redis_eta_management.go b/backend/gestion/db/redis_eta_management.go index 187d7a51..6569d2ea 100644 --- a/backend/gestion/db/redis_eta_management.go +++ b/backend/gestion/db/redis_eta_management.go @@ -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) diff --git a/backend/gestion/db/redis_helpers.go b/backend/gestion/db/redis_helpers.go index 31c5bb7e..e2c198aa 100644 --- a/backend/gestion/db/redis_helpers.go +++ b/backend/gestion/db/redis_helpers.go @@ -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 diff --git a/backend/gestion/db/redis_queue_assignment.go b/backend/gestion/db/redis_queue_assignment.go index 4013c126..8ace1e37 100644 --- a/backend/gestion/db/redis_queue_assignment.go +++ b/backend/gestion/db/redis_queue_assignment.go @@ -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) diff --git a/backend/gestion/db/redis_queue_clean_up.go b/backend/gestion/db/redis_queue_clean_up.go index 0a445648..2382858d 100644 --- a/backend/gestion/db/redis_queue_clean_up.go +++ b/backend/gestion/db/redis_queue_clean_up.go @@ -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, diff --git a/backend/gestion/db/redis_queue_management.go b/backend/gestion/db/redis_queue_management.go index 13a5103a..427b1a9b 100644 --- a/backend/gestion/db/redis_queue_management.go +++ b/backend/gestion/db/redis_queue_management.go @@ -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) diff --git a/backend/gestion/go.mod b/backend/gestion/go.mod index d16f4379..3f409a19 100644 --- a/backend/gestion/go.mod +++ b/backend/gestion/go.mod @@ -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 ) diff --git a/backend/gestion/handlers/address.go b/backend/gestion/handlers/address.go index 8222754a..78e4fd65 100644 --- a/backend/gestion/handlers/address.go +++ b/backend/gestion/handlers/address.go @@ -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 } diff --git a/backend/gestion/handlers/alert.go b/backend/gestion/handlers/alert.go index ab9f46ee..b2fd2669 100644 --- a/backend/gestion/handlers/alert.go +++ b/backend/gestion/handlers/alert.go @@ -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 } diff --git a/backend/gestion/handlers/auth.go b/backend/gestion/handlers/auth.go index 0a0e5aa4..c54d7cb8 100644 --- a/backend/gestion/handlers/auth.go +++ b/backend/gestion/handlers/auth.go @@ -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 } diff --git a/backend/gestion/handlers/cabine.go b/backend/gestion/handlers/cabine.go index 5ba313af..dc3b2794 100644 --- a/backend/gestion/handlers/cabine.go +++ b/backend/gestion/handlers/cabine.go @@ -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) } diff --git a/backend/gestion/handlers/cancel_command.go b/backend/gestion/handlers/cancel_command.go index 7cdccda0..5dd4cb02 100644 --- a/backend/gestion/handlers/cancel_command.go +++ b/backend/gestion/handlers/cancel_command.go @@ -339,19 +339,22 @@ func GetAllCancelledOrders(c *gin.Context) { } } + cancelReason, _ := order["cancel_reason"].(string) enrichedOrder := map[string]any{ - "id": order["id"], - "username": order["username"], - "total_prix": order["total_prix"], - "created_at": order["created_at"], - "updated_at": order["updated_at"], - "items_count": len(items), + "id": order["id"], + "username": order["username"], + "total_prix": order["total_prix"], + "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, } } diff --git a/backend/gestion/handlers/categories.go b/backend/gestion/handlers/categories.go index 66cd0e8f..7f3c8801 100644 --- a/backend/gestion/handlers/categories.go +++ b/backend/gestion/handlers/categories.go @@ -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"}) } diff --git a/backend/gestion/handlers/client_tracking.go b/backend/gestion/handlers/client_tracking.go index 2f1ddb14..7bee7851 100644 --- a/backend/gestion/handlers/client_tracking.go +++ b/backend/gestion/handlers/client_tracking.go @@ -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"], diff --git a/backend/gestion/handlers/commands.go b/backend/gestion/handlers/commands.go index bb355292..23927419 100644 --- a/backend/gestion/handlers/commands.go +++ b/backend/gestion/handlers/commands.go @@ -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 } @@ -964,12 +955,13 @@ func GetCommandItemsWithDetails(c *gin.Context) { } commandInfo := map[string]interface{}{ - "id": items[0]["command_id"], - "command_status": items[0]["command_status"], - "command_address": items[0]["command_address"], - "total_prix": items[0]["total_prix"], - "livreur_assign": items[0]["livreur_assign"], - "command_created_at": items[0]["command_created_at"], + "id": items[0]["command_id"], + "command_status": items[0]["command_status"], + "command_address": items[0]["command_address"], + "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 } diff --git a/backend/gestion/handlers/deleviry.go b/backend/gestion/handlers/deleviry.go index fa2f48c9..a3aab941 100644 --- a/backend/gestion/handlers/deleviry.go +++ b/backend/gestion/handlers/deleviry.go @@ -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) diff --git a/backend/gestion/handlers/delivery_admin.go b/backend/gestion/handlers/delivery_admin.go index 4bc3cfe0..d026df9f 100644 --- a/backend/gestion/handlers/delivery_admin.go +++ b/backend/gestion/handlers/delivery_admin.go @@ -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 } diff --git a/backend/gestion/handlers/geoloca.go b/backend/gestion/handlers/geoloca.go index d3278eec..8423bff7 100644 --- a/backend/gestion/handlers/geoloca.go +++ b/backend/gestion/handlers/geoloca.go @@ -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, @@ -76,8 +66,7 @@ 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(), + "error": "Données invalides", }) return } @@ -89,8 +78,7 @@ func FindNearestDeliveryPerson(c *gin.Context) { location, err := geoService.GeocodeAddress(req.Address) if err != nil { c.JSON(http.StatusBadRequest, gin.H{ - "error": "Impossible de géocoder l'adresse", - "details": err.Error(), + "error": "Impossible de géocoder l'adresse", }) return } @@ -110,8 +98,7 @@ func FindNearestDeliveryPerson(c *gin.Context) { // Valider les coordonnées if err := services.ValidateCoordinates(targetCoords); err != nil { c.JSON(http.StatusBadRequest, gin.H{ - "error": "Coordonnées invalides", - "details": err.Error(), + "error": "Coordonnées invalides", }) return } @@ -135,8 +122,7 @@ func FindNearestDeliveryPerson(c *gin.Context) { nearest, err := geoService.FindNearestDeliveryPersonFast(targetCoords, usernames) if err != nil { c.JSON(http.StatusNotFound, gin.H{ - "error": "Aucun livreur avec position GPS valide", - "details": err.Error(), + "error": "Aucun livreur avec position GPS valide", }) return } @@ -193,8 +179,7 @@ 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(), + "error": "Données invalides", }) return } @@ -205,8 +190,7 @@ func GetAllDeliveryDistances(c *gin.Context) { location, err := geoService.GeocodeAddress(req.Address) if err != nil { c.JSON(http.StatusBadRequest, gin.H{ - "error": "Impossible de géocoder l'adresse", - "details": err.Error(), + "error": "Impossible de géocoder l'adresse", }) return } @@ -224,8 +208,7 @@ 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(), + "error": "Coordonnées invalides", }) return } @@ -246,8 +229,7 @@ func GetAllDeliveryDistances(c *gin.Context) { distances, err := geoService.GetAllDeliveryDistances(targetCoords, usernames) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Erreur calcul des distances", - "details": err.Error(), + "error": "Erreur calcul des distances", }) 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 } @@ -375,8 +356,7 @@ func AutoAssignNearestDeliveryPerson(c *gin.Context) { travelTime, distance, err := calculateTravelTimeWithTomTom(geoService, singleDeliveryman, location.Latitude, location.Longitude) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Erreur calcul ETA", - "details": err.Error(), + "error": "Erreur calcul ETA", }) return } @@ -385,8 +365,7 @@ func AutoAssignNearestDeliveryPerson(c *gin.Context) { err = database.AssignCommandToDeliverymanQueueWithCoords(commandID, singleDeliveryman, travelTime, location.Latitude, location.Longitude, address) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Erreur lors de l'assignation", - "details": err.Error(), + "error": "Erreur lors de l'assignation", }) return } @@ -440,8 +419,7 @@ func AutoAssignNearestDeliveryPerson(c *gin.Context) { travelTime, distance, err := calculateTravelTimeWithTomTom(geoService, leastLoaded, location.Latitude, location.Longitude) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Erreur calcul ETA", - "details": err.Error(), + "error": "Erreur calcul ETA", }) return } @@ -450,8 +428,7 @@ func AutoAssignNearestDeliveryPerson(c *gin.Context) { err = database.ForceAssignCommandToDeliverymanWithCoords(commandID, leastLoaded, travelTime, location.Latitude, location.Longitude, address) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Erreur lors de l'assignation forcée", - "details": err.Error(), + "error": "Erreur lors de l'assignation forcée", }) return } @@ -506,8 +483,7 @@ func AutoAssignNearestDeliveryPerson(c *gin.Context) { nearest, err := geoService.FindNearestDeliveryPersonFast(targetCoords, usernames) if err != nil { c.JSON(http.StatusNotFound, gin.H{ - "error": "Aucun livreur avec position GPS valide", - "details": err.Error(), + "error": "Aucun livreur avec position GPS valide", }) return } @@ -527,8 +503,7 @@ func AutoAssignNearestDeliveryPerson(c *gin.Context) { err = database.AssignCommandToDeliverymanQueueWithCoords(commandID, nearest.Username, travelTime, location.Latitude, location.Longitude, address) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Erreur lors de l'assignation", - "details": err.Error(), + "error": "Erreur lors de l'assignation", }) return } @@ -582,8 +557,7 @@ func AutoAssignAllPendingCommands(c *gin.Context) { commands, err := database.GetAllCommands("pending", "") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Erreur récupération des commandes", - "details": err.Error(), + "error": "Erreur récupération des commandes", }) return } @@ -742,8 +716,7 @@ func GetAllDeliveryQueues(c *gin.Context) { overview, err := database.GetAllQueuesOverview() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Erreur récupération des queues", - "details": err.Error(), + "error": "Erreur récupération des queues", }) return } @@ -802,8 +775,7 @@ func GetDeliverymanQueue(c *gin.Context) { queueInfo, err := database.GetDeliverymanQueueInfo(username) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Erreur récupération de la queue", - "details": err.Error(), + "error": "Erreur récupération de la queue", }) 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", diff --git a/backend/gestion/handlers/gps.go b/backend/gestion/handlers/gps.go index 0e88af82..21d8343d 100644 --- a/backend/gestion/handlers/gps.go +++ b/backend/gestion/handlers/gps.go @@ -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 } diff --git a/backend/gestion/handlers/history.go b/backend/gestion/handlers/history.go index 9cf09ed0..d05ab4ce 100644 --- a/backend/gestion/handlers/history.go +++ b/backend/gestion/handlers/history.go @@ -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 } diff --git a/backend/gestion/handlers/panier.go b/backend/gestion/handlers/panier.go index 26cd9196..69124d43 100644 --- a/backend/gestion/handlers/panier.go +++ b/backend/gestion/handlers/panier.go @@ -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 @@ -611,12 +595,13 @@ func ValidateBasket(c *gin.Context) { // ============================================ newBalance, _ := database.GetClientReferralBalance(usernameStr) resp := gin.H{ - "success": true, - "command_id": commandID, - "delivery_address": req.DeliveryAddress, - "status": "pending", - "referral_used": referralUsed, - "referral_balance": newBalance, + "success": true, + "command_id": commandID, + "client_order_number": command.ClientOrderID, + "delivery_address": req.DeliveryAddress, + "status": "pending", + "referral_used": referralUsed, + "referral_balance": newBalance, } if assigned { @@ -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 diff --git a/backend/gestion/handlers/redis_services.go b/backend/gestion/handlers/redis_services.go index 6a193328..10042071 100644 --- a/backend/gestion/handlers/redis_services.go +++ b/backend/gestion/handlers/redis_services.go @@ -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 } @@ -137,12 +135,8 @@ 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)", - }, + "error": "Données invalides - latitude et longitude requises", + "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 } diff --git a/backend/gestion/handlers/referral.go b/backend/gestion/handlers/referral.go index a7f1b195..7840b42b 100644 --- a/backend/gestion/handlers/referral.go +++ b/backend/gestion/handlers/referral.go @@ -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 } diff --git a/backend/gestion/handlers/settings.go b/backend/gestion/handlers/settings.go index 6bd783eb..f4652039 100644 --- a/backend/gestion/handlers/settings.go +++ b/backend/gestion/handlers/settings.go @@ -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, diff --git a/backend/gestion/handlers/update_profile.go b/backend/gestion/handlers/update_profile.go index c95bb331..400a8a72 100644 --- a/backend/gestion/handlers/update_profile.go +++ b/backend/gestion/handlers/update_profile.go @@ -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 } diff --git a/backend/gestion/handlers/validation_deleviry.go b/backend/gestion/handlers/validation_deleviry.go index 545201a0..3b88f48f 100644 --- a/backend/gestion/handlers/validation_deleviry.go +++ b/backend/gestion/handlers/validation_deleviry.go @@ -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) } diff --git a/backend/gestion/middleware/session_middleware.go b/backend/gestion/middleware/session_middleware.go index 155832e8..db921ba6 100644 --- a/backend/gestion/middleware/session_middleware.go +++ b/backend/gestion/middleware/session_middleware.go @@ -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) diff --git a/backend/gestion/models/client.go b/backend/gestion/models/client.go index 485e9715..67eed2d0 100644 --- a/backend/gestion/models/client.go +++ b/backend/gestion/models/client.go @@ -4,20 +4,23 @@ package models import "time" type Client struct { - ID int `gorm:"primaryKey;autoIncrement" json:"id"` - Username string `gorm:"column:username" json:"username"` - Password string `gorm:"column:password" json:"-"` - Nom string `gorm:"column:nom" json:"nom"` - Prenom string `gorm:"column:prenom" json:"prenom"` - Telephone string `gorm:"column:telephone" json:"telephone"` - Command int `gorm:"column:command" json:"command"` - PointsExtra map[string]int `gorm:"-" json:"points_extra"` - Amende float64 `gorm:"column:amende" json:"amende"` - CancellationsCount int `gorm:"column:cancellations_count" json:"cancellations_count"` - LastPenaltyReason string `gorm:"column:last_penalty_reason" json:"last_penalty_reason"` - MustChangePassword bool `gorm:"column:must_change_password" json:"must_change_password"` - ReferralBalance float64 `gorm:"column:referral_balance" json:"referral_balance"` - CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + ID int `gorm:"primaryKey;autoIncrement" json:"id"` + 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;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;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" } diff --git a/backend/gestion/models/commands.go b/backend/gestion/models/commands.go index c3399a9d..9de1102c 100644 --- a/backend/gestion/models/commands.go +++ b/backend/gestion/models/commands.go @@ -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"` diff --git a/backend/gestion/models/payment.go b/backend/gestion/models/payment.go index 1b759117..a48cff61 100644 --- a/backend/gestion/models/payment.go +++ b/backend/gestion/models/payment.go @@ -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"` diff --git a/backend/gestion/models/settings.go b/backend/gestion/models/settings.go index 60b27c60..d714b40b 100644 --- a/backend/gestion/models/settings.go +++ b/backend/gestion/models/settings.go @@ -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 diff --git a/backend/gestion/models/user.go b/backend/gestion/models/user.go index e5481fe7..99fd199e 100644 --- a/backend/gestion/models/user.go +++ b/backend/gestion/models/user.go @@ -3,12 +3,14 @@ package models import "time" type User struct { - ID int `json:"id" gorm:"primaryKey;autoIncrement"` - Username string `json:"username" gorm:"column:username;uniqueIndex"` + ID int `json:"id" gorm:"primaryKey;autoIncrement"` + Username string `json:"username" gorm:"column:username;not null"` Password string `json:"password,omitempty" gorm:"column:password"` - Role string `json:"role" gorm:"column:role"` - CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` - UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` + 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"` } func (User) TableName() string { return "users" } diff --git a/backend/gestion/routes/routes.go b/backend/gestion/routes/routes.go index 9bee1fd9..d5457e8e 100644 --- a/backend/gestion/routes/routes.go +++ b/backend/gestion/routes/routes.go @@ -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 @@ -103,8 +104,8 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services cartGroupV1.DELETE("/telegram/unlink", handlers.UnlinkClientTelegram) // 👤 PROFIL CLIENT - cartGroupV1.GET("/profile", handlers.GetMyProfile) // ✅ Récupérer mon profil - cartGroupV1.PUT("/profile/update", handlers.UpdateMyProfile) // ✅ Modifier mon profil + cartGroupV1.GET("/profile", handlers.GetMyProfile) // ✅ Récupérer mon profil + cartGroupV1.PUT("/profile/update", handlers.UpdateMyProfile) // ✅ Modifier mon profil // 🎁 PARRAINAGE CLIENT cartGroupV1.GET("/referral/balance", handlers.GetMyReferralBalance) @@ -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) } diff --git a/backend/gestion/utils/errors.go b/backend/gestion/utils/errors.go new file mode 100644 index 00000000..74558cba --- /dev/null +++ b/backend/gestion/utils/errors.go @@ -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}) +} diff --git a/backend/gestion/workers/cron_auto_assign.go b/backend/gestion/workers/cron_auto_assign.go index 0f79bcb5..e8c5e04e 100644 --- a/backend/gestion/workers/cron_auto_assign.go +++ b/backend/gestion/workers/cron_auto_assign.go @@ -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) } diff --git a/frontend-prep/src/pages/User/ProductDetail.tsx b/frontend-prep/src/pages/User/ProductDetail.tsx index 9b0dd804..a58404a9 100644 --- a/frontend-prep/src/pages/User/ProductDetail.tsx +++ b/frontend-prep/src/pages/User/ProductDetail.tsx @@ -240,7 +240,7 @@ function ProductDetail() {
m.type === "image")?.url || ""} alt={product.name} className="product-detail-image" />