// ============================================ // handlers/basket_handlers_CORRIGES.go // ============================================ package handlers import ( "gestion/db" "gestion/models" "gestion/services" "log" "net/http" "github.com/gin-gonic/gin" ) type BasketsRequest struct { Username string `json:"username"` ProductID int `json:"product_id"` NameProduct string `json:"name_product"` Category string `json:"category"` Quantity float64 `json:"quantity"` } // ============================================ // ✅ SÉCURISÉ: AddProductsBasket // ============================================ // POST /api/v1/panier/add func AddProductsBasket(c *gin.Context) { database := c.MustGet("database").(*db.Database) var req BasketsRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Requête invalide", "details": err.Error()}) return } username, ok := c.Get("username") if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentification requise"}) return } req.Username = username.(string) if req.Quantity <= 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "Quantité invalide"}) return } // Si product_id fourni par le mobile, on l'utilise directement (plus fiable) if req.ProductID > 0 { stock, err := database.GetProductStockByID(req.ProductID) if err != nil { log.Printf("❌ [ADD_PANIER] Produit %d non trouvé: %v", req.ProductID, err) c.JSON(http.StatusNotFound, gin.H{"error": "Produit non trouvé"}) return } if stock < req.Quantity { c.JSON(http.StatusBadRequest, gin.H{"error": "Stock insuffisant", "available": stock}) return } panier, err := database.AddProductInBasketByID(req.Username, req.ProductID, req.Quantity) if err != nil { log.Printf("❌ [ADD_PANIER] Erreur ajout product_id=%d qty=%.3f: %v", req.ProductID, req.Quantity, err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Impossible d'ajouter le produit au panier", "details": err.Error()}) return } if err := database.DecrementProductStockByID(req.ProductID, req.Quantity); err != nil { log.Printf("❌ [ADD_PANIER] Erreur décrement stock product_id=%d: %v", req.ProductID, err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Impossible de réserver le stock"}) return } c.JSON(http.StatusCreated, gin.H{"success": true, "message": "Produit ajouté au panier avec succès", "panier": panier}) return } // Fallback : recherche par nom+catégorie (compatibilité) if req.NameProduct == "" || req.Category == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "product_id ou name_product+category requis"}) return } stock, err := database.GetProductStock(req.NameProduct, req.Category) if err != nil { log.Printf("❌ [ADD_PANIER] Produit '%s'/'%s' non trouvé: %v", req.NameProduct, req.Category, err) c.JSON(http.StatusNotFound, gin.H{"error": "Produit non trouvé"}) return } if stock < req.Quantity { c.JSON(http.StatusBadRequest, gin.H{"error": "Stock insuffisant", "available": stock}) return } panier, err := database.AddProductInBasket(req.Username, req.NameProduct, req.Quantity, req.Category) if err != nil { log.Printf("❌ [ADD_PANIER] Erreur ajout '%s': %v", req.NameProduct, err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Impossible d'ajouter le produit au panier", "details": err.Error()}) return } if err := database.DecrementProductStock(req.NameProduct, req.Category, req.Quantity); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Impossible de réserver le stock"}) return } c.JSON(http.StatusCreated, gin.H{"success": true, "message": "Produit ajouté au panier avec succès", "panier": panier}) } // ============================================ // ============================================ // GET /api/v1/panier/:username // Récupère le panier du client authentifié func GetAllBaskets(c *gin.Context) { database := c.MustGet("database").(*db.Database) username := c.Param("username") log.Printf("📦 [GET_PANIER] Requête pour: %s", username) if username == "" { log.Printf("❌ [GET_PANIER] Username manquant") c.JSON(http.StatusBadRequest, gin.H{"error": "Le paramètre 'username' est requis"}) return } // ✅ SÉCURITÉ 1: Récupérer username depuis le contexte (du JWT validé) authUsername, hasAuth := c.Get("username") if !hasAuth { log.Printf("❌ [GET_PANIER] Username manquant dans JWT") c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentification requise"}) return } authUsernameStr := authUsername.(string) // ✅ SÉCURITÉ 2: Vérifier que c'est bien l'utilisateur de la session if username != authUsernameStr { log.Printf("❌ [GET_PANIER] ⚠️ TENTATIVE D'ACCÈS AU PANIER NON AUTORISÉE!") log.Printf(" Username du JWT: %s", authUsernameStr) log.Printf(" Username demandé: %s", username) c.JSON(http.StatusForbidden, gin.H{ "error": "Vous ne pouvez accéder qu'à votre panier", }) return } // ✅ SÉCURITÉ 3: Forcer l'utilisation du username du JWT username = authUsernameStr // ✅ SÉCURITÉ 4: Vérifier que c'est un CLIENT _, err := database.GetClientByUsername(username) if err != nil { log.Printf("❌ [GET_PANIER] Client inexistant: %s", username) c.JSON(http.StatusNotFound, gin.H{"error": "Utilisateur inexistant"}) return } baskets, err := database.GetAllProductsInBasket(username) if err != nil { log.Printf("❌ [GET_PANIER] Erreur récupération: %v", err) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur lors de la récupération du panier", "details": err.Error(), }) return } var totalAmount float64 for _, item := range baskets { totalAmount += item.Price // price = prix total de la ligne (cumul des ajouts) } log.Printf("✅ [GET_PANIER] Panier %s: %d articles, total=%.2f€", username, len(baskets), totalAmount) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Panier récupéré avec succès", "panier": baskets, "count": len(baskets), "total_amount": totalAmount, }) } // ============================================ // ✅ SÉCURISÉ: DeleteProductFromBasket // ============================================ // DELETE /api/v1/panier/remove // Supprime un produit du panier func DeleteProductFromBasket(c *gin.Context) { database := c.MustGet("database").(*db.Database) var req struct { ID int `json:"id" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { log.Printf("❌ [DEL_PANIER] Erreur JSON: %v", err) c.JSON(http.StatusBadRequest, gin.H{ "error": "Données requises manquantes", "details": err.Error(), }) return } // ✅ SÉCURITÉ 1: Récupérer username depuis le contexte (du JWT validé) authUsername, hasAuth := c.Get("username") if !hasAuth { log.Printf("❌ [DEL_PANIER] Username manquant dans JWT") c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentification requise"}) return } authUsernameStr := authUsername.(string) log.Printf("🗑️ [DEL_PANIER] Suppression article: id=%d, client=%s", req.ID, authUsernameStr) // ✅ SÉCURITÉ 2: Vérifier que l'article appartient à ce client var itemUsername string err := database.QueryRow( "SELECT username FROM baskets WHERE id = $1", req.ID, ).Scan(&itemUsername) if err != nil { log.Printf("❌ [DEL_PANIER] Article non trouvé: id=%d", req.ID) c.JSON(http.StatusNotFound, gin.H{"error": "Article non trouvé"}) return } if itemUsername != authUsernameStr { log.Printf("❌ [DEL_PANIER] ⚠️ TENTATIVE DE SUPPRESSION NON AUTORISÉE!") log.Printf(" Client JWT: %s", authUsernameStr) log.Printf(" Propriétaire article: %s", itemUsername) c.JSON(http.StatusForbidden, gin.H{ "error": "Vous ne pouvez supprimer que vos articles", }) return } // Supprimer l'article err = database.DeleteProductFromBasket(req.ID) if err != nil { log.Printf("❌ [DEL_PANIER] Erreur suppression: %v", err) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur lors de la suppression", "details": err.Error(), }) return } log.Printf("✅ [DEL_PANIER] Article %d supprimé", req.ID) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Produit supprimé du panier avec succès", "item_id": req.ID, "stock_released": true, }) } // ============================================ // ✅ SÉCURISÉ: ClearBasket // ============================================ // DELETE /api/v1/panier/clear // Vide le panier du client func ClearBasket(c *gin.Context) { database := c.MustGet("database").(*db.Database) // ✅ SÉCURITÉ 1: Récupérer username depuis le contexte (du JWT validé) authUsername, hasAuth := c.Get("username") if !hasAuth { log.Printf("❌ [CLEAR_PANIER] Username manquant dans JWT") c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentification requise"}) return } authUsernameStr := authUsername.(string) log.Printf("🧹 [CLEAR_PANIER] Vider panier de: %s", authUsernameStr) baskets, err := database.GetAllProductsInBasket(authUsernameStr) if err != nil { log.Printf("❌ [CLEAR_PANIER] Erreur récupération: %v", err) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur lors du vidage du panier", }) return } err = database.ClearBasket(authUsernameStr) if err != nil { log.Printf("❌ [CLEAR_PANIER] Erreur vidage: %v", err) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur lors du vidage du panier", "details": err.Error(), }) return } log.Printf("✅ [CLEAR_PANIER] Panier %s vidé: %d articles supprimés", authUsernameStr, len(baskets)) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Panier vidé avec succès", "stock_released": len(baskets), }) } // ============================================ // ✅ SÉCURISÉ: ValidateBasket - CHECKOUT FINAL // ============================================ // POST /api/v1/checkout // Crée la commande depuis le panier et le vide // ⚠️ SEUL ENDPOINT DE CRÉATION DE COMMANDE (CreateCommandFromBasket supprimé) func ValidateBasket(c *gin.Context) { database := c.MustGet("database").(*db.Database) geoService := c.MustGet("geoService").(*services.GeoService) username, exists := c.Get("username") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "Utilisateur non authentifié"}) return } usernameStr := username.(string) var req struct { DeliveryAddress string `json:"delivery_address" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil || req.DeliveryAddress == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Adresse de livraison requise"}) return } cmd := &models.Command{DeliveryAddress: req.DeliveryAddress} if err := database.CheckAddress(cmd); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(), "corrected_address": cmd.DeliveryAddress}) return } req.DeliveryAddress = cmd.DeliveryAddress log.Printf("🛒 [CHECKOUT] Début checkout pour: %s", usernameStr) // ============================================ // 1️⃣ Vérifier que le panier n'est pas vide // ============================================ items, err := database.GetBasketItems(usernameStr) if err != nil { log.Printf("❌ [CHECKOUT] Erreur récupération panier: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Impossible de récupérer le panier", "details": err.Error()}) return } if len(items) == 0 { log.Printf("❌ [CHECKOUT] Panier vide") c.JSON(http.StatusBadRequest, gin.H{"error": "Panier vide"}) return } log.Printf("🛒 [CHECKOUT] Panier: %d articles", len(items)) // ============================================ // 2️⃣ Créer la commande (qui décrémente automatiquement le stock) // ============================================ command, err := database.CreateCommandWithAddress(usernameStr, req.DeliveryAddress) if err != nil { log.Printf("❌ [CHECKOUT] Erreur création commande: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur création commande", "details": err.Error()}) return } commandID := command.ID log.Printf("✅ [CHECKOUT] Commande %d créée", commandID) // ============================================ // 3️⃣ Vider le panier // ============================================ err = database.ClearBasket(usernameStr) if err != nil { log.Printf("❌ [CHECKOUT] Erreur vidage panier: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Impossible de vider le panier", "details": err.Error()}) return } log.Printf("🧹 [CHECKOUT] Panier vidé") // ============================================ // 4️⃣ Auto-assignation livreur (optionnel) // ============================================ var assigned bool var assignInfo gin.H location, err := geoService.GeocodeAddress(req.DeliveryAddress) 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 } nearest, err := geoService.FindNearestDeliveryPersonFast(services.Coordinates{ Latitude: location.Latitude, Longitude: location.Longitude, }, usernames) if err == nil { log.Printf("👤 [CHECKOUT] Livreur le plus proche: %s (%.2f km)", nearest.Username, nearest.Distance) // ✅ CORRECTION: Utiliser CalculateETAWithTomTom au lieu de GetETAWithTraffic travelTime, distance, err := services.CalculateETAWithTomTom( nearest.Location, services.Coordinates{ Latitude: location.Latitude, Longitude: location.Longitude, }, ) if err != nil { // Fallback sur ETA simple travelTime = nearest.EstimatedTime distance = nearest.Distance log.Printf("⚠️ [CHECKOUT] Fallback ETA: %d min", travelTime) } log.Printf("⏱️ [CHECKOUT] ETA calculé: %d min, distance: %.2f km", travelTime, distance) // Assigner la commande au livreur err = database.AssignCommandToDeliverymanQueueWithCoords( commandID, nearest.Username, travelTime, location.Latitude, location.Longitude, req.DeliveryAddress, ) if err != nil { log.Printf("⚠️ [CHECKOUT] Erreur assignation: %v", err) } else { // Mettre à jour le statut du livreur err = database.SetDeliveryPersonStatus(nearest.Username, "busy", commandID) if err != nil { log.Printf("⚠️ [CHECKOUT] Erreur mise à jour statut livreur: %v", err) } assigned = true assignInfo = gin.H{ "username": nearest.Username, "distance_km": distance, "travel_time": travelTime, } log.Printf("✅ [CHECKOUT] Commande assignée à %s", nearest.Username) } } else { log.Printf("⚠️ [CHECKOUT] Aucun livreur trouvé: %v", err) } } else { log.Printf("⚠️ [CHECKOUT] Aucun livreur actif disponible") } } else { log.Printf("⚠️ [CHECKOUT] Erreur géocodage: %v", err) } // ============================================ // 5️⃣ Réponse // ============================================ resp := gin.H{ "success": true, "command_id": commandID, "delivery_address": req.DeliveryAddress, "status": "pending", } if assigned { resp["message"] = "Commande créée et livreur assigné automatiquement" resp["auto_assigned"] = true resp["assigned_to"] = assignInfo resp["status"] = "assigned" log.Printf("✅ [CHECKOUT] Réponse 200 - Commande %d assignée", commandID) } else { resp["message"] = "Commande créée - En attente d'assignation" resp["auto_assigned"] = false log.Printf("✅ [CHECKOUT] Réponse 200 - Commande %d en attente", commandID) } c.JSON(http.StatusOK, resp) }