// ============================================ // handlers/basket_handlers_CORRIGES.go // ============================================ package handlers import ( "fmt" "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 || req.NameProduct == "" || req.Category == "" { 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 } if err := database.DecrementProductStockByID(req.ProductID, req.Quantity); err != nil { log.Printf("❌ [ADD_PANIER] Erreur décrement stock product_id=%d: %v", req.ProductID, err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Impossible d'ajouter le produit au panier", "details": err.Error()}) 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 } c.JSON(http.StatusCreated, gin.H{"success": true, "message": "Produit ajouté au panier avec succès", "panier": panier}) return } } // ============================================ // ============================================ // 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"` UseReferralBalance bool `json:"use_referral_balance"` } 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)) // ============================================ // 1️⃣b Vérifier le minimum de commande selon la zone // ============================================ var cartTotal float64 for _, item := range items { if price, ok := item["price"].(float64); ok { cartTotal += price } } // Récupérer les paramètres globaux (zones + parrainage) appSettings, _ := database.GetSettings() // Récupérer le solde parrainage disponible (seulement si le système est activé) var referralBalance float64 if req.UseReferralBalance && appSettings.ReferralEnabled { referralBalance, _ = database.GetClientReferralBalance(usernameStr) } zoneResult := checkDeliveryZone(req.DeliveryAddress, cartTotal, appSettings.PostalZones) if !zoneResult.OK { if zoneResult.ZoneName == "inconnue" { log.Printf("❌ [CHECKOUT] Aucun code postal trouvé dans l'adresse: %s", req.DeliveryAddress) c.JSON(http.StatusBadRequest, gin.H{ "error": "Adresse invalide : aucun code postal détecté", }) } else if zoneResult.ZoneName == "hors zone" { log.Printf("❌ [CHECKOUT] Code postal %s hors zone de livraison", zoneResult.PostalCode) c.JSON(http.StatusBadRequest, gin.H{ "error": "Livraison non disponible pour ce code postal", "postal_code": zoneResult.PostalCode, }) } else { log.Printf("❌ [CHECKOUT] Total %.2f€ insuffisant pour %s (minimum %.2f€)", cartTotal, zoneResult.ZoneName, zoneResult.MinAmount) c.JSON(http.StatusBadRequest, gin.H{ "error": fmt.Sprintf("Montant minimum de commande non atteint pour votre zone (%.0f€ minimum)", zoneResult.MinAmount), "zone": zoneResult.ZoneName, "minimum": zoneResult.MinAmount, "cart_total": cartTotal, "missing": zoneResult.MinAmount - cartTotal, "postal_code": zoneResult.PostalCode, "referral_balance": referralBalance, }) } return } // Règle parrainage : après déduction du crédit, le client doit toujours payer au minimum le seuil de zone. // Ex : zone 50€, crédit 50€ → panier doit être >= 100€ var referralUsed float64 if req.UseReferralBalance && referralBalance > 0 { effectivePayment := cartTotal - referralBalance if effectivePayment < zoneResult.MinAmount { needed := zoneResult.MinAmount + referralBalance log.Printf("❌ [CHECKOUT] Crédit parrainage %.2f€ mais panier insuffisant: %.2f€ < %.2f€ requis", referralBalance, cartTotal, needed) c.JSON(http.StatusBadRequest, gin.H{ "error": fmt.Sprintf("Avec %.2f€ de crédit parrainage, votre commande doit atteindre %.2f€ (minimum zone %.0f€ + crédit utilisé)", referralBalance, needed, zoneResult.MinAmount), "zone": zoneResult.ZoneName, "minimum": needed, "cart_total": cartTotal, "missing": needed - cartTotal, "referral_balance": referralBalance, "postal_code": zoneResult.PostalCode, }) return } referralUsed = referralBalance } log.Printf("✅ [CHECKOUT] Zone OK: %s, total=%.2f€ >= %.2f€, crédit parrainage utilisé: %.2f€", zoneResult.ZoneName, cartTotal, zoneResult.MinAmount, referralUsed) // ============================================ // 2️⃣ Débiter le parrainage AVANT la commande (évite double-spend) // ============================================ if referralUsed > 0 { if err := database.DebitReferralBalance(usernameStr, referralUsed); err != nil { log.Printf("❌ [CHECKOUT] Solde parrainage insuffisant pour %s: %v", usernameStr, err) c.JSON(http.StatusBadRequest, gin.H{"error": "Solde parrainage insuffisant ou déjà utilisé"}) return } log.Printf("✅ [CHECKOUT] Crédit parrainage -%.2f€ débité pour %s", referralUsed, usernameStr) } command, err := database.CreateCommandWithAddress(usernameStr, req.DeliveryAddress) if err != nil { if referralUsed > 0 { _ = database.RestoreReferralBalance(usernameStr, referralUsed) } log.Printf("❌ [CHECKOUT] Erreur création commande: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur création commande"}) return } commandID := command.ID if referralUsed > 0 { if err := database.SetCommandReferralUsed(commandID, referralUsed); err != nil { log.Printf("⚠️ [CHECKOUT] Impossible de sauvegarder referral_used sur commande: %v", err) } } log.Printf("✅ [CHECKOUT] Commande %d créée", commandID) // Notifier immédiatement tous les admins et agents cabine go database.NotifyAllAdminCabine(commandID, usernameStr, req.DeliveryAddress) // ============================================ // 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) } // Notifier le livreur de la nouvelle commande notifMsg := fmt.Sprintf("Nouvelle commande #%d assignée - Livraison dans ~%d min (%.2f km)", commandID, travelTime, distance) if referralUsed > 0 { notifMsg += fmt.Sprintf(" | Parrainage client: -%.2f€", referralUsed) } if notifErr := database.NotifyLivreur(nearest.Username, commandID, "new_assignment", notifMsg); notifErr != nil { log.Printf("⚠️ [CHECKOUT] Erreur notification livreur: %v", notifErr) } // Notifier le client clientMsg := fmt.Sprintf("Votre commande #%d est confirmée ! Un livreur est en route.", commandID) database.NotifyClient(usernameStr, commandID, "assigned", clientMsg) 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 // ============================================ newBalance, _ := database.GetClientReferralBalance(usernameStr) resp := gin.H{ "success": true, "command_id": commandID, "delivery_address": req.DeliveryAddress, "status": "pending", "referral_used": referralUsed, "referral_balance": newBalance, } 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) }