package db import ( "database/sql" "fmt" "gestion/models" "log" "strings" "time" ) func sanitizeString(s string) string { sanitized := strings.Map(func(r rune) rune { if r < 32 || r == 127 { return -1 } return r }, s) if len(sanitized) > 1000 { sanitized = sanitized[:1000] } return strings.TrimSpace(sanitized) } func validateAddress(address string) error { address = strings.TrimSpace(address) if address == "" { return fmt.Errorf("adresse vide non autorisée") } if len(address) > 500 { return fmt.Errorf("adresse trop longue (max 500 caractères)") } if strings.ContainsAny(address, "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0B\x0C\x0E\x0F") { return fmt.Errorf("adresse contient des caractères de contrôle interdits") } return nil } // validateCommandStatus vérifie si le statut est valide func validateCommandStatus(status string) error { validStatuses := map[string]bool{ "pending": true, "assigned": true, "en_route": true, "livre": true, "approved": true, "cancelled": true, "disabled": true, "support": true, } if !validStatuses[status] { return fmt.Errorf("statut invalide: %s", status) } return nil } 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) 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 } 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` var commandID int var createdAt, updatedAt time.Time err = d.QueryRow(commandQuery, username, "pending", adresse, totalPrix).Scan( &commandID, &createdAt, &updatedAt, ) if err != nil { 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" } itemQuery := `INSERT INTO command_items (command_id, produit, product_id, quantite, prix) VALUES ($1, $2, $3, $4, $5)` _, err = d.Exec(itemQuery, commandID, productName, item.ProductID, item.Quantity, item.Price) if err != nil { return nil, fmt.Errorf("erreur lors de l'insertion des items: %w", err) } } // Vider le panier clearBasketQuery := `DELETE FROM baskets WHERE username = $1` _, err = d.Exec(clearBasketQuery, username) if err != nil { return nil, fmt.Errorf("erreur lors du vidage du panier: %w", err) } command := &models.Command{ ID: commandID, Status: "pending", Total: totalPrix, } return command, nil } 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 } if err := validateAddress(deliveryAddress); err != nil { 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) } clientNom := "" clientPrenom := "" clientTelephone := "" if client != nil { clientNom = sanitizeString(client.Nom) clientPrenom = sanitizeString(client.Prenom) 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) 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 } if len(basketItems) == 0 { return nil, fmt.Errorf("le panier est vide") } // ✅ SÉCURITÉ: Vérifier que le total est cohérent 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` var commandID int var createdAt, updatedAt time.Time err = d.QueryRow(commandQuery, username, "pending", deliveryAddress, totalPrix).Scan( &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 == "" { productName = fmt.Sprintf("Produit #%d", item.ProductID) } err = d.InsertCommandItemWithClientInfo( commandID, productName, item.ProductID, item.Quantity, item.Price, username, clientNom, clientPrenom, clientTelephone, deliveryAddress, ) if err != nil { log.Printf("❌ Erreur INSERT command_items: %v", err) 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 { log.Printf("⚠️ Erreur décrémentation stock produit %d: %v", item.ProductID, err) return nil, fmt.Errorf("erreur mise à jour stock: %w", err) } rowsAffected, _ := result.RowsAffected() if rowsAffected == 0 { return nil, fmt.Errorf("stock insuffisant pour produit %d", 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, Status: "pending", Total: totalPrix, DeliveryAddress: deliveryAddress, CreatedAt: createdAt, UpdatedAt: updatedAt, } 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 if username != "" { if err := validateUsername(username); err != nil { return nil, err } } if status != "" { if err := validateCommandStatus(status); err != nil { return nil, err } } 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 // Filtrage du statut if status == "" { query += ` AND c.status IN ('pending', 'assigned', 'en_route', 'livre')` } else { query += fmt.Sprintf(" AND c.status = $%d", argPosition) args = append(args, status) 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)) 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) } // ✅ SÉCURITÉ: Sanitization de l'adresse adresse = sanitizeString(adresse) 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("✅ [GetAllCommands] %d commandes récupérées", len(commands)) return commands, nil } func (d *Database) GetCommandCount() (int, error) { var count int err := d.QueryRow("SELECT COUNT(*) FROM commandes").Scan(&count) if err != nil { return 0, fmt.Errorf("erreur récupération count commandes: %w", err) } return count, nil } 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 FROM commandes WHERE id = $1` var commandID int var username, status, adresse string var livreurAssign sql.NullString var totalPrix float64 var createdAt, updatedAt time.Time err := d.QueryRow(query, id).Scan( &commandID, &username, &status, &adresse, &totalPrix, &livreurAssign, &createdAt, &updatedAt, ) if err == sql.ErrNoRows { return nil, fmt.Errorf("commande non trouvée") } if err != nil { return nil, fmt.Errorf("erreur lors de la récupération de la commande: %w", err) } command := map[string]interface{}{ "id": commandID, "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 } return command, nil } func (d *Database) GetCommandAddress(commandID int) (string, error) { var address string query := `SELECT adresse FROM commandes WHERE id = $1` err := d.QueryRow(query, commandID).Scan(&address) if err == sql.ErrNoRows { return "", fmt.Errorf("commande non trouvée") } if err != nil { return "", fmt.Errorf("erreur lors de la récupération de l'adresse: %w", err) } return address, nil } 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)") } if strings.TrimSpace(deliveryAddress) == "" { return fmt.Errorf("adresse vide non autorisée") } query := `UPDATE commandes SET adresse = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2` result, err := d.Exec(query, deliveryAddress, commandID) if err != nil { return fmt.Errorf("erreur lors de la mise à jour de l'adresse: %w", err) } rowsAffected, err := result.RowsAffected() if err != nil { return fmt.Errorf("erreur lors de la vérification: %w", err) } if rowsAffected == 0 { return fmt.Errorf("commande non trouvée") } log.Printf("✅ Adresse commande %d mise à jour", commandID) return nil } // 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", "livre", "approved", "cancelled", "disabled", "support"} isValid := false for _, vs := range validStatuses { if status == vs { isValid = true break } } if !isValid { return fmt.Errorf("statut invalide: %s", status) } query := `UPDATE commandes SET status = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2` result, err := d.Exec(query, status, commandID) if err != nil { return fmt.Errorf("erreur lors de la mise à jour du statut: %w", err) } rowsAffected, err := result.RowsAffected() if err != nil { return fmt.Errorf("erreur lors de la vérification des lignes affectées: %w", err) } if rowsAffected == 0 { return fmt.Errorf("commande non trouvée") } 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) query := `INSERT INTO command_logs (command_id, status, message, author, created_at) VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP)` _, 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 } return nil } // GetCommandLogs récupère tous les logs d'une commande func (d *Database) GetCommandLogs(commandID int) ([]map[string]interface{}, error) { query := `SELECT id, command_id, status, message, author, created_at FROM command_logs WHERE command_id = $1 ORDER BY created_at ASC` rows, err := d.Query(query, commandID) 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 } defer rows.Close() var logs []map[string]interface{} for rows.Next() { var id, commandID int var status, message, author string var createdAt time.Time err := rows.Scan(&id, &commandID, &status, &message, &author, &createdAt) if err != nil { return nil, fmt.Errorf("erreur lors du scan du log: %w", err) } logEntry := map[string]interface{}{ "id": id, "command_id": commandID, "status": status, "message": message, "author": author, "created_at": createdAt, } logs = append(logs, logEntry) } if err = rows.Err(); err != nil { return nil, fmt.Errorf("erreur lors de l'itération des résultats: %w", err) } 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", "livre", "approved", "cancelled", "disabled", "support"} 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 r }, message) // Limiter à 1000 caractères if len(sanitized) > 1000 { sanitized = sanitized[:1000] } return sanitized } 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 // ✅ ÉTAPE 2: SELECT FOR UPDATE pour verrouiller la commande var currentStatus, cmdUsername, livreurAssign string var totalPrix float64 err = tx.QueryRow(` SELECT status, username, COALESCE(livreur_assign, ''), total_prix FROM commandes WHERE id = $1 FOR UPDATE `, commandID).Scan(¤tStatus, &cmdUsername, &livreurAssign, &totalPrix) if err == sql.ErrNoRows { log.Printf("❌ [ValidateAtomic] Commande %d non trouvée", commandID) return 0, fmt.Errorf("commande non trouvée") } if err != nil { log.Printf("❌ [ValidateAtomic] Erreur SELECT: %v", err) return 0, fmt.Errorf("erreur lecture commande: %w", err) } 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", "support", "livre"} isValid := false for _, s := range validStatuses { if currentStatus == s { isValid = true break } } 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 WHERE id = $1 AND status = $2 `, commandID, currentStatus) if err != nil { log.Printf("❌ [ValidateAtomic] Erreur UPDATE: %v", err) return 0, fmt.Errorf("erreur mise à jour statut: %w", err) } rows, _ := result.RowsAffected() if rows == 0 { log.Printf("❌ [ValidateAtomic] Commande %d déjà modifiée (race condition évitée)", commandID) 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) return 0, fmt.Errorf("erreur attribution points: %w", err) } totalPoints = points 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 WHERE username = $1 `, 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) `, commandID, "approved", fmt.Sprintf("Livraison validée par admin %s - %d points attribués", adminUsername, totalPoints), adminUsername) 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) } 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) } }() } // ✅ É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) if cmdUsername != "" { clientCacheKey := fmt.Sprintf("client:%s", cmdUsername) Redis.Del(RedisCtx, clientCacheKey) clientCommandsCacheKey := fmt.Sprintf("client:%s:commands", cmdUsername) Redis.Del(RedisCtx, clientCommandsCacheKey) } log.Printf("✅ [ValidateAtomic] Caches invalidés pour cmd %d", commandID) }() return totalPoints, nil } // ApproveDeliveryAtomic - Version atomique pour approbation client func (d *Database) ApproveDeliveryAtomic(commandID int, username string) (int, error) { log.Printf("🔒 [ApproveAtomic] START - cmd=%d, client=%s", commandID, username) // ✅ ÉTAPE 1: Démarrer une transaction tx, err := d.Begin() if err != nil { log.Printf("❌ [ApproveAtomic] Erreur début transaction: %v", err) return 0, fmt.Errorf("erreur transaction: %w", err) } defer tx.Rollback() // ✅ ÉTAPE 2: SELECT FOR UPDATE pour verrouiller la commande var currentStatus, cmdUsername, livreurAssign string err = tx.QueryRow(` SELECT status, username, COALESCE(livreur_assign, '') FROM commandes WHERE id = $1 FOR UPDATE `, commandID).Scan(¤tStatus, &cmdUsername, &livreurAssign) if err == sql.ErrNoRows { log.Printf("❌ [ApproveAtomic] Commande %d non trouvée", commandID) return 0, fmt.Errorf("commande non trouvée") } if err != nil { log.Printf("❌ [ApproveAtomic] Erreur SELECT: %v", err) return 0, fmt.Errorf("erreur lecture commande: %w", err) } log.Printf("📋 [ApproveAtomic] Commande trouvée - status=%s, owner=%s", currentStatus, cmdUsername) // ✅ ÉTAPE 3: Vérifier propriété if cmdUsername != username { log.Printf("❌ [ApproveAtomic] Commande n'appartient pas à %s (propriétaire: %s)", username, cmdUsername) return 0, fmt.Errorf("cette commande ne vous appartient pas") } // ✅ ÉTAPE 4: Vérifier le statut if currentStatus != "livre" { log.Printf("❌ [ApproveAtomic] Statut invalide: %s (attendu: livre)", currentStatus) return 0, fmt.Errorf("commande doit être en statut 'livre' (statut actuel: %s)", currentStatus) } // ✅ ÉTAPE 5: UPDATE avec vérification du statut result, err := tx.Exec(` UPDATE commandes SET status = 'approved', updated_at = CURRENT_TIMESTAMP WHERE id = $1 AND status = 'livre' AND username = $2 `, commandID, username) if err != nil { log.Printf("❌ [ApproveAtomic] Erreur UPDATE: %v", err) return 0, fmt.Errorf("erreur mise à jour statut: %w", err) } rows, _ := result.RowsAffected() if rows == 0 { log.Printf("❌ [ApproveAtomic] Commande %d déjà modifiée (race condition évitée)", commandID) return 0, fmt.Errorf("commande déjà approuvée ou modifiée") } log.Printf("✅ [ApproveAtomic] Statut mis à jour: livre → approved") // ✅ ÉTAPE 6: Calculer et ajouter les points totalPoints, err := d.CalculateAndAddPointsForCommandTx(tx, commandID, username) if err != nil { log.Printf("❌ [ApproveAtomic] Erreur calcul points: %v", err) return 0, fmt.Errorf("erreur attribution points: %w", err) } log.Printf("✅ [ApproveAtomic] %d points attribués à %s", totalPoints, username) // ✅ ÉTAPE 7: Incrémenter le compteur de commandes _, err = tx.Exec(` UPDATE clients SET command = command + 1, updated_at = CURRENT_TIMESTAMP WHERE username = $1 `, username) if err != nil { log.Printf("⚠️ [ApproveAtomic] Erreur incrémentation compteur: %v", err) // On ne bloque pas pour ça } // ✅ ÉTAPE 8: Ajouter un log _, err = tx.Exec(` INSERT INTO command_logs (command_id, status, message, author, created_at) VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP) `, commandID, "approved", fmt.Sprintf("Livraison confirmée par le client %s - %d points attribués", username, totalPoints), username) if err != nil { log.Printf("⚠️ [ApproveAtomic] Erreur ajout log: %v", err) } // ✅ ÉTAPE 9: Commit if err := tx.Commit(); err != nil { log.Printf("❌ [ApproveAtomic] Erreur COMMIT: %v", err) return 0, fmt.Errorf("erreur commit transaction: %w", err) } log.Printf("🎉 [ApproveAtomic] SUCCÈS - Commande %d approuvée, %d points attribués", commandID, totalPoints) // ✅ ÉTAPE 10: Optimiser queue livreur (async, après commit) if livreurAssign != "" { log.Printf("📦 [ApproveAtomic] Optimisation queue pour livreur: %s", livreurAssign) go func() { err := d.CompleteDeliveryAndProcessNext(livreurAssign, commandID) if err != nil { log.Printf("⚠️ [ApproveAtomic] Erreur optimisation queue: %v", err) } }() } // ✅ ÉTAPE 11: Invalider caches (async) go func() { commandCacheKey := fmt.Sprintf("command:%d", commandID) Redis.Del(RedisCtx, commandCacheKey) clientCacheKey := fmt.Sprintf("client:%s", username) Redis.Del(RedisCtx, clientCacheKey) clientCommandsCacheKey := fmt.Sprintf("client:%s:commands", username) Redis.Del(RedisCtx, clientCommandsCacheKey) log.Printf("✅ [ApproveAtomic] Caches invalidés") }() return totalPoints, nil } // ApproveDeliveryAtomicByStaff - Confirmation de réception par admin ou cabine à la place du client func (d *Database) ApproveDeliveryAtomicByStaff(commandID int, staffUsername string) (int, string, error) { log.Printf("🔒 [ApproveAtomicStaff] START - cmd=%d, staff=%s", commandID, staffUsername) tx, err := d.Begin() if err != nil { return 0, "", fmt.Errorf("erreur transaction: %w", err) } defer tx.Rollback() var currentStatus, clientUsername, livreurAssign string err = tx.QueryRow(` SELECT status, username, COALESCE(livreur_assign, '') FROM commandes WHERE id = $1 FOR UPDATE `, commandID).Scan(¤tStatus, &clientUsername, &livreurAssign) if err == sql.ErrNoRows { return 0, "", fmt.Errorf("commande non trouvée") } if err != nil { return 0, "", fmt.Errorf("erreur lecture commande: %w", err) } if currentStatus != "livre" { return 0, "", fmt.Errorf("commande doit être en statut 'livre' (statut actuel: %s)", currentStatus) } result, err := tx.Exec(` UPDATE commandes SET status = 'approved', updated_at = CURRENT_TIMESTAMP WHERE id = $1 AND status = 'livre' `, commandID) if err != nil { return 0, "", fmt.Errorf("erreur mise à jour statut: %w", err) } rows, _ := result.RowsAffected() if rows == 0 { return 0, "", fmt.Errorf("commande déjà approuvée ou modifiée") } totalPoints, err := d.CalculateAndAddPointsForCommandTx(tx, commandID, clientUsername) if err != nil { return 0, "", fmt.Errorf("erreur attribution points: %w", err) } _, err = tx.Exec(` UPDATE clients SET command = command + 1, updated_at = CURRENT_TIMESTAMP WHERE username = $1 `, clientUsername) if err != nil { log.Printf("⚠️ [ApproveAtomicStaff] Erreur incrémentation compteur: %v", err) } _, err = tx.Exec(` INSERT INTO command_logs (command_id, status, message, author, created_at) VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP) `, commandID, "approved", fmt.Sprintf("Réception confirmée par %s au nom du client %s - %d points attribués", staffUsername, clientUsername, totalPoints), staffUsername) if err != nil { log.Printf("⚠️ [ApproveAtomicStaff] Erreur log: %v", err) } if err := tx.Commit(); err != nil { return 0, "", fmt.Errorf("erreur commit: %w", err) } log.Printf("🎉 [ApproveAtomicStaff] SUCCÈS - cmd=%d approuvée par %s, %d points → client %s", commandID, staffUsername, totalPoints, clientUsername) if livreurAssign != "" { go func() { if err := d.CompleteDeliveryAndProcessNext(livreurAssign, commandID); err != nil { log.Printf("⚠️ [ApproveAtomicStaff] Erreur queue: %v", err) } }() } go func() { Redis.Del(RedisCtx, fmt.Sprintf("command:%d", commandID)) Redis.Del(RedisCtx, fmt.Sprintf("client:%s", clientUsername)) Redis.Del(RedisCtx, fmt.Sprintf("client:%s:commands", clientUsername)) }() return totalPoints, clientUsername, nil }