From 5380abe8ed86e4ff0d178aed45cdc28819a79605 Mon Sep 17 00:00:00 2001 From: Xor290 Date: Fri, 27 Mar 2026 22:23:46 +0100 Subject: [PATCH] chore: refacto --- backend/gestion/db/db_cancel_command.go | 136 ++---- backend/gestion/db/db_clients.go | 393 +++--------------- backend/gestion/db/db_command_items.go | 56 +-- backend/gestion/db/db_command_priority.go | 21 +- backend/gestion/db/db_commands.go | 311 +++----------- backend/gestion/db/db_delivery.go | 98 +---- backend/gestion/db/db_delivery_issues.go | 5 +- backend/gestion/db/db_delivery_manage.go | 41 +- backend/gestion/db/db_delivery_mode.go | 94 +++++ backend/gestion/db/db_gps_link.go | 107 +---- backend/gestion/db/db_history.go | 71 +--- backend/gestion/db/db_init.go | 37 +- backend/gestion/db/db_jwt_tokens.go | 10 +- backend/gestion/db/db_media.go | 110 ++--- backend/gestion/db/db_notifications.go | 144 ++----- backend/gestion/db/db_product.go | 27 +- backend/gestion/db/db_queue_auto_next.go | 31 +- backend/gestion/db/db_referral.go | 6 - backend/gestion/db/db_sanction_client.go | 91 ++-- backend/gestion/db/db_settings.go | 129 +++--- backend/gestion/db/db_telegram.go | 121 ++++++ backend/gestion/db/db_users.go | 17 +- .../db/redis_deliveryman_assignment.go | 10 - .../db/redis_deliveryman_management.go | 12 +- backend/gestion/db/redis_eta_management.go | 19 +- backend/gestion/db/redis_position.go | 10 - backend/gestion/db/redis_pubsub.go | 4 +- backend/gestion/db/redis_queue_assignment.go | 14 +- backend/gestion/db/redis_queue_info.go | 34 +- backend/gestion/db/redis_queue_management.go | 297 ++++--------- backend/gestion/db/redis_session.go | 1 - backend/gestion/handlers/auth.go | 205 ++------- backend/gestion/handlers/cabine.go | 74 +--- backend/gestion/handlers/cancel_command.go | 86 +--- backend/gestion/handlers/categories.go | 4 - backend/gestion/handlers/client_tracking.go | 20 +- backend/gestion/handlers/commands.go | 29 +- backend/gestion/handlers/crypto_payment.go | 2 - backend/gestion/handlers/deleviry.go | 108 ++--- backend/gestion/handlers/delivery_admin.go | 57 +-- backend/gestion/handlers/eta.go | 8 - backend/gestion/handlers/geoloca.go | 8 - backend/gestion/handlers/gps.go | 48 +++ backend/gestion/handlers/history.go | 4 +- backend/gestion/handlers/notifications.go | 182 -------- backend/gestion/handlers/panier.go | 27 +- backend/gestion/handlers/product.go | 38 +- backend/gestion/handlers/redis_services.go | 2 +- backend/gestion/handlers/settings.go | 44 +- backend/gestion/handlers/telegram.go | 292 +++++++++++++ backend/gestion/handlers/update_profile.go | 52 +-- .../gestion/handlers/validation_deleviry.go | 6 + backend/gestion/handlers/zones.go | 4 +- backend/gestion/main.go | 98 ++--- .../gestion/middleware/clock_middleware.go | 3 +- backend/gestion/models/auth.go | 54 +++ backend/gestion/models/client.go | 6 +- backend/gestion/models/settings.go | 80 ++++ backend/gestion/models/telegram.go | 21 + backend/gestion/models/update_profile.go | 6 +- backend/gestion/routes/routes.go | 36 +- backend/gestion/services/telegram.go | 129 ++++++ backend/gestion/utils/auth.go | 60 +++ backend/gestion/utils/cabine.go | 14 + backend/gestion/utils/delivery.go | 44 ++ backend/gestion/workers/cron_auto_assign.go | 13 +- backend/gestion/workers/redis_worker.go | 3 +- 67 files changed, 1751 insertions(+), 2573 deletions(-) create mode 100644 backend/gestion/db/db_delivery_mode.go create mode 100644 backend/gestion/db/db_telegram.go create mode 100644 backend/gestion/handlers/telegram.go create mode 100644 backend/gestion/models/auth.go create mode 100644 backend/gestion/models/settings.go create mode 100644 backend/gestion/models/telegram.go create mode 100644 backend/gestion/services/telegram.go create mode 100644 backend/gestion/utils/auth.go create mode 100644 backend/gestion/utils/cabine.go create mode 100644 backend/gestion/utils/delivery.go diff --git a/backend/gestion/db/db_cancel_command.go b/backend/gestion/db/db_cancel_command.go index 21741fec..ea7f3ad7 100644 --- a/backend/gestion/db/db_cancel_command.go +++ b/backend/gestion/db/db_cancel_command.go @@ -10,6 +10,7 @@ import ( "database/sql" "fmt" "log" + "slices" "time" ) @@ -17,17 +18,15 @@ import ( // ANNULATION ATOMIQUE // ============================================ -func (d *Database) CancelCommandAtomic(commandID int, username, reason string, force bool) (int, map[string]int, error) { +func (d *Database) CancelCommandAtomic(commandID int, username, reason string, force bool) (int, error) { log.Printf("🔒 [CancelAtomic] START - cmd=%d, user=%s, force=%v", commandID, username, force) - // ✅ TRANSACTION tx, err := d.Begin() if err != nil { - return 0, nil, fmt.Errorf("erreur transaction: %w", err) + return 0, fmt.Errorf("erreur transaction: %w", err) } defer tx.Rollback() - // ✅ SELECT FOR UPDATE - Verrouiller la ligne var currentStatus, cmdUsername, livreurAssign string err = tx.QueryRow(` SELECT status, username, COALESCE(livreur_assign, '') @@ -37,163 +36,101 @@ func (d *Database) CancelCommandAtomic(commandID int, username, reason string, f `, commandID).Scan(¤tStatus, &cmdUsername, &livreurAssign) if err == sql.ErrNoRows { - return 0, nil, fmt.Errorf("commande non trouvĂ©e") + return 0, fmt.Errorf("commande non trouvĂ©e") } if err != nil { - return 0, nil, err + return 0, err } log.Printf("📋 [CancelAtomic] TrouvĂ©e - status=%s, owner=%s, livreur=%s", currentStatus, cmdUsername, livreurAssign) - // ✅ VÉRIFIER PROPRIÉTÉ if cmdUsername != username { - return 0, nil, fmt.Errorf("commande ne vous appartient pas") + return 0, fmt.Errorf("commande ne vous appartient pas") } - // ✅ VÉRIFIER STATUT nonCancellableStatuses := []string{"livre", "approved", "cancelled", "disabled"} - for _, s := range nonCancellableStatuses { - if currentStatus == s { - return 0, nil, fmt.Errorf("impossible d'annuler") - } + if slices.Contains(nonCancellableStatuses, currentStatus) { + return 0, fmt.Errorf("impossible d'annuler") } - // ============================================ - // ✅ FIX: DÉTECTION CORRECTE DE L'ANNULATION TARDIVE - // ============================================ - // Une annulation est tardive UNIQUEMENT si : - // 1. Un livreur est assignĂ© - // 2. Le statut est "en_route" (livreur parti) OU "arrived" (livreur arrivĂ©) - // 3. OU une ETA a Ă©tĂ© dĂ©finie (ce qui signifie que le livreur est en route) - isLateCancel := false - if livreurAssign != "" { - // Cas 1: Statut en_route ou arrived = toujours tardif if currentStatus == "en_route" || currentStatus == "arrived" { isLateCancel = true log.Printf("⚠ [CancelAtomic] Annulation TARDIVE dĂ©tectĂ©e - Statut: %s", currentStatus) + } else if d.CheckCommandETAExistsAndValid(commandID) { + isLateCancel = true + log.Printf("⚠ [CancelAtomic] Annulation TARDIVE dĂ©tectĂ©e - ETA dĂ©finie") } else { - // Cas 2: Pour les autres statuts, vĂ©rifier si une ETA existe - hasRealETA := d.CheckCommandETAExistsAndValid(commandID) - if hasRealETA { - isLateCancel = true - log.Printf("⚠ [CancelAtomic] Annulation TARDIVE dĂ©tectĂ©e - ETA dĂ©finie") - } else { - log.Printf("✅ [CancelAtomic] Annulation SANS PÉNALITÉ - Statut: %s, Pas d'ETA valide", currentStatus) - } + log.Printf("✅ [CancelAtomic] Annulation SANS PÉNALITÉ - Statut: %s, Pas d'ETA valide", currentStatus) } } else { log.Printf("✅ [CancelAtomic] Annulation SANS PÉNALITÉ - Aucun livreur assignĂ©") } - // ✅ SI ANNULATION TARDIVE SANS CONFIRMATION if isLateCancel && !force { - return 0, nil, fmt.Errorf("confirmation requise") + return 0, fmt.Errorf("confirmation requise") } - // ✅ UPDATE STATUT (avec vĂ©rification pour Ă©viter race condition) result, err := tx.Exec(` UPDATE commandes SET status = 'cancelled', updated_at = CURRENT_TIMESTAMP WHERE id = $1 AND status = $2 AND username = $3 `, commandID, currentStatus, username) - if err != nil { - return 0, nil, err + return 0, err } - - rows, _ := result.RowsAffected() - if rows == 0 { - return 0, nil, fmt.Errorf("commande dĂ©jĂ  modifiĂ©e") + if rows, _ := result.RowsAffected(); rows == 0 { + return 0, fmt.Errorf("commande dĂ©jĂ  modifiĂ©e") } - log.Printf("✅ [CancelAtomic] Statut mis Ă  jour: %s → cancelled", currentStatus) - // ✅ REMBOURSER LE STOCK ATOMIQUEMENT - _, err = tx.Exec(` + if _, err = tx.Exec(` UPDATE products p SET stock = stock + ci.quantite, updated_at = CURRENT_TIMESTAMP FROM command_items ci WHERE ci.command_id = $1 AND ci.product_id = p.id - `, commandID) - - if err != nil { + `, commandID); err != nil { log.Printf("⚠ [CancelAtomic] Erreur remboursement stock: %v", err) } else { log.Printf("✅ [CancelAtomic] Stock remboursĂ©") } - // ============================================ - // APPLIQUER PÉNALITÉ SI ANNULATION TARDIVE - // ============================================ penalty := 0 - pointsLost := map[string]int{"weed": 0, "zipette": 0} - - if isLateCancel && force { + if isLateCancel { log.Printf("⚠ [CancelAtomic] Annulation tardive confirmĂ©e - Application pĂ©nalitĂ©") - - // ✅ RÉCUPÉRER LES POINTS ACTUELS - var currentPointsWeed, currentPointsZipette int - err := tx.QueryRow(` - SELECT point, point_zipette FROM clients WHERE username = $1 - `, username).Scan(¤tPointsWeed, ¤tPointsZipette) - - if err == nil { - pointsLost["weed"] = currentPointsWeed - pointsLost["zipette"] = currentPointsZipette - - // ✅ CALCULER PÉNALITÉ - penalty, _ = d.CalculateCancellationPenalty(username) - - // ✅ APPLIQUER: Remettre points Ă  0 + Ajouter pĂ©nalitĂ© + IncrĂ©menter compteur - _, err = tx.Exec(` - UPDATE clients - SET point = 0, - point_zipette = 0, - amende = amende + $1, - cancellations_count = COALESCE(cancellations_count, 0) + 1, - updated_at = CURRENT_TIMESTAMP - WHERE username = $2 - `, penalty, username) - - if err != nil { - log.Printf("❌ [CancelAtomic] Erreur pĂ©nalitĂ©: %v", err) - } else { - log.Printf("⚠ [CancelAtomic] PĂ©nalitĂ©: %d pts, Points perdus: weed=%d, zipette=%d", - penalty, pointsLost["weed"], pointsLost["zipette"]) - } + penalty, _ = d.CalculateCancellationPenalty(username) + if _, err = tx.Exec(` + UPDATE clients + SET amende = amende + $1, + cancellations_count = COALESCE(cancellations_count, 0) + 1, + updated_at = CURRENT_TIMESTAMP + WHERE username = $2 + `, penalty, username); err != nil { + log.Printf("❌ [CancelAtomic] Erreur pĂ©nalitĂ©: %v", err) + } else { + log.Printf("⚠ [CancelAtomic] PĂ©nalitĂ©: %d appliquĂ©e Ă  %s", penalty, username) } } - // ✅ LOG - _, err = tx.Exec(` + if _, err = tx.Exec(` INSERT INTO command_logs (command_id, status, message, author, created_at) - VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP) - `, commandID, "cancelled", - fmt.Sprintf("AnnulĂ©e par %s - Raison: %s", username, reason), - username) - - if err != nil { + VALUES ($1, 'cancelled', $2, $3, CURRENT_TIMESTAMP) + `, commandID, fmt.Sprintf("AnnulĂ©e par %s - Raison: %s", username, reason), username); err != nil { log.Printf("⚠ [CancelAtomic] Erreur log: %v", err) } - // ✅ COMMIT if err := tx.Commit(); err != nil { - return 0, nil, fmt.Errorf("erreur commit: %w", err) + return 0, fmt.Errorf("erreur commit: %w", err) } - // ✅ NETTOYER QUEUE (async, aprĂšs commit) if livreurAssign != "" { go func() { - err := d.CleanupCompletedCommandFromQueue(commandID, livreurAssign) - if err != nil { + if err := d.CleanupCompletedCommandFromQueue(commandID, livreurAssign); err != nil { log.Printf("⚠ [CancelAtomic] Erreur cleanup queue: %v", err) } }() } - - // ✅ INVALIDER CACHES (async) go func() { Redis.Del(RedisCtx, fmt.Sprintf("command:%d", commandID), @@ -203,8 +140,7 @@ func (d *Database) CancelCommandAtomic(commandID int, username, reason string, f }() log.Printf("🎉 [CancelAtomic] SUCCÈS - Commande %d annulĂ©e", commandID) - - return penalty, pointsLost, nil + return penalty, nil } // ============================================ diff --git a/backend/gestion/db/db_clients.go b/backend/gestion/db/db_clients.go index 892eacdc..2fd3d80a 100644 --- a/backend/gestion/db/db_clients.go +++ b/backend/gestion/db/db_clients.go @@ -10,8 +10,8 @@ import ( ) func (d *Database) CreateClient(client *models.Client) error { - query := `INSERT INTO clients (username, password, nom, prenom, telephone, command, point, point_zipette, amende, must_change_password, created_at) - VALUES ($1, $2, $3, $4, $5, 0, 0, 0, 0.0, $6, CURRENT_TIMESTAMP) + query := `INSERT INTO clients (username, password, nom, prenom, telephone, command, amende, must_change_password, created_at) + VALUES ($1, $2, $3, $4, $5, 0, 0.0, $6, CURRENT_TIMESTAMP) RETURNING id, created_at` err := d.QueryRow(query, client.Username, client.Password, client.Nom, client.Prenom, client.Telephone, client.MustChangePassword).Scan( @@ -29,9 +29,10 @@ func (d *Database) CreateClient(client *models.Client) error { // GetClientByID rĂ©cupĂšre un client par son ID func (d *Database) GetClientByID(id int) (*models.Client, error) { var client models.Client - query := `SELECT id, username, password, nom, prenom, telephone, command, point, point_zipette, amende, created_at + query := `SELECT id, username, password, nom, prenom, telephone, command, amende, COALESCE(points_extra, '{}'::jsonb), created_at FROM clients WHERE id = $1` + var pointsExtraJSON []byte err := d.QueryRow(query, id).Scan( &client.ID, &client.Username, @@ -40,9 +41,8 @@ func (d *Database) GetClientByID(id int) (*models.Client, error) { &client.Prenom, &client.Telephone, &client.Command, - &client.Point, - &client.PointZipette, &client.Amende, + &pointsExtraJSON, &client.CreatedAt, ) @@ -53,12 +53,16 @@ func (d *Database) GetClientByID(id int) (*models.Client, error) { return nil, fmt.Errorf("erreur lors de la rĂ©cupĂ©ration du client: %w", err) } + if len(pointsExtraJSON) > 0 { + json.Unmarshal(pointsExtraJSON, &client.PointsExtra) + } + return &client, nil } // GetAllClients rĂ©cupĂšre tous les clients func (d *Database) GetAllClients() ([]*models.Client, error) { - query := `SELECT id, username, password, nom, prenom, telephone, command, point, point_zipette, amende, referral_balance, COALESCE(points_extra, '{}'::jsonb), created_at + query := `SELECT id, username, password, nom, prenom, telephone, command, amende, referral_balance, COALESCE(points_extra, '{}'::jsonb), created_at FROM clients ORDER BY created_at DESC` rows, err := d.Query(query) @@ -79,8 +83,6 @@ func (d *Database) GetAllClients() ([]*models.Client, error) { &client.Prenom, &client.Telephone, &client.Command, - &client.Point, - &client.PointZipette, &client.Amende, &client.ReferralBalance, &pointsExtraJSON, @@ -107,8 +109,8 @@ func (d *Database) GetAllClients() ([]*models.Client, error) { func (d *Database) UpdateClient(client *models.Client) error { query := `UPDATE clients SET username = $1, password = $2, nom = $3, prenom = $4, telephone = $5, - command = $6, point = $7, point_zipette = $8, amende = $9 - WHERE id = $10` + command = $6, amende = $7 + WHERE id = $8` result, err := d.Exec(query, client.Username, @@ -117,8 +119,6 @@ func (d *Database) UpdateClient(client *models.Client) error { client.Prenom, client.Telephone, client.Command, - client.Point, - client.PointZipette, client.Amende, client.ID, ) @@ -135,13 +135,11 @@ func (d *Database) UpdateClient(client *models.Client) error { return fmt.Errorf("client non trouvĂ©") } - log.Printf("✅ Client mis Ă  jour: %s (ID: %d)", client.Username, client.ID) return nil } // DeleteClient supprime un client func (d *Database) DeleteClient(id int) error { - // ✅ MODIFIÉ : Supprimer tous les tokens du client avec le user_type "client" _ = d.RevokeAllUserTokens(id, "client") query := `DELETE FROM clients WHERE id = $1` @@ -160,7 +158,6 @@ func (d *Database) DeleteClient(id int) error { return fmt.Errorf("client non trouvĂ©") } - log.Printf("✅ Client supprimĂ© (ID: %d)", id) return nil } @@ -182,7 +179,6 @@ func (d *Database) UpdateClientPassword(clientID int, hashedPassword string) err return fmt.Errorf("client non trouvĂ©") } - log.Printf("✅ Mot de passe client mis Ă  jour (ID: %d)", clientID) return nil } @@ -204,7 +200,6 @@ func (d *Database) UpdateClientPasswordAndClearFlag(clientID int, hashedPassword return fmt.Errorf("client non trouvĂ©") } - log.Printf("✅ Mot de passe client mis Ă  jour + must_change_password=false (ID: %d)", clientID) return nil } @@ -239,8 +234,7 @@ func (d *Database) GetClientStats(clientID int) (map[string]interface{}, error) "total_commands": totalCommands, "pending_commands": pendingCommands, "completed_commands": completedCommands, - "points": client.Point, - "points_zipette": client.PointZipette, + "points_extra": client.PointsExtra, "amende": client.Amende, "member_since": client.CreatedAt, } @@ -279,7 +273,6 @@ func (d *Database) PayClientPenalties(username string, amountPaid float64) error return fmt.Errorf("montant insuffisant: %.2f payĂ©, %.2f requis", amountPaid, currentAmount) } - // RĂ©initialiser les pĂ©nalitĂ©s query := `UPDATE clients SET amende = 0, updated_at = CURRENT_TIMESTAMP WHERE username = $1` @@ -298,9 +291,6 @@ func (d *Database) PayClientPenalties(username string, amountPaid float64) error return fmt.Errorf("client non trouvĂ©") } - log.Printf("✅ [PayClientPenalties] PĂ©nalitĂ©s rĂ©glĂ©es pour %s", username) - - // Invalider le cache Redis du client cacheKey := fmt.Sprintf("client:%s", username) Redis.Del(RedisCtx, cacheKey) @@ -329,27 +319,22 @@ func (d *Database) IncrementClientCommandCount(username string) error { return nil } -// ✅ NOUVELLE FONCTION: Ajouter des points selon la catĂ©gorie -func (d *Database) AddClientPointsByCategory(username string, points int, category string) error { - var query string - - if category == "zipette&co" { - query = `UPDATE clients - SET point_zipette = point_zipette + $1 - WHERE username = $2` - log.Printf("🎁 [ADD_POINTS] Ajout de %d points ZIPETTE Ă  %s", points, username) - } else { - query = `UPDATE clients - SET point = point + $1 - WHERE username = $2` - log.Printf("🎁 [ADD_POINTS] Ajout de %d points WEED/HASH Ă  %s", points, username) +func (d *Database) AddClientPointsByCategory(username string, points int, poolKey string) error { + if poolKey == "" { + poolKey = "pool_0" } - - result, err := d.Exec(query, points, username) + result, err := d.Exec(` + UPDATE clients + SET points_extra = jsonb_set( + COALESCE(points_extra, '{}'::jsonb), + ARRAY[$2], + to_jsonb(COALESCE((points_extra->>$2)::int, 0) + $3) + ), updated_at = CURRENT_TIMESTAMP + WHERE username = $1 + `, username, poolKey, points) if err != nil { return fmt.Errorf("erreur lors de l'ajout de points: %w", err) } - rowsAffected, err := result.RowsAffected() if err != nil { return fmt.Errorf("erreur lors de la vĂ©rification: %w", err) @@ -357,19 +342,31 @@ func (d *Database) AddClientPointsByCategory(username string, points int, catego if rowsAffected == 0 { return fmt.Errorf("client non trouvĂ©") } - - log.Printf("✅ %d points (%s) ajoutĂ©s au client %s (EN DB)", points, category, username) + log.Printf("✅ %d points (key=%s) ajoutĂ©s au client %s", points, poolKey, username) return nil } -// ✅ ANCIENNE FONCTION CONSERVÉE POUR COMPATIBILITÉ (utilise weed/hash par dĂ©faut) -func (d *Database) AddClientPoints(username string, points int) error { - return d.AddClientPointsByCategory(username, points, "weed_hash") +func (d *Database) CalculateAndAddPointsForCommand(commandID int, username string) (int, error) { + tx, err := d.Begin() + if err != nil { + return 0, fmt.Errorf("erreur transaction: %w", err) + } + defer tx.Rollback() + + points, _, err := d.CalculateAndAddPointsForCommandTx(tx, commandID, username) + if err != nil { + return 0, err + } + + if err := tx.Commit(); err != nil { + return 0, fmt.Errorf("erreur commit: %w", err) + } + return points, nil } func (d *Database) GetClientByTelephone(telephone string) (*models.Client, error) { client := &models.Client{} - query := `SELECT id, username, password, nom, prenom, telephone, command, point, point_zipette, amende, created_at + query := `SELECT id, username, password, nom, prenom, telephone, command, amende, created_at FROM clients WHERE telephone = $1` err := d.QueryRow(query, telephone).Scan( @@ -380,8 +377,6 @@ func (d *Database) GetClientByTelephone(telephone string) (*models.Client, error &client.Prenom, &client.Telephone, &client.Command, - &client.Point, - &client.PointZipette, &client.Amende, &client.CreatedAt, ) @@ -400,7 +395,7 @@ func (d *Database) GetClientByTelephone(telephone string) (*models.Client, error func (d *Database) GetClientByUsername(username string) (*models.Client, error) { client := &models.Client{} var pointsExtraJSON []byte - query := `SELECT id, username, password, nom, prenom, telephone, command, point, point_zipette, amende, must_change_password, COALESCE(points_extra, '{}'::jsonb), created_at + query := `SELECT id, username, password, nom, prenom, telephone, command, amende, must_change_password, COALESCE(points_extra, '{}'::jsonb), created_at FROM clients WHERE username = $1` err := d.QueryRow(query, username).Scan( @@ -411,8 +406,6 @@ func (d *Database) GetClientByUsername(username string) (*models.Client, error) &client.Prenom, &client.Telephone, &client.Command, - &client.Point, - &client.PointZipette, &client.Amende, &client.MustChangePassword, &pointsExtraJSON, @@ -485,23 +478,25 @@ func (d *Database) CheckClientCanOrder(username string) (bool, float64, error) { } // ResetClientPoint rĂ©initialise les points d'un client. -// poolIdx=0 → point, poolIdx=1 → point_zipette, poolIdx=-1 → tous -// poolIdx>=2 → points_extra[extraPoolKey] +// extraPoolKey != "" → reset points_extra[extraPoolKey] uniquement +// extraPoolKey == "" (poolIdx=-1) → reset total points_extra func (d *Database) ResetClientPoint(username string, poolIdx int, extraPoolKey string) error { var query string switch { - case poolIdx == 0: - query = `UPDATE clients SET point = 0, updated_at = CURRENT_TIMESTAMP WHERE username = $1` - case poolIdx == 1: - query = `UPDATE clients SET point_zipette = 0, updated_at = CURRENT_TIMESTAMP WHERE username = $1` - case poolIdx >= 2 && extraPoolKey != "": + case extraPoolKey != "": _, err := d.Exec( `UPDATE clients SET points_extra = points_extra - $2, updated_at = CURRENT_TIMESTAMP WHERE username = $1`, username, extraPoolKey, ) + if err != nil { + log.Printf("❌ [ResetClientPointAdmin] Erreur UPDATE extra: %v", err) + } else { + cacheKey := fmt.Sprintf("client:%s", username) + Redis.Del(RedisCtx, cacheKey) + } return err - default: // -1 → reset total - query = `UPDATE clients SET point = 0, point_zipette = 0, points_extra = '{}'::jsonb, updated_at = CURRENT_TIMESTAMP WHERE username = $1` + default: // -1 ou poolIdx sans clĂ© → reset total + query = `UPDATE clients SET points_extra = '{}'::jsonb, updated_at = CURRENT_TIMESTAMP WHERE username = $1` } result, err := d.Exec(query, username) @@ -649,240 +644,6 @@ func (d *Database) GetClientPenaltiesStats() (map[string]interface{}, error) { return result, nil } -// ✅ FONCTION MODIFIÉE: Calculer les points sans cumuler entre catĂ©gories -// À remplacer dans db/clients.go Ă  partir de la ligne 498 - -// À remplacer dans db/clients.go - -func (d *Database) CalculatePointsForCommand(commandID int) (int, string, error) { - query := ` - SELECT p.category, ci.prix, ci.quantite - FROM command_items ci - JOIN products p ON ci.product_id = p.id - WHERE ci.command_id = $1 - ` - - rows, err := d.Query(query, commandID) - if err != nil { - return 0, "", fmt.Errorf("erreur rĂ©cupĂ©ration items: %w", err) - } - defer rows.Close() - - zipetteTotal := 0.0 - weedTotal := 0.0 - grosSemiTotal := 0.0 - - for rows.Next() { - var category string - var prix float64 - var quantite int - - if err := rows.Scan(&category, &prix, &quantite); err != nil { - log.Printf("⚠ Erreur scan item: %v", err) - continue - } - - itemTotal := prix // Prix dĂ©jĂ  calculĂ© pour la quantitĂ© - categoryLower := strings.ToLower(category) - - if categoryLower == "zipette&co" || categoryLower == "zipette_co" { - zipetteTotal += itemTotal - } else if categoryLower == "gros&semi" || categoryLower == "gros_semi" { - grosSemiTotal += itemTotal - } else { - weedTotal += itemTotal - } - } - - log.Printf("💰 [CALC_POINTS] Cmd %d - Zipette: %.2f€, Weed: %.2f€, GrosSemi: %.2f€", - commandID, zipetteTotal, weedTotal, grosSemiTotal) - - // ✅ CALCUL POINTS WEED&HASH - weedPoints := 0 - if weedTotal > 0 { - switch { - case weedTotal >= 30 && weedTotal <= 50: - weedPoints = 1 - case weedTotal >= 60 && weedTotal <= 150: - weedPoints = 2 - case weedTotal >= 160 && weedTotal <= 300: - weedPoints = 3 - case weedTotal >= 310 && weedTotal <= 400: - weedPoints = 5 - case weedTotal >= 400: - weedPoints = 10 - } - if weedPoints > 0 { - log.Printf("🎁 [CALC_POINTS] Weed: %.2f€ → %d points", weedTotal, weedPoints) - } - } - - // ✅ CALCUL POINTS ZIPETTE&CO - zipettePoints := 0 - if zipetteTotal > 0 { - switch { - case zipetteTotal >= 30 && zipetteTotal <= 100: - zipettePoints = 1 - case zipetteTotal >= 110 && zipetteTotal <= 200: - zipettePoints = 2 - case zipetteTotal >= 210: - zipettePoints = 3 - } - if zipettePoints > 0 { - log.Printf("🎁 [CALC_POINTS] Zipette: %.2f€ → %d points", zipetteTotal, zipettePoints) - } - } - - totalPoints := zipettePoints + weedPoints - - if totalPoints == 0 { - if grosSemiTotal > 0 && zipetteTotal == 0 && weedTotal == 0 { - log.Printf("â„č [CALC_POINTS] Cmd %d - CatĂ©gorie GROS&SEMI uniquement (%.2f€) → 0 points", - commandID, grosSemiTotal) - return 0, "gros&semi", nil - } - log.Printf("â„č [CALC_POINTS] Cmd %d - Aucun montant Ă©ligible aux points", commandID) - return 0, "unknown", nil - } - - var dominantCategory string - if zipetteTotal >= weedTotal { - dominantCategory = "zipette&co" - } else { - dominantCategory = "weed&hash" - } - - log.Printf("✅ [CALC_POINTS] Cmd %d - Total: %d points (Zipette: %d, Weed: %d)", - commandID, totalPoints, zipettePoints, weedPoints) - - return totalPoints, dominantCategory, nil -} - -// ✅ FONCTION CORRIGÉE: Calculer et ajouter les points sĂ©parĂ©ment avec le BON BARÈME -func (d *Database) CalculateAndAddPointsForCommand(commandID int, username string) (int, error) { - query := ` - SELECT p.category, ci.prix, ci.quantite - FROM command_items ci - JOIN products p ON ci.product_id = p.id - WHERE ci.command_id = $1 - ` - - rows, err := d.Query(query, commandID) - if err != nil { - return 0, fmt.Errorf("erreur rĂ©cupĂ©ration items: %w", err) - } - defer rows.Close() - - zipetteTotal := 0.0 - weedTotal := 0.0 - grosSemiTotal := 0.0 - - for rows.Next() { - var category string - var prix float64 - var quantite int - - if err := rows.Scan(&category, &prix, &quantite); err != nil { - log.Printf("⚠ Erreur scan item: %v", err) - continue - } - - // ✅ prix contient dĂ©jĂ  le total pour la quantitĂ© - itemTotal := prix - categoryLower := strings.ToLower(category) - - if categoryLower == "zipette&co" || categoryLower == "zipette_co" { - zipetteTotal += itemTotal - } else if categoryLower == "gros&semi" || categoryLower == "gros_semi" { - grosSemiTotal += itemTotal - } else { - weedTotal += itemTotal - } - } - - log.Printf("💰 [CALC_POINTS] Cmd %d - Zipette: %.2f€, Weed: %.2f€, GrosSemi: %.2f€", - commandID, zipetteTotal, weedTotal, grosSemiTotal) - - // ✅ CALCUL POINTS WEED&HASH - NOUVEAU BARÈME - // De 30 Ă  50€ -> 1 point - // De 60 Ă  150€ -> 2 points - // De 160 Ă  300€ -> 3 points - // De 310 Ă  400€ -> 5 points - // 400€ et + -> 10 points - weedPoints := 0 - if weedTotal > 0 { - switch { - case weedTotal >= 30 && weedTotal <= 50: - weedPoints = 1 - case weedTotal >= 60 && weedTotal <= 150: - weedPoints = 2 - case weedTotal >= 160 && weedTotal <= 300: - weedPoints = 3 - case weedTotal >= 310 && weedTotal <= 400: - weedPoints = 5 - case weedTotal >= 400: - weedPoints = 10 - } - if weedPoints > 0 { - log.Printf("🎁 [CALC_POINTS] Weed: %.2f€ → %d points", weedTotal, weedPoints) - } - } - - // ✅ CALCUL POINTS ZIPETTE&CO - NOUVEAU BARÈME - // De 30 Ă  100€ -> 1 point - // De 110 Ă  200€ -> 2 points (je suppose que c'est 110 et non 1100) - // De 210€ et + -> 3 points - zipettePoints := 0 - if zipetteTotal > 0 { - switch { - case zipetteTotal >= 30 && zipetteTotal <= 100: - zipettePoints = 1 - case zipetteTotal >= 110 && zipetteTotal <= 200: - zipettePoints = 2 - case zipetteTotal >= 210: - zipettePoints = 3 - } - if zipettePoints > 0 { - log.Printf("🎁 [CALC_POINTS] Zipette: %.2f€ → %d points", zipetteTotal, zipettePoints) - } - } - - totalPoints := 0 - - // ✅ AJOUTER LES POINTS SÉPARÉMENT PAR CATÉGORIE - if zipettePoints > 0 { - if err := d.AddClientPointsByCategory(username, zipettePoints, "zipette&co"); err != nil { - log.Printf("❌ Erreur ajout points Zipette: %v", err) - } else { - totalPoints += zipettePoints - log.Printf("✅ %d points ZIPETTE ajoutĂ©s Ă  %s", zipettePoints, username) - } - } - - if weedPoints > 0 { - if err := d.AddClientPointsByCategory(username, weedPoints, "weed&hash"); err != nil { - log.Printf("❌ Erreur ajout points Weed: %v", err) - } else { - totalPoints += weedPoints - log.Printf("✅ %d points WEED ajoutĂ©s Ă  %s", weedPoints, username) - } - } - - if totalPoints == 0 { - if grosSemiTotal > 0 && zipetteTotal == 0 && weedTotal == 0 { - log.Printf("â„č [CALC_POINTS] Cmd %d - CatĂ©gorie GROS&SEMI uniquement → 0 points", commandID) - } else { - log.Printf("â„č [CALC_POINTS] Cmd %d - Aucun point Ă©ligible (Weed: %.2f€, Zipette: %.2f€)", - commandID, weedTotal, zipetteTotal) - } - } - - log.Printf("✅ [CALC_POINTS] Cmd %d - Total: %d points (Zipette: %d, Weed: %d)", - commandID, totalPoints, zipettePoints, weedPoints) - - return totalPoints, nil -} - func (d *Database) CalculateAndAddPointsForCommandTx(tx *sql.Tx, commandID int, username string) (int, string, error) { log.Printf("💰 [CalcPointsTx] START - cmd=%d, user=%s", commandID, username) @@ -982,8 +743,6 @@ func (d *Database) CalculateAndAddPointsForCommandTx(tx *sql.Tx, commandID int, pointCategory = "points" } - // Écrire tous les pools dans points_extra[pool.Key] (stockage dynamique) - // + maintenir les colonnes legacy point/point_zipette pour la compatibilitĂ© admin for i, pool := range pools { if poolPts[i] == 0 { continue @@ -1004,29 +763,6 @@ func (d *Database) CalculateAndAddPointsForCommandTx(tx *sql.Tx, commandID int, log.Printf("💰 [CalcPointsTx] pool[%d] (%s / key=%s): +%d pts", i, pool.Name, pool.Key, poolPts[i]) } - // Maintenir colonnes legacy pour affichage admin - pts0 := poolPts[0] - pts1 := 0 - if len(pools) >= 2 { - pts1 = poolPts[1] - } - var legacyErr error - var result sql.Result - if pts1 == 0 { - result, legacyErr = tx.Exec(`UPDATE clients SET point = point + $1, updated_at = CURRENT_TIMESTAMP WHERE username = $2`, pts0, username) - } else { - result, legacyErr = tx.Exec(`UPDATE clients SET point = point + $1, point_zipette = point_zipette + $2, updated_at = CURRENT_TIMESTAMP WHERE username = $3`, pts0, pts1, username) - } - if legacyErr != nil { - log.Printf("❌ [CalcPointsTx] Erreur UPDATE colonnes legacy: %v", legacyErr) - return 0, "", fmt.Errorf("erreur mise Ă  jour points: %w", legacyErr) - } - affected, _ := result.RowsAffected() - if affected == 0 { - log.Printf("⚠ [CalcPointsTx] Client %s non trouvĂ©", username) - return 0, "", fmt.Errorf("client non trouvĂ©") - } - log.Printf("🎉 [CalcPointsTx] SUCCÈS - %d points [%s] → %s", totalPoints, pointCategory, username) return totalPoints, pointCategory, nil @@ -1066,22 +802,3 @@ func (d *Database) CanUserAccessCommand( return exists, err } - -// SaveClientPushToken enregistre le push token Expo d'un client -func (d *Database) SaveClientPushToken(clientID int, pushToken string) error { - _, err := d.Exec(`UPDATE clients SET push_token = $1 WHERE id = $2`, pushToken, clientID) - return err -} - -// DeleteClientPushToken supprime le push token d'un client -func (d *Database) DeleteClientPushToken(clientID int) error { - _, err := d.Exec(`UPDATE clients SET push_token = NULL WHERE id = $1`, clientID) - return err -} - -// GetClientPushToken retourne le push token d'un client par son username -func (d *Database) GetClientPushToken(username string) (string, error) { - var token string - err := d.QueryRow(`SELECT COALESCE(push_token, '') FROM clients WHERE username = $1`, username).Scan(&token) - return token, err -} diff --git a/backend/gestion/db/db_command_items.go b/backend/gestion/db/db_command_items.go index 3c6c9784..e762a3bb 100644 --- a/backend/gestion/db/db_command_items.go +++ b/backend/gestion/db/db_command_items.go @@ -216,7 +216,7 @@ func (d *Database) GetCommandItems(commandID int) ([]map[string]interface{}, err c.referral_used, c.livreur_assign, c.created_at as command_created_at, - COALESCE(p.category, 'weed_hash') as category + p.category FROM command_items ci LEFT JOIN commandes c ON ci.command_id = c.id LEFT JOIN products p ON ci.product_id = p.id @@ -301,14 +301,7 @@ func (d *Database) GetCommandItems(commandID int) ([]map[string]interface{}, err return items, nil } -// ============================================ -// GET COMMAND ITEMS BY USERNAME - VERSION SÉCURISÉE + FIX NULL -// ============================================ - func (d *Database) GetCommandItemsByUsername(username string) ([]map[string]interface{}, error) { - log.Printf("📩 [GetCommandItemsByUsername] START - username=%s", username) - - // ✅ VALIDATION if err := validateUsername(username); err != nil { log.Printf("❌ [GetCommandItemsByUsername] %v", err) return nil, err @@ -352,7 +345,7 @@ func (d *Database) GetCommandItemsByUsername(username string) ([]map[string]inte for rows.Next() { var id, commandID int var quantite float64 - var productID sql.NullInt64 // ✅ FIX: NullInt64 + var productID sql.NullInt64 var produit, clientUsername, clientNom, clientPrenom, clientTelephone string var deliveryAddress, status sql.NullString var prix, totalPrix float64 @@ -370,28 +363,26 @@ func (d *Database) GetCommandItemsByUsername(username string) ([]map[string]inte return nil, fmt.Errorf("erreur scan: %w", err) } - // ✅ CONVERTIR NullInt64 productIDValue := 0 if productID.Valid { productIDValue = int(productID.Int64) } item := map[string]interface{}{ - "id": id, - "command_id": commandID, - "produit": produit, - "product_id": productIDValue, // ✅ FIX - "quantite": quantite, - "prix": prix, - "client_username": clientUsername, - "client_nom": clientNom, - "client_prenom": clientPrenom, - "client_telephone": clientTelephone, - "delivery_address": deliveryAddress.String, - "status": status.String, - "created_at": createdAt, - "updated_at": updatedAt, - // Infos commande + "id": id, + "command_id": commandID, + "produit": produit, + "product_id": productIDValue, + "quantite": quantite, + "prix": prix, + "client_username": clientUsername, + "client_nom": clientNom, + "client_prenom": clientPrenom, + "client_telephone": clientTelephone, + "delivery_address": deliveryAddress.String, + "status": status.String, + "created_at": createdAt, + "updated_at": updatedAt, "command_status": commandStatus.String, "command_address": commandAddress.String, "total_prix": totalPrix, @@ -410,10 +401,6 @@ func (d *Database) GetCommandItemsByUsername(username string) ([]map[string]inte return items, nil } -// ============================================ -// DELETE COMMAND ITEM - ADMIN ONLY -// ============================================ - func (d *Database) DeleteCommandItem(commandID, itemID int) error { log.Printf("đŸ—‘ïž [DeleteCommandItem] START - commandID=%d, itemID=%d", commandID, itemID) @@ -451,18 +438,10 @@ func (d *Database) DeleteCommandItem(commandID, itemID int) error { log.Printf("⚠ [DeleteCommandItem] Erreur maj total commande: %v", err) } - log.Printf("✅ [DeleteCommandItem] Item %d supprimĂ© de la commande %d", itemID, commandID) return nil } -// ============================================ -// UPDATE COMMAND ITEM STATUS - VERSION SÉCURISÉE -// ============================================ - func (d *Database) UpdateCommandItemStatus(itemID int, status string) error { - log.Printf("📝 [UpdateCommandItemStatus] START - itemID=%d, status=%s", itemID, status) - - // ✅ VALIDATION if err := validateItemID(itemID); err != nil { log.Printf("❌ [UpdateCommandItemStatus] %v", err) return err @@ -473,7 +452,6 @@ func (d *Database) UpdateCommandItemStatus(itemID int, status string) error { return err } - // ✅ VÉRIFIER QUE L'ITEM EXISTE var exists bool checkQuery := `SELECT EXISTS(SELECT 1 FROM command_items WHERE id = $1)` err := d.QueryRow(checkQuery, itemID).Scan(&exists) @@ -486,7 +464,6 @@ func (d *Database) UpdateCommandItemStatus(itemID int, status string) error { return fmt.Errorf("item %d non trouvĂ©", itemID) } - // ✅ UPDATE query := `UPDATE command_items SET status = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2` @@ -507,6 +484,5 @@ func (d *Database) UpdateCommandItemStatus(itemID int, status string) error { return fmt.Errorf("item non trouvĂ©") } - log.Printf("✅ Statut item %d mis Ă  jour: %s", itemID, status) return nil } diff --git a/backend/gestion/db/db_command_priority.go b/backend/gestion/db/db_command_priority.go index def847b0..597b730e 100644 --- a/backend/gestion/db/db_command_priority.go +++ b/backend/gestion/db/db_command_priority.go @@ -10,11 +10,11 @@ import ( "fmt" "gestion/models" "log" + "slices" "time" ) // GetAllCommandsOldestFirst rĂ©cupĂšre les commandes triĂ©es par anciennetĂ© (plus anciennes en premier) -// UtilisĂ© pour le systĂšme de priorisation automatique func (d *Database) GetAllCommandsOldestFirst(status, username string) ([]map[string]interface{}, error) { query := `SELECT c.id, c.username, c.status, c.adresse, c.total_prix, c.livreur_assign, c.created_at, c.updated_at @@ -27,13 +27,8 @@ func (d *Database) GetAllCommandsOldestFirst(status, username string) ([]map[str // Filtrer par status if status != "" { validStatuses := []string{"pending", "assigned", "en_route", "livre", "approved", "cancelled", "disabled"} - isValid := false - for _, vs := range validStatuses { - if status == vs { - isValid = true - break - } - } + + isValid := slices.Contains(validStatuses, status) if !isValid { return nil, fmt.Errorf("statut invalide: %s", status) } @@ -50,11 +45,8 @@ func (d *Database) GetAllCommandsOldestFirst(status, username string) ([]map[str argPosition++ } - // ✅ TRI PAR ANCIENNETÉ: Les plus anciennes d'abord (ASC) query += " ORDER BY c.created_at ASC" - log.Printf("🔍 [PRIORITY] Query: %s | Args: %v", query, args) - rows, err := d.Query(query, args...) if err != nil { return nil, fmt.Errorf("erreur rĂ©cupĂ©ration commandes prioritaires: %w", err) @@ -97,8 +89,6 @@ func (d *Database) GetAllCommandsOldestFirst(status, username string) ([]map[str return nil, fmt.Errorf("erreur itĂ©ration rĂ©sultats: %w", err) } - log.Printf("✅ [PRIORITY] %d commandes rĂ©cupĂ©rĂ©es (ordre: plus anciennes → plus rĂ©centes)", len(commands)) - return commands, nil } @@ -145,9 +135,6 @@ func (d *Database) GetOldestPendingCommand() (map[string]interface{}, error) { command["livreur_assign"] = nil } - log.Printf("📌 [PRIORITY] Commande la plus ancienne: ID=%d, créée le %s", - id, createdAt.Format("2006-01-02 15:04:05")) - return command, nil } @@ -199,8 +186,6 @@ func (d *Database) GetPendingCommandsWithPriority() ([]*models.CommandPriority, return nil, fmt.Errorf("erreur itĂ©ration rĂ©sultats prioritĂ©: %w", err) } - log.Printf("✅ [PRIORITY] %d commandes avec score de prioritĂ© calculĂ©", len(commands)) - return commands, nil } diff --git a/backend/gestion/db/db_commands.go b/backend/gestion/db/db_commands.go index 92044f12..d7a4b63c 100644 --- a/backend/gestion/db/db_commands.go +++ b/backend/gestion/db/db_commands.go @@ -5,6 +5,7 @@ import ( "fmt" "gestion/models" "log" + "slices" "strings" "time" ) @@ -42,6 +43,35 @@ func validateAddress(address string) error { return nil } +type basketItem struct { + ProductID int + Quantity float64 + Price float64 +} + +func (d *Database) fetchBasketItems(username string) ([]basketItem, float64, error) { + rows, err := d.Query(`SELECT product_id, quantity, price FROM baskets WHERE username = $1`, username) + if err != nil { + return nil, 0, fmt.Errorf("erreur rĂ©cupĂ©ration panier: %w", err) + } + defer rows.Close() + + var items []basketItem + total := 0.0 + for rows.Next() { + var item basketItem + if err := rows.Scan(&item.ProductID, &item.Quantity, &item.Price); err != nil { + return nil, 0, fmt.Errorf("erreur scan panier: %w", err) + } + items = append(items, item) + total += item.Price + } + if err := rows.Err(); err != nil { + return nil, 0, err + } + return items, total, nil +} + // validateCommandStatus vĂ©rifie si le statut est valide func validateCommandStatus(status string) error { validStatuses := map[string]bool{ @@ -63,48 +93,22 @@ func validateCommandStatus(status string) error { } func (d *Database) CreateCommand(username string) (*models.Command, error) { - // RĂ©cupĂ©rer le client pour obtenir son adresse var adresse string clientQuery := `SELECT username FROM clients WHERE username = $1` err := d.QueryRow(clientQuery, username).Scan(&adresse) if err != nil { - // Si le client n'existe pas dans la table clients, utiliser une adresse par dĂ©faut adresse = "Adresse non spĂ©cifiĂ©e" } - // RĂ©cupĂ©rer les items du panier - basketQuery := `SELECT product_id, quantity, price FROM baskets WHERE username = $1` - rows, err := d.Query(basketQuery, username) + basketItems, totalPrix, err := d.fetchBasketItems(username) if err != nil { - return nil, fmt.Errorf("erreur lors de la rĂ©cupĂ©ration du panier: %w", err) - } - defer rows.Close() - - type BasketItem struct { - ProductID int - Quantity float64 - Price float64 - } - - var basketItems []BasketItem - totalPrix := 0.0 - - for rows.Next() { - var item BasketItem - if err := rows.Scan(&item.ProductID, &item.Quantity, &item.Price); err != nil { - return nil, fmt.Errorf("erreur lors du scan du panier: %w", err) - } - basketItems = append(basketItems, item) - - // ✅ FIX: Ne PAS multiplier par quantity - totalPrix += item.Price // Prix dĂ©jĂ  calculĂ© pour les grammes + return nil, err } if len(basketItems) == 0 { return nil, fmt.Errorf("le panier est vide") } - // CrĂ©er la commande commandQuery := `INSERT INTO commandes (username, status, adresse, total_prix, created_at, updated_at) VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) RETURNING id, created_at, updated_at` @@ -120,9 +124,7 @@ func (d *Database) CreateCommand(username string) (*models.Command, error) { return nil, fmt.Errorf("erreur lors de la crĂ©ation de la commande: %w", err) } - // InsĂ©rer les items de la commande for _, item := range basketItems { - // RĂ©cupĂ©rer le nom du produit productName, err := d.GetProductNameByID(item.ProductID) if err != nil { productName = "Produit inconnu" @@ -136,7 +138,6 @@ func (d *Database) CreateCommand(username string) (*models.Command, error) { } } - // Vider le panier clearBasketQuery := `DELETE FROM baskets WHERE username = $1` _, err = d.Exec(clearBasketQuery, username) if err != nil { @@ -153,7 +154,6 @@ func (d *Database) CreateCommand(username string) (*models.Command, error) { } func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (*models.Command, error) { - // ✅ SÉCURITÉ: Validation des entrĂ©es if err := validateUsername(username); err != nil { return nil, err } @@ -162,9 +162,6 @@ func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (* return nil, err } - log.Printf("📝 [CreateCommandWithAddress] START - user=%s", username) - - // RĂ©cupĂ©rer les infos du client client, err := d.GetClientByUsername(username) if err != nil { log.Printf("⚠ Client non trouvĂ©: %v", err) @@ -179,51 +176,26 @@ func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (* clientTelephone = sanitizeString(client.Telephone) } - // RĂ©cupĂ©rer les items du panier - basketQuery := `SELECT product_id, quantity, price FROM baskets WHERE username = $1` - rows, err := d.Query(basketQuery, username) + basketItems, totalPrix, err := d.fetchBasketItems(username) if err != nil { log.Printf("❌ Erreur query basket: %v", err) - return nil, fmt.Errorf("erreur rĂ©cupĂ©ration panier: %w", err) - } - defer rows.Close() - - type BasketItem struct { - ProductID int - Quantity float64 - Price float64 - } - - var basketItems []BasketItem - totalPrix := 0.0 - - for rows.Next() { - var item BasketItem - if err := rows.Scan(&item.ProductID, &item.Quantity, &item.Price); err != nil { - return nil, fmt.Errorf("erreur scan panier: %w", err) - } - - // ✅ SÉCURITÉ: Validation des valeurs - if item.ProductID <= 0 || item.Quantity <= 0 || item.Price < 0 { - return nil, fmt.Errorf("donnĂ©es panier invalides") - } - - basketItems = append(basketItems, item) - totalPrix += item.Price + return nil, err } if len(basketItems) == 0 { return nil, fmt.Errorf("le panier est vide") } - // ✅ SÉCURITÉ: VĂ©rifier que le total est cohĂ©rent + for _, item := range basketItems { + if item.ProductID <= 0 || item.Quantity <= 0 || item.Price < 0 { + return nil, fmt.Errorf("donnĂ©es panier invalides") + } + } + if totalPrix <= 0 || totalPrix > 100000 { return nil, fmt.Errorf("montant de commande invalide: %.2f€", totalPrix) } - log.Printf("✅ Panier validĂ©: %d items, total=%.2f€", len(basketItems), totalPrix) - - // CrĂ©er la commande en base commandQuery := `INSERT INTO commandes (username, status, adresse, total_prix, created_at, updated_at) VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) RETURNING id, created_at, updated_at` @@ -235,13 +207,9 @@ func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (* &commandID, &createdAt, &updatedAt, ) if err != nil { - log.Printf("❌ Erreur INSERT commande: %v", err) return nil, fmt.Errorf("erreur crĂ©ation commande: %w", err) } - log.Printf("✅ Commande %d créée en base", commandID) - - // InsĂ©rer les items de la commande AVEC les infos client for _, item := range basketItems { productName, err := d.GetProductNameByID(item.ProductID) if err != nil || productName == "" { @@ -265,7 +233,6 @@ func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (* return nil, fmt.Errorf("erreur insertion items: %w", err) } - // DĂ©crĂ©menter le stock du produit stockQuery := `UPDATE products SET stock = stock - $1 WHERE id = $2 AND stock >= $1` result, err := d.Exec(stockQuery, item.Quantity, item.ProductID) if err != nil { @@ -275,28 +242,22 @@ func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (* rowsAffected, _ := result.RowsAffected() if rowsAffected == 0 { - // Le stock Ă©tait dĂ©jĂ  rĂ©servĂ© lors de l'ajout au panier (DecrementProductStockByID). - // On ne bloque pas la commande : tous les articles doivent ĂȘtre insĂ©rĂ©s. log.Printf("⚠ [CHECKOUT] Stock dĂ©jĂ  rĂ©servĂ© pour produit %d (double rĂ©servation panier/checkout)", item.ProductID) } } - // Vider le panier clearBasketQuery := `DELETE FROM baskets WHERE username = $1` _, err = d.Exec(clearBasketQuery, username) if err != nil { log.Printf("⚠ Erreur vidage panier: %v", err) } - // Ajouter un log de commande sanitizedAddress := sanitizeLogMessage(deliveryAddress) d.AddCommandLog(commandID, "created", fmt.Sprintf("Commande créée - Adresse: %s - Total: %.2f€ - Client: %s %s", sanitizedAddress, totalPrix, sanitizeLogMessage(clientNom), sanitizeLogMessage(clientPrenom)), username) - log.Printf("🎉 SUCCÈS - Commande %d | User: %s | Total: %.2f€", commandID, username, totalPrix) - command := &models.Command{ ID: commandID, Username: username, @@ -310,9 +271,7 @@ func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (* return command, nil } -// ✅ CORRECTION MAJEURE: Utiliser des paramĂštres prĂ©parĂ©s au lieu de fmt.Sprintf -func (d *Database) GetAllCommands(status, username string) ([]map[string]interface{}, error) { - // ✅ SÉCURITÉ: Validation des paramĂštres +func (d *Database) GetAllCommands(status, username string) ([]map[string]any, error) { if username != "" { if err := validateUsername(username); err != nil { return nil, err @@ -327,14 +286,14 @@ func (d *Database) GetAllCommands(status, username string) ([]map[string]interfa 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.proposed_address, c.address_proposal_status, + ROW_NUMBER() OVER (PARTITION BY c.username ORDER BY c.id) AS client_order_number FROM commandes c WHERE 1=1` args := []interface{}{} argPosition := 1 - // Filtrage du statut if status == "" { query += ` AND c.status IN ('pending', 'assigned', 'en_route', 'arrived', 'livre')` } else { @@ -343,16 +302,13 @@ func (d *Database) GetAllCommands(status, username string) ([]map[string]interfa argPosition++ } - // Filtrage du username if username != "" { query += fmt.Sprintf(" AND c.username = $%d", argPosition) args = append(args, username) argPosition++ } - query += " ORDER BY c.created_at DESC LIMIT 1000" // ✅ Protection DoS - - log.Printf("🔍 [GetAllCommands] Query avec %d args", len(args)) + query += " ORDER BY c.created_at DESC LIMIT 1000" rows, err := d.Query(query, args...) if err != nil { @@ -360,7 +316,7 @@ func (d *Database) GetAllCommands(status, username string) ([]map[string]interfa } defer rows.Close() - var commands []map[string]interface{} + var commands []map[string]any for rows.Next() { var id int var username, status, adresse string @@ -369,16 +325,16 @@ func (d *Database) GetAllCommands(status, username string) ([]map[string]interfa var addressProposalStatus string var totalPrix float64 var createdAt, updatedAt time.Time + var clientOrderNumber int - err := rows.Scan(&id, &username, &status, &adresse, &totalPrix, &livreurAssign, &createdAt, &updatedAt, &proposedAddress, &addressProposalStatus) + err := rows.Scan(&id, &username, &status, &adresse, &totalPrix, &livreurAssign, &createdAt, &updatedAt, &proposedAddress, &addressProposalStatus, &clientOrderNumber) if err != nil { return nil, fmt.Errorf("erreur lors du scan de la commande: %w", err) } - // ✅ SÉCURITÉ: Sanitization de l'adresse adresse = sanitizeString(adresse) - command := map[string]interface{}{ + command := map[string]any{ "id": id, "username": username, "status": status, @@ -387,6 +343,7 @@ func (d *Database) GetAllCommands(status, username string) ([]map[string]interfa "created_at": createdAt, "updated_at": updatedAt, "address_proposal_status": addressProposalStatus, + "client_order_number": clientOrderNumber, } if livreurAssign.Valid { @@ -408,8 +365,6 @@ func (d *Database) GetAllCommands(status, username string) ([]map[string]interfa return nil, fmt.Errorf("erreur lors de l'itĂ©ration des rĂ©sultats: %w", err) } - log.Printf("✅ [GetAllCommands] %d commandes rĂ©cupĂ©rĂ©es", len(commands)) - return commands, nil } @@ -427,11 +382,11 @@ func (d *Database) SetCommandReferralUsed(commandID int, amount float64) error { return err } -func (d *Database) GetCommandByID(id int) (map[string]interface{}, error) { - // ✅ DĂ©jĂ  sĂ©curisĂ© avec paramĂštre $1 - query := `SELECT id, username, status, adresse, total_prix, livreur_assign, created_at, updated_at, - proposed_address, address_proposal_status, referral_used - FROM commandes WHERE id = $1` +func (d *Database) GetCommandByID(id int) (map[string]any, error) { + query := `SELECT c.id, c.username, c.status, c.adresse, c.total_prix, c.livreur_assign, c.created_at, c.updated_at, + c.proposed_address, c.address_proposal_status, c.referral_used, + (SELECT COUNT(*) FROM commandes c2 WHERE c2.username = c.username AND c2.id <= c.id) AS client_order_number + FROM commandes c WHERE c.id = $1` var commandID int var username, status, adresse string @@ -440,6 +395,7 @@ func (d *Database) GetCommandByID(id int) (map[string]interface{}, error) { var addressProposalStatus string var totalPrix, referralUsed float64 var createdAt, updatedAt time.Time + var clientOrderNumber int err := d.QueryRow(query, id).Scan( &commandID, @@ -453,6 +409,7 @@ func (d *Database) GetCommandByID(id int) (map[string]interface{}, error) { &proposedAddress, &addressProposalStatus, &referralUsed, + &clientOrderNumber, ) if err == sql.ErrNoRows { @@ -462,7 +419,7 @@ func (d *Database) GetCommandByID(id int) (map[string]interface{}, error) { return nil, fmt.Errorf("erreur lors de la rĂ©cupĂ©ration de la commande: %w", err) } - command := map[string]interface{}{ + command := map[string]any{ "id": commandID, "username": username, "status": status, @@ -472,6 +429,7 @@ func (d *Database) GetCommandByID(id int) (map[string]interface{}, error) { "updated_at": updatedAt, "address_proposal_status": addressProposalStatus, "referral_used": referralUsed, + "client_order_number": clientOrderNumber, } if livreurAssign.Valid { @@ -505,7 +463,6 @@ func (d *Database) GetCommandAddress(commandID int) (string, error) { } func (d *Database) UpdateCommandAddress(commandID int, deliveryAddress string) error { - // ✅ SÉCURITÉ: Validation de l'adresse if len(deliveryAddress) > 500 { return fmt.Errorf("adresse trop longue (max 500 caractĂšres)") } @@ -565,7 +522,6 @@ func (d *Database) ProposeAddressChange(commandID int, proposedAddress, proposed func (d *Database) RespondToAddressProposal(commandID int, clientUsername string, accepted bool) error { var query string if accepted { - // Remplace l'adresse par la proposition query = `UPDATE commandes SET adresse = proposed_address, proposed_address = NULL, address_proposal_status = 'accepted', updated_at = CURRENT_TIMESTAMP @@ -600,17 +556,8 @@ func (d *Database) RespondToAddressProposal(commandID int, clientUsername string // UpdateCommandStatus met Ă  jour le statut d'une commande func (d *Database) UpdateCommandStatus(commandID int, status string) error { - // ✅ SÉCURITÉ: Validation du statut - validStatuses := []string{"pending", "assigned", "en_route", "arrived", "livre", "approved", "cancelled", "disabled"} - isValid := false - for _, vs := range validStatuses { - if status == vs { - isValid = true - break - } - } - if !isValid { - return fmt.Errorf("statut invalide: %s", status) + if err := validateCommandStatus(status); err != nil { + return err } query := `UPDATE commandes SET status = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2` @@ -632,9 +579,7 @@ func (d *Database) UpdateCommandStatus(commandID int, status string) error { return nil } -// AddCommandLog ajoute un log pour une commande func (d *Database) AddCommandLog(commandID int, status, message, author string) error { - // ✅ SÉCURITÉ: Sanitize le message pour Ă©viter l'injection dans les logs sanitizedMessage := sanitizeLogMessage(message) sanitizedAuthor := sanitizeLogMessage(author) @@ -643,7 +588,6 @@ func (d *Database) AddCommandLog(commandID int, status, message, author string) _, err := d.Exec(query, commandID, status, sanitizedMessage, sanitizedAuthor) if err != nil { - // Si la table n'existe pas, on ne retourne pas d'erreur pour ne pas bloquer log.Printf("⚠ Avertissement: impossible d'ajouter le log (table command_logs peut-ĂȘtre manquante): %v", err) return nil } @@ -652,7 +596,7 @@ 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]interface{}, error) { +func (d *Database) GetCommandLogs(commandID int) ([]map[string]any, error) { query := `SELECT id, command_id, status, message, author, created_at FROM command_logs WHERE command_id = $1 @@ -662,11 +606,11 @@ func (d *Database) GetCommandLogs(commandID int) ([]map[string]interface{}, erro if err != nil { // Si la table n'existe pas, retourner un tableau vide au lieu d'une erreur log.Printf("⚠ Avertissement: impossible de rĂ©cupĂ©rer les logs: %v", err) - return []map[string]interface{}{}, nil + return []map[string]any{}, nil } defer rows.Close() - var logs []map[string]interface{} + var logs []map[string]any for rows.Next() { var id, commandID int var status, message, author string @@ -677,7 +621,7 @@ func (d *Database) GetCommandLogs(commandID int) ([]map[string]interface{}, erro return nil, fmt.Errorf("erreur lors du scan du log: %w", err) } - logEntry := map[string]interface{}{ + logEntry := map[string]any{ "id": id, "command_id": commandID, "status": status, @@ -695,108 +639,14 @@ func (d *Database) GetCommandLogs(commandID int) ([]map[string]interface{}, erro return logs, nil } -// ✅ CORRECTION MAJEURE: Utiliser des paramĂštres prĂ©parĂ©s -func (d *Database) GetCommandsWithFilter(status, username string, excludeApproved bool) ([]map[string]interface{}, error) { - query := `SELECT c.id, c.username, c.status, c.adresse, c.total_prix, c.livreur_assign, c.created_at, c.updated_at - FROM commandes c WHERE 1=1` - - args := []interface{}{} - argPosition := 1 - - // ✅ Filtrer par username si fourni - if username != "" { - query += fmt.Sprintf(" AND c.username = $%d", argPosition) - args = append(args, username) - argPosition++ - } - - // ✅ Filtrer par status si fourni avec validation - if status != "" { - validStatuses := []string{"pending", "assigned", "en_route", "arrived", "livre", "approved", "cancelled", "disabled"} - isValid := false - for _, vs := range validStatuses { - if status == vs { - isValid = true - break - } - } - if !isValid { - return nil, fmt.Errorf("statut invalide") - } - - query += fmt.Sprintf(" AND c.status = $%d", argPosition) - args = append(args, status) - argPosition++ - } - - // ✅ Exclure les commandes approved si demandĂ© - if excludeApproved { - query += " AND c.status != 'approved'" - log.Printf("🔍 [FILTER] Exclusion des commandes approved activĂ©e") - } - - query += " ORDER BY c.created_at DESC" - - log.Printf("🔍 [FILTER] Query: %s | Args: %v", query, args) - - rows, err := d.Query(query, args...) - if err != nil { - return nil, fmt.Errorf("erreur lors de la rĂ©cupĂ©ration des commandes: %w", err) - } - defer rows.Close() - - var commands []map[string]interface{} - for rows.Next() { - var id int - var username, status, adresse string - var livreurAssign sql.NullString - var totalPrix float64 - var createdAt, updatedAt time.Time - - err := rows.Scan(&id, &username, &status, &adresse, &totalPrix, &livreurAssign, &createdAt, &updatedAt) - if err != nil { - return nil, fmt.Errorf("erreur lors du scan de la commande: %w", err) - } - - command := map[string]interface{}{ - "id": id, - "username": username, - "status": status, - "adresse": adresse, - "total_prix": totalPrix, - "created_at": createdAt, - "updated_at": updatedAt, - } - - if livreurAssign.Valid { - command["livreur_assign"] = livreurAssign.String - } else { - command["livreur_assign"] = nil - } - - commands = append(commands, command) - } - - if err = rows.Err(); err != nil { - return nil, fmt.Errorf("erreur lors de l'itĂ©ration des rĂ©sultats: %w", err) - } - - log.Printf("✅ [FILTER] %d commandes trouvĂ©es (excludeApproved=%v)", len(commands), excludeApproved) - - return commands, nil -} - -// ✅ NOUVELLE FONCTION: Sanitize les messages de log func sanitizeLogMessage(message string) string { - // Supprimer les caractĂšres de contrĂŽle et limiter la longueur sanitized := strings.Map(func(r rune) rune { if r < 32 || r == 127 { - return -1 // Supprimer les caractĂšres de contrĂŽle + return -1 } return r }, message) - // Limiter Ă  1000 caractĂšres if len(sanitized) > 1000 { sanitized = sanitized[:1000] } @@ -805,17 +655,13 @@ func sanitizeLogMessage(message string) string { } func (d *Database) ValidateDeliveryAtomic(commandID int, adminUsername string) (int, error) { - log.Printf("🔒 [ValidateAtomic] START - cmd=%d, admin=%s", commandID, adminUsername) - - // ✅ ÉTAPE 1: DĂ©marrer une transaction tx, err := d.Begin() if err != nil { log.Printf("❌ [ValidateAtomic] Erreur dĂ©but transaction: %v", err) return 0, fmt.Errorf("erreur transaction: %w", err) } - defer tx.Rollback() // Rollback automatique si pas de commit + defer tx.Rollback() - // ✅ ÉTAPE 2: SELECT FOR UPDATE pour verrouiller la commande var currentStatus, cmdUsername, livreurAssign string var totalPrix float64 err = tx.QueryRow(` @@ -837,28 +683,19 @@ func (d *Database) ValidateDeliveryAtomic(commandID int, adminUsername string) ( log.Printf("📋 [ValidateAtomic] Commande trouvĂ©e - status=%s, client=%s, livreur=%s", currentStatus, cmdUsername, livreurAssign) - // ✅ ÉTAPE 3: VĂ©rifier que le statut permet la validation validStatuses := []string{"assigned", "en_route", "pending", "livre"} - isValid := false - for _, s := range validStatuses { - if currentStatus == s { - isValid = true - break - } - } + isValid := slices.Contains(validStatuses, currentStatus) if !isValid { log.Printf("❌ [ValidateAtomic] Statut invalide pour validation: %s", currentStatus) return 0, fmt.Errorf("statut invalide pour validation: %s", currentStatus) } - // ✅ ÉTAPE 4: VĂ©rifier si dĂ©jĂ  approuvĂ©e (double protection) if currentStatus == "approved" { log.Printf("⚠ [ValidateAtomic] Commande %d dĂ©jĂ  approuvĂ©e", commandID) return 0, fmt.Errorf("commande dĂ©jĂ  approuvĂ©e") } - // ✅ ÉTAPE 5: UPDATE avec vĂ©rification du statut (protection race condition) result, err := tx.Exec(` UPDATE commandes SET status = 'approved', updated_at = CURRENT_TIMESTAMP @@ -876,14 +713,10 @@ func (d *Database) ValidateDeliveryAtomic(commandID int, adminUsername string) ( return 0, fmt.Errorf("commande dĂ©jĂ  modifiĂ©e par une autre requĂȘte") } - log.Printf("✅ [ValidateAtomic] Statut mis Ă  jour: %s → approved", currentStatus) - - // ✅ ÉTAPE 6: Calculer et ajouter les points au client totalPoints := 0 if cmdUsername != "" { log.Printf("🔍 [ValidateAtomic] Calcul points pour client: %s", cmdUsername) - // Utiliser la version transactionnelle du calcul de points points, _, err := d.CalculateAndAddPointsForCommandTx(tx, commandID, cmdUsername) if err != nil { log.Printf("❌ [ValidateAtomic] Erreur calcul/ajout points: %v", err) @@ -893,7 +726,6 @@ func (d *Database) ValidateDeliveryAtomic(commandID int, adminUsername string) ( log.Printf("✅ [ValidateAtomic] %d points attribuĂ©s Ă  %s", totalPoints, cmdUsername) - // ✅ ÉTAPE 7: IncrĂ©menter le compteur de commandes du client _, err = tx.Exec(` UPDATE clients SET command = command + 1, updated_at = CURRENT_TIMESTAMP @@ -901,13 +733,11 @@ func (d *Database) ValidateDeliveryAtomic(commandID int, adminUsername string) ( `, cmdUsername) if err != nil { log.Printf("⚠ [ValidateAtomic] Erreur incrĂ©mentation compteur: %v", err) - // On ne bloque pas pour ça } else { log.Printf("✅ [ValidateAtomic] Compteur commandes incrĂ©mentĂ© pour %s", cmdUsername) } } - // ✅ ÉTAPE 8: Ajouter un log dans command_logs _, err = tx.Exec(` INSERT INTO command_logs (command_id, status, message, author, created_at) VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP) @@ -917,10 +747,8 @@ func (d *Database) ValidateDeliveryAtomic(commandID int, adminUsername string) ( if err != nil { log.Printf("⚠ [ValidateAtomic] Erreur ajout log: %v", err) - // On ne bloque pas pour ça } - // ✅ ÉTAPE 9: Commit de la transaction if err := tx.Commit(); err != nil { log.Printf("❌ [ValidateAtomic] Erreur COMMIT: %v", err) return 0, fmt.Errorf("erreur commit transaction: %w", err) @@ -929,12 +757,9 @@ func (d *Database) ValidateDeliveryAtomic(commandID int, adminUsername string) ( log.Printf("🎉 [ValidateAtomic] SUCCÈS - Commande %d validĂ©e, %d points attribuĂ©s", commandID, totalPoints) - // ✅ ÉTAPE 10: Optimiser la queue du livreur (APRÈS le commit) - // Cette opĂ©ration est faite en dehors de la transaction car elle touche Redis if livreurAssign != "" { log.Printf("📩 [ValidateAtomic] Optimisation queue pour livreur: %s", livreurAssign) go func() { - // Async pour ne pas bloquer la rĂ©ponse err := d.CompleteDeliveryAndProcessNext(livreurAssign, commandID) if err != nil { log.Printf("⚠ [ValidateAtomic] Erreur optimisation queue: %v", err) @@ -942,9 +767,7 @@ func (d *Database) ValidateDeliveryAtomic(commandID int, adminUsername string) ( }() } - // ✅ ÉTAPE 11: Invalider les caches Redis (APRÈS le commit) go func() { - // Async pour ne pas bloquer commandCacheKey := fmt.Sprintf("command:%d", commandID) Redis.Del(RedisCtx, commandCacheKey) diff --git a/backend/gestion/db/db_delivery.go b/backend/gestion/db/db_delivery.go index bcb1eade..698d5905 100644 --- a/backend/gestion/db/db_delivery.go +++ b/backend/gestion/db/db_delivery.go @@ -4,11 +4,12 @@ import ( "database/sql" "fmt" "log" + "slices" "time" ) // GetAvailableDeliveryPersons rĂ©cupĂšre tous les livreurs disponibles -func (d *Database) GetAvailableDeliveryPersons() ([]map[string]interface{}, error) { +func (d *Database) GetAvailableDeliveryPersons() ([]map[string]any, error) { query := `SELECT id, username, total, livraison FROM users WHERE role = 'livreur' @@ -20,7 +21,7 @@ func (d *Database) GetAvailableDeliveryPersons() ([]map[string]interface{}, erro } defer rows.Close() - var livreurs []map[string]interface{} + var livreurs []map[string]any for rows.Next() { var id int var username string @@ -31,7 +32,7 @@ func (d *Database) GetAvailableDeliveryPersons() ([]map[string]interface{}, erro return nil, fmt.Errorf("erreur lors du scan du livreur: %w", err) } - livreur := map[string]interface{}{ + livreur := map[string]any{ "id": id, "username": username, "total": total, @@ -47,17 +48,13 @@ func (d *Database) GetAvailableDeliveryPersons() ([]map[string]interface{}, erro func (d *Database) AssignDeliveryPerson(commandID int, livreurUsername string) error { log.Printf("📩 [AssignDeliveryPerson] START - commandID=%d, livreur=%s", commandID, livreurUsername) - // ✅ DÉMARRER UNE TRANSACTION tx, err := d.Begin() if err != nil { log.Printf("❌ Erreur dĂ©marrage transaction: %v", err) return fmt.Errorf("erreur dĂ©marrage transaction: %w", err) } - defer tx.Rollback() // Rollback automatique si non commitĂ© + defer tx.Rollback() - // ============================================ - // ÉTAPE 1: VĂ©rifier le livreur (DANS la transaction) - // ============================================ var role string checkQuery := `SELECT role FROM users WHERE username = $1 FOR UPDATE` err = tx.QueryRow(checkQuery, livreurUsername).Scan(&role) @@ -74,18 +71,12 @@ func (d *Database) AssignDeliveryPerson(commandID int, livreurUsername string) e return fmt.Errorf("l'utilisateur n'est pas un livreur") } - log.Printf(" ✅ Livreur valide: %s", livreurUsername) - - // ============================================ - // ÉTAPE 2: VĂ©rifier et VERROUILLER la commande - // ✅ FOR UPDATE empĂȘche les modifications concurrentes - // ============================================ var currentStatus string var currentLivreur sql.NullString statusQuery := `SELECT status, livreur_assign FROM commandes WHERE id = $1 - FOR UPDATE` // ⚠ VERROUILLAGE CRITIQUE + FOR UPDATE` err = tx.QueryRow(statusQuery, commandID).Scan(¤tStatus, ¤tLivreur) if err == sql.ErrNoRows { @@ -97,33 +88,14 @@ func (d *Database) AssignDeliveryPerson(commandID int, livreurUsername string) e return fmt.Errorf("erreur lors de la vĂ©rification de la commande: %w", err) } - log.Printf(" ✅ Commande trouvĂ©e: status=%s, livreur_assign=%s", - currentStatus, currentLivreur.String) - - // ============================================ - // ÉTAPE 3: VĂ©rifier que la commande est assignable - // ============================================ - - // ✅ VĂ©rifier le statut (pending ou assigned pour permettre la rĂ©assignation) validStatusesForAssignment := []string{"pending", "assigned"} - isValidStatus := false - for _, vs := range validStatusesForAssignment { - if currentStatus == vs { - isValidStatus = true - break - } - } + isValidStatus := slices.Contains(validStatusesForAssignment, currentStatus) if !isValidStatus { log.Printf("❌ Statut invalide pour assignation: %s", currentStatus) return fmt.Errorf("commande en statut '%s', impossible d'assigner un livreur", currentStatus) } - log.Printf(" ✅ Statut valide pour assignation: %s", currentStatus) - - // ============================================ - // ÉTAPE 4: Assigner le livreur (ATOMIQUE) - // ============================================ updateQuery := `UPDATE commandes SET livreur_assign = $1, status = 'assigned', @@ -147,11 +119,6 @@ func (d *Database) AssignDeliveryPerson(commandID int, livreurUsername string) e return fmt.Errorf("impossible d'assigner la commande (dĂ©jĂ  assignĂ©e ou statut changĂ©)") } - log.Printf(" ✅ Commande assignĂ©e au livreur: %s", livreurUsername) - - // ============================================ - // ÉTAPE 5: Ajouter un log (DANS la transaction) - // ============================================ logQuery := `INSERT INTO command_logs (command_id, status, message, author, created_at) VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP)` @@ -163,28 +130,22 @@ func (d *Database) AssignDeliveryPerson(commandID int, livreurUsername string) e // Non bloquant } - // ============================================ - // ✅ COMMIT de la transaction - // ============================================ err = tx.Commit() if err != nil { log.Printf("❌ Erreur COMMIT: %v", err) return fmt.Errorf("erreur commit transaction: %w", err) } - log.Printf("🎉 [AssignDeliveryPerson] SUCCÈS - Commande %d assignĂ©e Ă  %s", commandID, livreurUsername) - log.Printf(" Workflow: pending → ✅ assigned (TRANSACTION COMMITTED)") - return nil } // GetDeliveryPersonCommands rĂ©cupĂšre les commandes assignĂ©es Ă  un livreur -func (d *Database) GetDeliveryPersonCommands(livreurUsername string, status string) ([]map[string]interface{}, error) { +func (d *Database) GetDeliveryPersonCommands(livreurUsername string, status string) ([]map[string]any, error) { query := `SELECT id, username, status, adresse, total_prix, livreur_assign, created_at, updated_at FROM commandes WHERE livreur_assign = $1` - args := []interface{}{livreurUsername} + args := []any{livreurUsername} if status != "" { query += " AND status = $2" @@ -199,7 +160,7 @@ func (d *Database) GetDeliveryPersonCommands(livreurUsername string, status stri } defer rows.Close() - var commands []map[string]interface{} + var commands []map[string]any for rows.Next() { var id int var username, status, adresse string @@ -212,7 +173,7 @@ func (d *Database) GetDeliveryPersonCommands(livreurUsername string, status stri return nil, fmt.Errorf("erreur lors du scan: %w", err) } - command := map[string]interface{}{ + command := map[string]any{ "id": id, "username": username, "status": status, @@ -228,7 +189,6 @@ func (d *Database) GetDeliveryPersonCommands(livreurUsername string, status stri return commands, nil } -// ✅ NOUVELLE MÉTHODE: IncrementLivreurDeliveryCount incrĂ©mente le compteur de livraisons d'un livreur func (d *Database) IncrementLivreurDeliveryCount(livreurUsername string) error { query := `UPDATE users SET livraison = livraison + 1, @@ -249,14 +209,10 @@ func (d *Database) IncrementLivreurDeliveryCount(livreurUsername string) error { return fmt.Errorf("livreur non trouvĂ©") } - log.Printf("✅ Livraison incrĂ©mentĂ©e pour le livreur: %s", livreurUsername) return nil } func (d *Database) ApproveDelivery(commandID int, clientUsername string) error { - log.Printf("📝 [ApproveDelivery] START - commandID=%d, client=%s", commandID, clientUsername) - - // ✅ DÉMARRER UNE TRANSACTION tx, err := d.Begin() if err != nil { log.Printf("❌ Erreur dĂ©marrage transaction: %v", err) @@ -264,16 +220,13 @@ func (d *Database) ApproveDelivery(commandID int, clientUsername string) error { } defer tx.Rollback() - // ============================================ - // ÉTAPE 1: VĂ©rifier et VERROUILLER la commande - // ============================================ var commandUsername, currentStatus string var livreurAssign sql.NullString checkQuery := `SELECT username, status, livreur_assign FROM commandes WHERE id = $1 - FOR UPDATE` // ⚠ VERROUILLAGE CRITIQUE + FOR UPDATE` err = tx.QueryRow(checkQuery, commandID).Scan(&commandUsername, ¤tStatus, &livreurAssign) if err == sql.ErrNoRows { @@ -283,25 +236,14 @@ func (d *Database) ApproveDelivery(commandID int, clientUsername string) error { return fmt.Errorf("erreur lors de la vĂ©rification de la commande: %w", err) } - // ============================================ - // ÉTAPE 2: Validations mĂ©tier - // ============================================ - - // ✅ VĂ©rifier que c'est bien la commande du client if commandUsername != clientUsername { return fmt.Errorf("cette commande ne vous appartient pas") } - // ✅ VĂ©rifier le statut if currentStatus != "livre" { return fmt.Errorf("cette commande n'est pas encore livrĂ©e (statut actuel: %s)", currentStatus) } - log.Printf(" ✅ Validations OK: client=%s, status=%s", commandUsername, currentStatus) - - // ============================================ - // ÉTAPE 3: Mettre Ă  jour le statut (ATOMIQUE) - // ============================================ updateQuery := `UPDATE commandes SET status = 'approved', updated_at = CURRENT_TIMESTAMP @@ -322,11 +264,6 @@ func (d *Database) ApproveDelivery(commandID int, clientUsername string) error { return fmt.Errorf("impossible d'approuver: statut changĂ© ou commande introuvable") } - log.Printf(" ✅ Statut mis Ă  jour: livre → approved") - - // ============================================ - // ÉTAPE 4: IncrĂ©menter le compteur du livreur (ATOMIQUE) - // ============================================ if livreurAssign.Valid && livreurAssign.String != "" { incrementQuery := `UPDATE users SET livraison = livraison + 1, @@ -337,7 +274,6 @@ func (d *Database) ApproveDelivery(commandID int, clientUsername string) error { result, err := tx.Exec(incrementQuery, livreurAssign.String) if err != nil { log.Printf("⚠ Erreur incrĂ©mentation livreur: %v", err) - // Non bloquant mais on continue dans la transaction } else { rows, _ := result.RowsAffected() if rows > 0 { @@ -346,9 +282,6 @@ func (d *Database) ApproveDelivery(commandID int, clientUsername string) error { } } - // ============================================ - // ÉTAPE 5: Ajouter un log (DANS la transaction) - // ============================================ logQuery := `INSERT INTO command_logs (command_id, status, message, author, created_at) VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP)` @@ -357,20 +290,13 @@ func (d *Database) ApproveDelivery(commandID int, clientUsername string) error { clientUsername) if err != nil { log.Printf("⚠ Erreur ajout log: %v", err) - // Non bloquant } - // ============================================ - // ✅ COMMIT de la transaction - // ============================================ err = tx.Commit() if err != nil { log.Printf("❌ Erreur COMMIT: %v", err) return fmt.Errorf("erreur commit transaction: %w", err) } - log.Printf("🎉 [ApproveDelivery] SUCCÈS - Commande %d approuvĂ©e par %s", commandID, clientUsername) - log.Printf(" Workflow: livre → ✅ approved (TRANSACTION COMMITTED)") - return nil } diff --git a/backend/gestion/db/db_delivery_issues.go b/backend/gestion/db/db_delivery_issues.go index 7cf6bb07..4922718c 100644 --- a/backend/gestion/db/db_delivery_issues.go +++ b/backend/gestion/db/db_delivery_issues.go @@ -8,13 +8,13 @@ import ( func (d *Database) GetDeliveryIssues(status string) ([]models.DeliveryIssue, error) { query := ` - SELECT id, command_id, issue_type, description, status, + SELECT id, command_id, issue_type, description, status, reported_by, COALESCE(resolved_by, ''), COALESCE(resolution, ''), created_at, updated_at FROM delivery_issues ` - var args []interface{} + var args []any if status != "" { query += " WHERE status = $1" args = append(args, status) @@ -76,7 +76,6 @@ func (d *Database) CreateDeliveryIssue(commandID int, issueType, description, re return nil, fmt.Errorf("erreur crĂ©ation problĂšme: %w", err) } - log.Printf("✅ ProblĂšme créé: ID=%d, Type=%s, Commande=%d", issue.ID, issueType, commandID) return &issue, nil } diff --git a/backend/gestion/db/db_delivery_manage.go b/backend/gestion/db/db_delivery_manage.go index d70d0b44..ff09a12b 100644 --- a/backend/gestion/db/db_delivery_manage.go +++ b/backend/gestion/db/db_delivery_manage.go @@ -19,10 +19,6 @@ var allowedStatuses = map[string]bool{ "busy": true, } -// ============================================ -// 📊 STATISTIQUES LIVREUR -// ============================================ - // CountDeliveriesByStatus compte les livraisons d'un livreur par statut func (d *Database) CountDeliveriesByStatus(livreurUsername string, statuses string) (int, error) { if statuses == "" { @@ -231,17 +227,6 @@ func (d *Database) GetDeliveryPersonHistory(livreurUsername string, limit, offse return history, nil } -// ============================================ -// 📍 GESTION POSITION GPS -// ============================================ -// ⚠ REMARQUE: Les fonctions UpdateDeliveryPersonLocation et GetDeliveryPersonLocation -// sont dĂ©jĂ  dĂ©finies dans db/delivery_db.go -// Nous rĂ©utilisons ces fonctions existantes au lieu de les redĂ©finir ici - -// ============================================ -// 🔄 GESTION STATUT LIVREUR -// ============================================ - // GetDeliveryPersonStatus rĂ©cupĂšre le statut d'un livreur func (d *Database) GetDeliveryPersonStatus(livreurUsername string) (string, error) { statusKey := fmt.Sprintf("delivery:status:%s", livreurUsername) @@ -277,10 +262,6 @@ func (d *Database) UpdateDeliveryPersonStatus(livreurUsername string, status str return nil } -// ============================================ -// 📩 GESTION QUEUE LIVREUR -// ============================================ - // GetDeliverymanQueueSize rĂ©cupĂšre la taille de la queue d'un livreur func (d *Database) GetDeliverymanQueueSize(livreurUsername string) (int, error) { queueKey := fmt.Sprintf("delivery:queue:%s", livreurUsername) @@ -312,10 +293,6 @@ func (d *Database) GetDeliverymanQueue(livreurUsername string) ([]int, error) { return queue, nil } -// ============================================ -// 🔧 FONCTIONS UTILITAIRES -// ============================================ - // UpdateCommandLivreur met Ă  jour le livreur assignĂ© Ă  une commande func (d *Database) UpdateCommandLivreur(commandID int, livreurUsername string) error { query := `UPDATE commandes @@ -338,19 +315,15 @@ func (d *Database) UpdateCommandLivreur(commandID int, livreurUsername string) e return nil } -// ============================================ -// 📊 STATISTIQUES SYSTÈME -// ============================================ - // GetAllDeliveryPersonsStats rĂ©cupĂšre les stats de tous les livreurs -func (d *Database) GetAllDeliveryPersonsStats() ([]map[string]interface{}, error) { +func (d *Database) GetAllDeliveryPersonsStats() ([]map[string]any, error) { // RĂ©cupĂ©rer tous les livreurs livreurs, err := d.GetAvailableDeliveryPersons() if err != nil { return nil, fmt.Errorf("erreur rĂ©cupĂ©ration livreurs: %w", err) } - var stats []map[string]interface{} + var stats []map[string]any for _, livreur := range livreurs { username := livreur["username"].(string) @@ -361,7 +334,7 @@ func (d *Database) GetAllDeliveryPersonsStats() ([]map[string]interface{}, error queueSize, _ := d.GetDeliverymanQueueSize(username) status, _ := d.GetDeliveryPersonStatus(username) - statEntry := map[string]interface{}{ + statEntry := map[string]any{ "username": username, "total_deliveries": totalDeliveries, "completed_deliveries": completedDeliveries, @@ -375,10 +348,6 @@ func (d *Database) GetAllDeliveryPersonsStats() ([]map[string]interface{}, error return stats, nil } -// ============================================ -// 🔍 RECHERCHE & FILTRAGE -// ============================================ - // GetDeliveryPersonsByStatus rĂ©cupĂšre les livreurs par statut func (d *Database) GetDeliveryPersonsByStatus(status string) ([]string, error) { // RĂ©cupĂ©rer tous les livreurs @@ -411,10 +380,6 @@ func (d *Database) GetAvailableDeliveryPersonsCount() (int, error) { return len(availableLivreurs), nil } -// ============================================ -// đŸ—‘ïž SUPPRESSION & NETTOYAGE -// ============================================ - // ClearDeliveryPersonData supprime toutes les donnĂ©es d'un livreur (admin uniquement) func (d *Database) ClearDeliveryPersonData(livreurUsername string) error { log.Printf("đŸ—‘ïž [ClearDeliveryData] Nettoyage donnĂ©es pour: %s", livreurUsername) diff --git a/backend/gestion/db/db_delivery_mode.go b/backend/gestion/db/db_delivery_mode.go new file mode 100644 index 00000000..d880f369 --- /dev/null +++ b/backend/gestion/db/db_delivery_mode.go @@ -0,0 +1,94 @@ +package db + +import ( + "fmt" + "log" +) + +// GetCommandCategories retourne les catĂ©gories distinctes des produits d'une commande +func (d *Database) GetCommandCategories(commandID int) ([]string, error) { + query := ` + SELECT DISTINCT p.category + FROM command_items ci + JOIN products p ON p.id = ci.product_id + WHERE ci.command_id = $1 AND p.category IS NOT NULL AND p.category != ''` + + rows, err := d.Query(query, commandID) + if err != nil { + return nil, fmt.Errorf("erreur lecture catĂ©gories commande %d: %w", commandID, err) + } + defer rows.Close() + + var categories []string + for rows.Next() { + var cat string + if err := rows.Scan(&cat); err == nil { + categories = append(categories, cat) + } + } + return categories, rows.Err() +} + +// GetEligibleDeliverymenForCommand retourne les usernames des livreurs Ă©ligibles pour une commande. +// En mode "single" → tous les livreurs actifs. +// En mode "category_based" → le(s) livreur(s) assignĂ©(s) aux catĂ©gories de la commande. +// Si aucune correspondance → fallback sur tous les livreurs actifs. +func (d *Database) GetEligibleDeliverymenForCommand(commandID int) ([]string, error) { + settings, err := d.GetSettings() + if err != nil { + log.Printf("⚠ [DELIVERY_MODE] Erreur lecture settings: %v — fallback single", err) + } + + allActive := func() ([]string, error) { + livreurs, err := d.GetAllActiveDeliveryPersons() + if err != nil { + return nil, err + } + names := make([]string, len(livreurs)) + for i, l := range livreurs { + names[i] = l.Username + } + return names, nil + } + + if settings.DeliveryMode.Mode != "category_based" || len(settings.DeliveryMode.CategoryRoutes) == 0 { + return allActive() + } + + // Mode category_based : rĂ©cupĂ©rer les catĂ©gories de la commande + categories, err := d.GetCommandCategories(commandID) + if err != nil || len(categories) == 0 { + log.Printf("⚠ [DELIVERY_MODE] Cmd %d: catĂ©gories non trouvĂ©es — fallback single", commandID) + return allActive() + } + + // Construire un set des catĂ©gories de la commande + catSet := make(map[string]bool, len(categories)) + for _, c := range categories { + catSet[c] = true + } + + // Trouver les livreurs dont la route intersecte les catĂ©gories + eligible := make(map[string]bool) + for _, route := range settings.DeliveryMode.CategoryRoutes { + for _, routeCat := range route.Categories { + if catSet[routeCat] { + eligible[route.DeliverymanUsername] = true + break + } + } + } + + if len(eligible) == 0 { + log.Printf("⚠ [DELIVERY_MODE] Cmd %d: aucun livreur pour catĂ©gories %v — fallback single", commandID, categories) + return allActive() + } + + result := make([]string, 0, len(eligible)) + for u := range eligible { + result = append(result, u) + } + + log.Printf("🎯 [DELIVERY_MODE] Cmd %d: livreurs Ă©ligibles %v (catĂ©gories: %v)", commandID, result, categories) + return result, nil +} diff --git a/backend/gestion/db/db_gps_link.go b/backend/gestion/db/db_gps_link.go index dbab9cf2..3a33b86c 100644 --- a/backend/gestion/db/db_gps_link.go +++ b/backend/gestion/db/db_gps_link.go @@ -7,7 +7,6 @@ package db import ( "fmt" - "net/url" ) // MapLinks contient les liens vers diffĂ©rentes plateformes de cartographie @@ -22,126 +21,48 @@ type MapLinks struct { HereMaps string `json:"here_maps"` } +func wazeAppLink(lat, lon float64) string { + return fmt.Sprintf("waze://?ll=%.6f,%.6f&navigate=yes", lat, lon) +} + // GenerateMapLinks gĂ©nĂšre tous les liens de cartes pour une position GPS func (d *Database) GenerateMapLinks(lat, lon float64, label string) MapLinks { - // Encoder le label pour l'URL - encodedLabel := url.QueryEscape(label) - return MapLinks{ - // Google Maps (Web) - GoogleMaps: fmt.Sprintf( - "https://www.google.com/maps?q=%.6f,%.6f&label=%s", - lat, lon, encodedLabel, - ), - - // Google Maps (App - Deep link) - GoogleMapsApp: fmt.Sprintf( - "https://maps.google.com/?q=%.6f,%.6f", - lat, lon, - ), - - // Waze (Web) - Waze: fmt.Sprintf( - "https://www.waze.com/ul?ll=%.6f,%.6f&navigate=yes", - lat, lon, - ), - - // Waze (App - Deep link) - WazeApp: fmt.Sprintf( - "waze://?ll=%.6f,%.6f&navigate=yes", - lat, lon, - ), - - // Apple Maps (iOS/macOS) - AppleMaps: fmt.Sprintf( - "http://maps.apple.com/?ll=%.6f,%.6f&q=%s", - lat, lon, encodedLabel, - ), - - // OpenStreetMap - OpenStreetMap: fmt.Sprintf( - "https://www.openstreetmap.org/?mlat=%.6f&mlon=%.6f#map=16/%.6f/%.6f", - lat, lon, lat, lon, - ), - - // Bing Maps - BingMaps: fmt.Sprintf( - "https://www.bing.com/maps?cp=%.6f~%.6f&lvl=16", - lat, lon, - ), - - // HERE Maps - HereMaps: fmt.Sprintf( - "https://wego.here.com/?map=%.6f,%.6f,16,normal", - lat, lon, - ), + WazeApp: wazeAppLink(lat, lon), } } -// GenerateNavigationLink gĂ©nĂšre un lien de navigation depuis une origine vers une destination +// GenerateNavigationLink gĂ©nĂšre un lien de navigation vers une destination +// fromLat/fromLon sont ignorĂ©s : Waze part toujours de la position GPS courante func (d *Database) GenerateNavigationLink(fromLat, fromLon, toLat, toLon float64, platform string) string { - switch platform { - case "google": - return fmt.Sprintf( - "https://www.google.com/maps/dir/?api=1&origin=%.6f,%.6f&destination=%.6f,%.6f&travelmode=driving", - fromLat, fromLon, toLat, toLon, - ) - - case "waze": - return fmt.Sprintf( - "https://www.waze.com/ul?ll=%.6f,%.6f&navigate=yes&from=%.6f,%.6f", - toLat, toLon, fromLat, fromLon, - ) - - case "apple": - return fmt.Sprintf( - "http://maps.apple.com/?saddr=%.6f,%.6f&daddr=%.6f,%.6f", - fromLat, fromLon, toLat, toLon, - ) - - default: - return fmt.Sprintf( - "https://www.google.com/maps/dir/?api=1&origin=%.6f,%.6f&destination=%.6f,%.6f", - fromLat, fromLon, toLat, toLon, - ) - } + return wazeAppLink(toLat, toLon) } // GenerateMapLinksForCommand gĂ©nĂšre les liens de navigation pour une commande -// (depuis la position du livreur vers la destination de la commande) func (d *Database) GenerateMapLinksForCommand(commandID int, deliverymanUsername string) (map[string]string, error) { - // 1. RĂ©cupĂ©rer la position du livreur - livreurLat, livreurLon, err := d.GetDeliveryPersonLocation(deliverymanUsername) + _, _, err := d.GetDeliveryPersonLocation(deliverymanUsername) if err != nil { return nil, fmt.Errorf("position livreur non disponible: %w", err) } - // 2. RĂ©cupĂ©rer la destination de la commande command, err := d.GetCommandByID(commandID) if err != nil { return nil, fmt.Errorf("commande non trouvĂ©e: %w", err) } var destLat, destLon float64 - if command["dest_latitude"] != nil { - if latVal, ok := command["dest_latitude"].(float64); ok { - destLat = latVal - } + if v, ok := command["dest_latitude"].(float64); ok { + destLat = v } - if command["dest_longitude"] != nil { - if lonVal, ok := command["dest_longitude"].(float64); ok { - destLon = lonVal - } + if v, ok := command["dest_longitude"].(float64); ok { + destLon = v } if destLat == 0 && destLon == 0 { return nil, fmt.Errorf("coordonnĂ©es destination invalides") } - // 3. GĂ©nĂ©rer les liens de navigation return map[string]string{ - "google_maps": d.GenerateNavigationLink(livreurLat, livreurLon, destLat, destLon, "google"), - "waze": d.GenerateNavigationLink(livreurLat, livreurLon, destLat, destLon, "waze"), - "apple_maps": d.GenerateNavigationLink(livreurLat, livreurLon, destLat, destLon, "apple"), + "waze_app": wazeAppLink(destLat, destLon), }, nil } diff --git a/backend/gestion/db/db_history.go b/backend/gestion/db/db_history.go index 36f87d4e..6c77dfcd 100644 --- a/backend/gestion/db/db_history.go +++ b/backend/gestion/db/db_history.go @@ -9,17 +9,14 @@ import ( "database/sql" "fmt" "log" + "maps" "time" ) // GetCompletedCommandsByUsername rĂ©cupĂšre toutes les commandes terminĂ©es (approved) d'un utilisateur -// ✅ Retourne uniquement les commandes avec status = "approved" -// ✅ OrdonnĂ©es par date de crĂ©ation dĂ©croissante (plus rĂ©centes en premier) -func (d *Database) GetCompletedCommandsByUsername(username string) ([]map[string]interface{}, error) { - log.Printf("📚 [GetCompletedCommands] START - username=%s", username) - +func (d *Database) GetCompletedCommandsByUsername(username string) ([]map[string]any, error) { query := ` - SELECT + SELECT id, username, status, @@ -40,7 +37,7 @@ func (d *Database) GetCompletedCommandsByUsername(username string) ([]map[string } defer rows.Close() - var commands []map[string]interface{} + var commands []map[string]any for rows.Next() { var id int @@ -64,7 +61,7 @@ func (d *Database) GetCompletedCommandsByUsername(username string) ([]map[string return nil, fmt.Errorf("erreur lors du scan de la commande: %w", err) } - command := map[string]interface{}{ + command := map[string]any{ "id": id, "username": username, "status": status, @@ -74,7 +71,6 @@ func (d *Database) GetCompletedCommandsByUsername(username string) ([]map[string "updated_at": updatedAt, } - // Ajouter livreur_assign seulement s'il n'est pas NULL if livreurAssign.Valid { command["livreur_assign"] = livreurAssign.String } else { @@ -89,23 +85,17 @@ func (d *Database) GetCompletedCommandsByUsername(username string) ([]map[string return nil, fmt.Errorf("erreur lors de l'itĂ©ration des rĂ©sultats: %w", err) } - log.Printf("✅ [GetCompletedCommands] %d commandes terminĂ©es trouvĂ©es pour %s", len(commands), username) return commands, nil } // GetCompletedCommandsWithItems rĂ©cupĂšre les commandes terminĂ©es avec leurs items -// ✅ Retourne les commandes approved avec tous les dĂ©tails -func (d *Database) GetCompletedCommandsWithItems(username string) ([]map[string]interface{}, error) { - log.Printf("📚 [GetCompletedWithItems] START - username=%s", username) - - // 1. RĂ©cupĂ©rer les commandes terminĂ©es +func (d *Database) GetCompletedCommandsWithItems(username string) ([]map[string]any, error) { commands, err := d.GetCompletedCommandsByUsername(username) if err != nil { return nil, err } - // 2. Pour chaque commande, rĂ©cupĂ©rer les items - var enrichedCommands []map[string]interface{} + var enrichedCommands []map[string]any for _, command := range commands { commandID, ok := command["id"].(int) @@ -117,31 +107,24 @@ func (d *Database) GetCompletedCommandsWithItems(username string) ([]map[string] items, err := d.GetCommandItems(commandID) if err != nil { log.Printf("⚠ [GetCompletedWithItems] Erreur items pour cmd %d: %v", commandID, err) - items = []map[string]interface{}{} + items = []map[string]any{} } - // Enrichir la commande - enrichedCommand := make(map[string]interface{}) - for k, v := range command { - enrichedCommand[k] = v - } + enrichedCommand := make(map[string]any) + maps.Copy(enrichedCommand, command) enrichedCommand["items"] = items enrichedCommand["items_count"] = len(items) enrichedCommands = append(enrichedCommands, enrichedCommand) } - log.Printf("✅ [GetCompletedWithItems] %d commandes enrichies", len(enrichedCommands)) return enrichedCommands, nil } // GetCommandsStatsByUsername rĂ©cupĂšre les statistiques des commandes d'un utilisateur -// ✅ Compte total, approved, pending, cancelled, etc. -func (d *Database) GetCommandsStatsByUsername(username string) (map[string]interface{}, error) { - log.Printf("📊 [GetCommandsStats] START - username=%s", username) - +func (d *Database) GetCommandsStatsByUsername(username string) (map[string]any, error) { query := ` - SELECT + SELECT COUNT(*) FILTER (WHERE status = 'approved') as approved_count, COUNT(*) FILTER (WHERE status = 'pending') as pending_count, COUNT(*) FILTER (WHERE status = 'assigned') as assigned_count, @@ -172,7 +155,7 @@ func (d *Database) GetCommandsStatsByUsername(username string) (map[string]inter return nil, fmt.Errorf("erreur lors de la rĂ©cupĂ©ration des statistiques: %w", err) } - stats := map[string]interface{}{ + stats := map[string]any{ "approved_count": approvedCount, "pending_count": pendingCount, "assigned_count": assignedCount, @@ -183,19 +166,15 @@ func (d *Database) GetCommandsStatsByUsername(username string) (map[string]inter "total_spent": totalSpent, } - log.Printf("✅ [GetCommandsStats] Stats calculĂ©es: total=%d, approved=%d, spent=%.2f€", - totalCount, approvedCount, totalSpent) - return stats, nil } // GetCommandsByStatus rĂ©cupĂšre les commandes d'un utilisateur par statut -// ✅ Permet de filtrer par un statut spĂ©cifique -func (d *Database) GetCommandsByStatus(username, status string) ([]map[string]interface{}, error) { +func (d *Database) GetCommandsByStatus(username, status string) ([]map[string]any, error) { log.Printf("📋 [GetCommandsByStatus] START - username=%s, status=%s", username, status) query := ` - SELECT + SELECT id, username, status, @@ -211,12 +190,11 @@ func (d *Database) GetCommandsByStatus(username, status string) ([]map[string]in rows, err := d.Query(query, username, status) if err != nil { - log.Printf("❌ [GetCommandsByStatus] Erreur query: %v", err) return nil, fmt.Errorf("erreur lors de la rĂ©cupĂ©ration des commandes: %w", err) } defer rows.Close() - var commands []map[string]interface{} + var commands []map[string]any for rows.Next() { var id int @@ -240,7 +218,7 @@ func (d *Database) GetCommandsByStatus(username, status string) ([]map[string]in return nil, fmt.Errorf("erreur lors du scan: %w", err) } - command := map[string]interface{}{ + command := map[string]any{ "id": id, "username": username, "status": status, @@ -260,21 +238,16 @@ func (d *Database) GetCommandsByStatus(username, status string) ([]map[string]in } if err = rows.Err(); err != nil { - log.Printf("❌ [GetCommandsByStatus] Erreur itĂ©ration: %v", err) return nil, fmt.Errorf("erreur lors de l'itĂ©ration: %w", err) } - - log.Printf("✅ [GetCommandsByStatus] %d commandes trouvĂ©es", len(commands)) return commands, nil } // GetRecentCompletedOrders rĂ©cupĂšre les N derniĂšres commandes terminĂ©es d'un utilisateur -// ✅ Utile pour afficher les derniĂšres commandes dans le dashboard -func (d *Database) GetRecentCompletedOrders(username string, limit int) ([]map[string]interface{}, error) { - log.Printf("📚 [GetRecentCompleted] START - username=%s, limit=%d", username, limit) +func (d *Database) GetRecentCompletedOrders(username string, limit int) ([]map[string]any, error) { query := ` - SELECT + SELECT id, username, status, @@ -296,7 +269,7 @@ func (d *Database) GetRecentCompletedOrders(username string, limit int) ([]map[s } defer rows.Close() - var commands []map[string]interface{} + var commands []map[string]any for rows.Next() { var id int @@ -320,7 +293,7 @@ func (d *Database) GetRecentCompletedOrders(username string, limit int) ([]map[s return nil, fmt.Errorf("erreur lors du scan: %w", err) } - command := map[string]interface{}{ + command := map[string]any{ "id": id, "username": username, "status": status, @@ -340,10 +313,8 @@ func (d *Database) GetRecentCompletedOrders(username string, limit int) ([]map[s } if err = rows.Err(); err != nil { - log.Printf("❌ [GetRecentCompleted] Erreur itĂ©ration: %v", err) return nil, fmt.Errorf("erreur lors de l'itĂ©ration: %w", err) } - log.Printf("✅ [GetRecentCompleted] %d commandes rĂ©centes trouvĂ©es", len(commands)) return commands, nil } diff --git a/backend/gestion/db/db_init.go b/backend/gestion/db/db_init.go index 51db1936..7b021b5e 100644 --- a/backend/gestion/db/db_init.go +++ b/backend/gestion/db/db_init.go @@ -2,7 +2,6 @@ package db import ( "database/sql" - "encoding/json" "fmt" "log" "os" @@ -197,37 +196,6 @@ func InitDB() *Database { log.Fatalf("❌ Erreur migration clients.points_extra: %v", err) } - // Migration: copier point/point_zipette → points_extra[pool.Key] selon les clĂ©s admin - { - var poolsJSON string - _ = database.QueryRow(`SELECT value FROM app_settings WHERE key = 'points_pools'`).Scan(&poolsJSON) - if poolsJSON != "" { - var pools []struct { - Key string `json:"key"` - } - if jsonErr := json.Unmarshal([]byte(poolsJSON), &pools); jsonErr == nil { - if len(pools) > 0 && pools[0].Key != "" { - k0 := pools[0].Key - if _, mErr := database.Exec(`UPDATE clients SET points_extra = points_extra || jsonb_build_object($1::text, point) WHERE point > 0 AND NOT (points_extra ? $1)`, k0); mErr != nil { - log.Printf("⚠ Migration points_extra pool[0] (%s): %v", k0, mErr) - } else { - log.Printf("✅ Migration points_extra pool[0] key=%s", k0) - } - } - if len(pools) > 1 && pools[1].Key != "" { - k1 := pools[1].Key - if _, mErr := database.Exec(`UPDATE clients SET points_extra = points_extra || jsonb_build_object($1::text, point_zipette) WHERE point_zipette > 0 AND NOT (points_extra ? $1)`, k1); mErr != nil { - log.Printf("⚠ Migration points_extra pool[1] (%s): %v", k1, mErr) - } else { - log.Printf("✅ Migration points_extra pool[1] key=%s", k1) - } - } - } - } else { - log.Printf("â„č Migration points_extra: aucun pool configurĂ© (app_settings vide)") - } - } - // Lancer le nettoyage pĂ©riodique des tokens expirĂ©s go database.cleanExpiredTokensPeriodically() @@ -267,8 +235,6 @@ func (db *Database) createTables() error { prenom VARCHAR(100) NOT NULL, telephone VARCHAR(20) NOT NULL UNIQUE, command INTEGER DEFAULT 0, - point INTEGER DEFAULT 0, - point_zipette INTEGER DEFAULT 0, amende NUMERIC(10,2) DEFAULT 0.0, cancel_commande INTEGER DEFAULT 0, cancellations_count INTEGER DEFAULT 0 NOT NULL, @@ -436,8 +402,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_clients_point_zipette ON clients(point_zipette) WHERE point_zipette > 0;`, - `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_jwt_tokens.go b/backend/gestion/db/db_jwt_tokens.go index 79cb8434..640de53a 100644 --- a/backend/gestion/db/db_jwt_tokens.go +++ b/backend/gestion/db/db_jwt_tokens.go @@ -79,7 +79,7 @@ func (d *Database) RevokeAllUserTokens(userID int, userType string) error { } // GetUserActiveTokens rĂ©cupĂšre tous les tokens actifs d'un utilisateur -func (d *Database) GetUserActiveTokens(userID int, userType string) ([]map[string]interface{}, error) { +func (d *Database) GetUserActiveTokens(userID int, userType string) ([]map[string]any, error) { query := `SELECT id, token, date_save, date_fin FROM jwt_tokens WHERE user_id = $1 AND user_type = $2 AND date_fin > $3 @@ -91,7 +91,7 @@ func (d *Database) GetUserActiveTokens(userID int, userType string) ([]map[strin } defer rows.Close() - var tokens []map[string]interface{} + var tokens []map[string]any for rows.Next() { var id int var token string @@ -102,7 +102,7 @@ func (d *Database) GetUserActiveTokens(userID int, userType string) ([]map[strin return nil, fmt.Errorf("erreur lors du scan: %w", err) } - tokens = append(tokens, map[string]interface{}{ + tokens = append(tokens, map[string]any{ "id": id, "token": token[:20] + "...", // Tronquer pour la sĂ©curitĂ© "date_save": dateSave, @@ -114,7 +114,7 @@ func (d *Database) GetUserActiveTokens(userID int, userType string) ([]map[strin return tokens, nil } -func (d *Database) GetTokenInfo(token string) (map[string]interface{}, error) { +func (d *Database) GetTokenInfo(token string) (map[string]any, error) { query := `SELECT user_id, user_type, date_save, date_fin FROM jwt_tokens WHERE token = $1 AND date_fin > $2` @@ -131,7 +131,7 @@ func (d *Database) GetTokenInfo(token string) (map[string]interface{}, error) { return nil, fmt.Errorf("erreur lors de la rĂ©cupĂ©ration des infos du token: %w", err) } - tokenInfo := map[string]interface{}{ + tokenInfo := map[string]any{ "user_id": userID, "user_type": userType, "date_save": dateSave, diff --git a/backend/gestion/db/db_media.go b/backend/gestion/db/db_media.go index 0be19f85..54140cf0 100644 --- a/backend/gestion/db/db_media.go +++ b/backend/gestion/db/db_media.go @@ -5,13 +5,17 @@ import ( "fmt" "gestion/models" "log" + "slices" "strings" "time" ) -// ============================================ -// VALIDATION HELPERS -// ============================================ +type MediaInterface interface { + GetProductID() int + GetType() string + GetURL() string + SetID(int) +} // validateMediaID vĂ©rifie la validitĂ© d'un ID mĂ©dia func validateMediaID(mediaID int) error { @@ -40,13 +44,9 @@ func validateMediaType(mediaType string) error { validTypes := []string{"image", "video"} mediaType = strings.ToLower(strings.TrimSpace(mediaType)) - - for _, valid := range validTypes { - if mediaType == valid { - return nil - } + if slices.Contains(validTypes, mediaType) { + return nil } - return fmt.Errorf("type de mĂ©dia invalide: %s (autorisĂ©: image, video)", mediaType) } @@ -61,7 +61,7 @@ func validateMediaURL(url string) error { } // ✅ PROTECTION PATH TRAVERSAL - if strings.Contains(url, "..") { + if strings.Contains(url, "..") || strings.Contains(url, "...") || strings.Contains(url, "..//") { return fmt.Errorf("path traversal dĂ©tectĂ© dans l'URL") } @@ -88,16 +88,9 @@ func validateMediaURL(url string) error { // CREATE MEDIA - VERSION SÉCURISÉE // ============================================ -func (db *Database) CreateMedia(media interface{}) error { +func (db *Database) CreateMedia(media any) error { log.Printf("🔒 [CreateMedia] START - Type: %T", media) - type MediaInterface interface { - GetProductID() int - GetType() string - GetURL() string - SetID(int) - } - // ✅ TYPE ASSERTION SÉCURISÉE m, ok := media.(MediaInterface) if !ok { @@ -115,32 +108,33 @@ func (db *Database) CreateMedia(media interface{}) error { return fmt.Errorf("type de mĂ©dia invalide") } - // ✅ VALIDATION COMPLÈTE productID := m.GetProductID() mediaType := m.GetType() mediaURL := m.GetURL() - log.Printf("📋 [CreateMedia] ProductID=%d, Type=%s, URL=%s", productID, mediaType, mediaURL) - - // Valider le product ID if err := validateProductID(productID); err != nil { log.Printf("❌ [CreateMedia] %v", err) return err } - // Valider le type if err := validateMediaType(mediaType); err != nil { log.Printf("❌ [CreateMedia] %v", err) return err } - // Valider l'URL if err := validateMediaURL(mediaURL); err != nil { log.Printf("❌ [CreateMedia] %v", err) return err } - // ✅ VÉRIFIER QUE LE PRODUIT EXISTE + if err := db.InsertMedia(m, productID, mediaURL, mediaType); err != nil { + log.Printf("❌ [InsertMedia] %v", err) + return err + } + return nil +} + +func (db *Database) InsertMedia(m any, productID int, mediaURL any, mediaType string) error { var exists bool checkQuery := `SELECT EXISTS(SELECT 1 FROM products WHERE id = $1)` err := db.QueryRow(checkQuery, productID).Scan(&exists) @@ -153,7 +147,6 @@ func (db *Database) CreateMedia(media interface{}) error { return fmt.Errorf("produit %d n'existe pas", productID) } - // ✅ INSÉRER LE MÉDIA query := `INSERT INTO media (product_id, url, type, created_at) VALUES ($1, $2, $3, $4) RETURNING id` @@ -165,10 +158,10 @@ func (db *Database) CreateMedia(media interface{}) error { log.Printf("❌ [CreateMedia] Erreur INSERT: %v", err) return fmt.Errorf("erreur crĂ©ation mĂ©dia: %w", err) } - - m.SetID(mediaID) + if mi, ok := m.(MediaInterface); ok { + mi.SetID(mediaID) + } log.Printf("✅ [CreateMedia] MĂ©dia créé: ID=%d, Type=%s", mediaID, mediaType) - return nil } @@ -274,9 +267,6 @@ func (db *Database) GetMediaByProductID(productID int) ([]models.Media, error) { // ============================================ func (db *Database) UpdateMedia(media *models.Media) error { - log.Printf("🔄 [UpdateMedia] START - ID=%d", media.ID) - - // ✅ VALIDATION if media == nil { return fmt.Errorf("mĂ©dia nil") } @@ -297,16 +287,9 @@ func (db *Database) UpdateMedia(media *models.Media) error { } // ✅ VÉRIFIER QUE LE MÉDIA EXISTE - var exists bool - checkQuery := `SELECT EXISTS(SELECT 1 FROM media WHERE id = $1)` - err := db.QueryRow(checkQuery, media.ID).Scan(&exists) - if err != nil { - log.Printf("❌ [UpdateMedia] Erreur vĂ©rification: %v", err) - return fmt.Errorf("erreur vĂ©rification mĂ©dia: %w", err) - } - if !exists { - log.Printf("❌ [UpdateMedia] MĂ©dia %d n'existe pas", media.ID) - return fmt.Errorf("mĂ©dia %d non trouvĂ©", media.ID) + if err := db.CheckMediaExists(media); err != nil { + log.Printf("❌ [UpdateMedia] %v", err) + return err } // ✅ UPDATE @@ -331,33 +314,33 @@ func (db *Database) UpdateMedia(media *models.Media) error { return nil } -// ============================================ -// DELETE MEDIA - VERSION SÉCURISÉE -// ============================================ +func (db *Database) CheckMediaExists(media *models.Media) error { + var exists bool + checkQuery := `SELECT EXISTS(SELECT 1 FROM media WHERE id = $1)` + err := db.QueryRow(checkQuery, media.ID).Scan(&exists) + if err != nil { + log.Printf("❌ [UpdateMedia] Erreur vĂ©rification: %v", err) + return fmt.Errorf("erreur vĂ©rification mĂ©dia: %w", err) + } + if !exists { + log.Printf("❌ [UpdateMedia] MĂ©dia %d n'existe pas", media.ID) + return fmt.Errorf("mĂ©dia %d non trouvĂ©", media.ID) + } + return nil +} func (db *Database) DeleteMedia(mediaID int) error { - log.Printf("đŸ—‘ïž [DeleteMedia] START - ID=%d", mediaID) - - // ✅ VALIDATION if err := validateMediaID(mediaID); err != nil { log.Printf("❌ [DeleteMedia] %v", err) return err } - // ✅ VÉRIFIER QUE LE MÉDIA EXISTE - var exists bool - checkQuery := `SELECT EXISTS(SELECT 1 FROM media WHERE id = $1)` - err := db.QueryRow(checkQuery, mediaID).Scan(&exists) - if err != nil { - log.Printf("❌ [DeleteMedia] Erreur vĂ©rification: %v", err) - return fmt.Errorf("erreur vĂ©rification mĂ©dia: %w", err) - } - if !exists { - log.Printf("❌ [DeleteMedia] MĂ©dia %d n'existe pas", mediaID) - return fmt.Errorf("mĂ©dia %d non trouvĂ©", mediaID) + media := models.Media{ID: mediaID} + if err := db.CheckMediaExists(&media); err != nil { + log.Printf("❌ [DeleteMedia] %v", err) + return err } - // ✅ DELETE query := `DELETE FROM media WHERE id = $1` result, err := db.Exec(query, mediaID) @@ -377,20 +360,14 @@ func (db *Database) DeleteMedia(mediaID int) error { return nil } -// ============================================ -// DELETE MEDIA BY PRODUCT ID - VERSION SÉCURISÉE -// ============================================ - func (db *Database) DeleteMediaByProductID(productID int) error { log.Printf("đŸ—‘ïž [DeleteMediaByProductID] START - ProductID=%d", productID) - // ✅ VALIDATION if err := validateProductID(productID); err != nil { log.Printf("❌ [DeleteMediaByProductID] %v", err) return err } - // ✅ VÉRIFIER QUE LE PRODUIT EXISTE var exists bool checkQuery := `SELECT EXISTS(SELECT 1 FROM products WHERE id = $1)` err := db.QueryRow(checkQuery, productID).Scan(&exists) @@ -403,7 +380,6 @@ func (db *Database) DeleteMediaByProductID(productID int) error { return fmt.Errorf("produit %d non trouvĂ©", productID) } - // ✅ DELETE query := `DELETE FROM media WHERE product_id = $1` result, err := db.Exec(query, productID) diff --git a/backend/gestion/db/db_notifications.go b/backend/gestion/db/db_notifications.go index 758a1f37..31d77058 100644 --- a/backend/gestion/db/db_notifications.go +++ b/backend/gestion/db/db_notifications.go @@ -1,12 +1,10 @@ package db import ( - "bytes" - "database/sql" "encoding/json" "fmt" + "gestion/services" "log" - "net/http" "time" ) @@ -14,7 +12,7 @@ func (d *Database) NotifyClient(username string, commandID int, notifType, messa // Sauvegarder la notification dans Redis notifKey := fmt.Sprintf("notifications:%s", username) - notification := map[string]interface{}{ + notification := map[string]any{ "command_id": commandID, "type": notifType, "message": message, @@ -26,90 +24,22 @@ func (d *Database) NotifyClient(username string, commandID int, notifType, messa Redis.LPush(RedisCtx, notifKey, notifJSON) Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour) - // Envoyer push notification si le client a un token enregistrĂ© - pushToken, err := d.GetClientPushToken(username) - if err == nil && pushToken != "" { - go sendExpoPush(pushToken, "Uber Stup", message, commandID, notifType) + // Diffusion Telegram si le client a liĂ© son compte + if services.TelegramBot != nil && services.TelegramBot.IsConfigured() { + if chatID, ok, err := d.GetClientTelegramChatID(username); err == nil && ok { + go services.TelegramBot.SendMessage(chatID, fmt.Sprintf("🔔 Notification\n\n%s", message)) + } } log.Printf("📬 Notification envoyĂ©e Ă  %s: %s", username, message) return nil } -func sendExpoPush(token, title, body string, commandID int, notifType string) { - sendExpoPushWithChannel(token, title, body, commandID, notifType, "orders") -} - -func sendExpoPushWithChannel(token, title, body string, commandID int, notifType, channelID string) { - payload := map[string]interface{}{ - "to": token, - "title": title, - "body": body, - "channelId": channelID, - "data": map[string]interface{}{ - "command_id": commandID, - "type": notifType, - }, - "sound": "default", - } - - jsonBody, err := json.Marshal(payload) - if err != nil { - log.Printf("❌ [EXPO_PUSH] Erreur marshal: %v", err) - return - } - - req, err := http.NewRequest("POST", "https://exp.host/--/api/v2/push/send", bytes.NewBuffer(jsonBody)) - if err != nil { - log.Printf("❌ [EXPO_PUSH] Erreur crĂ©ation requĂȘte: %v", err) - return - } - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Accept", "application/json") - req.Header.Set("Accept-Encoding", "gzip, deflate") - - client := &http.Client{Timeout: 10 * time.Second} - resp, err := client.Do(req) - if err != nil { - log.Printf("❌ [EXPO_PUSH] Erreur envoi: %v", err) - return - } - defer resp.Body.Close() - - log.Printf("✅ [EXPO_PUSH] Push envoyĂ© Ă  %s (channel: %s, status: %d)", token, channelID, resp.StatusCode) -} - -// ============================================ -// PUSH TOKEN - LIVREURS (table users) -// ============================================ - -// SaveUserPushToken sauvegarde le token push d'un livreur dans la table users -func (d *Database) SaveUserPushToken(username, token string) error { - _, err := d.Exec(`UPDATE users SET push_token = $1 WHERE username = $2`, token, username) - return err -} - -// GetUserPushToken rĂ©cupĂšre le token push d'un livreur depuis la table users -func (d *Database) GetUserPushToken(username string) (string, error) { - var token sql.NullString - err := d.QueryRow(`SELECT push_token FROM users WHERE username = $1`, username).Scan(&token) - if err != nil || !token.Valid { - return "", err - } - return token.String, nil -} - -// DeleteUserPushToken supprime le token push d'un livreur -func (d *Database) DeleteUserPushToken(username string) error { - _, err := d.Exec(`UPDATE users SET push_token = NULL WHERE username = $1`, username) - return err -} - -// NotifyLivreur envoie une notification in-app (Redis) + push Expo Ă  un livreur +// NotifyLivreur envoie une notification in-app (Redis) Ă  un livreur func (d *Database) NotifyLivreur(username string, commandID int, notifType, message string) error { notifKey := fmt.Sprintf("notifications:%s", username) - notification := map[string]interface{}{ + notification := map[string]any{ "command_id": commandID, "type": notifType, "message": message, @@ -121,10 +51,11 @@ func (d *Database) NotifyLivreur(username string, commandID int, notifType, mess Redis.LPush(RedisCtx, notifKey, notifJSON) Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour) - // Push notification si le livreur a un token enregistrĂ© - pushToken, err := d.GetUserPushToken(username) - if err == nil && pushToken != "" { - go sendExpoPushWithChannel(pushToken, "Nouvelle commande assignĂ©e", message, commandID, notifType, "deliveries") + // Diffusion Telegram si le livreur a liĂ© son compte + if services.TelegramBot != nil && services.TelegramBot.IsConfigured() { + if chatID, ok, err := d.GetUserTelegramChatID(username); err == nil && ok { + go services.TelegramBot.SendMessage(chatID, fmt.Sprintf("🔔 Notification\n\n%s", message)) + } } log.Printf("📬 [LIVREUR_NOTIF] Notification envoyĂ©e Ă  %s: %s", username, message) @@ -135,7 +66,7 @@ func (d *Database) NotifyLivreur(username string, commandID int, notifType, mess // et envoie un push aux ceux qui ont un token. AppelĂ© dĂšs qu'une nouvelle commande est créée. func (d *Database) NotifyAllAdminCabine(commandID int, clientUsername, deliveryAddr string) { rows, err := d.Query( - `SELECT username, COALESCE(push_token, '') FROM users WHERE role IN ('admin','cabine')`, + `SELECT username FROM users WHERE role IN ('admin','cabine')`, ) if err != nil { log.Printf("❌ [ADMIN_NOTIF] Erreur lecture users admin/cabine: %v", err) @@ -145,7 +76,7 @@ func (d *Database) NotifyAllAdminCabine(commandID int, clientUsername, deliveryA msg := fmt.Sprintf("Nouvelle commande #%d de %s — %s", commandID, clientUsername, deliveryAddr) - notification := map[string]interface{}{ + notification := map[string]any{ "command_id": commandID, "type": "new_order", "message": msg, @@ -154,29 +85,34 @@ func (d *Database) NotifyAllAdminCabine(commandID int, clientUsername, deliveryA } notifJSON, _ := json.Marshal(notification) - sent := 0 + count := 0 for rows.Next() { - var username, token string - if err := rows.Scan(&username, &token); err != nil { + var username string + if err := rows.Scan(&username); err != nil { continue } notifKey := fmt.Sprintf("notifications:%s", username) Redis.LPush(RedisCtx, notifKey, notifJSON) Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour) - if token != "" { - go sendExpoPushWithChannel(token, "Nouvelle commande", msg, commandID, "new_order", "orders") - sent++ + // Diffusion Telegram individuelle + if services.TelegramBot != nil && services.TelegramBot.IsConfigured() { + if chatID, ok, err := d.GetUserTelegramChatID(username); err == nil && ok { + capturedChatID := chatID + capturedMsg := msg + go services.TelegramBot.SendMessage(capturedChatID, fmt.Sprintf("🔔 Nouvelle commande\n\n%s", capturedMsg)) + } } + count++ } - log.Printf("📬 [ADMIN_NOTIF] Notif Redis + push (%d tokens) pour commande #%d", sent, commandID) + log.Printf("📬 [ADMIN_NOTIF] Notif Redis (%d users) pour commande #%d", count, commandID) } // NotifyAllAdminCabineAlert envoie une notification Redis + push Ă  tous les admins/cabines // lors du dĂ©clenchement d'une alerte par un livreur. func (d *Database) NotifyAllAdminCabineAlert(alertID int, livreurUsername, alertMessage string) { rows, err := d.Query( - `SELECT username, COALESCE(push_token, '') FROM users WHERE role IN ('admin','cabine')`, + `SELECT username FROM users WHERE role IN ('admin','cabine')`, ) if err != nil { log.Printf("❌ [ALERT_NOTIF] Erreur lecture users admin/cabine: %v", err) @@ -184,10 +120,9 @@ func (d *Database) NotifyAllAdminCabineAlert(alertID int, livreurUsername, alert } defer rows.Close() - title := "🚹 Alerte livreur" body := fmt.Sprintf("%s — livreur : %s", alertMessage, livreurUsername) - notification := map[string]interface{}{ + notification := map[string]any{ "alert_id": alertID, "type": "alert", "message": body, @@ -196,22 +131,27 @@ func (d *Database) NotifyAllAdminCabineAlert(alertID int, livreurUsername, alert } notifJSON, _ := json.Marshal(notification) - sent := 0 + count := 0 for rows.Next() { - var username, token string - if err := rows.Scan(&username, &token); err != nil { + var username string + if err := rows.Scan(&username); err != nil { continue } notifKey := fmt.Sprintf("notifications:%s", username) Redis.LPush(RedisCtx, notifKey, notifJSON) Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour) - if token != "" { - go sendExpoPushWithChannel(token, title, body, alertID, "alert", "orders") - sent++ + // Diffusion Telegram individuelle + if services.TelegramBot != nil && services.TelegramBot.IsConfigured() { + if chatID, ok, err := d.GetUserTelegramChatID(username); err == nil && ok { + capturedChatID := chatID + capturedBody := body + go services.TelegramBot.SendMessage(capturedChatID, fmt.Sprintf("🚹 Alerte livreur\n\n%s", capturedBody)) + } } + count++ } - log.Printf("🚹 [ALERT_NOTIF] Notif Redis + push (%d tokens) pour alerte #%d de %s", sent, alertID, livreurUsername) + log.Printf("🚹 [ALERT_NOTIF] Notif Redis (%d users) pour alerte #%d de %s", count, alertID, livreurUsername) } // AddDeliveryRating ajoute une note pour un livreur diff --git a/backend/gestion/db/db_product.go b/backend/gestion/db/db_product.go index ad1e5da6..09c19c1f 100644 --- a/backend/gestion/db/db_product.go +++ b/backend/gestion/db/db_product.go @@ -9,7 +9,7 @@ import ( ) // CreateProduct crĂ©e un nouveau produit avec ses prix -func (db *Database) CreateProduct(product interface{}) error { +func (db *Database) CreateProduct(product any) error { log.Printf("🔍 [DB CreateProduct] Type reçu: %T", product) log.Printf("🔍 [DB CreateProduct] Valeur: %+v", product) @@ -27,7 +27,6 @@ func (db *Database) CreateProduct(product interface{}) error { p, ok := product.(ProductInterface) if !ok { - // VĂ©rifier si c'est un pointeur vers models.Product if prodPtr, isPtr := product.(*models.Product); isPtr { log.Printf("✅ [DB CreateProduct] C'est un *models.Product, utilisons-le directement") p = prodPtr // ça fonctionne maintenant car *models.Product implĂ©mente ProductInterface @@ -221,8 +220,28 @@ func (db *Database) GetProductsByCategory(category string) ([]models.Product, er return products, nil } -func (db *Database) UpdateProduct(productID int, product interface{}) error { - // À implĂ©menter selon vos besoins +func (db *Database) UpdateProduct(productID int, name, category, description, unit string, stock float64, prices []models.ProductPrice) error { + _, err := db.Exec(` + UPDATE products + SET name = $1, category = $2, description = $3, stock = $4, unit = $5, updated_at = $6 + WHERE id = $7 + `, name, category, description, stock, unit, time.Now(), productID) + if err != nil { + return fmt.Errorf("erreur mise Ă  jour produit: %w", err) + } + + db.Exec(`DELETE FROM product_prices WHERE product_id = $1`, productID) + + for _, price := range prices { + _, err := db.Exec(` + INSERT INTO product_prices (product_id, quantity, price) + VALUES ($1, $2, $3) + `, productID, price.Quantity, price.Price) + if err != nil { + log.Printf("❌ [UpdateProduct] Erreur prix: %v", err) + } + } + return nil } diff --git a/backend/gestion/db/db_queue_auto_next.go b/backend/gestion/db/db_queue_auto_next.go index 83f9b599..6bc17060 100644 --- a/backend/gestion/db/db_queue_auto_next.go +++ b/backend/gestion/db/db_queue_auto_next.go @@ -10,10 +10,10 @@ import ( "fmt" "gestion/models" "log" + "slices" ) // ProcessNextCommandForDeliveryman traite automatiquement la prochaine commande -// AppelĂ© aprĂšs qu'une commande soit livrĂ©e ou annulĂ©e func (d *Database) ProcessNextCommandForDeliveryman(deliveryman string) error { log.Printf("🔄 [NEXT_COMMAND] Traitement prochaine commande pour %s", deliveryman) @@ -34,9 +34,6 @@ func (d *Database) ProcessNextCommandForDeliveryman(deliveryman string) error { return nil } - log.Printf("📋 [NEXT_COMMAND] %d commande(s) dans la queue de %s", len(commandIDs), deliveryman) - - // ✅ BOUCLE: Essayer toutes les commandes jusqu'Ă  trouver une valide for i, cmdIDStr := range commandIDs { commandID := extractCommandID(cmdIDStr) if commandID <= 0 { @@ -74,32 +71,16 @@ func (d *Database) ProcessNextCommandForDeliveryman(deliveryman string) error { currentStatus, _ := command["status"].(string) log.Printf("📊 [NEXT_COMMAND] Commande %d: statut = '%s'", commandID, currentStatus) - // ✅ Si la commande n'est plus assignable, la retirer et passer Ă  la suivante nonAssignableStatuses := []string{"livre", "approved", "cancelled", "disabled"} - isNonAssignable := false - for _, s := range nonAssignableStatuses { - if currentStatus == s { - isNonAssignable = true - break - } - } - - if isNonAssignable { - log.Printf("⚠ [NEXT_COMMAND] Commande %d en statut '%s' - Retrait de la queue", commandID, currentStatus) + if slices.Contains(nonAssignableStatuses, currentStatus) { d.RemoveCommandFromAllQueues(commandID, deliveryman) - continue // Essayer la suivante + continue } - // ✅ Commande valide trouvĂ©e ! - log.Printf("✅ [NEXT_COMMAND] Commande %d prĂȘte pour %s (statut: %s)", commandID, deliveryman, currentStatus) - - // Optimiser la queue par proximitĂ© si possible go d.OptimizeDeliverymanQueueByProximity(deliveryman) - // ✅ Mettre Ă  jour le statut basĂ© sur la queue d.UpdateDeliverymanStatusBasedOnQueue(deliveryman) - // Ajouter un log d.AddCommandLog(commandID, "next_in_queue", fmt.Sprintf("Commande suivante dans la queue de %s", deliveryman), "system") @@ -107,9 +88,6 @@ func (d *Database) ProcessNextCommandForDeliveryman(deliveryman string) error { return nil } - // ✅ Si on arrive ici, toutes les commandes Ă©taient invalides - log.Printf("⚠ [NEXT_COMMAND] Toutes les commandes de %s Ă©taient invalides - Queue vidĂ©e", deliveryman) - // Mettre le livreur en available d.SetDeliveryPersonStatus(deliveryman, "available", 0) d.UpdateDeliverymanStatusBasedOnQueue(deliveryman) @@ -138,10 +116,8 @@ func (d *Database) RemoveCommandFromDeliverymanQueue(deliveryman string, command log.Printf("✅ [RemoveFromDeliverymanQueue] Commande %d retirĂ©e", commandID) } - // Supprimer les donnĂ©es Redis.Del(RedisCtx, commandKey) - // DĂ©crĂ©menter le compteur counterKey := fmt.Sprintf("queue:deliveryman:%s:count", deliveryman) Redis.Decr(RedisCtx, counterKey) @@ -149,7 +125,6 @@ func (d *Database) RemoveCommandFromDeliverymanQueue(deliveryman string, command } // CleanupCompletedCommandFromQueue nettoie une commande terminĂ©e et prĂ©pare la suivante -// À appeler depuis les handlers d'annulation et de livraison func (d *Database) CleanupCompletedCommandFromQueue(commandID int, deliveryman string) error { log.Printf("đŸ§č [CLEANUP] Nettoyage commande %d pour %s", commandID, deliveryman) diff --git a/backend/gestion/db/db_referral.go b/backend/gestion/db/db_referral.go index 8e7df660..ecea58ab 100644 --- a/backend/gestion/db/db_referral.go +++ b/backend/gestion/db/db_referral.go @@ -70,13 +70,7 @@ func (d *Database) DebitReferralBalance(username string, amount float64) error { return tx.Commit() } -// RestoreReferralBalance restaure le solde parrainage si la commande Ă©choue aprĂšs le dĂ©bit. -func (d *Database) RestoreReferralBalance(username string, amount float64) error { - return d.CreditClientReferral(username, amount) -} - // UseClientReferralBalance dĂ©duit un montant du solde parrainage dans une transaction. -// Retourne une erreur si le solde est insuffisant. func (d *Database) UseClientReferralBalance(tx *sql.Tx, username string, amount float64) error { if amount <= 0 { return nil diff --git a/backend/gestion/db/db_sanction_client.go b/backend/gestion/db/db_sanction_client.go index 43d1fb0e..53935b9f 100644 --- a/backend/gestion/db/db_sanction_client.go +++ b/backend/gestion/db/db_sanction_client.go @@ -7,7 +7,9 @@ package db import ( "fmt" + "gestion/models" "log" + "sort" ) // GetClientCancellationsCount rĂ©cupĂšre le nombre d'annulations tardives d'un client @@ -45,51 +47,53 @@ func (d *Database) IncrementClientCancellationsCount(username string) error { return fmt.Errorf("client non trouvĂ©") } - log.Printf("✅ [IncrementCancellations] Compteur incrĂ©mentĂ© pour %s", username) - - // Invalider le cache Redis cacheKey := fmt.Sprintf("client:%s", username) Redis.Del(RedisCtx, cacheKey) return nil } -// CalculateCancellationPenalty calcule la pĂ©nalitĂ© selon l'historique -// 1Ăšre fois: 20 points -// 2Ăšme fois: 50 points -// 3Ăšme fois: 100 points -// 4Ăšme+ fois: 150 points +// penaltyForCount retourne le montant du palier applicable pour un nombre d'annulations donnĂ© +func penaltyForCount(count int, tiers []models.PenaltyTier) int { + if len(tiers) == 0 { + return 0 + } + sorted := make([]models.PenaltyTier, len(tiers)) + copy(sorted, tiers) + sort.Slice(sorted, func(i, j int) bool { + return sorted[i].MinCancel > sorted[j].MinCancel + }) + for _, t := range sorted { + if count >= t.MinCancel { + return t.Amount + } + } + return sorted[len(sorted)-1].Amount +} + +// CalculateCancellationPenalty calcule la pĂ©nalitĂ© selon l'historique et le barĂšme configurĂ© func (d *Database) CalculateCancellationPenalty(username string) (int, error) { count, err := d.GetClientCancellationsCount(username) if err != nil { return 0, err } - var penalty int - switch { - case count == 0: - penalty = 20 // PremiĂšre annulation tardive - case count == 1: - penalty = 50 // DeuxiĂšme annulation tardive - case count == 2: - penalty = 100 // TroisiĂšme annulation tardive - default: - penalty = 150 // À partir de la 4Ăšme annulation + settings, err := d.GetSettings() + if err != nil { + log.Printf("⚠ [CalculatePenalty] Impossible de charger les settings, barĂšme par dĂ©faut: %v", err) + settings = DefaultSettings() } + penalty := penaltyForCount(count, settings.PenaltyTiers) + log.Printf("💰 [CalculatePenalty] Client %s - Annulations: %d → PĂ©nalitĂ©: %d points", username, count, penalty) return penalty, nil } -// ApplyCancellationPenalty applique une pĂ©nalitĂ©, incrĂ©mente le compteur et REMET TOUS LES POINTS À ZÉRO -// ✅ MODIFIÉ: RĂ©cupĂšre les points AVANT de les remettre Ă  zĂ©ro pour le log dans le handler +// ApplyCancellationPenalty applique une pĂ©nalitĂ© et incrĂ©mente le compteur d'annulations func (d *Database) ApplyCancellationPenalty(username string) (int, error) { - // ✅ ÉTAPE 0: RĂ©cupĂ©rer les points AVANT modification (pour le handler) - // Note: Le handler rĂ©cupĂšre aussi les points, mais on garde cette fonction autonome - - // Calculer la pĂ©nalitĂ© AVANT d'incrĂ©menter penalty, err := d.CalculateCancellationPenalty(username) if err != nil { return 0, err @@ -97,16 +101,12 @@ func (d *Database) ApplyCancellationPenalty(username string) (int, error) { log.Printf("⚠ [ApplyCancellationPenalty] Client %s - PĂ©nalitĂ© calculĂ©e: %d points", username, penalty) - // ✅ ÉTAPE 1: IncrĂ©menter le compteur d'annulations if err := d.IncrementClientCancellationsCount(username); err != nil { return 0, err } - // ✅ ÉTAPE 2: Mettre l'amende au montant de la pĂ©nalitĂ© ET remettre TOUS les points Ă  zĂ©ro query := `UPDATE clients SET amende = $1, - point = 0, - point_zipette = 0, updated_at = CURRENT_TIMESTAMP WHERE username = $2` @@ -124,9 +124,8 @@ func (d *Database) ApplyCancellationPenalty(username string) (int, error) { return 0, fmt.Errorf("client non trouvĂ©") } - log.Printf("✅ [ApplyCancellationPenalty] %d points d'amende appliquĂ©s Ă  %s + TOUS points remis Ă  0 (weed + zipette)", penalty, username) + log.Printf("✅ [ApplyCancellationPenalty] Amende %d appliquĂ©e Ă  %s", penalty, username) - // Invalider le cache Redis cacheKey := fmt.Sprintf("client:%s", username) Redis.Del(RedisCtx, cacheKey) @@ -134,42 +133,30 @@ func (d *Database) ApplyCancellationPenalty(username string) (int, error) { } // GetClientCancellationHistory rĂ©cupĂšre l'historique d'annulations d'un client -func (d *Database) GetClientCancellationHistory(username string) (map[string]interface{}, error) { +func (d *Database) GetClientCancellationHistory(username string) (map[string]any, error) { + nextPenalty, err := d.CalculateCancellationPenalty(username) + if err != nil { + return nil, err + } + count, err := d.GetClientCancellationsCount(username) if err != nil { return nil, err } - // Calculer la prochaine pĂ©nalitĂ© - var nextPenalty int - switch count { - case 0: - nextPenalty = 20 - case 1: - nextPenalty = 50 - case 2: - nextPenalty = 100 - default: - nextPenalty = 150 - } + settings, _ := d.GetSettings() - // RĂ©cupĂ©rer l'amende actuelle client, err := d.GetClientByUsername(username) var currentAmende float64 if err == nil { currentAmende = client.Amende } - return map[string]interface{}{ + return map[string]any{ "cancellations_count": count, "current_amende": currentAmende, "next_penalty": nextPenalty, - "penalty_scale": map[string]int{ - "1st": 20, - "2nd": 50, - "3rd": 100, - "4th+": 150, - }, - "warning": "TOUS les points de fidĂ©litĂ© (weed/hash ET zipette) seront remis Ă  zĂ©ro lors de la prochaine annulation tardive", + "penalty_tiers": settings.PenaltyTiers, + "warning": "Une amende sera appliquĂ©e lors de la prochaine annulation tardive", }, nil } diff --git a/backend/gestion/db/db_settings.go b/backend/gestion/db/db_settings.go index 4ab836d3..97f46ae0 100644 --- a/backend/gestion/db/db_settings.go +++ b/backend/gestion/db/db_settings.go @@ -3,52 +3,20 @@ package db import ( "encoding/json" "fmt" + "gestion/models" ) -// DaySchedule reprĂ©sente les horaires de livraison pour un jour de la semaine -type DaySchedule struct { - Enabled bool `json:"enabled"` - OpenTime string `json:"open_time"` // ex: "09:00" - CloseTime string `json:"close_time"` // ex: "20:00" -} - -// DeliverySchedule reprĂ©sente les horaires de livraison pour chaque jour -type DeliverySchedule struct { - Monday DaySchedule `json:"monday"` - Tuesday DaySchedule `json:"tuesday"` - Wednesday DaySchedule `json:"wednesday"` - Thursday DaySchedule `json:"thursday"` - Friday DaySchedule `json:"friday"` - Saturday DaySchedule `json:"saturday"` - Sunday DaySchedule `json:"sunday"` -} - // DefaultDeliverySchedule retourne un planning de livraison par dĂ©faut (tous les jours, 9h-20h) -func DefaultDeliverySchedule() DeliverySchedule { - day := DaySchedule{Enabled: true, OpenTime: "09:00", CloseTime: "20:00"} - return DeliverySchedule{ +func DefaultDeliverySchedule() models.DeliverySchedule { + day := models.DaySchedule{Enabled: true, OpenTime: "09:00", CloseTime: "20:00"} + return models.DeliverySchedule{ Monday: day, Tuesday: day, Wednesday: day, Thursday: day, Friday: day, Saturday: day, Sunday: day, } } -// PostalZone reprĂ©sente une zone de livraison avec un minimum de commande -type PostalZone struct { - Name string `json:"name"` - MinAmount float64 `json:"min_amount"` - Codes []string `json:"codes"` -} - -// PointsTier reprĂ©sente un palier du barĂšme de points -// Si Max == 0, il n'y a pas de borne supĂ©rieure (illimitĂ©) -type PointsTier struct { - Min float64 `json:"min"` - Max float64 `json:"max"` // 0 = illimitĂ© - Points int `json:"points"` -} - // CalcPointsFromTiers retourne le nombre de points correspondant au total selon les paliers -func CalcPointsFromTiers(total float64, tiers []PointsTier) int { +func CalcPointsFromTiers(total float64, tiers []models.PointsTier) int { for _, t := range tiers { if total >= t.Min && (t.Max == 0 || total <= t.Max) { return t.Points @@ -57,44 +25,25 @@ func CalcPointsFromTiers(total float64, tiers []PointsTier) int { return 0 } -// PointsPool reprĂ©sente un type de point personnalisable par l'admin -// Pool[0] → colonne `point`, Pool[1] → colonne `point_zipette` -type PointsPool struct { - Key string `json:"key"` // identifiant interne (ex: "pool_0") - Name string `json:"name"` // nom affichĂ© (ex: "Cannabis", "Accessoires") - Categories []string `json:"categories"` // catĂ©gories de produits assignĂ©es Ă  ce pool - Tiers []PointsTier `json:"tiers"` // barĂšme de points -} - -// AppSettings contient les paramĂštres globaux de l'application -type AppSettings struct { - PenaltiesEnabled bool `json:"penalties_enabled"` - ShowAmendeScore bool `json:"show_amende_score"` // afficher le score d'amendes aux clients/cabine - 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 - 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 - NowPaymentsIPNSecret string `json:"nowpayments_ipn_secret"` // secret IPN NowPayments - NowPaymentsCurrencies []string `json:"nowpayments_currencies"` // cryptos acceptĂ©es (ex: ["btc","eth","ltc"]) - DeliverySchedule DeliverySchedule `json:"delivery_schedule"` // horaires de livraison par jour - PostalZones []PostalZone `json:"postal_zones"` // zones de livraison avec minimum de commande -} - // DefaultSettings retourne les paramĂštres par dĂ©faut -func DefaultSettings() AppSettings { - return AppSettings{ +func DefaultSettings() models.AppSettings { + return models.AppSettings{ PenaltiesEnabled: true, ShowAmendeScore: true, - PointsEnabled: true, - ReferralEnabled: true, - PointsPools: []PointsPool{ + PenaltyTiers: []models.PenaltyTier{ + {MinCancel: 0, Amount: 20}, + {MinCancel: 1, Amount: 50}, + {MinCancel: 2, Amount: 100}, + {MinCancel: 3, Amount: 150}, + }, + PointsEnabled: true, + ReferralEnabled: true, + PointsPools: []models.PointsPool{ { Key: "pool_0", Name: "Pool 1", Categories: []string{}, - Tiers: []PointsTier{ + Tiers: []models.PointsTier{ {Min: 30, Max: 50, Points: 1}, {Min: 60, Max: 150, Points: 2}, {Min: 160, Max: 300, Points: 3}, @@ -106,15 +55,19 @@ func DefaultSettings() AppSettings { Key: "pool_1", Name: "Pool 2", Categories: []string{}, - Tiers: []PointsTier{ + Tiers: []models.PointsTier{ {Min: 30, Max: 100, Points: 1}, {Min: 110, Max: 200, Points: 2}, {Min: 210, Max: 0, Points: 3}, }, }, }, + DeliveryMode: models.DeliveryModeConfig{ + Mode: "single", + CategoryRoutes: []models.CategoryRoute{}, + }, DeliverySchedule: DefaultDeliverySchedule(), - PostalZones: []PostalZone{ + PostalZones: []models.PostalZone{ {Name: "Zone 30€", MinAmount: 30, Codes: []string{"44000", "44100", "44200", "44300"}}, {Name: "Zone 50€", MinAmount: 50, Codes: []string{ "44400", "44880", "44120", "44230", "44115", @@ -128,7 +81,7 @@ func DefaultSettings() AppSettings { } // GetSettings rĂ©cupĂšre les paramĂštres depuis la DB -func (d *Database) GetSettings() (AppSettings, error) { +func (d *Database) GetSettings() (models.AppSettings, error) { settings := DefaultSettings() rows, err := d.Query(`SELECT key, value FROM app_settings`) @@ -150,7 +103,7 @@ func (d *Database) GetSettings() (AppSettings, error) { case "points_enabled": settings.PointsEnabled = value == "true" case "points_pools": - var pools []PointsPool + var pools []models.PointsPool if err := json.Unmarshal([]byte(value), &pools); err == nil { settings.PointsPools = pools } @@ -170,22 +123,31 @@ func (d *Database) GetSettings() (AppSettings, error) { settings.NowPaymentsCurrencies = currencies } case "delivery_schedule": - var sched DeliverySchedule + var sched models.DeliverySchedule if err := json.Unmarshal([]byte(value), &sched); err == nil { settings.DeliverySchedule = sched } case "postal_zones": - var zones []PostalZone + var zones []models.PostalZone if err := json.Unmarshal([]byte(value), &zones); err == nil { settings.PostalZones = zones } + case "telegram_bot_token": + settings.TelegramBotToken = value + case "telegram_bot_username": + settings.TelegramBotUsername = value + case "delivery_mode": + var mode models.DeliveryModeConfig + if err := json.Unmarshal([]byte(value), &mode); err == nil { + settings.DeliveryMode = mode + } } } return settings, rows.Err() } // UpdateSettings sauvegarde les paramĂštres dans la DB -func (d *Database) UpdateSettings(s AppSettings) error { +func (d *Database) UpdateSettings(s models.AppSettings) error { boolStr := func(b bool) string { if b { return "true" @@ -194,7 +156,7 @@ func (d *Database) UpdateSettings(s AppSettings) error { } if s.PointsPools == nil { - s.PointsPools = []PointsPool{} + s.PointsPools = []models.PointsPool{} } // S'assurer que chaque pool a des slices non-nil for i := range s.PointsPools { @@ -202,7 +164,7 @@ func (d *Database) UpdateSettings(s AppSettings) error { s.PointsPools[i].Categories = []string{} } if s.PointsPools[i].Tiers == nil { - s.PointsPools[i].Tiers = []PointsTier{} + s.PointsPools[i].Tiers = []models.PointsTier{} } } @@ -248,13 +210,24 @@ func (d *Database) UpdateSettings(s AppSettings) error { pairs = append(pairs, [2]string{"delivery_schedule", string(schedJSON)}) if s.PostalZones == nil { - s.PostalZones = []PostalZone{} + s.PostalZones = []models.PostalZone{} } zonesJSON, err := json.Marshal(s.PostalZones) if err != nil { return fmt.Errorf("erreur sĂ©rialisation postal_zones: %w", err) } pairs = append(pairs, [2]string{"postal_zones", string(zonesJSON)}) + pairs = append(pairs, [2]string{"telegram_bot_token", s.TelegramBotToken}) + pairs = append(pairs, [2]string{"telegram_bot_username", s.TelegramBotUsername}) + + if s.DeliveryMode.CategoryRoutes == nil { + s.DeliveryMode.CategoryRoutes = []models.CategoryRoute{} + } + deliveryModeJSON, err := json.Marshal(s.DeliveryMode) + if err != nil { + return fmt.Errorf("erreur sĂ©rialisation delivery_mode: %w", err) + } + pairs = append(pairs, [2]string{"delivery_mode", string(deliveryModeJSON)}) for _, p := range pairs { if _, err = tx.Exec(upsert, p[0], p[1]); err != nil { diff --git a/backend/gestion/db/db_telegram.go b/backend/gestion/db/db_telegram.go new file mode 100644 index 00000000..44ed1210 --- /dev/null +++ b/backend/gestion/db/db_telegram.go @@ -0,0 +1,121 @@ +package db + +import ( + "crypto/rand" + "database/sql" + "encoding/hex" + "encoding/json" + "fmt" + "gestion/models" + "log" + "time" +) + +// MigrateAddTelegramColumns ajoute les colonnes telegram_chat_id si elles n'existent pas +func (d *Database) MigrateAddTelegramColumns() { + migrations := []string{ + `ALTER TABLE clients ADD COLUMN IF NOT EXISTS telegram_chat_id BIGINT`, + `ALTER TABLE users ADD COLUMN IF NOT EXISTS telegram_chat_id BIGINT`, + } + for _, q := range migrations { + if _, err := d.Exec(q); err != nil { + log.Printf("⚠ [TELEGRAM_MIGRATION] %v", err) + } + } + log.Println("✅ [TELEGRAM] Colonnes telegram_chat_id vĂ©rifiĂ©es") +} + +const linkTokenTTL = 10 * time.Minute + +// GenerateLinkToken crĂ©e un token alĂ©atoire sĂ©curisĂ© et le stocke dans Redis (10 min) +func GenerateLinkToken(username, role string) (string, error) { + raw := make([]byte, 16) + if _, err := rand.Read(raw); err != nil { + return "", fmt.Errorf("gĂ©nĂ©ration token: %w", err) + } + token := hex.EncodeToString(raw) + + data := models.TelegramLinkData{Username: username, Role: role} + val, err := json.Marshal(data) + if err != nil { + return "", err + } + + key := fmt.Sprintf("telegram:link:%s", token) + if err := Redis.Set(RedisCtx, key, val, linkTokenTTL).Err(); err != nil { + return "", fmt.Errorf("Redis SET: %w", err) + } + return token, nil +} + +// ValidateAndConsumeLinkToken valide le token, retourne les donnĂ©es, puis le supprime +func ValidateAndConsumeLinkToken(token string) (username, role string, err error) { + key := fmt.Sprintf("telegram:link:%s", token) + + val, err := Redis.Get(RedisCtx, key).Bytes() + if err != nil { + return "", "", fmt.Errorf("token invalide ou expirĂ©") + } + + var data models.TelegramLinkData + if err := json.Unmarshal(val, &data); err != nil { + return "", "", fmt.Errorf("donnĂ©es corrompues") + } + + Redis.Del(RedisCtx, key) + + return data.Username, data.Role, nil +} + +func (d *Database) SaveClientTelegramChatID(username string, chatID int64) error { + _, err := d.Exec(`UPDATE clients SET telegram_chat_id = $1 WHERE username = $2`, chatID, username) + return err +} + +func (d *Database) GetClientTelegramChatID(username string) (int64, bool, error) { + var chatID sql.NullInt64 + err := d.QueryRow(`SELECT telegram_chat_id FROM clients WHERE username = $1`, username).Scan(&chatID) + if err != nil { + return 0, false, err + } + return chatID.Int64, chatID.Valid, nil +} + +func (d *Database) DeleteClientTelegramChatID(username string) error { + _, err := d.Exec(`UPDATE clients SET telegram_chat_id = NULL WHERE username = $1`, username) + return err +} + +func (d *Database) SaveUserTelegramChatID(username string, chatID int64) error { + _, err := d.Exec(`UPDATE users SET telegram_chat_id = $1 WHERE username = $2`, chatID, username) + return err +} + +func (d *Database) GetUserTelegramChatID(username string) (int64, bool, error) { + var chatID sql.NullInt64 + err := d.QueryRow(`SELECT telegram_chat_id FROM users WHERE username = $1`, username).Scan(&chatID) + if err != nil { + return 0, false, err + } + return chatID.Int64, chatID.Valid, nil +} + +func (d *Database) DeleteUserTelegramChatID(username string) error { + _, err := d.Exec(`UPDATE users SET telegram_chat_id = NULL WHERE username = $1`, username) + return err +} + +// GetUserByTelegramChatID retrouve un utilisateur (clients + users) par chat_id +func (d *Database) GetUserByTelegramChatID(chatID int64) (username, role string, err error) { + err = d.QueryRow(`SELECT username FROM clients WHERE telegram_chat_id = $1`, chatID).Scan(&username) + if err == nil { + return username, "client", nil + } + + err = d.QueryRow(`SELECT username, role FROM users WHERE telegram_chat_id = $1`, chatID).Scan(&username, &role) + if err == nil { + return username, role, nil + } + + return "", "", fmt.Errorf("aucun compte liĂ© Ă  ce chat_id") +} diff --git a/backend/gestion/db/db_users.go b/backend/gestion/db/db_users.go index a8b9c7da..639dc0ac 100644 --- a/backend/gestion/db/db_users.go +++ b/backend/gestion/db/db_users.go @@ -9,8 +9,8 @@ import ( ) func (d *Database) CreateUser(user *models.User) error { - query := `INSERT INTO users (username, password, role, created_at, updated_at) - VALUES ($1, $2, $3, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + query := `INSERT INTO users (username, password, role, created_at, updated_at) + VALUES ($1, $2, $3, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) RETURNING id, created_at, updated_at` var createdAt, updatedAt time.Time @@ -28,7 +28,7 @@ func (d *Database) CreateUser(user *models.User) error { // GetAllUsers rĂ©cupĂšre tous les utilisateurs func (d *Database) GetAllUsers() ([]*models.User, error) { - query := `SELECT id, username, password, role, created_at, updated_at + query := `SELECT id, username, password, role, created_at, updated_at FROM users ORDER BY created_at DESC` rows, err := d.Query(query) @@ -100,8 +100,8 @@ func (d *Database) GetAllDeliveryMen() ([]*models.User, error) { // UpdateUser met Ă  jour un utilisateur existant func (d *Database) UpdateUser(user *models.User) error { - query := `UPDATE users - SET username = $1, password = $2, role = $3, updated_at = CURRENT_TIMESTAMP + query := `UPDATE users + SET username = $1, password = $2, role = $3, updated_at = CURRENT_TIMESTAMP WHERE id = $4` result, err := d.Exec(query, user.Username, user.Password, user.Role, user.ID) @@ -123,7 +123,6 @@ func (d *Database) UpdateUser(user *models.User) error { // DeleteUser supprime un utilisateur func (d *Database) DeleteUser(id int) error { - // RĂ©cupĂ©rer le rĂŽle de l'utilisateur avant de le supprimer var role string err := d.QueryRow(`SELECT role FROM users WHERE id = $1`, id).Scan(&role) if err != nil { @@ -133,7 +132,6 @@ func (d *Database) DeleteUser(id int) error { return fmt.Errorf("erreur lors de la rĂ©cupĂ©ration du rĂŽle: %w", err) } - // ✅ MODIFIÉ : Supprimer tous les tokens de l'utilisateur _ = d.RevokeAllUserTokens(id, role) query := `DELETE FROM users WHERE id = $1` @@ -159,7 +157,7 @@ func (d *Database) DeleteUser(id int) error { // GetUserByID rĂ©cupĂšre un utilisateur par son ID func (d *Database) GetUserByID(id int) (*models.User, error) { user := &models.User{} - query := `SELECT id, username, password, role, created_at, updated_at + query := `SELECT id, username, password, role, created_at, updated_at FROM users WHERE id = $1` var createdAt, updatedAt time.Time @@ -184,7 +182,7 @@ func (d *Database) GetUserByID(id int) (*models.User, error) { func (d *Database) GetUserByUsername(username string) (*models.User, error) { user := &models.User{} - query := `SELECT id, username, password, role + query := `SELECT id, username, password, role FROM users WHERE username = $1` err := d.QueryRow(query, username).Scan( @@ -194,7 +192,6 @@ func (d *Database) GetUserByUsername(username string) (*models.User, error) { &user.Role, ) - // ✅ VĂ©rifier sql.ErrNoRows et retourner une erreur if err == sql.ErrNoRows { return nil, fmt.Errorf("utilisateur non trouvĂ©") } diff --git a/backend/gestion/db/redis_deliveryman_assignment.go b/backend/gestion/db/redis_deliveryman_assignment.go index d0922b9e..e9d1b308 100644 --- a/backend/gestion/db/redis_deliveryman_assignment.go +++ b/backend/gestion/db/redis_deliveryman_assignment.go @@ -9,7 +9,6 @@ import ( ) // FindLeastLoadedDeliveryman trouve le livreur avec le moins de commandes ET qui peut accepter -// ✅ Respecte le statut BUSY (queue >= 10) func (d *Database) FindLeastLoadedDeliveryman() (string, error) { keys, err := Redis.Keys(RedisCtx, "delivery:status:*").Result() if err != nil || len(keys) == 0 { @@ -30,7 +29,6 @@ func (d *Database) FindLeastLoadedDeliveryman() (string, error) { var status models.DeliveryPersonStatus json.Unmarshal([]byte(data), &status) - // ✅ Ignorer les livreurs offline if status.Status == "offline" { continue } @@ -70,7 +68,6 @@ func (d *Database) FindLeastLoadedDeliveryman() (string, error) { } // FindAvailableOrLeastLoadedDeliveryman trouve un livreur disponible ou le moins chargĂ© -// ✅ Respecte le statut BUSY func (d *Database) FindAvailableOrLeastLoadedDeliveryman() (string, string, int, error) { keys, err := Redis.Keys(RedisCtx, "delivery:status:*").Result() if err != nil || len(keys) == 0 { @@ -96,7 +93,6 @@ func (d *Database) FindAvailableOrLeastLoadedDeliveryman() (string, string, int, continue } - // ✅ NOUVEAU: VĂ©rifier si le livreur peut accepter if !d.CanDeliverymanAcceptCommands(username) { continue } @@ -104,7 +100,6 @@ func (d *Database) FindAvailableOrLeastLoadedDeliveryman() (string, string, int, queueKey := fmt.Sprintf("queue:deliveryman:%s", username) queueSize, _ := Redis.ZCard(RedisCtx, queueKey).Result() - // PrioritĂ©: available > busy avec moins de commandes if status.Status == "available" && queueSize == 0 { return username, "available", 0, nil } @@ -124,7 +119,6 @@ func (d *Database) FindAvailableOrLeastLoadedDeliveryman() (string, string, int, } // GetLeastLoadedDeliverymanForced retourne le livreur avec le moins de commandes (SANS limite) -// ⚠ À utiliser uniquement pour assignation forcĂ©e par admin func (d *Database) GetLeastLoadedDeliverymanForced() (string, int64, error) { activeUsernames, err := d.GetAllActiveDeliverymenUsernames() if err != nil || len(activeUsernames) == 0 { @@ -144,9 +138,6 @@ func (d *Database) GetLeastLoadedDeliverymanForced() (string, int64, error) { } } - log.Printf("⚠ [FORCED] %s sĂ©lectionnĂ© (%d commandes - SANS LIMITE)", - leastLoaded, minQueueSize) - return leastLoaded, minQueueSize, nil } @@ -157,7 +148,6 @@ func (d *Database) AreAllDeliverymenAtCapacity() (bool, int, error) { return false, 0, fmt.Errorf("aucun livreur actif") } - // Si un seul livreur, jamais Ă  capacitĂ© max if len(activeUsernames) == 1 { return false, 1, nil } diff --git a/backend/gestion/db/redis_deliveryman_management.go b/backend/gestion/db/redis_deliveryman_management.go index e50fa8c2..8a6a0992 100644 --- a/backend/gestion/db/redis_deliveryman_management.go +++ b/backend/gestion/db/redis_deliveryman_management.go @@ -10,9 +10,8 @@ import ( ) func (d *Database) UpdateDeliveryPersonLocation(username string, lat, lon float64) error { - // 1ïžâƒŁ Mettre Ă  jour la position GPS dans Redis key := fmt.Sprintf("delivery:location:%s", username) - location := map[string]interface{}{ + location := map[string]any{ "latitude": lat, "longitude": lon, "last_update": time.Now().Unix(), @@ -25,25 +24,20 @@ func (d *Database) UpdateDeliveryPersonLocation(username string, lat, lon float6 log.Printf("📍 Position GPS mise Ă  jour pour %s: (%.6f, %.6f)", username, lat, lon) - // 2ïžâƒŁ ✅ NOUVEAU: Auto-initialiser/synchroniser le statut statusKey := fmt.Sprintf("delivery:status:%s", username) statusData, err := Redis.Get(RedisCtx, statusKey).Result() if err != nil || statusData == "" { - // ✅ Pas de statut → CrĂ©er "available" par dĂ©faut log.Printf("🆕 [INIT_STATUS] CrĂ©ation statut 'available' pour %s (premiĂšre position GPS)", username) d.SetDeliveryPersonStatus(username, "available", 0) } else { - // ✅ Statut existe → VĂ©rifier s'il faut le rĂ©activer var status models.DeliveryPersonStatus json.Unmarshal([]byte(statusData), &status) if status.Status == "offline" { - // Si le livreur Ă©tait offline et envoie sa position → Le remettre available log.Printf("🔄 [REACTIVATE] %s passe de 'offline' Ă  'available' (position GPS reçue)", username) d.SetDeliveryPersonStatus(username, "available", 0) } else { - // ✅ Statut actif → Synchroniser basĂ© sur la queue go d.UpdateDeliverymanStatusBasedOnQueue(username) } } @@ -66,12 +60,11 @@ func (d *Database) GetDeliveryPersonLocation(username string) (float64, float64, return 0, 0, fmt.Errorf("aucune donnĂ©e de position pour %s", username) } - var location map[string]interface{} + var location map[string]any if err := json.Unmarshal([]byte(data), &location); err != nil { return 0, 0, fmt.Errorf("erreur parsing JSON Redis: %w", err) } - // Extraire latitude avec gestion de type robuste var lat, lon float64 if v, ok := location["latitude"]; ok && v != nil { switch val := v.(type) { @@ -88,7 +81,6 @@ func (d *Database) GetDeliveryPersonLocation(username string) (float64, float64, } } - // Extraire longitude avec gestion de type robuste if v, ok := location["longitude"]; ok && v != nil { switch val := v.(type) { case float64: diff --git a/backend/gestion/db/redis_eta_management.go b/backend/gestion/db/redis_eta_management.go index 98bd3b37..187d7a51 100644 --- a/backend/gestion/db/redis_eta_management.go +++ b/backend/gestion/db/redis_eta_management.go @@ -3,7 +3,6 @@ package db import ( "fmt" "gestion/services" - "log" "strconv" "time" @@ -16,7 +15,7 @@ func (d *Database) SetCommandETA(commandID, minutes int) error { now := time.Now() arrivalTime := now.Add(time.Duration(minutes) * time.Minute) - eta := map[string]interface{}{ + eta := map[string]any{ "command_id": commandID, "total_eta_minutes": minutes, "eta_minutes": minutes, @@ -37,9 +36,6 @@ func (d *Database) SetCommandETA(commandID, minutes int) error { d.ScheduleETANotifications(commandID, minutes) - log.Printf("✅ ETA dĂ©fini pour commande %d: %d minutes (arrivĂ©e: %s)", - commandID, minutes, arrivalTime.Format("15:04")) - return nil } @@ -86,7 +82,6 @@ func (d *Database) ScheduleETANotifications(commandID, etaMinutes int) error { }) } - log.Printf("✅ Notifications programmĂ©es pour commande %d (ETA: %d min)", commandID, etaMinutes) return nil } @@ -121,15 +116,12 @@ func (d *Database) SendETANotification(commandID int, notifType string) { channel := fmt.Sprintf("notifications:command:%d", commandID) Redis.Publish(RedisCtx, channel, message) - - log.Printf("📱 Notification envoyĂ©e: %s", message) } // CalculateETAForDeliveryman calcule l'ETA entre un livreur et une destination func (d *Database) CalculateETAForDeliveryman(deliveryman string, destLat, destLng float64) int { livreurLat, livreurLng, err := d.GetDeliveryPersonLocation(deliveryman) if err != nil { - log.Printf("⚠ Position livreur %s non trouvĂ©e, utilisation ETA par dĂ©faut", deliveryman) return services.MinETA } @@ -145,8 +137,6 @@ func (d *Database) CalculateETAForDeliveryman(deliveryman string, destLat, destL distance := services.CalculateDistance(from, to) eta := services.CalculateETA(distance) - log.Printf("📍 ETA calculĂ© pour %s: %.2f km -> %d min", deliveryman, distance, eta) - return eta } @@ -163,7 +153,7 @@ func (d *Database) CalculateETABetweenPoints(lat1, lng1, lat2, lng2 float64) int return services.CalculateETA(distance) } -func (d *Database) GetDeliverymanQueueStats(deliveryman string) (map[string]interface{}, error) { +func (d *Database) GetDeliverymanQueueStats(deliveryman string) (map[string]any, error) { return d.GetDeliverymanQueueInfo(deliveryman) } @@ -173,7 +163,7 @@ func (d *Database) SetCommandETAWithDetails(commandID, totalETA, queuePosition i now := time.Now() arrivalTime := now.Add(time.Duration(totalETA) * time.Minute) - eta := map[string]interface{}{ + eta := map[string]any{ "command_id": commandID, "total_eta_minutes": totalETA, "queue_position": queuePosition, @@ -189,8 +179,5 @@ func (d *Database) SetCommandETAWithDetails(commandID, totalETA, queuePosition i Redis.Expire(RedisCtx, key, 4*time.Hour) - log.Printf("✅ ETA dĂ©taillĂ© pour commande %d: Total=%dmin Position=%d", - commandID, totalETA, queuePosition) - return nil } diff --git a/backend/gestion/db/redis_position.go b/backend/gestion/db/redis_position.go index 763d73d8..3973f4fa 100644 --- a/backend/gestion/db/redis_position.go +++ b/backend/gestion/db/redis_position.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "gestion/models" - "log" "math" "time" ) @@ -74,10 +73,6 @@ func (d *Database) UpdateLivreurPosition(authUsername string, lat, lon float64, return fmt.Errorf("erreur mise Ă  jour statut: %w", err) } - // đŸ”č 5ïžâƒŁ Logs anonymisĂ©s (troncature) - log.Printf("📍 Position GPS mise Ă  jour pour %s: (lat: %.4f, lon: %.4f)", authUsername, truncate(lat), truncate(lon)) - log.Printf("✅ Statut mis Ă  jour pour %s: %s", authUsername, status) - // đŸ”č 6ïžâƒŁ Publication Ă©vĂ©nement sĂ©curisĂ©e (Ă  sĂ©curiser cĂŽtĂ© subscriber) d.PublishDeliveryPersonLocationUpdate(authUsername, lat, lon) @@ -110,8 +105,3 @@ func isValidCoordinates(lat, lon float64) bool { lat >= -90 && lat <= 90 && lon >= -180 && lon <= 180 } - -// Tronque les coordonnĂ©es pour logs (4 dĂ©cimales suffisent pour anonymiser) -func truncate(f float64) float64 { - return math.Round(f*10000) / 10000 -} diff --git a/backend/gestion/db/redis_pubsub.go b/backend/gestion/db/redis_pubsub.go index 561bba4b..dcb82596 100644 --- a/backend/gestion/db/redis_pubsub.go +++ b/backend/gestion/db/redis_pubsub.go @@ -11,7 +11,7 @@ import ( func (d *Database) PublishCommandEvent(commandID int, eventType, message string) { channel := fmt.Sprintf("events:command:%d", commandID) - event := map[string]interface{}{ + event := map[string]any{ "command_id": commandID, "type": eventType, "message": message, @@ -24,7 +24,7 @@ func (d *Database) PublishCommandEvent(commandID int, eventType, message string) // PublishDeliveryPersonLocationUpdate publie un Ă©vĂ©nement de mise Ă  jour de position func (d *Database) PublishDeliveryPersonLocationUpdate(username string, lat, lon float64) { - message := map[string]interface{}{ + message := map[string]any{ "type": "location_update", "username": username, "latitude": lat, diff --git a/backend/gestion/db/redis_queue_assignment.go b/backend/gestion/db/redis_queue_assignment.go index 285c3b16..4013c126 100644 --- a/backend/gestion/db/redis_queue_assignment.go +++ b/backend/gestion/db/redis_queue_assignment.go @@ -26,7 +26,6 @@ func (d *Database) AssignCommandToDeliverymanQueue(commandID int, deliveryman st return fmt.Errorf("livreur %s a atteint le maximum de commandes (%d)", deliveryman, MAX_COMMANDS_PER_DELIVERYMAN) } - // ✅ MODIFIÉ: ETA = temps de trajet direct uniquement totalETA := estimatedTravelTime var lat, lng float64 @@ -86,9 +85,6 @@ func (d *Database) AssignCommandToDeliverymanQueue(commandID int, deliveryman st deliveryman, currentQueueSize+1, limitInfo, totalETA), "system") - log.Printf("✅ Commande %d -> Queue %s (pos: %d%s, ETA trajet: %d min)", - commandID, deliveryman, currentQueueSize+1, limitInfo, totalETA) - return nil } @@ -168,13 +164,10 @@ func (d *Database) AssignCommandToDeliverymanQueueWithCoords(commandID int, deli return fmt.Errorf("erreur ajout Ă  la queue: %w", err) } - // ============================================ - // ✅ CORRECTION: Mettre Ă  jour livreur_assign dans la DB - // ============================================ - updateQuery := `UPDATE commandes - SET livreur_assign = $1, + updateQuery := `UPDATE commandes + SET livreur_assign = $1, status = 'assigned', - updated_at = CURRENT_TIMESTAMP + updated_at = CURRENT_TIMESTAMP WHERE id = $2` _, err = d.Exec(updateQuery, deliveryman, commandID) @@ -213,7 +206,6 @@ func (d *Database) AssignCommandToDeliverymanQueueWithCoords(commandID int, deli return nil } -// ForceAssignCommandToDeliverymanWithCoords assigne une commande de force avec coordonnĂ©es // ForceAssignCommandToDeliverymanWithCoords assigne une commande de force avec coordonnĂ©es func (d *Database) ForceAssignCommandToDeliverymanWithCoords(commandID int, deliveryman string, estimatedTravelTime int, lat, lng float64, address string) error { command, err := d.GetCommandByID(commandID) diff --git a/backend/gestion/db/redis_queue_info.go b/backend/gestion/db/redis_queue_info.go index 5c4e57bb..5a27b350 100644 --- a/backend/gestion/db/redis_queue_info.go +++ b/backend/gestion/db/redis_queue_info.go @@ -8,27 +8,22 @@ import ( ) // GetDeliverymanQueueInfo rĂ©cupĂšre les infos de queue d'un livreur -func (d *Database) GetDeliverymanQueueInfo(deliveryman string) (map[string]interface{}, error) { +func (d *Database) GetDeliverymanQueueInfo(deliveryman string) (map[string]any, error) { queueKey := fmt.Sprintf("queue:deliveryman:%s", deliveryman) - // Nombre de commandes dans la queue queueSize, _ := Redis.ZCard(RedisCtx, queueKey).Result() - // RĂ©cupĂ©rer toutes les commandes commandIDs, _ := Redis.ZRange(RedisCtx, queueKey, 0, -1).Result() - // VĂ©rifier le nombre de livreurs actifs pour dĂ©terminer la limite activeCount, _ := d.CountActiveDeliverymen() - // RĂ©cupĂ©rer les dĂ©tails de chaque commande - var commands []map[string]interface{} + var commands []map[string]any for i, cmdIDStr := range commandIDs { commandID := extractCommandID(cmdIDStr) if commandID <= 0 { continue } - // RĂ©cupĂ©rer les donnĂ©es de la commande commandKey := fmt.Sprintf("queue:pending:%d", commandID) data, err := Redis.Get(RedisCtx, commandKey).Result() if err != nil { @@ -40,7 +35,7 @@ func (d *Database) GetDeliverymanQueueInfo(deliveryman string) (map[string]inter continue } - commands = append(commands, map[string]interface{}{ + commands = append(commands, map[string]any{ "position": i + 1, "command_id": commandID, "address": queueItem.Address, @@ -51,15 +46,12 @@ func (d *Database) GetDeliverymanQueueInfo(deliveryman string) (map[string]inter }) } - // DĂ©terminer si le livreur peut accepter plus de commandes canAcceptMore := true if activeCount > 1 { - // Plusieurs livreurs: limite de 10 canAcceptMore = queueSize < MAX_COMMANDS_PER_DELIVERYMAN } - // Si un seul livreur: pas de limite (canAcceptMore reste true) - return map[string]interface{}{ + return map[string]any{ "deliveryman": deliveryman, "queue_size": queueSize, "commands": commands, @@ -69,15 +61,13 @@ func (d *Database) GetDeliverymanQueueInfo(deliveryman string) (map[string]inter }, nil } -func (d *Database) GetAllQueuesOverview() (map[string]interface{}, error) { - overview := make(map[string]interface{}) +func (d *Database) GetAllQueuesOverview() (map[string]any, error) { + overview := make(map[string]any) - // Queue gĂ©nĂ©rale generalQueueSize, _ := Redis.ZCard(RedisCtx, "queue:pending:sorted").Result() overview["general_queue"] = generalQueueSize - // Queues par livreur avec dĂ©tails - deliverymanQueues := make(map[string]interface{}) + deliverymanQueues := make(map[string]any) keys, _ := Redis.Keys(RedisCtx, "queue:deliveryman:*").Result() for _, key := range keys { @@ -88,7 +78,7 @@ func (d *Database) GetAllQueuesOverview() (map[string]interface{}, error) { username := key[len("queue:deliveryman:"):] queueSize, _ := Redis.ZCard(RedisCtx, key).Result() - deliverymanQueues[username] = map[string]interface{}{ + deliverymanQueues[username] = map[string]any{ "queue_size": queueSize, "can_accept_more": queueSize < MAX_COMMANDS_PER_DELIVERYMAN, "capacity": fmt.Sprintf("%d/%d", queueSize, MAX_COMMANDS_PER_DELIVERYMAN), @@ -96,8 +86,6 @@ func (d *Database) GetAllQueuesOverview() (map[string]interface{}, error) { } overview["deliveryman_queues"] = deliverymanQueues - - // Total var totalPending int64 = generalQueueSize for _, key := range keys { if len(key) > 6 && key[len(key)-6:] == ":count" { @@ -112,11 +100,10 @@ func (d *Database) GetAllQueuesOverview() (map[string]interface{}, error) { } // GetQueueStats - Statistiques dĂ©taillĂ©es -func (d *Database) GetQueueStats() (map[string]interface{}, error) { +func (d *Database) GetQueueStats() (map[string]any, error) { normalCount, _ := Redis.ZCard(RedisCtx, "queue:pending:sorted").Result() priorityCount, _ := Redis.ZCard(RedisCtx, "queue:priority:sorted").Result() - // Compter les commandes dans les queues des livreurs var deliverymanQueueCount int64 keys, _ := Redis.Keys(RedisCtx, "queue:deliveryman:*").Result() for _, key := range keys { @@ -127,7 +114,6 @@ func (d *Database) GetQueueStats() (map[string]interface{}, error) { deliverymanQueueCount += count } - // Calculer temps d'attente moyen var totalWaitTime int64 var commandCount int64 @@ -154,7 +140,7 @@ func (d *Database) GetQueueStats() (map[string]interface{}, error) { avgWaitTime = int(totalWaitTime / commandCount) } - stats := map[string]interface{}{ + stats := map[string]any{ "total_pending": normalCount + priorityCount, "general_queue": normalCount, "priority_queue": priorityCount, diff --git a/backend/gestion/db/redis_queue_management.go b/backend/gestion/db/redis_queue_management.go index 33d93a72..13a5103a 100644 --- a/backend/gestion/db/redis_queue_management.go +++ b/backend/gestion/db/redis_queue_management.go @@ -16,8 +16,6 @@ import ( // ============================================ // UpdateDeliverymanStatusBasedOnQueue met Ă  jour automatiquement le statut -// ✅ Status = "busy" si queue >= 10 -// ✅ Status = "available" si queue < 10 func (d *Database) UpdateDeliverymanStatusBasedOnQueue(deliveryman string) error { queueKey := fmt.Sprintf("queue:deliveryman:%s", deliveryman) queueSize, err := Redis.ZCard(RedisCtx, queueKey).Result() @@ -25,7 +23,6 @@ func (d *Database) UpdateDeliverymanStatusBasedOnQueue(deliveryman string) error return fmt.Errorf("erreur rĂ©cupĂ©ration taille queue: %w", err) } - // RĂ©cupĂ©rer le statut actuel statusKey := fmt.Sprintf("delivery:status:%s", deliveryman) data, err := Redis.Get(RedisCtx, statusKey).Result() if err != nil { @@ -38,18 +35,14 @@ func (d *Database) UpdateDeliverymanStatusBasedOnQueue(deliveryman string) error return err } - // DĂ©terminer le nouveau statut var newStatus string var currentCommand int if queueSize >= MAX_COMMANDS_PER_DELIVERYMAN { - // 🔮 BUSY car queue pleine (10 commandes ou plus) newStatus = "busy" currentCommand = 0 log.Printf("🔮 [STATUS] %s -> BUSY (queue pleine: %d/10)", deliveryman, queueSize) } else { - // 🟱 AVAILABLE tant que queue < 10 - // Exception: si le livreur est en train de livrer (delivering), on garde ce statut if status.Status == "delivering" && status.CurrentCommand > 0 { newStatus = "delivering" currentCommand = status.CurrentCommand @@ -62,19 +55,16 @@ func (d *Database) UpdateDeliverymanStatusBasedOnQueue(deliveryman string) error } } - // Mettre Ă  jour le statut return d.SetDeliveryPersonStatus(deliveryman, newStatus, currentCommand) } // CanDeliverymanAcceptCommands vĂ©rifie si un livreur peut accepter de nouvelles commandes -// ✅ Retourne false si: status=busy ET queue>=10, ou status=offline func (d *Database) CanDeliverymanAcceptCommands(deliveryman string) bool { - // 1. VĂ©rifier le statut Redis statusKey := fmt.Sprintf("delivery:status:%s", deliveryman) data, err := Redis.Get(RedisCtx, statusKey).Result() if err != nil { log.Printf("⚠ [CHECK] Livreur %s sans statut Redis", deliveryman) - return true // Fallback: autoriser si pas de statut + return true } var status models.DeliveryPersonStatus @@ -82,9 +72,7 @@ func (d *Database) CanDeliverymanAcceptCommands(deliveryman string) bool { return true } - // 2. Si offline, refuser if status.Status == "offline" { - log.Printf("⚫ [CHECK] %s REFUSÉ: offline", deliveryman) return false } @@ -122,7 +110,6 @@ func (d *Database) GetAvailableDeliveryPersonsForAssignment() ([]models.Delivery continue } - // ✅ VĂ©rifier si le livreur peut accepter des commandes if d.CanDeliverymanAcceptCommands(status.Username) { available = append(available, status) } @@ -144,11 +131,9 @@ func (d *Database) AddToDeliverymanQueue(deliveryman string, queueItem models.Co return fmt.Errorf("erreur serialisation: %w", err) } - // Sauvegarder les donnĂ©es de la commande commandKey := fmt.Sprintf("queue:pending:%d", queueItem.CommandID) Redis.Set(RedisCtx, commandKey, data, 24*time.Hour) - // Ajouter Ă  la queue sorted set du livreur (score = timestamp) queueKey := fmt.Sprintf("queue:deliveryman:%s", deliveryman) score := float64(time.Now().Unix()) @@ -161,13 +146,9 @@ func (d *Database) AddToDeliverymanQueue(deliveryman string, queueItem models.Co return fmt.Errorf("erreur ajout queue Redis: %w", err) } - // IncrĂ©menter le compteur de commandes en attente pour ce livreur counterKey := fmt.Sprintf("queue:deliveryman:%s:count", deliveryman) Redis.Incr(RedisCtx, counterKey) - log.Printf("✅ Commande %d ajoutĂ©e Ă  la queue de %s", queueItem.CommandID, deliveryman) - - // ✅ NOUVEAU: Mettre Ă  jour automatiquement le statut go d.UpdateDeliverymanStatusBasedOnQueue(deliveryman) return nil @@ -184,8 +165,6 @@ func (d *Database) AddCommandToQueue(commandID int) error { if err != nil { return fmt.Errorf("commande introuvable: %w", err) } - - // Gestion des coordonnĂ©es (NULL safe) var lat, lng float64 if command["dest_latitude"] != nil { if latVal, ok := command["dest_latitude"].(float64); ok { @@ -198,13 +177,11 @@ func (d *Database) AddCommandToQueue(commandID int) error { } } - // RĂ©cupĂ©rer le total_prix avec gestion de type var totalPrice float64 if tp, ok := command["total_prix"].(float64); ok { totalPrice = tp } - // RĂ©cupĂ©rer l'adresse var address string if addr, ok := command["delivery_address"].(string); ok { address = addr @@ -221,7 +198,6 @@ func (d *Database) AddCommandToQueue(commandID int) error { EstimatedETA: 0, } - // Ajouter Ă  la queue gĂ©nĂ©rale return d.AddToGeneralQueue(queueItem) } @@ -235,8 +211,6 @@ func (d *Database) AddCommandToSmartQueue(commandID int, address string) error { if err != nil { return fmt.Errorf("commande introuvable: %w", err) } - - // Gestion des coordonnĂ©es (NULL safe) var lat, lng float64 if command["dest_latitude"] != nil { if latVal, ok := command["dest_latitude"].(float64); ok { @@ -249,7 +223,6 @@ func (d *Database) AddCommandToSmartQueue(commandID int, address string) error { } } - // RĂ©cupĂ©rer le total_prix avec gestion de type var totalPrice float64 if tp, ok := command["total_prix"].(float64); ok { totalPrice = tp @@ -269,12 +242,10 @@ func (d *Database) AddCommandToSmartQueue(commandID int, address string) error { // ✅ MODIFIÉ: Utiliser FindLeastLoadedDeliveryman qui respecte maintenant le statut assignedDeliveryman, err := d.FindLeastLoadedDeliveryman() if err != nil { - // Aucun livreur trouvĂ©, ajouter Ă  la queue gĂ©nĂ©rale log.Printf("⚠ Aucun livreur trouvĂ©, ajout Ă  la queue gĂ©nĂ©rale") return d.AddToGeneralQueue(queueItem) } - // Ajouter la commande Ă  la queue spĂ©cifique du livreur (auto-update du statut) err = d.AddToDeliverymanQueue(assignedDeliveryman, queueItem) if err != nil { return fmt.Errorf("erreur ajout Ă  la queue du livreur: %w", err) @@ -282,7 +253,6 @@ func (d *Database) AddCommandToSmartQueue(commandID int, address string) error { log.Printf("📋 Commande %d assignĂ©e Ă  la queue de %s", commandID, assignedDeliveryman) - // Publier l'Ă©vĂ©nement d.PublishCommandEvent(commandID, "queued", fmt.Sprintf("En attente dans la queue de %s", assignedDeliveryman)) @@ -459,7 +429,6 @@ func (d *Database) ClearDeliverymanQueue(deliveryman string) error { continue } - // RĂ©cupĂ©rer les donnĂ©es de la commande commandKey := fmt.Sprintf("queue:pending:%d", commandID) data, err := Redis.Get(RedisCtx, commandKey).Result() if err != nil { @@ -471,15 +440,12 @@ func (d *Database) ClearDeliverymanQueue(deliveryman string) error { continue } - // Trouver un nouveau livreur newDeliveryman, err := d.FindLeastLoadedDeliveryman() if err != nil { - // Fallback: queue gĂ©nĂ©rale d.AddToGeneralQueue(queueItem) continue } - // RĂ©assigner Ă  un autre livreur if newDeliveryman != deliveryman { d.AddToDeliverymanQueue(newDeliveryman, queueItem) log.Printf("🔄 Commande %d rĂ©assignĂ©e de %s Ă  %s", @@ -491,9 +457,6 @@ func (d *Database) ClearDeliverymanQueue(deliveryman string) error { Redis.Del(RedisCtx, queueKey) Redis.Del(RedisCtx, fmt.Sprintf("queue:deliveryman:%s:count", deliveryman)) - log.Printf("đŸ—‘ïž Queue de %s vidĂ©e et redistribuĂ©e", deliveryman) - - // ✅ NOUVEAU: Mettre Ă  jour le statut go d.UpdateDeliverymanStatusBasedOnQueue(deliveryman) return nil @@ -521,227 +484,125 @@ func (d *Database) SetDeliveryPersonStatus(username, status string, commandID in // GetAvailableDeliveryPersonsRedis rĂ©cupĂšre les livreurs disponibles (LEGACY) func (d *Database) GetAvailableDeliveryPersonsRedis() ([]models.DeliveryPersonStatus, error) { - keys, err := Redis.Keys(RedisCtx, "delivery:status:*").Result() - if err != nil { - return nil, err - } - var available []models.DeliveryPersonStatus - - for _, key := range keys { - data, err := Redis.Get(RedisCtx, key).Result() - if err != nil { - continue + err := d.iterDeliveryStatuses(func(s models.DeliveryPersonStatus) { + if s.Status == "available" { + available = append(available, s) } - - var status models.DeliveryPersonStatus - json.Unmarshal([]byte(data), &status) - - if status.Status == "available" { - available = append(available, status) - } - } - - return available, nil + }) + return available, err } -// GetAllActiveDeliveryPersons - VERSION MISE À JOUR avec vĂ©rification capacitĂ© +// GetAllActiveDeliveryPersons retourne les livreurs actifs pouvant accepter des commandes func (d *Database) GetAllActiveDeliveryPersons() ([]models.DeliveryPersonStatus, error) { - keys, err := Redis.Keys(RedisCtx, "delivery:status:*").Result() - if err != nil { - return nil, err - } - var active []models.DeliveryPersonStatus - - for _, key := range keys { - data, err := Redis.Get(RedisCtx, key).Result() - if err != nil { - continue + err := d.iterDeliveryStatuses(func(s models.DeliveryPersonStatus) { + if s.Status != "offline" && d.CanDeliverymanAcceptCommands(s.Username) { + active = append(active, s) } - - var status models.DeliveryPersonStatus - json.Unmarshal([]byte(data), &status) - - if status.Status != "offline" { - // ✅ VĂ©rifier si le livreur peut accepter des commandes - if d.CanDeliverymanAcceptCommands(status.Username) { - active = append(active, status) - } - } - } - - return active, nil + }) + return active, err } // CountActiveDeliverymen compte le nombre de livreurs actifs (non offline) func (d *Database) CountActiveDeliverymen() (int, error) { - keys, err := Redis.Keys(RedisCtx, "delivery:status:*").Result() - if err != nil { - return 0, err - } - count := 0 - for _, key := range keys { - data, err := Redis.Get(RedisCtx, key).Result() - if err != nil { - continue - } - - var status models.DeliveryPersonStatus - json.Unmarshal([]byte(data), &status) - - if status.Status != "offline" { + err := d.iterDeliveryStatuses(func(s models.DeliveryPersonStatus) { + if s.Status != "offline" { count++ } - } - - return count, nil + }) + return count, err } func (d *Database) GetSingleActiveDeliveryman() (string, error) { - keys, err := Redis.Keys(RedisCtx, "delivery:status:*").Result() - if err != nil { - return "", err - } - - for _, key := range keys { - data, err := Redis.Get(RedisCtx, key).Result() - if err != nil { - continue - } - - var status models.DeliveryPersonStatus - json.Unmarshal([]byte(data), &status) - - if status.Status != "offline" { - return status.Username, nil + found := "" + d.iterDeliveryStatuses(func(s models.DeliveryPersonStatus) { //nolint + if found == "" && s.Status != "offline" { + found = s.Username } + }) + if found == "" { + return "", fmt.Errorf("aucun livreur actif trouvĂ©") } - - return "", fmt.Errorf("aucun livreur actif trouvĂ©") + return found, nil } // GetAllActiveDeliverymenUsernames retourne les usernames de tous les livreurs actifs func (d *Database) GetAllActiveDeliverymenUsernames() ([]string, error) { - keys, err := Redis.Keys(RedisCtx, "delivery:status:*").Result() - if err != nil { - return nil, err - } - - var activeUsernames []string - - for _, key := range keys { - data, err := Redis.Get(RedisCtx, key).Result() - if err != nil { - continue + var usernames []string + err := d.iterDeliveryStatuses(func(s models.DeliveryPersonStatus) { + if s.Status != "offline" { + usernames = append(usernames, s.Username) } - - var status models.DeliveryPersonStatus - json.Unmarshal([]byte(data), &status) - - if status.Status != "offline" { - activeUsernames = append(activeUsernames, status.Username) - } - } - - return activeUsernames, nil + }) + return usernames, err } -// ============================================ -// 🔄 SYNCHRONISATION DES STATUTS -// ============================================ - // SyncAllDeliverymanStatuses synchronise tous les statuts (Ă  appeler au dĂ©marrage) func (d *Database) SyncAllDeliverymanStatuses() error { + log.Println("🔄 [SYNC] Synchronisation des statuts livreurs...") + return d.iterDeliveryStatuses(func(s models.DeliveryPersonStatus) { + d.UpdateDeliverymanStatusBasedOnQueue(s.Username) + }) +} + +// GetDeliverymanCapacityReport gĂ©nĂšre un rapport dĂ©taillĂ© +func (d *Database) GetDeliverymanCapacityReport() (map[string]any, error) { + report := map[string]any{ + "total_deliverymen": 0, + "available": 0, + "busy_full": 0, + "busy_delivering": 0, + "offline": 0, + "details": []map[string]any{}, + } + + err := d.iterDeliveryStatuses(func(s models.DeliveryPersonStatus) { + queueKey := fmt.Sprintf("queue:deliveryman:%s", s.Username) + queueSize, _ := Redis.ZCard(RedisCtx, queueKey).Result() + canAccept := d.CanDeliverymanAcceptCommands(s.Username) + + report["total_deliverymen"] = report["total_deliverymen"].(int) + 1 + switch { + case s.Status == "offline": + report["offline"] = report["offline"].(int) + 1 + case s.Status == "busy" && queueSize >= MAX_COMMANDS_PER_DELIVERYMAN: + report["busy_full"] = report["busy_full"].(int) + 1 + case s.Status == "busy": + report["busy_delivering"] = report["busy_delivering"].(int) + 1 + case canAccept: + report["available"] = report["available"].(int) + 1 + } + + report["details"] = append(report["details"].([]map[string]any), map[string]any{ + "username": s.Username, + "status": s.Status, + "queue_size": queueSize, + "capacity": fmt.Sprintf("%d/10", queueSize), + "can_accept": canAccept, + "current_order": s.CurrentCommand, + }) + }) + return report, err +} + +// iterDeliveryStatuses itĂšre sur tous les statuts Redis des livreurs et appelle fn pour chacun. +func (d *Database) iterDeliveryStatuses(fn func(models.DeliveryPersonStatus)) error { keys, err := Redis.Keys(RedisCtx, "delivery:status:*").Result() if err != nil { return err } - - log.Println("🔄 [SYNC] Synchronisation des statuts livreurs...") - for _, key := range keys { data, err := Redis.Get(RedisCtx, key).Result() if err != nil { continue } - var status models.DeliveryPersonStatus if err := json.Unmarshal([]byte(data), &status); err != nil { continue } - - // Mettre Ă  jour le statut basĂ© sur la queue - d.UpdateDeliverymanStatusBasedOnQueue(status.Username) + fn(status) } - - log.Println("✅ [SYNC] Synchronisation terminĂ©e") - return nil } - -// ============================================ -// 📊 RAPPORT DE CAPACITÉ -// ============================================ - -// GetDeliverymanCapacityReport gĂ©nĂšre un rapport dĂ©taillĂ© -func (d *Database) GetDeliverymanCapacityReport() (map[string]interface{}, error) { - keys, err := Redis.Keys(RedisCtx, "delivery:status:*").Result() - if err != nil { - return nil, err - } - - report := map[string]interface{}{ - "total_deliverymen": 0, - "available": 0, - "busy_full": 0, // BUSY car queue pleine - "busy_delivering": 0, // BUSY car en livraison - "offline": 0, - "details": []map[string]interface{}{}, - } - - for _, key := range keys { - data, err := Redis.Get(RedisCtx, key).Result() - if err != nil { - continue - } - - var status models.DeliveryPersonStatus - if err := json.Unmarshal([]byte(data), &status); err != nil { - continue - } - - queueKey := fmt.Sprintf("queue:deliveryman:%s", status.Username) - queueSize, _ := Redis.ZCard(RedisCtx, queueKey).Result() - - canAccept := d.CanDeliverymanAcceptCommands(status.Username) - - detail := map[string]interface{}{ - "username": status.Username, - "status": status.Status, - "queue_size": queueSize, - "capacity": fmt.Sprintf("%d/10", queueSize), - "can_accept": canAccept, - "current_order": status.CurrentCommand, - } - - report["total_deliverymen"] = report["total_deliverymen"].(int) + 1 - - if status.Status == "offline" { - report["offline"] = report["offline"].(int) + 1 - } else if status.Status == "busy" { - if queueSize >= MAX_COMMANDS_PER_DELIVERYMAN { - report["busy_full"] = report["busy_full"].(int) + 1 - } else { - report["busy_delivering"] = report["busy_delivering"].(int) + 1 - } - } else if canAccept { - report["available"] = report["available"].(int) + 1 - } - - report["details"] = append(report["details"].([]map[string]interface{}), detail) - } - - return report, nil -} diff --git a/backend/gestion/db/redis_session.go b/backend/gestion/db/redis_session.go index c1395c4a..bb484301 100644 --- a/backend/gestion/db/redis_session.go +++ b/backend/gestion/db/redis_session.go @@ -78,7 +78,6 @@ func (d *Database) CreateClientSession(clientID int, username string, sessionID // Charger les infos du client (points, penalty) dans le cache if client, err := d.GetClientByUsername(username); err == nil { - sessionData.PointsCache = int(client.Point) sessionData.PenaltyCache = float64(client.Amende) sessionJSON, _ := json.Marshal(sessionData) Redis.Set(RedisCtx, sessionKey, sessionJSON, ttl) diff --git a/backend/gestion/handlers/auth.go b/backend/gestion/handlers/auth.go index c354945b..f563bfc2 100644 --- a/backend/gestion/handlers/auth.go +++ b/backend/gestion/handlers/auth.go @@ -1,14 +1,12 @@ package handlers import ( - "crypto/rand" - "encoding/hex" "gestion/db" "gestion/models" + "gestion/utils" "log" "net/http" "os" - "regexp" "strconv" "strings" "time" @@ -19,109 +17,16 @@ import ( "golang.org/x/crypto/bcrypt" ) -// ============================================ -// TYPES JWT CLAIMS (DuplicĂ©s dans middleware aussi) -// ============================================ - -type ClientClaims struct { - ClientID int `json:"client_id"` - Username string `json:"username"` - Role string `json:"role"` - SessionID string `json:"session_id"` - jwt.RegisteredClaims -} - -type AdminClaims struct { - UserID int `json:"user_id"` - Username string `json:"username"` - Role string `json:"role"` - SessionID string `json:"session_id"` - jwt.RegisteredClaims -} - -// ============================================ -// STRUCTURES REQUÊTE / RÉPONSE -// ============================================ - -type LoginRequest struct { - Username string `json:"username" binding:"required"` - Password string `json:"password" binding:"required"` -} - -type RegisterClientRequest struct { - Username string `json:"username" binding:"required,min=3,max=50"` - Password string `json:"password" binding:"required,min=8"` - Nom string `json:"nom" binding:"required,min=2,max=100"` - Prenom string `json:"prenom" binding:"required,min=2,max=100"` - Telephone string `json:"telephone" binding:"required"` -} - -type RegisterAdminRequest struct { - Username string `json:"username" binding:"required,min=3,max=50"` - Password string `json:"password" binding:"required,min=8"` - Role string `json:"role" binding:"required,oneof=admin cabine livreur"` -} - -type LoginResponse struct { - AccessToken string `json:"access_token"` - TokenType string `json:"token_type"` - ExpiresIn int `json:"expires_in"` - User interface{} `json:"user"` -} - -type ProfileResponse struct { - Username string `json:"username"` - Role string `json:"role"` -} - -// ============================================ -// VARIABLES & CONSTANTS -// ============================================ - var ( clientTokenDuration = 5 * time.Hour adminTokenDuration = 10 * time.Hour - userJWTSecret = []byte(os.Getenv("USER_JWT_SECRET")) // ✅ Pour clients - adminJWTSecret = []byte(os.Getenv("ADMIN_JWT_SECRET")) // ✅ Pour admin/cabine/livreur + userJWTSecret = []byte(os.Getenv("USER_JWT_SECRET")) + adminJWTSecret = []byte(os.Getenv("ADMIN_JWT_SECRET")) ) -// ============================================ -// UTILITAIRES -// ============================================ - -func generateSessionID() string { - bytes := make([]byte, 16) - rand.Read(bytes) - return hex.EncodeToString(bytes) -} - -// validatePhoneNumber valide le format du numĂ©ro de tĂ©lĂ©phone -func validatePhoneNumber(phone string) bool { - // Supprimer espaces/tirets - clean := regexp.MustCompile(`[\s\-\(\)]`).ReplaceAllString(phone, "") - - // Format international ou national - validFormat := regexp.MustCompile(`^(\+33|0)[1-9]\d{8}$`) - return validFormat.MatchString(clean) -} - -func normalizePhoneNumber(phone string) string { - clean := regexp.MustCompile(`[\s\-\(\)]`).ReplaceAllString(phone, "") - - // Convertir 06... en +336... - if strings.HasPrefix(clean, "0") { - return "+33" + clean[1:] - } - return clean -} - -// ============================================ -// GENERATION TOKENS -// ============================================ - func generateClientToken(client *models.Client) (string, error) { - sessionID := generateSessionID() - claims := ClientClaims{ + sessionID := utils.GenerateSessionID() + claims := models.ClientClaims{ ClientID: client.ID, Username: client.Username, Role: "client", @@ -135,7 +40,7 @@ func generateClientToken(client *models.Client) (string, error) { }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - tokenString, err := token.SignedString(userJWTSecret) // ✅ UTILISER userJWTSecret (CLIENT) + tokenString, err := token.SignedString(userJWTSecret) if err != nil { return "", err } @@ -143,8 +48,8 @@ func generateClientToken(client *models.Client) (string, error) { } func generateAdminToken(user *models.User) (string, error) { - sessionID := generateSessionID() - claims := AdminClaims{ + sessionID := utils.GenerateSessionID() + claims := models.AdminClaims{ UserID: user.ID, Username: user.Username, Role: user.Role, // ← "admin" ou "cabine" ou "livreur" @@ -158,21 +63,16 @@ func generateAdminToken(user *models.User) (string, error) { }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - tokenString, err := token.SignedString(adminJWTSecret) // ✅ UTILISER adminJWTSecret (ADMIN/CABINE/LIVREUR) + tokenString, err := token.SignedString(adminJWTSecret) if err != nil { return "", err } return tokenString, nil } -// ============================================ -// HANDLERS AUTHENTIFICATION CLIENT -// ============================================ - // RegisterClient crĂ©e un nouveau compte client -// POST /api/v1/auth/register func RegisterClient(c *gin.Context) { - var req RegisterClientRequest + var req models.RegisterClientRequest if err := c.ShouldBindJSON(&req); err != nil { log.Printf("❌ [REGISTER_CLIENT] Erreur binding: %v", err) c.JSON(http.StatusBadRequest, gin.H{ @@ -183,7 +83,7 @@ func RegisterClient(c *gin.Context) { } // Validation tĂ©lĂ©phone - if !validatePhoneNumber(req.Telephone) { + if !utils.ValidatePhoneNumber(req.Telephone) { log.Printf("❌ [REGISTER_CLIENT] TĂ©lĂ©phone invalide: %s", req.Telephone) c.JSON(http.StatusBadRequest, gin.H{ "error": "NumĂ©ro de tĂ©lĂ©phone invalide", @@ -191,7 +91,7 @@ func RegisterClient(c *gin.Context) { return } - normalizedPhone := normalizePhoneNumber(req.Telephone) + normalizedPhone := utils.NormalizePhoneNumber(req.Telephone) database := c.MustGet("database").(*db.Database) // VĂ©rifier username unique @@ -256,9 +156,7 @@ func RegisterClient(c *gin.Context) { client.Password = "" - log.Printf("✅ [REGISTER_CLIENT] Client créé: %s (ID=%d)", client.Username, client.ID) - - c.JSON(http.StatusCreated, LoginResponse{ + c.JSON(http.StatusCreated, models.LoginResponse{ AccessToken: token, TokenType: "Bearer", ExpiresIn: int(clientTokenDuration.Seconds()), @@ -275,20 +173,19 @@ func RegisterClient(c *gin.Context) { } // AdminCreateClient crĂ©e un client depuis l'interface admin (sans session ni token) -// POST /api/v2/admin/protected/clients func AdminCreateClient(c *gin.Context) { - var req RegisterClientRequest + var req models.RegisterClientRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "DonnĂ©es invalides"}) return } - if !validatePhoneNumber(req.Telephone) { + if !utils.ValidatePhoneNumber(req.Telephone) { c.JSON(http.StatusBadRequest, gin.H{"error": "NumĂ©ro de tĂ©lĂ©phone invalide"}) return } - normalizedPhone := normalizePhoneNumber(req.Telephone) + normalizedPhone := utils.NormalizePhoneNumber(req.Telephone) database := c.MustGet("database").(*db.Database) if existing, _ := database.GetClientByUsername(req.Username); existing != nil { @@ -322,8 +219,6 @@ func AdminCreateClient(c *gin.Context) { return } - log.Printf("✅ [ADMIN_CREATE_CLIENT] Client créé par admin: %s (ID=%d)", client.Username, client.ID) - c.JSON(http.StatusCreated, gin.H{ "message": "Client créé avec succĂšs", "client": gin.H{ @@ -337,9 +232,8 @@ func AdminCreateClient(c *gin.Context) { } // LoginClient authentifie un client -// POST /api/v1/auth/login func LoginClient(c *gin.Context) { - var req LoginRequest + var req models.LoginRequest if err := c.ShouldBindJSON(&req); err != nil { log.Printf("❌ [LOGIN_CLIENT] Erreur binding: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": "DonnĂ©es invalides"}) @@ -361,7 +255,6 @@ func LoginClient(c *gin.Context) { return } - // ✅ LIGNE 302 CORRIGÉE - GĂ©rer l'erreur ! token, err := generateClientToken(client) if err != nil { log.Printf("❌ [LOGIN_CLIENT] Erreur gĂ©nĂ©ration token: %v", err) @@ -382,9 +275,7 @@ func LoginClient(c *gin.Context) { log.Printf("⚠ [LOGIN_CLIENT] Erreur session Redis: %v", err) } - log.Printf("✅ [LOGIN_CLIENT] Client authentifiĂ©: %s", req.Username) - - c.JSON(http.StatusOK, LoginResponse{ + c.JSON(http.StatusOK, models.LoginResponse{ AccessToken: token, TokenType: "Bearer", ExpiresIn: int(clientTokenDuration.Seconds()), @@ -402,7 +293,6 @@ func LoginClient(c *gin.Context) { } // ChangePassword permet Ă  un client de changer son mot de passe -// PUT /api/v1/auth/change-password func ChangePassword(c *gin.Context) { var req struct { CurrentPassword string `json:"current_password" binding:"required"` @@ -442,16 +332,13 @@ func ChangePassword(c *gin.Context) { return } - log.Printf("✅ [CHANGE_PASSWORD] Mot de passe changĂ©: ID=%d", clientID) c.JSON(http.StatusOK, gin.H{"success": true, "message": "Mot de passe mis Ă  jour avec succĂšs"}) } // LogoutClient dĂ©connecte un client -// POST /api/v1/auth/logout func LogoutClient(c *gin.Context) { database := c.MustGet("database").(*db.Database) - // Invalider la session Redis clientID, hasClientID := c.Get("client_id") if hasClientID && clientID != nil { if err := database.InvalidateSession(clientID.(int)); err != nil { @@ -459,26 +346,18 @@ func LogoutClient(c *gin.Context) { } } - // RĂ©voquer le JWT token authHeader := c.GetHeader("Authorization") if authHeader != "" && strings.HasPrefix(authHeader, "Bearer ") { tokenStr := strings.TrimPrefix(authHeader, "Bearer ") database.RevokeToken(tokenStr) } - log.Printf("✅ [LOGOUT_CLIENT] Client dĂ©connectĂ©") - c.JSON(http.StatusOK, gin.H{"message": "DĂ©connexion rĂ©ussie"}) } -// ============================================ -// HANDLERS AUTHENTIFICATION ADMIN -// ============================================ - // RegisterAdmin crĂ©e un nouvel utilisateur admin/cabine/livreur -// POST /api/v1/auth/admin/register func RegisterAdmin(c *gin.Context) { - var req RegisterAdminRequest + var req models.RegisterAdminRequest if err := c.ShouldBindJSON(&req); err != nil { log.Printf("❌ [REGISTER_ADMIN] Erreur binding: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": "DonnĂ©es invalides"}) @@ -487,7 +366,6 @@ func RegisterAdmin(c *gin.Context) { database := c.MustGet("database").(*db.Database) - // VĂ©rifier que l'user n'existe pas if existingUser, _ := database.GetUserByUsername(req.Username); existingUser != nil { log.Printf("❌ [REGISTER_ADMIN] Username dĂ©jĂ  utilisĂ©: %s", req.Username) c.JSON(http.StatusConflict, gin.H{"error": "Nom d'utilisateur dĂ©jĂ  utilisĂ©"}) @@ -507,9 +385,6 @@ func RegisterAdmin(c *gin.Context) { return } - log.Printf("✅ [REGISTER_ADMIN] User créé: %s (role=%s, ID=%d)", user.Username, user.Role, user.ID) - - // GĂ©nĂ©rer le token token, err := generateAdminToken(user) if err != nil { log.Printf("❌ [REGISTER_ADMIN] Erreur gĂ©nĂ©ration token: %v", err) @@ -517,11 +392,6 @@ func RegisterAdmin(c *gin.Context) { return } - log.Printf("✅ [REGISTER_ADMIN] Token gĂ©nĂ©rĂ©: %s...", token[:50]) - - // ============================================ - // ✅ CRITICAL FIX: ENREGISTRER LE TOKEN EN DB - // ============================================ expiresAt := time.Now().Add(adminTokenDuration) if err := database.SaveToken(user.ID, user.Role, token, expiresAt); err != nil { log.Printf("❌ [REGISTER_ADMIN] Erreur SaveToken: %v", err) @@ -529,11 +399,9 @@ func RegisterAdmin(c *gin.Context) { return } - log.Printf("✅ [REGISTER_ADMIN] Token enregistrĂ© en DB pour user ID: %d", user.ID) - user.Password = "" - c.JSON(http.StatusCreated, LoginResponse{ + c.JSON(http.StatusCreated, models.LoginResponse{ AccessToken: token, TokenType: "Bearer", ExpiresIn: int(adminTokenDuration.Seconds()), @@ -542,10 +410,8 @@ func RegisterAdmin(c *gin.Context) { } // LoginAdmin authentifie un admin/cabine/livreur -// POST /api/v1/auth/admin/login -// ✅ AMÉLIORÉ: Meilleure gestion d'erreurs func LoginAdmin(c *gin.Context) { - var req LoginRequest + var req models.LoginRequest if err := c.ShouldBindJSON(&req); err != nil { log.Printf("❌ [LOGIN_ADMIN] Erreur binding: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": "DonnĂ©es invalides"}) @@ -561,14 +427,12 @@ func LoginAdmin(c *gin.Context) { return } - // VĂ©rifier que c'est un admin/cabine/livreur if user.Role != "admin" && user.Role != "cabine" && user.Role != "livreur" { log.Printf("❌ [LOGIN_ADMIN] RĂŽle invalide: %s", user.Role) c.JSON(http.StatusUnauthorized, gin.H{"error": "AccĂšs non autorisĂ©"}) return } - // VĂ©rifier le mot de passe if user.Password == "" { log.Printf("❌ [LOGIN_ADMIN] Mot de passe vide en base") c.JSON(http.StatusUnauthorized, gin.H{"error": "Identifiants invalides"}) @@ -581,7 +445,6 @@ func LoginAdmin(c *gin.Context) { return } - // GĂ©nĂ©rer le token token, _ := generateAdminToken(user) expiresAt := time.Now().Add(adminTokenDuration) @@ -591,9 +454,7 @@ func LoginAdmin(c *gin.Context) { return } - log.Printf("✅ [LOGIN_ADMIN] User authentifiĂ©: %s (role=%s)", user.Username, user.Role) - - c.JSON(http.StatusOK, LoginResponse{ + c.JSON(http.StatusOK, models.LoginResponse{ AccessToken: token, TokenType: "Bearer", ExpiresIn: int(adminTokenDuration.Seconds()), @@ -606,7 +467,6 @@ func LoginAdmin(c *gin.Context) { } // LogoutAdmin dĂ©connecte un admin/cabine/livreur -// POST /api/v1/auth/admin/logout func LogoutAdmin(c *gin.Context) { database := c.MustGet("database").(*db.Database) @@ -645,14 +505,14 @@ func GetCurrentClient(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "client": gin.H{ - "id": client.ID, - "username": client.Username, - "nom": client.Nom, - "prenom": client.Prenom, - "telephone": client.Telephone, - "command": client.Command, - "point": client.Point, - "amende": client.Amende, + "id": client.ID, + "username": client.Username, + "nom": client.Nom, + "prenom": client.Prenom, + "telephone": client.Telephone, + "command": client.Command, + "amende": client.Amende, + "points_extra": client.PointsExtra, }, }) } @@ -675,7 +535,7 @@ func GetCurrentAdmin(c *gin.Context) { log.Printf("✅ [GET_CURRENT_ADMIN] User rĂ©cupĂ©rĂ©: %s", user.Username) c.JSON(http.StatusOK, gin.H{ - "user": ProfileResponse{ + "user": models.ProfileResponse{ Username: user.Username, Role: user.Role, }, @@ -795,8 +655,7 @@ func GetAllClients(c *gin.Context) { "prenom": cl.Prenom, "telephone": cl.Telephone, "command": cl.Command, - "point": cl.Point, - "points_zipette": cl.PointZipette, + "points_extra": cl.PointsExtra, "amende": cl.Amende, "cancellations_count": cl.CancellationsCount, "last_penalty_reason": cl.LastPenaltyReason, diff --git a/backend/gestion/handlers/cabine.go b/backend/gestion/handlers/cabine.go index 02d6f3f7..5ba313af 100644 --- a/backend/gestion/handlers/cabine.go +++ b/backend/gestion/handlers/cabine.go @@ -9,8 +9,10 @@ import ( "encoding/json" "fmt" "gestion/db" + "gestion/utils" "log" "net/http" + "slices" "strconv" "time" @@ -22,7 +24,6 @@ import ( // ============================================ // SetCommandDestinationCoordinates stocke les coordonnĂ©es destination en Redis -// POST /api/v2/admin/protected/orders/:id/set-destination func SetCommandDestinationCoordinates(c *gin.Context) { database := c.MustGet("database").(*db.Database) @@ -69,15 +70,10 @@ func SetCommandDestinationCoordinates(c *gin.Context) { return } - // VĂ©rifier que la commande existe - command, err := database.GetCommandByID(commandID) - if err != nil { + if !utils.CheckCommand(commandID, database) { c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvĂ©e"}) - return } - log.Printf("Command: %v", command) - // Stocker en Redis avec format JSON destCacheKey := fmt.Sprintf("command:destination:%d", commandID) coordsJSON, _ := json.Marshal(map[string]float64{ "lat": req.Latitude, @@ -134,12 +130,12 @@ func GetClientProfile(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "success": true, "client": gin.H{ - "id": client.ID, - "username": client.Username, - "command": client.Command, - "point": client.Point, - "amende": client.Amende, - "created_at": client.CreatedAt, + "id": client.ID, + "username": client.Username, + "command": client.Command, + "amende": client.Amende, + "points_extra": client.PointsExtra, + "created_at": client.CreatedAt, }, }) } @@ -170,8 +166,8 @@ func GetClientFullHistory(c *gin.Context) { "client": gin.H{ "username": client.Username, "total_commands": client.Command, - "points": client.Point, "amende": client.Amende, + "points_extra": client.PointsExtra, }, "commands": commands, "count": len(commands), @@ -209,15 +205,7 @@ func UpdateCommandAddressCabine(c *gin.Context) { status, _ := command["status"].(string) allowedStatuses := []string{"pending", "", "assigned"} - isAllowed := false - for _, s := range allowedStatuses { - if status == s { - isAllowed = true - break - } - } - - if !isAllowed { + if !slices.Contains(allowedStatuses, status) { c.JSON(http.StatusBadRequest, gin.H{ "error": "Impossible de modifier l'adresse d'une commande en cours ou terminĂ©e", "current_status": status, @@ -288,10 +276,6 @@ func GetLivreurPosition(c *gin.Context) { }) } -// ============================================ -// 4. DELIVERY TRACKING CLIENT (SANS GPS) -// ============================================ - func GetDeliveryTrackingClient(c *gin.Context) { database := c.MustGet("database").(*db.Database) @@ -394,10 +378,6 @@ func GetDeliveryTracking(c *gin.Context) { c.JSON(http.StatusOK, response) } -// ============================================ -// 6. DELIVERY ISSUES -// ============================================ - func GetDeliveryIssues(c *gin.Context) { database := c.MustGet("database").(*db.Database) @@ -630,24 +610,6 @@ func ForceValidateDelivery(c *gin.Context) { return } - validStatuses := []string{"assigned", "in_transit", "en_route", "en_cours", "pending", "priority"} - isValidStatus := false - for _, vs := range validStatuses { - if status == vs { - isValidStatus = true - break - } - } - - if !isValidStatus && status != "livre" { - c.JSON(http.StatusBadRequest, gin.H{ - "error": "Commande ne peut pas ĂȘtre validĂ©e de force dans ce statut", - "current_status": status, - "valid_statuses": validStatuses, - }) - return - } - if status == "livre" { c.JSON(http.StatusBadRequest, gin.H{ "error": "Cette commande a dĂ©jĂ  Ă©tĂ© validĂ©e", @@ -656,6 +618,16 @@ func ForceValidateDelivery(c *gin.Context) { return } + validStatuses := []string{"assigned", "en_route", "pending", "priority"} + if !slices.Contains(validStatuses, status) { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Commande ne peut pas ĂȘtre validĂ©e de force dans ce statut", + "current_status": status, + "valid_statuses": validStatuses, + }) + return + } + err = database.UpdateCommandStatus(commandID, "livre") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ @@ -668,7 +640,6 @@ func ForceValidateDelivery(c *gin.Context) { clientUsername, _ := command["username"].(string) livreurAssign, _ := command["livreur_assign"].(string) - // Notifier le client if clientUsername != "" { clientMsg := fmt.Sprintf("Votre commande #%d a Ă©tĂ© livrĂ©e. Merci !", commandID) database.NotifyClient(clientUsername, commandID, "livre", clientMsg) @@ -678,12 +649,11 @@ func ForceValidateDelivery(c *gin.Context) { log.Printf("⚠ Erreur compteur commandes: %v", err) } - if err := database.AddClientPoints(clientUsername, 10); err != nil { + if err := database.AddClientPointsByCategory(clientUsername, 10, ""); err != nil { log.Printf("⚠ Erreur ajout points: %v", err) } if livreurAssign != "" { - log.Printf("📩 Commande %d validĂ©e de force par admin - Optimisation queue de %s...", commandID, livreurAssign) err := database.CompleteDeliveryAndProcessNext(livreurAssign, commandID) if err != nil { log.Printf("⚠ Erreur optimisation: %v", err) diff --git a/backend/gestion/handlers/cancel_command.go b/backend/gestion/handlers/cancel_command.go index 8f3be5f2..e98d3fec 100644 --- a/backend/gestion/handlers/cancel_command.go +++ b/backend/gestion/handlers/cancel_command.go @@ -18,10 +18,6 @@ import ( "github.com/gin-gonic/gin" ) -// ============================================ -// RATE LIMITING -// ============================================ - var ( cancelRateLimitMap = make(map[string][]time.Time) cancelMaxRequests = 5 // Max 5 annulations @@ -49,16 +45,10 @@ func checkCancelRateLimit(key string) bool { return true } -// ============================================ -// HELPERS DE SÉCURITÉ -// ============================================ - func validateReason(reason string) string { - // Limiter la longueur if len(reason) > 500 { reason = reason[:500] } - // Sanitizer reason = strings.Map(func(r rune) rune { if r < 32 || r == 127 { return -1 @@ -73,10 +63,6 @@ func validateReason(reason string) string { return reason } -// ============================================ -// 1ïžâƒŁ ANNULATION PAR LE CLIENT - VERSION SÉCURISÉE -// ============================================ - func CancelCommandByClient(c *gin.Context) { database := c.MustGet("database").(*db.Database) @@ -115,19 +101,12 @@ func CancelCommandByClient(c *gin.Context) { req.Reason = validateReason(req.Reason) - log.Printf("đŸš« [CANCEL_CLIENT] Client %s annule cmd %d (force=%v)", username, commandID, req.Force) - - // ============================================ - // UTILISER LA FONCTION ATOMIQUE - // ============================================ - penalty, pointsLost, err := database.CancelCommandAtomic(commandID, username, req.Reason, req.Force) + penalty, err := database.CancelCommandAtomic(commandID, username, req.Reason, req.Force) if err != nil { log.Printf("❌ [CANCEL_CLIENT] Erreur: %v", err) - // ✅ GESTION SPÉCIALE POUR "confirmation requise" if err.Error() == "confirmation requise" { - // ✅ RÉCUPÉRER LES INFORMATIONS DE LA COMMANDE command, errCmd := database.GetCommandByID(commandID) if errCmd != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvĂ©e"}) @@ -137,28 +116,14 @@ func CancelCommandByClient(c *gin.Context) { livreurAssign, _ := command["livreur_assign"].(string) currentStatus, _ := command["status"].(string) - // ✅ VÉRIFIER SI ETA EXISTE (VERSION CORRIGÉE) hasETA := false if livreurAssign != "" { - // ✅ FIX: Utiliser la nouvelle fonction qui vĂ©rifie VRAIMENT l'ETA hasETA = database.CheckCommandETAExistsAndValid(commandID) } - // ✅ CALCULER LA PÉNALITÉ QUI SERA APPLIQUÉE nextPenalty, _ := database.CalculateCancellationPenalty(username) cancelCount, _ := database.GetClientCancellationsCount(username) - // ✅ RÉCUPÉRER LES POINTS ACTUELS - client, _ := database.GetClientByUsername(username) - currentPointsWeed := 0 - currentPointsZipette := 0 - if client != nil { - currentPointsWeed = client.Point - currentPointsZipette = client.PointZipette - } - totalPoints := currentPointsWeed + currentPointsZipette - - // ✅ CONSTRUIRE LA RÉPONSE EN FONCTION DE hasETA response := gin.H{ "success": false, "warning": true, @@ -170,7 +135,6 @@ func CancelCommandByClient(c *gin.Context) { } if hasETA { - // ⚠ CAS 1: LIVREUR EN ROUTE (ETA dĂ©finie) = PÉNALITÉ TOTALE log.Printf("⚠ [CANCEL_CLIENT] Annulation tardive avec ETA - Status: %s, Livreur: %s", currentStatus, livreurAssign) response["message"] = "⚠ Un livreur est en route vers votre adresse (ETA dĂ©finie)" @@ -180,27 +144,21 @@ func CancelCommandByClient(c *gin.Context) { "has_eta": true, } response["penalty_warning"] = gin.H{ - "will_apply": true, - "penalty_amount": nextPenalty, - "current_violations": cancelCount, - "current_points_weed": currentPointsWeed, - "current_points_zipette": currentPointsZipette, - "total_points": totalPoints, - "points_will_reset": true, + "will_apply": true, + "penalty_amount": nextPenalty, + "current_violations": cancelCount, "message": fmt.Sprintf( - "⚠ ATTENTION: Une amende de %d points sera appliquĂ©e ET tous vos points (%d weed/hash + %d zipette = %d total) seront remis Ă  zĂ©ro!", - nextPenalty, currentPointsWeed, currentPointsZipette, totalPoints, + "⚠ ATTENTION: Une amende de %d sera appliquĂ©e pour annulation tardive", + nextPenalty, ), "scale": gin.H{ - "1st_cancel": "20 points + remise Ă  zĂ©ro TOTALE", - "2nd_cancel": "50 points + remise Ă  zĂ©ro TOTALE", - "3rd_cancel": "100 points + remise Ă  zĂ©ro TOTALE", - "4th+_cancel": "150 points + remise Ă  zĂ©ro TOTALE", - "your_next": fmt.Sprintf("%d points + remise Ă  zĂ©ro de tous vos %d points", nextPenalty, totalPoints), + "1st_cancel": 20, + "2nd_cancel": 50, + "3rd_cancel": 100, + "4th+_cancel": 150, }, } } else { - // â„č CAS 2: LIVREUR ASSIGNÉ MAIS PAS EN ROUTE (PAS D'ETA) = PAS DE PÉNALITÉ log.Printf("â„č [CANCEL_CLIENT] Livreur assignĂ© mais pas d'ETA - Annulation sans pĂ©nalitĂ©") response["message"] = "â„č Un livreur est assignĂ© mais n'est pas encore en route" @@ -274,11 +232,8 @@ func CancelCommandByClient(c *gin.Context) { if penalty > 0 { response["penalty"] = gin.H{ - "penalty_points": penalty, - "points_weed_lost": pointsLost["weed"], - "points_zipette_lost": pointsLost["zipette"], - "total_points_lost": pointsLost["weed"] + pointsLost["zipette"], - "warning": "Une pĂ©nalitĂ© a Ă©tĂ© appliquĂ©e et vos points ont Ă©tĂ© remis Ă  zĂ©ro", + "penalty_amount": penalty, + "warning": "Une amende a Ă©tĂ© appliquĂ©e pour annulation tardive", } } else { response["info"] = "Aucune pĂ©nalitĂ© appliquĂ©e" @@ -325,10 +280,6 @@ func GetMyCancellationHistory(c *gin.Context) { }) } -// ============================================ -// LISTE DES COMMANDES ANNULÉES - VERSION SÉCURISÉE -// ============================================ - func GetAllCancelledOrders(c *gin.Context) { database := c.MustGet("database").(*db.Database) @@ -345,7 +296,6 @@ func GetAllCancelledOrders(c *gin.Context) { return } - // ✅ VALIDATION des paramĂštres filterUsername := c.Query("username") if len(filterUsername) > 100 { c.JSON(http.StatusBadRequest, gin.H{"error": "Username trop long"}) @@ -373,14 +323,14 @@ func GetAllCancelledOrders(c *gin.Context) { } // ✅ ENRICHIR les donnĂ©es (sans exposer d'infos sensibles inutiles) - var enrichedOrders []map[string]interface{} + var enrichedOrders []map[string]any for _, order := range cancelledOrders { orderID, _ := order["id"].(int) items, _ := database.GetCommandItems(orderID) logs, _ := database.GetCommandLogs(orderID) - var cancellationLog map[string]interface{} + var cancellationLog map[string]any for _, logEntry := range logs { status, _ := logEntry["status"].(string) if status == "cancelled" { @@ -389,7 +339,7 @@ func GetAllCancelledOrders(c *gin.Context) { } } - enrichedOrder := map[string]interface{}{ + enrichedOrder := map[string]any{ "id": order["id"], "username": order["username"], "total_prix": order["total_prix"], @@ -419,10 +369,6 @@ func GetAllCancelledOrders(c *gin.Context) { }) } -// ============================================ -// SUPPRESSION PAR CABINE - VERSION SÉCURISÉE -// ============================================ - func DeleteCommandByCabine(c *gin.Context) { database := c.MustGet("database").(*db.Database) @@ -447,12 +393,10 @@ func DeleteCommandByCabine(c *gin.Context) { log.Printf("đŸ—‘ïž [DELETE_COMMAND] %s (%s) supprime cmd %d", username, userRole, commandID) - // ✅ UTILISER LA FONCTION ATOMIQUE err = database.DeleteCommandAtomic(commandID, username, userRole) if err != nil { log.Printf("❌ [DELETE_COMMAND] Erreur: %v", err) - // ❌ Ne pas exposer les dĂ©tails de l'erreur if err.Error() == "commande non trouvĂ©e" { c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvĂ©e"}) } else { diff --git a/backend/gestion/handlers/categories.go b/backend/gestion/handlers/categories.go index 088f1732..66cd0e8f 100644 --- a/backend/gestion/handlers/categories.go +++ b/backend/gestion/handlers/categories.go @@ -10,7 +10,6 @@ import ( "github.com/gin-gonic/gin" ) -// GET /api/v1/categories — public func GetCategories(c *gin.Context) { database := c.MustGet("database").(*db.Database) @@ -27,7 +26,6 @@ func GetCategories(c *gin.Context) { }) } -// POST /api/v2/admin/protected/categories — admin func CreateCategory(c *gin.Context) { database := c.MustGet("database").(*db.Database) @@ -111,7 +109,6 @@ func UpdateCategory(c *gin.Context) { }) } -// DELETE /api/v2/admin/protected/categories/:id — admin func DeleteCategory(c *gin.Context) { database := c.MustGet("database").(*db.Database) @@ -131,6 +128,5 @@ func DeleteCategory(c *gin.Context) { return } - log.Printf("✅ [CATEGORIES] SupprimĂ©e: %d", id) c.JSON(http.StatusOK, gin.H{"success": true, "message": "CatĂ©gorie supprimĂ©e"}) } diff --git a/backend/gestion/handlers/client_tracking.go b/backend/gestion/handlers/client_tracking.go index 96a56c5b..2f1ddb14 100644 --- a/backend/gestion/handlers/client_tracking.go +++ b/backend/gestion/handlers/client_tracking.go @@ -10,7 +10,6 @@ import ( ) // GetCommandStatus - Statut temps rĂ©el d'une commande -// GET /api/v1/commands/:id/status func GetCommandStatus(c *gin.Context) { database := c.MustGet("database").(*db.Database) @@ -28,16 +27,12 @@ func GetCommandStatus(c *gin.Context) { return } - log.Printf("📊 [STATUS] Client %s demande statut cmd %d", usernameStr, commandID) - - // RĂ©cupĂ©rer la commande command, err := database.GetCommandByID(commandID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvĂ©e"}) return } - // ✅ VÉRIFIER PROPRIÉTÉ cmdUsername, _ := command["username"].(string) if cmdUsername != usernameStr { log.Printf("❌ [STATUS] AccĂšs refusĂ© - cmd de %s demandĂ©e par %s", cmdUsername, usernameStr) @@ -47,10 +42,8 @@ func GetCommandStatus(c *gin.Context) { return } - // RĂ©cupĂ©rer ETA etaData, _ := database.GetCommandETA(commandID) - // RĂ©cupĂ©rer infos livreur (si assignĂ©) livreurInfo := gin.H{ "assigned": false, } @@ -63,7 +56,6 @@ func GetCommandStatus(c *gin.Context) { } } - // Mapper le statut en message lisible statusMessage := getStatusMessage(command["status"].(string)) c.JSON(http.StatusOK, gin.H{ @@ -80,7 +72,6 @@ func GetCommandStatus(c *gin.Context) { } // GetMyCommandsWithTracking - Liste des commandes avec suivi -// GET /api/v1/my-commands func GetMyCommandsWithTracking(c *gin.Context) { database := c.MustGet("database").(*db.Database) @@ -147,7 +138,6 @@ func GetMyCommandsWithTracking(c *gin.Context) { } // GetCommandTracking - Suivi dĂ©taillĂ© d'une commande -// GET /api/v1/commands/:id/tracking func GetCommandTracking(c *gin.Context) { database := c.MustGet("database").(*db.Database) @@ -174,13 +164,10 @@ func GetCommandTracking(c *gin.Context) { return } - // RĂ©cupĂ©rer logs logs, _ := database.GetCommandLogs(commandID) - // ETA etaData, _ := database.GetCommandETA(commandID) - // Timeline (basĂ© sur les logs) timeline := buildTimeline(logs) c.JSON(http.StatusOK, gin.H{ @@ -194,11 +181,6 @@ func GetCommandTracking(c *gin.Context) { }) } -// ============================================ -// HELPERS -// ============================================ - -// getStatusMessage retourne un message lisible pour le client func getStatusMessage(status string) string { messages := map[string]string{ "pending": "⏳ En attente d'assignation", @@ -219,7 +201,7 @@ func getStatusMessage(status string) string { } // buildTimeline construit une timeline depuis les logs -func buildTimeline(logs []map[string]interface{}) []gin.H { +func buildTimeline(logs []map[string]any) []gin.H { timeline := make([]gin.H, 0) for _, logEntry := range logs { diff --git a/backend/gestion/handlers/commands.go b/backend/gestion/handlers/commands.go index 6299941b..bb355292 100644 --- a/backend/gestion/handlers/commands.go +++ b/backend/gestion/handlers/commands.go @@ -3,6 +3,7 @@ package handlers import ( "fmt" "gestion/db" + "gestion/utils" "log" "net/http" "strconv" @@ -42,9 +43,6 @@ func checkRateLimit(key string) bool { return true } -// ============================================ -// GESTION ADRESSE & ADMIN -// ============================================ func safeGetUsername(c *gin.Context) (string, error) { username, exists := c.Get("username") if !exists { @@ -71,13 +69,11 @@ func validateAddress(address string) error { } // UpdateCommandAddress met Ă  jour l'adresse de livraison d'une commande -// PUT /api/v1/admin/commands/:id/address func UpdateCommandAddress(c *gin.Context) { database := c.MustGet("database").(*db.Database) - // ✅ VĂ©rification du rĂŽle - if c.GetString("role") != "admin" { - log.Printf("❌ [UPD_ADDR] AccĂšs refusĂ© - role=%s", c.GetString("role")) + userRole := c.GetString("role") + if !utils.CheckRoleAdmin(c, userRole) { c.JSON(http.StatusForbidden, gin.H{"error": "AccĂšs refusĂ©"}) return } @@ -111,13 +107,11 @@ func UpdateCommandAddress(c *gin.Context) { return } - // ✅ Validation de l'adresse if err := validateAddress(req.DeliveryAddress); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - // ✅ Logs sanitizĂ©s log.Printf("📝 [UPD_ADDR] Admin %s modifie cmd %d", adminUsername, commandID) command, err := database.GetCommandByID(commandID) @@ -156,13 +150,11 @@ func UpdateCommandAddress(c *gin.Context) { } // ProposeAddressChange propose une nouvelle adresse au client pour validation -// POST /api/v2/admin/protected/orders/:id/propose-address -// POST /api/v1/cabine/commands/:id/propose-address func ProposeAddressChange(c *gin.Context) { database := c.MustGet("database").(*db.Database) userRole := c.GetString("role") - if userRole != "admin" && userRole != "cabine" { + if !utils.CheckRoleAdmin(c, userRole) && !utils.CheckRoleCabine(c, userRole) { c.JSON(http.StatusForbidden, gin.H{"error": "AccĂšs refusĂ©"}) return } @@ -218,7 +210,6 @@ func ProposeAddressChange(c *gin.Context) { database.NotifyClient(clientUsername, commandID, "address_proposal", msg) } - log.Printf("✅ [PROPOSE_ADDR] Commande %d - nouvelle adresse proposĂ©e par %s", commandID, staffUsername) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Nouvelle adresse proposĂ©e au client", @@ -231,6 +222,11 @@ func ProposeAddressChange(c *gin.Context) { func RespondToAddressProposal(c *gin.Context) { database := c.MustGet("database").(*db.Database) + userRole := c.GetString("role") + if !utils.CheckRoleClient(c, userRole) { + c.JSON(http.StatusForbidden, gin.H{"error": "AccĂšs refusĂ©"}) + return + } clientUsername, err := safeGetUsername(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentification requise"}) @@ -276,8 +272,7 @@ func GetAllCommands(c *gin.Context) { username := c.Query("username") userRole := c.GetString("role") - if userRole != "admin" && userRole != "cabine" { - log.Printf("❌ [VALIDATE] AccĂšs refusĂ© - role=%s", userRole) + if !utils.CheckRoleAdmin(c, userRole) && !utils.CheckRoleCabine(c, userRole) { c.JSON(http.StatusForbidden, gin.H{"error": "AccĂšs refusĂ©"}) return } @@ -725,8 +720,7 @@ func GetClientCommandsHistory(c *gin.Context) { } else if client == nil { log.Printf("⚠ [HISTORY] client est NIL!") } else { - log.Printf("✅ [HISTORY] Client rĂ©cupĂ©rĂ©: username=%s, point=%d, point_zipette=%d", - client.Username, client.Point, client.PointZipette) + log.Printf("✅ [HISTORY] Client rĂ©cupĂ©rĂ©: username=%s", client.Username) } resp := gin.H{ @@ -760,7 +754,6 @@ func GetClientCommandsHistory(c *gin.Context) { "prenom": client.Prenom, "telephone": client.Telephone, "total_commands": client.Command, - "points": client.Point, "pool_points": poolPoints, "pool_names": poolNames, "penalties": client.Amende, diff --git a/backend/gestion/handlers/crypto_payment.go b/backend/gestion/handlers/crypto_payment.go index eeb5e96b..5f06d42a 100644 --- a/backend/gestion/handlers/crypto_payment.go +++ b/backend/gestion/handlers/crypto_payment.go @@ -13,7 +13,6 @@ import ( ) // IPNWebhook - POST /api/v1/webhooks/nowpayments -// Reçoit les callbacks de NowPayments lors des changements de statut func IPNWebhook(c *gin.Context) { database := c.MustGet("database").(*db.Database) np, ok := c.MustGet("nowpayments").(*services.NowPaymentsClient) @@ -44,7 +43,6 @@ func IPNWebhook(c *gin.Context) { payment, err := database.GetCryptoPaymentByNowPaymentID(payload.PaymentID.String()) if err != nil || payment == nil { log.Printf("[IPN] paiement introuvable: %s", payload.PaymentID.String()) - // 200 pour Ă©viter les retries NowPayments c.JSON(http.StatusOK, gin.H{"ok": true}) return } diff --git a/backend/gestion/handlers/deleviry.go b/backend/gestion/handlers/deleviry.go index 19cb29dc..91959b19 100644 --- a/backend/gestion/handlers/deleviry.go +++ b/backend/gestion/handlers/deleviry.go @@ -1,25 +1,18 @@ -// ============================================ -// handlers/delivery_handlers.go -// 🔧 VERSION MODIFIÉE avec ETA automatique -// ============================================ - package handlers import ( "encoding/json" "fmt" "gestion/db" + "gestion/utils" "log" - "math" "net/http" + "slices" "strconv" "github.com/gin-gonic/gin" ) -// ============================================ -// 🔧 GetMyDeliveries -// ============================================ func GetMyDeliveries(c *gin.Context) { database := c.MustGet("database").(*db.Database) @@ -46,7 +39,6 @@ func GetMyDeliveries(c *gin.Context) { return } - // ✹ FILTRAGE (SANS TÉLÉPHONE) filteredCommands := make([]gin.H, len(commands)) for i, cmd := range commands { commandID, _ := cmd["id"].(int) @@ -172,10 +164,6 @@ func GetDeliveryDetails(c *gin.Context) { }) } -// ============================================ -// 🔧 UpdateDeliveryStatus - VERSION MODIFIÉE -// ✅ CALCUL AUTOMATIQUE ETA lors du passage en "en_route" -// ============================================ func UpdateDeliveryStatus(c *gin.Context) { database := c.MustGet("database").(*db.Database) @@ -215,7 +203,6 @@ func UpdateDeliveryStatus(c *gin.Context) { return } - // ✅ VÉRIFIER PROPRIÉTÉ livreurAssign, _ := command["livreur_assign"].(string) if livreurAssign != usernameStr { log.Printf("❌ AccĂšs refusĂ© - assignĂ© Ă  %s", livreurAssign) @@ -225,7 +212,6 @@ func UpdateDeliveryStatus(c *gin.Context) { return } - // ✅ STATUTS VALIDES POUR LIVREUR (correspondant Ă  la DB) validStatuses := []string{ "assigned", "en_route", @@ -234,15 +220,7 @@ func UpdateDeliveryStatus(c *gin.Context) { "cancelled", } - isValid := false - for _, s := range validStatuses { - if req.Status == s { - isValid = true - break - } - } - - if !isValid { + if !slices.Contains(validStatuses, req.Status) { c.JSON(http.StatusBadRequest, gin.H{ "error": "Statut invalide", "valid_statuses": validStatuses, @@ -261,7 +239,7 @@ func UpdateDeliveryStatus(c *gin.Context) { destLon, _ := command["dest_longitude"].(float64) if destLat != 0 && destLon != 0 { - distance := calculateDistance(req.Latitude, req.Longitude, destLat, destLon) + distance := utils.CalculateDistance(req.Latitude, req.Longitude, destLat, destLon) log.Printf("📍 [GPS] Distance: %.2f m", distance) if distance > 100 { @@ -295,10 +273,8 @@ func UpdateDeliveryStatus(c *gin.Context) { if req.Status == "en_route" { log.Printf("🚗 [STATUS_LIVREUR] Passage en 'en_route' - Calcul ETA...") - // RĂ©cupĂ©rer les coordonnĂ©es destination var destLat, destLon float64 - // 1. Essayer le cache Redis destCacheKey := fmt.Sprintf("command:destination:%d", commandID) destData, err := db.Redis.Get(db.RedisCtx, destCacheKey).Result() if err == nil && destData != "" { @@ -326,22 +302,29 @@ func UpdateDeliveryStatus(c *gin.Context) { } } - // 3. Calculer l'ETA depuis la position du livreur if destLat != 0 && destLon != 0 { etaMinutes = database.CalculateETAForDeliveryman(usernameStr, destLat, destLon) - // DĂ©finir l'ETA dans Redis if err := database.SetCommandETA(commandID, etaMinutes); err != nil { log.Printf("⚠ [STATUS_LIVREUR] Erreur dĂ©finition ETA: %v", err) } else { log.Printf("✅ [STATUS_LIVREUR] ETA dĂ©fini: %d minutes", etaMinutes) - etaMessage = fmt.Sprintf("ArrivĂ©e prĂ©vue dans %d minutes", etaMinutes) + if etaMinutes >= 60 { + h := etaMinutes / 60 + m := etaMinutes % 60 + if m > 0 { + etaMessage = fmt.Sprintf("ArrivĂ©e prĂ©vue dans %dh%02d", h, m) + } else { + etaMessage = fmt.Sprintf("ArrivĂ©e prĂ©vue dans %dh", h) + } + } else { + etaMessage = fmt.Sprintf("ArrivĂ©e prĂ©vue dans %d minutes", etaMinutes) + } } } else { log.Printf("⚠ [STATUS_LIVREUR] CoordonnĂ©es destination manquantes - ETA par dĂ©faut") - etaMinutes = 30 // Fallback + etaMinutes = 30 database.SetCommandETA(commandID, etaMinutes) - etaMessage = "ArrivĂ©e prĂ©vue dans 30 minutes (estimation par dĂ©faut)" } // Mettre Ă  jour le statut du livreur en "delivering" @@ -352,7 +335,7 @@ func UpdateDeliveryStatus(c *gin.Context) { // Log message := req.Notes if message == "" { - message = getDeliveryStatusMessage(req.Status) + message = utils.GetDeliveryStatusMessage(req.Status) } if etaMessage != "" { message += fmt.Sprintf(" - %s", etaMessage) @@ -366,9 +349,21 @@ func UpdateDeliveryStatus(c *gin.Context) { switch req.Status { case "en_route": if etaMinutes > 0 { - clientMsg = fmt.Sprintf("Votre commande #%d est en route ! ArrivĂ©e dans ~%d min", commandID, etaMinutes) + var etaStr string + if etaMinutes >= 60 { + h := etaMinutes / 60 + m := etaMinutes % 60 + if m > 0 { + etaStr = fmt.Sprintf("%dh%02d", h, m) + } else { + etaStr = fmt.Sprintf("%dh", h) + } + } else { + etaStr = fmt.Sprintf("%d min", etaMinutes) + } + clientMsg = fmt.Sprintf("đŸ›” Votre commande #%d est en route ! ArrivĂ©e dans ~%s", commandID, etaStr) } else { - clientMsg = fmt.Sprintf("Votre commande #%d est en route !", commandID) + clientMsg = fmt.Sprintf("đŸ›” Votre commande #%d est en route !", commandID) } case "arrived": clientMsg = fmt.Sprintf("đŸ›” Votre livreur est lĂ  ! Il sera chez vous dans 5 minutes (commande #%d)", commandID) @@ -412,44 +407,3 @@ func UpdateDeliveryStatus(c *gin.Context) { c.JSON(http.StatusOK, response) } - -// ============================================ -// HELPERS -// ============================================ -func calculateDistance(lat1, lon1, lat2, lon2 float64) float64 { - const earthRadiusKm = 6371 - const metersPerKm = 1000 - - lat1Rad := degreesToRadians(lat1) - lon1Rad := degreesToRadians(lon1) - lat2Rad := degreesToRadians(lat2) - lon2Rad := degreesToRadians(lon2) - - dLat := lat2Rad - lat1Rad - dLon := lon2Rad - lon1Rad - - a := math.Sin(dLat/2)*math.Sin(dLat/2) + - math.Cos(lat1Rad)*math.Cos(lat2Rad)* - math.Sin(dLon/2)*math.Sin(dLon/2) - - c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a)) - return earthRadiusKm * c * metersPerKm -} - -func degreesToRadians(degrees float64) float64 { - return degrees * math.Pi / 180 -} - -func getDeliveryStatusMessage(status string) string { - messages := map[string]string{ - "assigned": "Commande assignĂ©e", - "en_route": "En route vers le client", - "arrived": "ArrivĂ© Ă  destination", - "livre": "Livraison effectuĂ©e", - "cancelled": "Livraison annulĂ©e", - } - if msg, ok := messages[status]; ok { - return msg - } - return fmt.Sprintf("Statut changĂ©: %s", status) -} diff --git a/backend/gestion/handlers/delivery_admin.go b/backend/gestion/handlers/delivery_admin.go index 50be7352..4bc3cfe0 100644 --- a/backend/gestion/handlers/delivery_admin.go +++ b/backend/gestion/handlers/delivery_admin.go @@ -8,6 +8,7 @@ package handlers import ( "fmt" "gestion/db" + "gestion/utils" "log" "net/http" "strconv" @@ -16,12 +17,7 @@ import ( "github.com/gin-gonic/gin" ) -// ============================================ -// 📊 GET DELIVERY PERSON DETAILS -// ============================================ - // GetDeliveryPersonDetails rĂ©cupĂšre les dĂ©tails complets d'un livreur -// GET /api/v2/admin/protected/delivery-persons/:username func GetDeliveryPersonDetails(c *gin.Context) { database := c.MustGet("database").(*db.Database) @@ -39,11 +35,6 @@ func GetDeliveryPersonDetails(c *gin.Context) { return } - log.Printf("đŸ‘€ [GET_DELIVERY_DETAILS] RĂ©cupĂ©ration dĂ©tails pour: %s", username) - - // ============================================ - // ÉTAPE 1: RĂ©cupĂ©rer les infos de base du livreur - // ============================================ livreur, err := database.GetUserByUsername(username) if err != nil { log.Printf("❌ [GET_DELIVERY_DETAILS] Livreur non trouvĂ©: %v", err) @@ -64,9 +55,6 @@ func GetDeliveryPersonDetails(c *gin.Context) { return } - // ============================================ - // ÉTAPE 2: RĂ©cupĂ©rer le statut et la position GPS - // ============================================ status, err := database.GetDeliveryPersonStatus(username) if err != nil { log.Printf("⚠ [GET_DELIVERY_DETAILS] Impossible de rĂ©cupĂ©rer le statut: %v", err) @@ -83,22 +71,13 @@ func GetDeliveryPersonDetails(c *gin.Context) { } } - // ============================================ - // ÉTAPE 3: RĂ©cupĂ©rer les stats de livraison - // ============================================ queueSize, _ := database.GetDeliverymanQueueSize(username) currentCommand, _ := database.GetCurrentCommand(username) - // Compter les livraisons totalDeliveries, _ := database.CountDeliveriesByStatus(username, "") completedDeliveries, _ := database.CountDeliveriesByStatus(username, "approved") pendingDeliveries, _ := database.CountDeliveriesByStatus(username, "assigned,en_route,livre") - log.Printf("✅ [GET_DELIVERY_DETAILS] DĂ©tails rĂ©cupĂ©rĂ©s pour %s", username) - - // ============================================ - // RÉPONSE - // ============================================ c.JSON(http.StatusOK, gin.H{ "success": true, "deliveryman": gin.H{ @@ -116,19 +95,12 @@ func GetDeliveryPersonDetails(c *gin.Context) { }) } -// ============================================ -// 🔄 UPDATE DELIVERY PERSON STATUS -// ============================================ - // UpdateDeliveryPersonStatusAdmin modifie le statut d'un livreur (Admin) -// PUT /api/v2/admin/protected/delivery-persons/:username/status func UpdateDeliveryPersonStatusAdmin(c *gin.Context) { database := c.MustGet("database").(*db.Database) - // ✅ SÉCURITÉ: Admin seulement userRole := c.GetString("role") - if userRole != "admin" { - log.Printf("❌ [UPDATE_DELIVERY_STATUS] AccĂšs refusĂ© - role=%s", userRole) + if !utils.CheckRoleAdmin(c, userRole) { c.JSON(http.StatusForbidden, gin.H{"error": "AccĂšs refusĂ©"}) return } @@ -172,9 +144,6 @@ func UpdateDeliveryPersonStatusAdmin(c *gin.Context) { log.Printf("📝 [UPDATE_DELIVERY_STATUS] Modification: %s → %s", username, req.Status) - // ============================================ - // VĂ©rifier que le livreur existe - // ============================================ livreur, err := database.GetUserByUsername(username) if err != nil { log.Printf("❌ [UPDATE_DELIVERY_STATUS] Livreur non trouvĂ©") @@ -189,9 +158,6 @@ func UpdateDeliveryPersonStatusAdmin(c *gin.Context) { return } - // ============================================ - // Mettre Ă  jour le statut - // ============================================ err = database.UpdateDeliveryPersonStatus(username, req.Status) if err != nil { log.Printf("❌ [UPDATE_DELIVERY_STATUS] Erreur mise Ă  jour: %v", err) @@ -215,16 +181,10 @@ func UpdateDeliveryPersonStatusAdmin(c *gin.Context) { }) } -// ============================================ -// 📊 GET DELIVERY PERSON STATS -// ============================================ - // GetDeliveryPersonStats rĂ©cupĂšre les statistiques d'un livreur -// GET /api/v2/admin/protected/delivery-persons/:username/stats func GetDeliveryPersonStats(c *gin.Context) { database := c.MustGet("database").(*db.Database) - // ✅ SÉCURITÉ: Admin seulement userRole := c.GetString("role") if userRole != "admin" { log.Printf("❌ [GET_DELIVERY_STATS] AccĂšs refusĂ© - role=%s", userRole) @@ -240,9 +200,6 @@ func GetDeliveryPersonStats(c *gin.Context) { log.Printf("📊 [GET_DELIVERY_STATS] Calcul stats pour: %s", username) - // ============================================ - // VĂ©rifier que le livreur existe - // ============================================ livreur, err := database.GetUserByUsername(username) if err != nil { log.Printf("❌ [GET_DELIVERY_STATS] Livreur non trouvĂ©") @@ -306,12 +263,7 @@ func GetDeliveryPersonStats(c *gin.Context) { }) } -// ============================================ -// 📜 GET DELIVERY PERSON HISTORY -// ============================================ - // GetDeliveryPersonHistory rĂ©cupĂšre l'historique des livraisons d'un livreur -// GET /api/v2/admin/protected/delivery-persons/:username/history?limit=20&offset=0 func GetDeliveryPersonHistory(c *gin.Context) { database := c.MustGet("database").(*db.Database) @@ -394,10 +346,6 @@ func GetDeliveryPersonHistory(c *gin.Context) { }) } -// ============================================ -// 📍 UPDATE DELIVERY PERSON LOCATION -// ============================================ - // UpdateDeliveryPersonLocationAdmin modifie la position GPS d'un livreur (Admin) // PUT /api/v2/admin/protected/delivery-persons/:username/location func UpdateDeliveryPersonLocationAdmin(c *gin.Context) { @@ -579,7 +527,6 @@ func RemoveCommandFromQueue(c *gin.Context) { if err != nil { log.Printf("⚠ [REMOVE_FROM_QUEUE] Impossible de rĂ©initialiser le statut: %v", err) } else { - // Retirer l'assignation du livreur database.UpdateCommandLivreur(commandID, "") log.Printf("✅ [REMOVE_FROM_QUEUE] Commande rĂ©initialisĂ©e en 'pending'") } diff --git a/backend/gestion/handlers/eta.go b/backend/gestion/handlers/eta.go index cf057e61..e1565463 100644 --- a/backend/gestion/handlers/eta.go +++ b/backend/gestion/handlers/eta.go @@ -18,11 +18,6 @@ import ( "github.com/gin-gonic/gin" ) -// ============================================ -// GET /api/v1/orders/:id/eta -// ✅ CORRECTION: ETA visible UNIQUEMENT si status >= en_route -// ============================================ - func GetOrderETA(c *gin.Context) { database := c.MustGet("database").(*db.Database) geoService := c.MustGet("geoService").(*services.GeoService) @@ -49,9 +44,6 @@ func GetOrderETA(c *gin.Context) { return } - log.Printf("📊 [ETA] START - commandID=%d, username=%s", commandID, username.(string)) - - // 3ïžâƒŁ RÉCUPÉRER LA COMMANDE command, err := database.GetCommandByID(commandID) if err != nil { log.Printf("❌ [ETA] Commande %d non trouvĂ©e", commandID) diff --git a/backend/gestion/handlers/geoloca.go b/backend/gestion/handlers/geoloca.go index 23da5bf5..d3278eec 100644 --- a/backend/gestion/handlers/geoloca.go +++ b/backend/gestion/handlers/geoloca.go @@ -22,8 +22,6 @@ import ( // ============================================ // GeocodeAddress convertit une adresse en coordonnĂ©es GPS -// POST /api/v1/geocode -// Body: {"address": "1600 Amphitheatre Parkway, Mountain View, CA"} func GeocodeAddress(c *gin.Context) { geoService := c.MustGet("geoService").(*services.GeoService) @@ -59,10 +57,6 @@ func GeocodeAddress(c *gin.Context) { }) } -// ============================================ -// RECHERCHE DU LIVREUR LE PLUS PROCHE -// ============================================ - // FindNearestDeliveryPerson trouve le livreur le plus proche d'une adresse func FindNearestDeliveryPerson(c *gin.Context) { database := c.MustGet("database").(*db.Database) @@ -181,8 +175,6 @@ func FindNearestDeliveryPerson(c *gin.Context) { // ============================================ // GetAllDeliveryDistances retourne tous les livreurs triĂ©s par distance -// POST /api/v2/admin/protected/delivery/distances -// Body: {"address": "123 Main St"} ou {"latitude": 48.8566, "longitude": 2.3522} func GetAllDeliveryDistances(c *gin.Context) { database := c.MustGet("database").(*db.Database) geoService := c.MustGet("geoService").(*services.GeoService) diff --git a/backend/gestion/handlers/gps.go b/backend/gestion/handlers/gps.go index 70295780..0e88af82 100644 --- a/backend/gestion/handlers/gps.go +++ b/backend/gestion/handlers/gps.go @@ -6,9 +6,11 @@ package handlers import ( + "fmt" "gestion/db" "log" "net/http" + "net/url" "strconv" "github.com/gin-gonic/gin" @@ -127,3 +129,49 @@ func GetCommandNavigationLinks(c *gin.Context) { "navigation_links": links, }) } + +// GetLivreurNavLink retourne le lien Waze App pour une livraison assignĂ©e au livreur connectĂ© +// GET /api/v1/livreur/deliveries/:id/nav-link +func GetLivreurNavLink(c *gin.Context) { + database := c.MustGet("database").(*db.Database) + username := c.GetString("username") + + commandID, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "ID invalide"}) + return + } + + command, err := database.GetCommandByID(commandID) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvĂ©e"}) + return + } + + livreurAssign, _ := command["livreur_assign"].(string) + if livreurAssign != username { + c.JSON(http.StatusForbidden, gin.H{"error": "Cette commande ne vous est pas assignĂ©e"}) + return + } + + // PrioritĂ© : coordonnĂ©es GPS de la destination + var wazeLink string + destLat, hasLat := command["dest_latitude"].(float64) + destLon, hasLon := command["dest_longitude"].(float64) + if hasLat && hasLon && destLat != 0 && destLon != 0 { + wazeLink = fmt.Sprintf("waze://?ll=%.6f,%.6f&navigate=yes", destLat, destLon) + log.Printf("đŸ—ș [NAV_LINK] Lien coords pour cmd %d: %s", commandID, wazeLink) + } else if adresse, _ := command["adresse"].(string); adresse != "" { + wazeLink = fmt.Sprintf("waze://?q=%s&navigate=yes", url.QueryEscape(adresse)) + log.Printf("đŸ—ș [NAV_LINK] Lien adresse pour cmd %d: %s", commandID, wazeLink) + } else { + c.JSON(http.StatusNotFound, gin.H{"error": "Aucune destination disponible pour cette commande"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "command_id": commandID, + "waze_app": wazeLink, + }) +} diff --git a/backend/gestion/handlers/history.go b/backend/gestion/handlers/history.go index fe791077..902e8ae5 100644 --- a/backend/gestion/handlers/history.go +++ b/backend/gestion/handlers/history.go @@ -83,7 +83,7 @@ func GetMyCompletedOrders(c *gin.Context) { response["client_stats"] = gin.H{ "username": client.Username, "total_commands": client.Command, - "points": client.Point, + "points_extra": client.PointsExtra, "pool_points": poolPoints, "pool_names": poolNames, "penalties": client.Amende, @@ -169,7 +169,7 @@ func GetMyCompletedOrdersWithItems(c *gin.Context) { "prenom": client.Prenom, "telephone": client.Telephone, "total_commands": client.Command, - "points": client.Point, + "points_extra": client.PointsExtra, "penalties": client.Amende, } } diff --git a/backend/gestion/handlers/notifications.go b/backend/gestion/handlers/notifications.go index b4ecd904..e26d2337 100644 --- a/backend/gestion/handlers/notifications.go +++ b/backend/gestion/handlers/notifications.go @@ -9,54 +9,6 @@ import ( "github.com/gin-gonic/gin" ) -// RegisterPushToken enregistre le push token Expo d'un client -// POST /api/v1/push-token -func RegisterPushToken(c *gin.Context) { - clientID := c.GetInt("client_id") - if clientID == 0 { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifiĂ©"}) - return - } - - var req struct { - PushToken string `json:"push_token" binding:"required"` - } - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "push_token requis"}) - return - } - - database := c.MustGet("database").(*db.Database) - if err := database.SaveClientPushToken(clientID, req.PushToken); err != nil { - log.Printf("❌ [PUSH_TOKEN] Erreur sauvegarde token pour client %d: %v", clientID, err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur enregistrement push token"}) - return - } - - log.Printf("✅ [PUSH_TOKEN] Token enregistrĂ© pour client %d", clientID) - c.JSON(http.StatusOK, gin.H{"success": true}) -} - -// UnregisterPushToken supprime le push token d'un client (au logout) -// DELETE /api/v1/push-token -func UnregisterPushToken(c *gin.Context) { - clientID := c.GetInt("client_id") - if clientID == 0 { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifiĂ©"}) - return - } - - database := c.MustGet("database").(*db.Database) - if err := database.DeleteClientPushToken(clientID); err != nil { - log.Printf("❌ [PUSH_TOKEN] Erreur suppression token pour client %d: %v", clientID, err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur suppression push token"}) - return - } - - log.Printf("✅ [PUSH_TOKEN] Token supprimĂ© pour client %d", clientID) - c.JSON(http.StatusOK, gin.H{"success": true}) -} - // GetClientNotifications retourne les notifications du client connectĂ© // GET /api/v1/notifications func GetClientNotifications(c *gin.Context) { @@ -107,54 +59,6 @@ func GetClientNotifications(c *gin.Context) { }) } -// RegisterLivreurPushToken enregistre le push token Expo d'un livreur -// POST /api/v1/livreur/push-token -func RegisterLivreurPushToken(c *gin.Context) { - username := c.GetString("username") - if username == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifiĂ©"}) - return - } - - var req struct { - PushToken string `json:"push_token" binding:"required"` - } - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "push_token requis"}) - return - } - - database := c.MustGet("database").(*db.Database) - if err := database.SaveUserPushToken(username, req.PushToken); err != nil { - log.Printf("❌ [LIVREUR_PUSH_TOKEN] Erreur sauvegarde token pour %s: %v", username, err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur enregistrement push token"}) - return - } - - log.Printf("✅ [LIVREUR_PUSH_TOKEN] Token enregistrĂ© pour livreur %s", username) - c.JSON(http.StatusOK, gin.H{"success": true}) -} - -// UnregisterLivreurPushToken supprime le push token d'un livreur (au logout) -// DELETE /api/v1/livreur/push-token -func UnregisterLivreurPushToken(c *gin.Context) { - username := c.GetString("username") - if username == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifiĂ©"}) - return - } - - database := c.MustGet("database").(*db.Database) - if err := database.DeleteUserPushToken(username); err != nil { - log.Printf("❌ [LIVREUR_PUSH_TOKEN] Erreur suppression token pour %s: %v", username, err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur suppression push token"}) - return - } - - log.Printf("✅ [LIVREUR_PUSH_TOKEN] Token supprimĂ© pour livreur %s", username) - c.JSON(http.StatusOK, gin.H{"success": true}) -} - // GetLivreurNotifications retourne les notifications du livreur connectĂ© // GET /api/v1/livreur/notifications func GetLivreurNotifications(c *gin.Context) { @@ -245,92 +149,6 @@ func MarkLivreurNotificationsRead(c *gin.Context) { }) } -// RegisterAdminPushToken enregistre le push token d'un admin -// POST /api/v2/admin/protected/push-token -func RegisterAdminPushToken(c *gin.Context) { - username := c.GetString("username") - if username == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifiĂ©"}) - return - } - var req struct { - PushToken string `json:"push_token" binding:"required"` - } - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "push_token requis"}) - return - } - database := c.MustGet("database").(*db.Database) - if err := database.SaveUserPushToken(username, req.PushToken); err != nil { - log.Printf("❌ [ADMIN_PUSH_TOKEN] Erreur sauvegarde pour %s: %v", username, err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur enregistrement push token"}) - return - } - log.Printf("✅ [ADMIN_PUSH_TOKEN] Token enregistrĂ© pour admin %s", username) - c.JSON(http.StatusOK, gin.H{"success": true}) -} - -// UnregisterAdminPushToken supprime le push token d'un admin (au logout) -// DELETE /api/v2/admin/protected/push-token -func UnregisterAdminPushToken(c *gin.Context) { - username := c.GetString("username") - if username == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifiĂ©"}) - return - } - database := c.MustGet("database").(*db.Database) - if err := database.DeleteUserPushToken(username); err != nil { - log.Printf("❌ [ADMIN_PUSH_TOKEN] Erreur suppression pour %s: %v", username, err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur suppression push token"}) - return - } - log.Printf("✅ [ADMIN_PUSH_TOKEN] Token supprimĂ© pour admin %s", username) - c.JSON(http.StatusOK, gin.H{"success": true}) -} - -// RegisterCabinePushToken enregistre le push token d'un agent cabine -// POST /api/v1/cabine/push-token -func RegisterCabinePushToken(c *gin.Context) { - username := c.GetString("username") - if username == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifiĂ©"}) - return - } - var req struct { - PushToken string `json:"push_token" binding:"required"` - } - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "push_token requis"}) - return - } - database := c.MustGet("database").(*db.Database) - if err := database.SaveUserPushToken(username, req.PushToken); err != nil { - log.Printf("❌ [CABINE_PUSH_TOKEN] Erreur sauvegarde pour %s: %v", username, err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur enregistrement push token"}) - return - } - log.Printf("✅ [CABINE_PUSH_TOKEN] Token enregistrĂ© pour cabine %s", username) - c.JSON(http.StatusOK, gin.H{"success": true}) -} - -// UnregisterCabinePushToken supprime le push token d'un agent cabine (au logout) -// DELETE /api/v1/cabine/push-token -func UnregisterCabinePushToken(c *gin.Context) { - username := c.GetString("username") - if username == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifiĂ©"}) - return - } - database := c.MustGet("database").(*db.Database) - if err := database.DeleteUserPushToken(username); err != nil { - log.Printf("❌ [CABINE_PUSH_TOKEN] Erreur suppression pour %s: %v", username, err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur suppression push token"}) - return - } - log.Printf("✅ [CABINE_PUSH_TOKEN] Token supprimĂ© pour cabine %s", username) - c.JSON(http.StatusOK, gin.H{"success": true}) -} - // MarkNotificationsRead marque toutes les notifications comme lues // POST /api/v1/notifications/read func MarkNotificationsRead(c *gin.Context) { diff --git a/backend/gestion/handlers/panier.go b/backend/gestion/handlers/panier.go index 1c40ba53..26cd9196 100644 --- a/backend/gestion/handlers/panier.go +++ b/backend/gestion/handlers/panier.go @@ -295,8 +295,8 @@ func ValidateBasket(c *gin.Context) { var req struct { DeliveryAddress string `json:"delivery_address" binding:"required"` UseReferralBalance bool `json:"use_referral_balance"` - PaymentMethod string `json:"payment_method"` // "cash" (dĂ©faut) ou "crypto" - PayCurrency string `json:"pay_currency"` // ex: "btc", "eth", "ltc" (requis si crypto) + PaymentMethod string `json:"payment_method"` // "cash" (dĂ©faut) ou "crypto" + PayCurrency string `json:"pay_currency"` // ex: "btc", "eth", "ltc" (requis si crypto) } if err := c.ShouldBindJSON(&req); err != nil || req.DeliveryAddress == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Adresse de livraison requise"}) @@ -310,6 +310,12 @@ func ValidateBasket(c *gin.Context) { } req.DeliveryAddress = cmd.DeliveryAddress + // VĂ©rifier que le client a liĂ© son compte Telegram + if _, linked, err := database.GetClientTelegramChatID(usernameStr); err != nil || !linked { + c.JSON(http.StatusForbidden, gin.H{"error": "Vous devez lier votre compte Telegram avant de commander"}) + return + } + log.Printf("🛒 [CHECKOUT] DĂ©but checkout pour: %s", usernameStr) // ============================================ @@ -430,7 +436,7 @@ func ValidateBasket(c *gin.Context) { command, err := database.CreateCommandWithAddress(usernameStr, req.DeliveryAddress) if err != nil { if referralUsed > 0 { - _ = database.RestoreReferralBalance(usernameStr, referralUsed) + _ = database.CreditClientReferral(usernameStr, referralUsed) } log.Printf("❌ [CHECKOUT] Erreur crĂ©ation commande: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur crĂ©ation commande"}) @@ -464,7 +470,7 @@ func ValidateBasket(c *gin.Context) { // Annuler la commande et restaurer le panier / parrainage _ = database.CancelCryptoCommand(commandID) if referralUsed > 0 { - _ = database.RestoreReferralBalance(usernameStr, referralUsed) + _ = database.CreditClientReferral(usernameStr, referralUsed) } log.Printf("❌ [CHECKOUT] Erreur crĂ©ation paiement NowPayments: %v", err) c.JSON(http.StatusBadGateway, gin.H{"error": "Impossible d'initier le paiement crypto"}) @@ -520,14 +526,9 @@ func ValidateBasket(c *gin.Context) { if err == nil { log.Printf("📍 [CHECKOUT] Adresse gĂ©ocodĂ©e: %.6f,%.6f", location.Latitude, location.Longitude) - activeLivreurs, err := database.GetAllActiveDeliveryPersons() - if err == nil && len(activeLivreurs) > 0 { - log.Printf("🚚 [CHECKOUT] %d livreurs actifs disponibles", len(activeLivreurs)) - - usernames := make([]string, len(activeLivreurs)) - for i, livreur := range activeLivreurs { - usernames[i] = livreur.Username - } + usernames, errEligible := database.GetEligibleDeliverymenForCommand(commandID) + if errEligible == nil && len(usernames) > 0 { + log.Printf("🚚 [CHECKOUT] %d livreur(s) Ă©ligible(s) disponibles", len(usernames)) nearest, err := geoService.FindNearestDeliveryPersonFast(services.Coordinates{ Latitude: location.Latitude, @@ -599,7 +600,7 @@ func ValidateBasket(c *gin.Context) { log.Printf("⚠ [CHECKOUT] Aucun livreur trouvĂ©: %v", err) } } else { - log.Printf("⚠ [CHECKOUT] Aucun livreur actif disponible") + log.Printf("⚠ [CHECKOUT] Aucun livreur Ă©ligible disponible") } } else { log.Printf("⚠ [CHECKOUT] Erreur gĂ©ocodage: %v", err) diff --git a/backend/gestion/handlers/product.go b/backend/gestion/handlers/product.go index 92160ff7..6b7fd506 100644 --- a/backend/gestion/handlers/product.go +++ b/backend/gestion/handlers/product.go @@ -12,7 +12,6 @@ import ( "path/filepath" "strconv" "strings" - "time" "github.com/gabriel-vasile/mimetype" "github.com/gin-gonic/gin" @@ -654,41 +653,12 @@ func UpdateProduct(c *gin.Context) { log.Printf("🔄 [UpdateProduct] %s met Ă  jour produit #%d", username, id) - // ✅ UPDATE PRODUIT - updateQuery := ` - UPDATE products - SET name = $1, category = $2, description = $3, stock = $4, unit = $5, updated_at = $6 - WHERE id = $7 - ` - - _, err = database.Exec(updateQuery, - updateData.Name, - updateData.Category, - updateData.Description, - updateData.Stock, - updateData.Unit, - time.Now(), - id, - ) - if err != nil { + if err := database.UpdateProduct(id, updateData.Name, updateData.Category, updateData.Description, updateData.Unit, updateData.Stock, updateData.Prices); err != nil { log.Printf("❌ [UpdateProduct] Erreur UPDATE: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur mise Ă  jour"}) return } - // ✅ UPDATE PRIX - database.Exec(`DELETE FROM product_prices WHERE product_id = $1`, id) - - for _, price := range updateData.Prices { - _, err := database.Exec(` - INSERT INTO product_prices (product_id, quantity, price) - VALUES ($1, $2, $3) - `, id, price.Quantity, price.Price) - if err != nil { - log.Printf("❌ [UpdateProduct] Erreur prix: %v", err) - } - } - // ✅ RÉCUPÉRER LE PRODUIT MIS À JOUR updatedProduct, _ := database.GetProductByID(id) media, _ := database.GetMediaByProductID(id) @@ -702,10 +672,6 @@ func UpdateProduct(c *gin.Context) { }) } -// ============================================ -// DELETE MEDIA - VERSION SÉCURISÉE -// ============================================ - func DeleteMedia(c *gin.Context) { database := c.MustGet("database").(*db.Database) @@ -756,8 +722,6 @@ func DeleteMedia(c *gin.Context) { }) } -// handlers/product_handlers_SECURED.go - func UploadMedia(c *gin.Context) { database := c.MustGet("database").(*db.Database) diff --git a/backend/gestion/handlers/redis_services.go b/backend/gestion/handlers/redis_services.go index 237f250e..6a193328 100644 --- a/backend/gestion/handlers/redis_services.go +++ b/backend/gestion/handlers/redis_services.go @@ -962,7 +962,7 @@ func ResetClientPointAdmin(c *gin.Context) { database := c.MustGet("database").(*db.Database) extraPoolKey := "" - if req.Pool >= 2 { + if req.Pool >= 0 { if settings, err := database.GetSettings(); err == nil && req.Pool < len(settings.PointsPools) { extraPoolKey = settings.PointsPools[req.Pool].Key } diff --git a/backend/gestion/handlers/settings.go b/backend/gestion/handlers/settings.go index ffc68446..6bd783eb 100644 --- a/backend/gestion/handlers/settings.go +++ b/backend/gestion/handlers/settings.go @@ -2,8 +2,11 @@ package handlers import ( "gestion/db" + "gestion/models" + "gestion/services" "log" "net/http" + "os" "github.com/gin-gonic/gin" ) @@ -27,18 +30,18 @@ func GetPublicSettings(c *gin.Context) { } c.JSON(http.StatusOK, gin.H{ - "success": true, - "penalties_enabled": settings.PenaltiesEnabled, - "show_amende_score": settings.ShowAmendeScore, - "points_enabled": settings.PointsEnabled, - "points_separated": len(settings.PointsPools) > 1, - "pool_names": poolNames, - "pool_keys": poolKeys, - "referral_enabled": settings.ReferralEnabled, - "delivery_schedule": settings.DeliverySchedule, - "crypto_payment_enabled": settings.CryptoPaymentEnabled, - "crypto_only": settings.CryptoOnly, - "nowpayments_currencies": settings.NowPaymentsCurrencies, + "success": true, + "penalties_enabled": settings.PenaltiesEnabled, + "show_amende_score": settings.ShowAmendeScore, + "points_enabled": settings.PointsEnabled, + "points_separated": len(settings.PointsPools) > 1, + "pool_names": poolNames, + "pool_keys": poolKeys, + "referral_enabled": settings.ReferralEnabled, + "delivery_schedule": settings.DeliverySchedule, + "crypto_payment_enabled": settings.CryptoPaymentEnabled, + "crypto_only": settings.CryptoOnly, + "nowpayments_currencies": settings.NowPaymentsCurrencies, }) } @@ -60,7 +63,7 @@ func GetSettings(c *gin.Context) { func UpdateSettings(c *gin.Context) { database := c.MustGet("database").(*db.Database) - var req db.AppSettings + var req models.AppSettings if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "ParamĂštres invalides"}) return @@ -72,7 +75,20 @@ func UpdateSettings(c *gin.Context) { return } - log.Printf("✅ [SETTINGS] Mise Ă  jour: penalties=%v, pools=%d", req.PenaltiesEnabled, len(req.PointsPools)) + // Recharger le service Telegram si le token/username a changĂ© + if services.TelegramBot != nil { + services.TelegramBot.Reload(req.TelegramBotToken, req.TelegramBotUsername) + if req.TelegramBotToken != "" { + log.Printf("✅ [SETTINGS] Service Telegram rechargĂ© (username: %s)", req.TelegramBotUsername) + if webhookURL := os.Getenv("TELEGRAM_WEBHOOK_URL"); webhookURL != "" { + if err := services.TelegramBot.SetWebhook(webhookURL); err != nil { + log.Printf("⚠ [SETTINGS] Erreur enregistrement webhook Telegram: %v", err) + } else { + log.Printf("✅ [SETTINGS] Webhook Telegram enregistrĂ©: %s", webhookURL) + } + } + } + } c.JSON(http.StatusOK, gin.H{"success": true, "settings": req}) } diff --git a/backend/gestion/handlers/telegram.go b/backend/gestion/handlers/telegram.go new file mode 100644 index 00000000..fcf94917 --- /dev/null +++ b/backend/gestion/handlers/telegram.go @@ -0,0 +1,292 @@ +package handlers + +import ( + "gestion/db" + "gestion/models" + "gestion/services" + "log" + "net/http" + "strings" + + "github.com/gin-gonic/gin" +) + +func TelegramWebhook(c *gin.Context) { + // VĂ©rification du secret webhook + secret := c.GetHeader("X-Telegram-Bot-Api-Secret-Token") + if services.TelegramBot == nil || !services.TelegramBot.ValidateWebhookSecret(secret) { + c.Status(http.StatusUnauthorized) + return + } + + var update models.TgUpdate + if err := c.ShouldBindJSON(&update); err != nil { + c.Status(http.StatusBadRequest) + return + } + + if update.Message == nil { + c.Status(http.StatusOK) + return + } + + text := strings.TrimSpace(update.Message.Text) + chatID := update.Message.Chat.ID + + // Commande /start — liaison de compte + if token, ok := strings.CutPrefix(text, "/start "); ok { + token = strings.TrimSpace(token) + handleLinkAccount(c, token, chatID) + return + } + + // Commande /start sans token — message d'accueil + if text == "/start" { + if services.TelegramBot != nil { + services.TelegramBot.SendMessage(chatID, + "👋 Bienvenue !\n\nPour lier votre compte, gĂ©nĂ©rez un token depuis l'application et envoyez /start <token>.") + } + } + + c.Status(http.StatusOK) +} + +func handleLinkAccount(c *gin.Context, token string, chatID int64) { + if token == "" { + if services.TelegramBot != nil { + services.TelegramBot.SendMessage(chatID, "❌ Token manquant. GĂ©nĂ©rez un nouveau token depuis l'application.") + } + c.Status(http.StatusOK) + return + } + + database := c.MustGet("database").(*db.Database) + + username, role, err := db.ValidateAndConsumeLinkToken(token) + if err != nil { + log.Printf("⚠ [TELEGRAM_LINK] Token invalide: %v", err) + if services.TelegramBot != nil { + services.TelegramBot.SendMessage(chatID, "❌ Token invalide ou expirĂ©. GĂ©nĂ©rez un nouveau token depuis l'application.") + } + c.Status(http.StatusOK) + return + } + + // Enregistrer le chat_id selon le rĂŽle + var saveErr error + switch role { + case "client": + saveErr = database.SaveClientTelegramChatID(username, chatID) + default: + saveErr = database.SaveUserTelegramChatID(username, chatID) + } + + if saveErr != nil { + log.Printf("❌ [TELEGRAM_LINK] Erreur sauvegarde chat_id pour %s (%s): %v", username, role, saveErr) + if services.TelegramBot != nil { + services.TelegramBot.SendMessage(chatID, "❌ Une erreur est survenue. RĂ©essayez.") + } + c.Status(http.StatusOK) + return + } + + log.Printf("✅ [TELEGRAM_LINK] Compte %s (%s) liĂ© au chat_id %d", username, role, chatID) + if services.TelegramBot != nil { + services.TelegramBot.SendMessage(chatID, + "✅ Compte liĂ© avec succĂšs !\n\nVous recevrez dĂ©sormais vos notifications ici.") + } + + c.Status(http.StatusOK) +} + +func GenerateClientLinkToken(c *gin.Context) { + username := c.GetString("username") + if username == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifiĂ©"}) + return + } + + if services.TelegramBot == nil || !services.TelegramBot.IsConfigured() { + c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Service Telegram non disponible"}) + return + } + + token, err := db.GenerateLinkToken(username, "client") + if err != nil { + log.Printf("❌ [TELEGRAM] Erreur gĂ©nĂ©ration token pour %s: %v", username, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur gĂ©nĂ©ration token"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "token": token, + "link_url": "https://t.me/" + services.TelegramBot.BotUsername + "?start=" + token, + "message": "/start " + token, + "expires_in": 600, + }) +} + +func GenerateLivreurLinkToken(c *gin.Context) { + username := c.GetString("username") + if username == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifiĂ©"}) + return + } + + if services.TelegramBot == nil || !services.TelegramBot.IsConfigured() { + c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Service Telegram non disponible"}) + return + } + + token, err := db.GenerateLinkToken(username, "livreur") + if err != nil { + log.Printf("❌ [TELEGRAM] Erreur gĂ©nĂ©ration token pour livreur %s: %v", username, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur gĂ©nĂ©ration token"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "token": token, + "link_url": "https://t.me/" + services.TelegramBot.BotUsername + "?start=" + token, + "message": "/start " + token, + "expires_in": 600, + }) +} + +func GenerateAdminLinkToken(c *gin.Context) { + username := c.GetString("username") + if username == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifiĂ©"}) + return + } + + if services.TelegramBot == nil || !services.TelegramBot.IsConfigured() { + c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Service Telegram non disponible"}) + return + } + + // RĂ©cupĂ©rer le rĂŽle rĂ©el depuis la DB + database := c.MustGet("database").(*db.Database) + user, err := database.GetUserByUsername(username) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Utilisateur introuvable"}) + return + } + + token, err := db.GenerateLinkToken(username, user.Role) + if err != nil { + log.Printf("❌ [TELEGRAM] Erreur gĂ©nĂ©ration token pour admin %s: %v", username, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur gĂ©nĂ©ration token"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "token": token, + "link_url": "https://t.me/" + services.TelegramBot.BotUsername + "?start=" + token, + "message": "/start " + token, + "expires_in": 600, + }) +} + +// GET /api/v1/telegram/status +func GetClientTelegramStatus(c *gin.Context) { + username := c.GetString("username") + if username == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifiĂ©"}) + return + } + + database := c.MustGet("database").(*db.Database) + _, linked, err := database.GetClientTelegramChatID(username) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur vĂ©rification"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "linked": linked, + "enabled": services.TelegramBot != nil && services.TelegramBot.IsConfigured(), + }) +} + +// GET /api/v1/livreur/telegram/status +func GetLivreurTelegramStatus(c *gin.Context) { + username := c.GetString("username") + if username == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifiĂ©"}) + return + } + + database := c.MustGet("database").(*db.Database) + _, linked, err := database.GetUserTelegramChatID(username) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur vĂ©rification"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "linked": linked, + "enabled": services.TelegramBot != nil && services.TelegramBot.IsConfigured(), + }) +} + +// ============================================ +// DÉLIAISON TELEGRAM +// ============================================ + +// DELETE /api/v1/telegram/unlink +func UnlinkClientTelegram(c *gin.Context) { + username := c.GetString("username") + if username == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifiĂ©"}) + return + } + + database := c.MustGet("database").(*db.Database) + if err := database.DeleteClientTelegramChatID(username); err != nil { + log.Printf("❌ [TELEGRAM_UNLINK] Erreur pour %s: %v", username, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur dĂ©liaison"}) + return + } + + log.Printf("✅ [TELEGRAM_UNLINK] Compte client %s dĂ©liĂ©", username) + c.JSON(http.StatusOK, gin.H{"success": true}) +} + +// DELETE /api/v1/livreur/telegram/unlink +func UnlinkLivreurTelegram(c *gin.Context) { + username := c.GetString("username") + if username == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifiĂ©"}) + return + } + + database := c.MustGet("database").(*db.Database) + if err := database.DeleteUserTelegramChatID(username); err != nil { + log.Printf("❌ [TELEGRAM_UNLINK] Erreur pour %s: %v", username, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur dĂ©liaison"}) + return + } + + log.Printf("✅ [TELEGRAM_UNLINK] Compte livreur %s dĂ©liĂ©", username) + c.JSON(http.StatusOK, gin.H{"success": true}) +} + +// DELETE /api/v2/admin/protected/telegram/unlink +func UnlinkAdminTelegram(c *gin.Context) { + username := c.GetString("username") + if username == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifiĂ©"}) + return + } + + database := c.MustGet("database").(*db.Database) + if err := database.DeleteUserTelegramChatID(username); err != nil { + log.Printf("❌ [TELEGRAM_UNLINK] Erreur pour %s: %v", username, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur dĂ©liaison"}) + return + } + + log.Printf("✅ [TELEGRAM_UNLINK] Compte admin %s dĂ©liĂ©", username) + c.JSON(http.StatusOK, gin.H{"success": true}) +} diff --git a/backend/gestion/handlers/update_profile.go b/backend/gestion/handlers/update_profile.go index 02d059a1..c95bb331 100644 --- a/backend/gestion/handlers/update_profile.go +++ b/backend/gestion/handlers/update_profile.go @@ -16,6 +16,7 @@ import ( "gestion/db" "gestion/models" + "gestion/utils" ) // ============================================ @@ -99,12 +100,12 @@ func UpdateMyProfile(c *gin.Context) { // Mise Ă  jour du tĂ©lĂ©phone if req.Telephone != "" && req.Telephone != client.Telephone { - if !validatePhoneNumber(req.Telephone) { + if !utils.ValidatePhoneNumber(req.Telephone) { log.Printf("❌ [UPDATE_MY_PROFILE] TĂ©lĂ©phone invalide: %s", req.Telephone) c.JSON(http.StatusBadRequest, gin.H{"error": "NumĂ©ro de tĂ©lĂ©phone invalide"}) return } - normalizedPhone := normalizePhoneNumber(req.Telephone) + normalizedPhone := utils.NormalizePhoneNumber(req.Telephone) // VĂ©rifier que le tĂ©lĂ©phone n'est pas dĂ©jĂ  utilisĂ© if existingClient, _ := database.GetClientByTelephone(normalizedPhone); existingClient != nil && existingClient.ID != clientID { @@ -202,8 +203,8 @@ func UpdateClientByAdmin(c *gin.Context) { } // ✅ LOG DEBUG - État initial - log.Printf("📊 [UPDATE_CLIENT_ADMIN] État initial - Command: %d, Point: %d, PointZipette: %d, Amende: %.2f", - client.Command, client.Point, client.PointZipette, client.Amende) + log.Printf("📊 [UPDATE_CLIENT_ADMIN] État initial - Command: %d, Amende: %.2f", + client.Command, client.Amende) // VĂ©rifier si des modifications sont demandĂ©es hasChanges := false @@ -254,12 +255,12 @@ func UpdateClientByAdmin(c *gin.Context) { // Mise Ă  jour du tĂ©lĂ©phone if req.Telephone != "" && req.Telephone != client.Telephone { - if !validatePhoneNumber(req.Telephone) { + if !utils.ValidatePhoneNumber(req.Telephone) { log.Printf("❌ [UPDATE_CLIENT_ADMIN] TĂ©lĂ©phone invalide: %s", req.Telephone) c.JSON(http.StatusBadRequest, gin.H{"error": "NumĂ©ro de tĂ©lĂ©phone invalide"}) return } - normalizedPhone := normalizePhoneNumber(req.Telephone) + normalizedPhone := utils.NormalizePhoneNumber(req.Telephone) // VĂ©rifier que le tĂ©lĂ©phone n'est pas dĂ©jĂ  utilisĂ© if existingClient, _ := database.GetClientByTelephone(normalizedPhone); existingClient != nil && existingClient.ID != clientID { @@ -272,29 +273,12 @@ func UpdateClientByAdmin(c *gin.Context) { log.Printf("✏ [UPDATE_CLIENT_ADMIN] TĂ©lĂ©phone modifiĂ©: %s", normalizedPhone) } - // ✅ CORRECTION: Mise Ă  jour du compteur de commandes (Admin uniquement) - // VĂ©rifier explicitement si le champ est prĂ©sent (mĂȘme si valeur = 0) if req.Command != nil && *req.Command != client.Command { client.Command = *req.Command hasChanges = true log.Printf("✏ [UPDATE_CLIENT_ADMIN] Commandes modifiĂ©es: %d → %d", client.Command, *req.Command) } - // ✅ CORRECTION: Mise Ă  jour des points weed/hash (Admin uniquement) - if req.Point != nil && *req.Point != client.Point { - client.Point = *req.Point - hasChanges = true - log.Printf("✏ [UPDATE_CLIENT_ADMIN] Points Weed modifiĂ©s: %d → %d", client.Point, *req.Point) - } - - // ✅ CORRECTION CRITIQUE: Mise Ă  jour des points zipette (Admin uniquement) - if req.PointsZipette != nil && *req.PointsZipette != client.PointZipette { - client.PointZipette = *req.PointsZipette - hasChanges = true - log.Printf("✏ [UPDATE_CLIENT_ADMIN] Points Zipette modifiĂ©s: %d → %d", client.PointZipette, *req.PointsZipette) - } - - // ✅ CORRECTION: Mise Ă  jour des amendes (Admin uniquement) if req.Amende != nil && *req.Amende != client.Amende { client.Amende = *req.Amende hasChanges = true @@ -310,9 +294,8 @@ func UpdateClientByAdmin(c *gin.Context) { return } - // ✅ LOG DEBUG - État avant sauvegarde - log.Printf("📊 [UPDATE_CLIENT_ADMIN] État avant save - Command: %d, Point: %d, PointZipette: %d, Amende: %.2f", - client.Command, client.Point, client.PointZipette, client.Amende) + log.Printf("📊 [UPDATE_CLIENT_ADMIN] État avant save - Command: %d, Amende: %.2f", + client.Command, client.Amende) // Sauvegarder les modifications if err := database.UpdateClient(client); err != nil { @@ -446,15 +429,14 @@ func UpdateUserByAdmin(c *gin.Context) { func sanitizeClient(client *models.Client) gin.H { return gin.H{ - "id": client.ID, - "username": client.Username, - "nom": client.Nom, - "prenom": client.Prenom, - "telephone": client.Telephone, - "command": client.Command, - "point": client.Point, - "points_zipette": client.PointZipette, // ✅ AJOUTÉ - "amende": client.Amende, + "id": client.ID, + "username": client.Username, + "nom": client.Nom, + "prenom": client.Prenom, + "telephone": client.Telephone, + "command": client.Command, + "points_extra": client.PointsExtra, + "amende": client.Amende, } } diff --git a/backend/gestion/handlers/validation_deleviry.go b/backend/gestion/handlers/validation_deleviry.go index 670bcb07..545201a0 100644 --- a/backend/gestion/handlers/validation_deleviry.go +++ b/backend/gestion/handlers/validation_deleviry.go @@ -352,6 +352,12 @@ func StartDelivery(c *gin.Context) { fmt.Sprintf("Livraison dĂ©marrĂ©e par %s", usernameStr), usernameStr) + // Notifier le client + if clientUsername, _ := command["username"].(string); clientUsername != "" { + msg := fmt.Sprintf("đŸ›” Votre commande #%d est en route !", commandID) + database.NotifyClient(clientUsername, commandID, "en_route", msg) + } + log.Printf("✅ [START] Livraison %d dĂ©marrĂ©e", commandID) c.JSON(http.StatusOK, gin.H{ diff --git a/backend/gestion/handlers/zones.go b/backend/gestion/handlers/zones.go index aeb26ef6..7eef8d81 100644 --- a/backend/gestion/handlers/zones.go +++ b/backend/gestion/handlers/zones.go @@ -1,7 +1,7 @@ package handlers import ( - "gestion/db" + "gestion/models" "regexp" ) @@ -37,7 +37,7 @@ type zoneCheckResult struct { // Les zones sont lues depuis la DB (settings.PostalZones). // Code postal introuvable → OK = false (refus). // Code postal hors de toutes les zones → OK = false (refus). -func checkDeliveryZone(deliveryAddress string, total float64, zones []db.PostalZone) zoneCheckResult { +func checkDeliveryZone(deliveryAddress string, total float64, zones []models.PostalZone) zoneCheckResult { code := extractPostalCode(deliveryAddress) if code == "" { return zoneCheckResult{PostalCode: "", ZoneName: "inconnue", MinAmount: 0, OK: false} diff --git a/backend/gestion/main.go b/backend/gestion/main.go index 57b2ff40..e0419225 100644 --- a/backend/gestion/main.go +++ b/backend/gestion/main.go @@ -47,6 +47,36 @@ func main() { geoService := services.NewGeoService(db.Redis, db.RedisCtx) log.Println("✅ Service de gĂ©olocalisation initialisĂ©") + // Initialisation du service Telegram + telegramService := services.NewTelegramService() + if telegramService.IsConfigured() { + log.Println("✅ Service Telegram initialisĂ©") + if webhookURL := os.Getenv("TELEGRAM_WEBHOOK_URL"); webhookURL != "" { + if err := telegramService.SetWebhook(webhookURL); err != nil { + log.Printf("⚠ [TELEGRAM] Erreur enregistrement webhook: %v", err) + } + } + } else { + log.Println("â„č Service Telegram dĂ©sactivĂ© (TELEGRAM_BOT_TOKEN non dĂ©fini)") + } + + // Migration: ajout des colonnes telegram_chat_id + database.MigrateAddTelegramColumns() + + // PrioritĂ© DB > .env pour la config Telegram + if dbSettings, err := database.GetSettings(); err == nil { + telegramService.Reload(dbSettings.TelegramBotToken, dbSettings.TelegramBotUsername) + if dbSettings.TelegramBotToken != "" { + log.Printf("✅ [TELEGRAM] Config chargĂ©e depuis la DB (username: %s)", dbSettings.TelegramBotUsername) + // Enregistrer le webhook si l'URL est configurĂ©e (et pas dĂ©jĂ  fait depuis l'env) + if webhookURL := os.Getenv("TELEGRAM_WEBHOOK_URL"); webhookURL != "" { + if err := telegramService.SetWebhook(webhookURL); err != nil { + log.Printf("⚠ [TELEGRAM] Erreur enregistrement webhook (DB reload): %v", err) + } + } + } + } + // ============================================ // đŸ§č NETTOYAGE INITIAL DES COMMANDES INVALIDES // ============================================ @@ -128,7 +158,6 @@ func main() { c.Next() }) - // Middleware NowPayments : injecte le client si crypto activĂ© dans les settings r.Use(func(c *gin.Context) { settings, err := database.GetSettings() if err == nil && settings.CryptoPaymentEnabled && settings.NowPaymentsAPIKey != "" { @@ -146,74 +175,7 @@ func main() { // ============================================ routes.SetupRoutes(r, database, geoService) - // ============================================ - // LANCEMENT DU SERVEUR - // ============================================ - printServerInfo() - if err := r.Run(":8080"); err != nil { log.Fatalf("❌ Erreur au lancement du serveur : %v", err) } } - -// printServerInfo affiche les infos du serveur au dĂ©marrage -func printServerInfo() { - log.Println("") - log.Println("═══════════════════════════════════════════════════════════") - log.Println("🚀 Serveur lancĂ© sur http://localhost:8080") - log.Println("═══════════════════════════════════════════════════════════") - log.Println("") - log.Println("📡 API Endpoints disponibles:") - log.Println("") - log.Println(" 🌍 PUBLIC:") - log.Println(" GET /api/v1/health - Health check") - log.Println(" GET /api/v1/products - Liste produits") - log.Println("") - log.Println(" đŸ‘€ CLIENTS:") - log.Println(" POST /api/v1/auth/register - Inscription") - log.Println(" POST /api/v1/auth/login - Connexion") - log.Println(" POST /api/v1/checkout - ✅ PASSER COMMANDE (avec auto-assign)") - log.Println(" GET /api/v1/my-commands - Mes commandes avec suivi") - log.Println(" GET /api/v1/commands/:id/status - ✅ Statut temps rĂ©el") - log.Println(" GET /api/v1/commands/:id/tracking - ✅ Suivi dĂ©taillĂ©") - log.Println(" POST /api/v1/commands/:id/approve - Confirmer livraison") - log.Println("") - log.Println(" 🚗 LIVREURS:") - log.Println(" GET /api/v1/livreur/deliveries - ✅ Mes livraisons (SANS tĂ©lĂ©phone client)") - log.Println(" GET /api/v1/livreur/deliveries/:id - ✅ DĂ©tail livraison filtrĂ©e") - log.Println(" PUT /api/v1/livreur/deliveries/:id/status - MAJ statut (avec GPS)") - log.Println(" POST /api/v1/livreur/location/update - MAJ position GPS") - log.Println(" GET /api/v1/livreur/queue - Ma queue de livraisons") - log.Println("") - log.Println(" 🔐 ADMIN:") - log.Println(" POST /api/v2/admin/auth/login - Connexion admin") - log.Println(" GET /api/v2/admin/protected/orders - Toutes les commandes") - log.Println(" POST /api/v2/admin/protected/orders/:id/auto-assign - ✅ Auto-assign GPS") - log.Println(" POST /api/v2/admin/protected/commands/auto-assign-all - ✅ Assigner toutes") - log.Println(" GET /api/v2/admin/protected/delivery/queues - Queues livreurs") - log.Println("") - log.Println("⏰ Workers & Services actifs:") - log.Println(" - ✅ AutoAssignmentCron (5min) - Assignation backup") - log.Println(" - ✅ QueueCleanupScheduler (5min) - đŸ§č Nettoyage commandes invalides") - log.Println(" - ✅ NotificationWorker (30s) - Notifications ETA") - log.Println(" - ✅ StockCleanupWorker (5min) - Nettoyage stock") - log.Println("") - log.Println("🎯 FonctionnalitĂ©s activĂ©es:") - log.Println(" 1. ✅ Auto-assignation au checkout (immĂ©diate)") - log.Println(" 2. ✅ Cron job backup (toutes les 5 min)") - log.Println(" 3. ✅ Validation stricte avant ajout Ă  la queue") - log.Println(" 4. ✅ Nettoyage automatique des commandes invalides") - log.Println(" 5. ✅ Vue livreur filtrĂ©e (sans tĂ©lĂ©phone)") - log.Println(" 6. ✅ Vue client avec suivi temps rĂ©el") - log.Println(" 7. ✅ Gestion automatique statut BUSY (queue >= 10)") - log.Println("") - log.Println("🔒 RĂšgles de validation automatique:") - log.Println(" - Username non vide ✓") - log.Println(" - Adresse de livraison non vide ✓") - log.Println(" - CoordonnĂ©es GPS valides (lat != 0, lng != 0) ✓") - log.Println(" - Date de crĂ©ation valide ✓") - log.Println(" - Prix total > 0 (recommandĂ©) ✓") - log.Println("") - log.Println("═══════════════════════════════════════════════════════════") - log.Println("") -} diff --git a/backend/gestion/middleware/clock_middleware.go b/backend/gestion/middleware/clock_middleware.go index 860ad12b..6c6c7af5 100644 --- a/backend/gestion/middleware/clock_middleware.go +++ b/backend/gestion/middleware/clock_middleware.go @@ -3,6 +3,7 @@ package middleware import ( "fmt" "gestion/db" + "gestion/models" "log" "net/http" "time" @@ -30,7 +31,7 @@ func OrderHoursMiddleware(c *gin.Context) { } sched := settings.DeliverySchedule - var day db.DaySchedule + var day models.DaySchedule switch weekday { case time.Monday: day = sched.Monday diff --git a/backend/gestion/models/auth.go b/backend/gestion/models/auth.go new file mode 100644 index 00000000..990f8853 --- /dev/null +++ b/backend/gestion/models/auth.go @@ -0,0 +1,54 @@ +package models + +import "github.com/golang-jwt/jwt/v5" + +type ClientClaims struct { + ClientID int `json:"client_id"` + Username string `json:"username"` + Role string `json:"role"` + SessionID string `json:"session_id"` + jwt.RegisteredClaims +} + +type AdminClaims struct { + UserID int `json:"user_id"` + Username string `json:"username"` + Role string `json:"role"` + SessionID string `json:"session_id"` + jwt.RegisteredClaims +} + +// ============================================ +// STRUCTURES REQUÊTE / RÉPONSE +// ============================================ + +type LoginRequest struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` +} + +type RegisterClientRequest struct { + Username string `json:"username" binding:"required,min=3,max=50"` + Password string `json:"password" binding:"required,min=8"` + Nom string `json:"nom" binding:"required,min=2,max=100"` + Prenom string `json:"prenom" binding:"required,min=2,max=100"` + Telephone string `json:"telephone" binding:"required"` +} + +type RegisterAdminRequest struct { + Username string `json:"username" binding:"required,min=3,max=50"` + Password string `json:"password" binding:"required,min=8"` + Role string `json:"role" binding:"required,oneof=admin cabine livreur"` +} + +type LoginResponse struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + User any `json:"user"` +} + +type ProfileResponse struct { + Username string `json:"username"` + Role string `json:"role"` +} diff --git a/backend/gestion/models/client.go b/backend/gestion/models/client.go index dfeca26a..8e6e410a 100644 --- a/backend/gestion/models/client.go +++ b/backend/gestion/models/client.go @@ -10,10 +10,8 @@ type Client struct { Nom string `json:"nom"` Prenom string `json:"prenom"` Telephone string `json:"telephone"` - Command int `json:"command"` - Point int `json:"point"` - PointZipette int `json:"points_zipette"` - PointsExtra map[string]int `json:"points_extra"` // pools[2+] + Command int `json:"command"` + PointsExtra map[string]int `json:"points_extra"` Amende float64 `json:"amende"` CancellationsCount int `json:"cancellations_count"` LastPenaltyReason string `json:"last_penalty_reason"` diff --git a/backend/gestion/models/settings.go b/backend/gestion/models/settings.go new file mode 100644 index 00000000..60b27c60 --- /dev/null +++ b/backend/gestion/models/settings.go @@ -0,0 +1,80 @@ +package models + +type PostalZone struct { + Name string `json:"name"` + MinAmount float64 `json:"min_amount"` + Codes []string `json:"codes"` +} + +// PointsTier reprĂ©sente un palier du barĂšme de points +// Si Max == 0, il n'y a pas de borne supĂ©rieure (illimitĂ©) +type PointsTier struct { + Min float64 `json:"min"` + Max float64 `json:"max"` // 0 = illimitĂ© + Points int `json:"points"` +} + +// DaySchedule reprĂ©sente les horaires de livraison pour un jour de la semaine +type DaySchedule struct { + Enabled bool `json:"enabled"` + OpenTime string `json:"open_time"` // ex: "09:00" + CloseTime string `json:"close_time"` // ex: "20:00" +} + +// DeliverySchedule reprĂ©sente les horaires de livraison pour chaque jour +type DeliverySchedule struct { + Monday DaySchedule `json:"monday"` + Tuesday DaySchedule `json:"tuesday"` + Wednesday DaySchedule `json:"wednesday"` + Thursday DaySchedule `json:"thursday"` + Friday DaySchedule `json:"friday"` + Saturday DaySchedule `json:"saturday"` + Sunday DaySchedule `json:"sunday"` +} + +type PointsPool struct { + Key string `json:"key"` // identifiant interne (ex: "pool_0") + Name string `json:"name"` // nom affichĂ© (ex: "Cannabis", "Accessoires") + Categories []string `json:"categories"` // catĂ©gories de produits assignĂ©es Ă  ce pool + Tiers []PointsTier `json:"tiers"` // barĂšme de points +} + +// PenaltyTier reprĂ©sente un palier d'amende : Ă  partir de MinCancel annulations, Amount est appliquĂ© +type PenaltyTier struct { + MinCancel int `json:"min_cancel"` // nombre d'annulations Ă  partir duquel ce palier s'applique + Amount int `json:"amount"` // montant de l'amende +} + +// CategoryRoute associe un livreur Ă  une ou plusieurs catĂ©gories de produits +type CategoryRoute struct { + DeliverymanUsername string `json:"deliveryman_username"` + Categories []string `json:"categories"` +} + +// DeliveryModeConfig configure le mode d'assignation des livreurs +// Mode "single" → un seul livreur, toutes catĂ©gories confondues +// Mode "category_based" → chaque livreur gĂšre ses catĂ©gories dĂ©diĂ©es +type DeliveryModeConfig struct { + Mode string `json:"mode"` // "single" | "category_based" + CategoryRoutes []CategoryRoute `json:"category_routes"` // utilisĂ© uniquement en mode category_based +} + +// AppSettings contient les paramĂštres globaux de l'application +type AppSettings struct { + PenaltiesEnabled bool `json:"penalties_enabled"` + ShowAmendeScore bool `json:"show_amende_score"` // afficher le score d'amendes aux clients/cabine + PenaltyTiers []PenaltyTier `json:"penalty_tiers"` // barĂšme des amendes (liste configurable) + 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 + 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 + NowPaymentsIPNSecret string `json:"nowpayments_ipn_secret"` // secret IPN NowPayments + NowPaymentsCurrencies []string `json:"nowpayments_currencies"` // cryptos acceptĂ©es (ex: ["btc","eth","ltc"]) + DeliverySchedule DeliverySchedule `json:"delivery_schedule"` // horaires de livraison par jour + PostalZones []PostalZone `json:"postal_zones"` // zones de livraison avec minimum de commande + TelegramBotToken string `json:"telegram_bot_token"` // token du bot Telegram (BotFather) + TelegramBotUsername string `json:"telegram_bot_username"` // username du bot (sans @) + DeliveryMode DeliveryModeConfig `json:"delivery_mode"` // mode d'assignation des livreurs +} diff --git a/backend/gestion/models/telegram.go b/backend/gestion/models/telegram.go new file mode 100644 index 00000000..86b29227 --- /dev/null +++ b/backend/gestion/models/telegram.go @@ -0,0 +1,21 @@ +package models + +type TgUpdate struct { + UpdateID int `json:"update_id"` + Message *TgMessage `json:"message"` +} + +type TgMessage struct { + MessageID int `json:"message_id"` + Chat TgChat `json:"chat"` + Text string `json:"text"` +} + +type TgChat struct { + ID int64 `json:"id"` +} + +type TelegramLinkData struct { + Username string `json:"username"` + Role string `json:"role"` // "client" | "livreur" | "admin" | "cabine" +} diff --git a/backend/gestion/models/update_profile.go b/backend/gestion/models/update_profile.go index 77403449..11821a05 100644 --- a/backend/gestion/models/update_profile.go +++ b/backend/gestion/models/update_profile.go @@ -20,8 +20,6 @@ type AdminUpdateClientRequest struct { Nom string `json:"nom,omitempty"` Prenom string `json:"prenom,omitempty"` Telephone string `json:"telephone,omitempty"` - Command *int `json:"command,omitempty"` - Point *int `json:"point,omitempty"` - PointsZipette *int `json:"points_zipette,omitempty"` - Amende *float64 `json:"amende,omitempty"` + Command *int `json:"command,omitempty"` + Amende *float64 `json:"amende,omitempty"` } diff --git a/backend/gestion/routes/routes.go b/backend/gestion/routes/routes.go index aca5b234..9bee1fd9 100644 --- a/backend/gestion/routes/routes.go +++ b/backend/gestion/routes/routes.go @@ -97,9 +97,10 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services cartGroupV1.GET("/notifications", handlers.GetClientNotifications) cartGroupV1.POST("/notifications/read", handlers.MarkNotificationsRead) - // đŸ“± PUSH TOKEN - cartGroupV1.POST("/push-token", handlers.RegisterPushToken) - cartGroupV1.DELETE("/push-token", handlers.UnregisterPushToken) + // đŸ€– TELEGRAM CLIENT + cartGroupV1.POST("/telegram/link-token", handlers.GenerateClientLinkToken) + cartGroupV1.GET("/telegram/status", handlers.GetClientTelegramStatus) + cartGroupV1.DELETE("/telegram/unlink", handlers.UnlinkClientTelegram) // đŸ‘€ PROFIL CLIENT cartGroupV1.GET("/profile", handlers.GetMyProfile) // ✅ RĂ©cupĂ©rer mon profil @@ -117,6 +118,11 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services // ============================================ router.POST("/api/v1/webhooks/nowpayments", handlers.IPNWebhook) + // ============================================ + // đŸ€– WEBHOOK TELEGRAM - PUBLIC (sĂ©curisĂ© par secret header) + // ============================================ + router.POST("/webhook/telegram", handlers.TelegramWebhook) + // ============================================ // 🌍 GÉOCODAGE PUBLIC (v1) // ============================================ @@ -146,14 +152,14 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services adminGroupV2 := router.Group("/api/v2/admin/protected") adminGroupV2.Use(middleware.AdminMiddleware) { - // PUSH TOKEN ADMIN - adminGroupV2.POST("/push-token", handlers.RegisterAdminPushToken) - adminGroupV2.DELETE("/push-token", handlers.UnregisterAdminPushToken) - // 🔔 NOTIFICATIONS ADMIN adminGroupV2.GET("/notifications", handlers.GetLivreurNotifications) adminGroupV2.POST("/notifications/read", handlers.MarkLivreurNotificationsRead) + // đŸ€– TELEGRAM ADMIN + adminGroupV2.POST("/telegram/link-token", handlers.GenerateAdminLinkToken) + adminGroupV2.DELETE("/telegram/unlink", handlers.UnlinkAdminTelegram) + // ============================================ // CLIENT - GESTION // ============================================ @@ -285,14 +291,14 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services cabineGroupV1.POST("/add/address", handlers.AddAddress) cabineGroupV1.DELETE("/delete/address", handlers.DeleteAddress) cabineGroupV1.GET("/addresses", handlers.GetAllAddress) - // PUSH TOKEN CABINE - cabineGroupV1.POST("/push-token", handlers.RegisterCabinePushToken) - cabineGroupV1.DELETE("/push-token", handlers.UnregisterCabinePushToken) - // 🔔 NOTIFICATIONS CABINE cabineGroupV1.GET("/notifications", handlers.GetLivreurNotifications) cabineGroupV1.POST("/notifications/read", handlers.MarkLivreurNotificationsRead) + // đŸ€– TELEGRAM CABINE + cabineGroupV1.POST("/telegram/link-token", handlers.GenerateAdminLinkToken) + cabineGroupV1.DELETE("/telegram/unlink", handlers.UnlinkAdminTelegram) + cabineGroupV1.GET("/commands/:id/items", handlers.ShowItems) cabineGroupV1.POST("/commands/:id/confirm-reception", handlers.StaffApproveDelivery) cabineGroupV1.POST("/commands/:id/assign", handlers.AssignDeliveryPerson) @@ -331,6 +337,7 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services livreurGroupV1.GET("/deliveries/:id", handlers.GetDeliveryDetails) // ✅ DĂ©tail filtrĂ© livreurGroupV1.POST("/deliveries/:id/start", handlers.StartDelivery) livreurGroupV1.PUT("/deliveries/:id/status", handlers.UpdateDeliveryStatus) // ✅ Avec GPS + livreurGroupV1.GET("/deliveries/:id/nav-link", handlers.GetLivreurNavLink) // Lien Waze App // ============================================ // POSITION GPS @@ -362,7 +369,10 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services // ============================================ livreurGroupV1.GET("/notifications", handlers.GetLivreurNotifications) livreurGroupV1.POST("/notifications/read", handlers.MarkLivreurNotificationsRead) - livreurGroupV1.POST("/push-token", handlers.RegisterLivreurPushToken) - livreurGroupV1.DELETE("/push-token", handlers.UnregisterLivreurPushToken) + + // đŸ€– TELEGRAM LIVREUR + livreurGroupV1.POST("/telegram/link-token", handlers.GenerateLivreurLinkToken) + livreurGroupV1.GET("/telegram/status", handlers.GetLivreurTelegramStatus) + livreurGroupV1.DELETE("/telegram/unlink", handlers.UnlinkLivreurTelegram) } } diff --git a/backend/gestion/services/telegram.go b/backend/gestion/services/telegram.go new file mode 100644 index 00000000..f3a0042d --- /dev/null +++ b/backend/gestion/services/telegram.go @@ -0,0 +1,129 @@ +package services + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "time" +) + +// TelegramBot est l'instance globale accessible depuis le package db +var TelegramBot *TelegramService + +type TelegramService struct { + botToken string + webhookSecret string + BotUsername string +} + +func NewTelegramService() *TelegramService { + svc := &TelegramService{ + botToken: os.Getenv("TELEGRAM_BOT_TOKEN"), + webhookSecret: os.Getenv("TELEGRAM_WEBHOOK_SECRET"), + BotUsername: os.Getenv("TELEGRAM_BOT_USERNAME"), + } + TelegramBot = svc + return svc +} + +func (t *TelegramService) IsConfigured() bool { + return t.botToken != "" +} + +// Reload met Ă  jour le token et le username (appelĂ© aprĂšs UpdateSettings) +func (t *TelegramService) Reload(token, username string) { + if token != "" { + t.botToken = token + } + if username != "" { + t.BotUsername = username + } +} + +func (t *TelegramService) ValidateWebhookSecret(header string) bool { + if t.webhookSecret == "" { + return true // pas de secret configurĂ© = acceptĂ© + } + return header == t.webhookSecret +} + +// SendMessage envoie un message texte (HTML) Ă  un chat Telegram +func (t *TelegramService) SendMessage(chatID int64, text string) error { + if !t.IsConfigured() { + return fmt.Errorf("telegram non configurĂ©") + } + + payload := map[string]interface{}{ + "chat_id": chatID, + "text": text, + "parse_mode": "HTML", + } + + body, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("marshal: %w", err) + } + + url := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", t.botToken) + req, err := http.NewRequest("POST", url, bytes.NewBuffer(body)) + if err != nil { + return fmt.Errorf("crĂ©ation requĂȘte: %w", err) + } + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("envoi: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("telegram API status %d", resp.StatusCode) + } + return nil +} + +// SetWebhook enregistre l'URL webhook auprĂšs de Telegram +func (t *TelegramService) SetWebhook(webhookURL string) error { + if !t.IsConfigured() { + return fmt.Errorf("telegram non configurĂ©") + } + + payload := map[string]interface{}{ + "url": webhookURL, + "allowed_updates": []string{"message"}, + } + if t.webhookSecret != "" { + payload["secret_token"] = t.webhookSecret + } + + body, err := json.Marshal(payload) + if err != nil { + return err + } + + url := fmt.Sprintf("https://api.telegram.org/bot%s/setWebhook", t.botToken) + req, err := http.NewRequest("POST", url, bytes.NewBuffer(body)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{Timeout: 15 * time.Second} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("setWebhook status %d", resp.StatusCode) + } + + log.Printf("✅ [TELEGRAM] Webhook enregistrĂ©: %s", webhookURL) + return nil +} diff --git a/backend/gestion/utils/auth.go b/backend/gestion/utils/auth.go new file mode 100644 index 00000000..a8e2ac07 --- /dev/null +++ b/backend/gestion/utils/auth.go @@ -0,0 +1,60 @@ +package utils + +import ( + "crypto/rand" + "encoding/hex" + "regexp" + "strings" + + "github.com/gin-gonic/gin" +) + +func GenerateSessionID() string { + bytes := make([]byte, 16) + rand.Read(bytes) + return hex.EncodeToString(bytes) +} + +func ValidatePhoneNumber(phone string) bool { + clean := regexp.MustCompile(`[\s\-\(\)]`).ReplaceAllString(phone, "") + + validFormat := regexp.MustCompile(`^(\+33|0)[1-9]\d{8}$`) + return validFormat.MatchString(clean) +} + +func NormalizePhoneNumber(phone string) string { + clean := regexp.MustCompile(`[\s\-\(\)]`).ReplaceAllString(phone, "") + + if strings.HasPrefix(clean, "0") { + return "+33" + clean[1:] + } + return clean +} + +func CheckRoleAdmin(c *gin.Context, role string) bool { + if role == "admin" { + return true + } + return false +} + +func CheckRoleClient(c *gin.Context, role string) bool { + if role == "client" { + return true + } + return false +} + +func CheckRoleCabine(c *gin.Context, role string) bool { + if role == "cabine" { + return true + } + return false +} + +func CheckRoleLivreur(c *gin.Context, role string) bool { + if role == "livreur" { + return true + } + return false +} diff --git a/backend/gestion/utils/cabine.go b/backend/gestion/utils/cabine.go new file mode 100644 index 00000000..9595db8f --- /dev/null +++ b/backend/gestion/utils/cabine.go @@ -0,0 +1,14 @@ +package utils + +import ( + "gestion/db" +) + +func CheckCommand(commandID int, database *db.Database) bool { + _, err := database.GetCommandByID(commandID) + if err != nil { + return false + } + + return true +} diff --git a/backend/gestion/utils/delivery.go b/backend/gestion/utils/delivery.go new file mode 100644 index 00000000..fc58aede --- /dev/null +++ b/backend/gestion/utils/delivery.go @@ -0,0 +1,44 @@ +package utils + +import ( + "fmt" + "math" +) + +func CalculateDistance(lat1, lon1, lat2, lon2 float64) float64 { + const earthRadiusKm = 6371 + const metersPerKm = 1000 + + lat1Rad := DegreesToRadians(lat1) + lon1Rad := DegreesToRadians(lon1) + lat2Rad := DegreesToRadians(lat2) + lon2Rad := DegreesToRadians(lon2) + + dLat := lat2Rad - lat1Rad + dLon := lon2Rad - lon1Rad + + a := math.Sin(dLat/2)*math.Sin(dLat/2) + + math.Cos(lat1Rad)*math.Cos(lat2Rad)* + math.Sin(dLon/2)*math.Sin(dLon/2) + + c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a)) + return earthRadiusKm * c * metersPerKm +} + +func DegreesToRadians(degrees float64) float64 { + return degrees * math.Pi / 180 +} + +func GetDeliveryStatusMessage(status string) string { + messages := map[string]string{ + "assigned": "Commande assignĂ©e", + "en_route": "En route vers le client", + "arrived": "ArrivĂ© Ă  destination", + "livre": "Livraison effectuĂ©e", + "cancelled": "Livraison annulĂ©e", + } + if msg, ok := messages[status]; ok { + return msg + } + return fmt.Sprintf("Statut changĂ©: %s", status) +} diff --git a/backend/gestion/workers/cron_auto_assign.go b/backend/gestion/workers/cron_auto_assign.go index 78a9d18d..d0c0d46d 100644 --- a/backend/gestion/workers/cron_auto_assign.go +++ b/backend/gestion/workers/cron_auto_assign.go @@ -126,18 +126,13 @@ func tryAssignCommandWithPriority( Longitude: location.Longitude, } - // 2. RĂ©cupĂ©rer livreurs actifs - activeLivreurs, err := database.GetAllActiveDeliveryPersons() - if err != nil || len(activeLivreurs) == 0 { - log.Printf("⚠ [CRON] Cmd %d - Aucun livreur disponible", commandID) + // 2. RĂ©cupĂ©rer livreurs Ă©ligibles (selon le mode single/category_based) + usernames, err := database.GetEligibleDeliverymenForCommand(commandID) + if err != nil || len(usernames) == 0 { + log.Printf("⚠ [CRON] Cmd %d - Aucun livreur Ă©ligible disponible", commandID) return false } - usernames := make([]string, len(activeLivreurs)) - for i, livreur := range activeLivreurs { - usernames[i] = livreur.Username - } - // 3. Trouver le plus proche nearest, err := geoService.FindNearestDeliveryPersonFast(targetCoords, usernames) if err != nil { diff --git a/backend/gestion/workers/redis_worker.go b/backend/gestion/workers/redis_worker.go index 393ef24f..0a42b3b3 100644 --- a/backend/gestion/workers/redis_worker.go +++ b/backend/gestion/workers/redis_worker.go @@ -148,7 +148,8 @@ func PointsSyncWorker(database *db.Database) { } // Mettre Ă  jour dans la DB principale - err = database.AddClientPoints(username, points) + pool := "pool_0" + err = database.AddClientPointsByCategory(username, points, pool) if err != nil { log.Printf("⚠ Erreur sync points pour %s: %v", username, err) continue