1003 lines
29 KiB
Go
1003 lines
29 KiB
Go
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 int
|
|
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 int
|
|
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
|
|
}
|